diff --git a/pkg/importer/oa2_importers.go b/pkg/importer/oa2_importers.go index b18d48321..a1d77ac8d 100644 --- a/pkg/importer/oa2_importers.go +++ b/pkg/importer/oa2_importers.go @@ -22,6 +22,9 @@ import ( "log" "net/http" "net/url" + "strconv" + "strings" + "time" "camlistore.org/pkg/blob" "camlistore.org/pkg/context" @@ -65,9 +68,9 @@ func (im ExtendedOAuth2) CallbackURLParameters(acctRef blob.Ref) url.Values { return url.Values{} } -// notOAuthTransport returns c's Transport, or its underlying transport if c.Transport +// NotOAuthTransport returns c's Transport, or its underlying transport if c.Transport // is an OAuth Transport. -func notOAuthTransport(c *http.Client) (tr http.RoundTripper) { +func NotOAuthTransport(c *http.Client) (tr http.RoundTripper) { tr = c.Transport if otr, ok := tr.(*oauth.Transport); ok { tr = otr.Transport @@ -101,7 +104,7 @@ func (im ExtendedOAuth2) ServeCallback(w http.ResponseWriter, r *http.Request, c // needs to have the access token that is obtained during Exchange. transport := &oauth.Transport{ Config: oauthConfig, - Transport: notOAuthTransport(ctx.HTTPClient()), + Transport: NotOAuthTransport(ctx.HTTPClient()), } token, err := transport.Exchange(code) log.Printf("Token = %#v, error %v", token, err) @@ -111,7 +114,7 @@ func (im ExtendedOAuth2) ServeCallback(w http.ResponseWriter, r *http.Request, c return } - picagoCtx := ctx.Context.New(context.WithHTTPClient(&http.Client{Transport: transport})) + picagoCtx := ctx.Context.New(context.WithHTTPClient(transport.Client())) defer picagoCtx.Cancel() userInfo, err := im.getUserInfo(picagoCtx, token.AccessToken) @@ -125,7 +128,7 @@ func (im ExtendedOAuth2) ServeCallback(w http.ResponseWriter, r *http.Request, c AcctAttrUserID, userInfo.ID, AcctAttrGivenName, userInfo.FirstName, AcctAttrFamilyName, userInfo.LastName, - AcctAttrAccessToken, token.AccessToken, + AcctAttrOAuthToken, encodeToken(token), ); err != nil { httputil.ServeError(w, r, fmt.Errorf("Error setting attribute: %v", err)) return @@ -133,6 +136,46 @@ func (im ExtendedOAuth2) ServeCallback(w http.ResponseWriter, r *http.Request, c http.Redirect(w, r, ctx.AccountURL(), http.StatusFound) } +// encodeToken encodes the oauth.Token as +// AccessToken + " " + RefreshToken + " " + Expiry.Unix() +func encodeToken(token *oauth.Token) string { + if token == nil { + return " 0" + } + var seconds int64 + if !token.Expiry.IsZero() { + seconds = token.Expiry.Unix() + } + return token.AccessToken + " " + token.RefreshToken + " " + strconv.FormatInt(seconds, 10) +} + +// DecodeToken decodes from format of access + " " + refresh + " " + expiry +// to oauth.Token. +// It does not return an error, just decodes till it can. +func DecodeToken(encoded string) oauth.Token { + var token oauth.Token + i := strings.IndexByte(encoded, ' ') + if i < 0 { + token.AccessToken = encoded + return token + } + token.AccessToken, encoded = encoded[:i], encoded[i+1:] + i = strings.IndexByte(encoded, ' ') + if i < 0 { + token.RefreshToken = encoded + return token + } + token.RefreshToken, encoded = encoded[:i], encoded[i+1:] + if len(encoded) == 0 { + return token + } + seconds, err := strconv.ParseInt(encoded, 10, 64) + if err == nil { + token.Expiry = time.Unix(seconds, 0) + } + return token +} + func (im ExtendedOAuth2) auth(ctx *SetupContext) (*oauth.Config, error) { clientId, secret, err := ctx.Credentials() if err != nil { diff --git a/pkg/importer/oauth.go b/pkg/importer/oauth.go index 969c4239a..0dd39458a 100644 --- a/pkg/importer/oauth.go +++ b/pkg/importer/oauth.go @@ -32,6 +32,8 @@ const ( AcctAttrTempSecret = "oauthTempSecret" AcctAttrAccessToken = "oauthAccessToken" AcctAttrAccessTokenSecret = "oauthAccessTokenSecret" + // AcctAttrOAuthToken stores `access + " " + refresh + " " + expiry` + AcctAttrOAuthToken = "oauthToken" ) // OAuth1 provides methods that the importer implementations can use to diff --git a/pkg/importer/picasa/picasa.go b/pkg/importer/picasa/picasa.go index 2af0bed27..7278086b2 100644 --- a/pkg/importer/picasa/picasa.go +++ b/pkg/importer/picasa/picasa.go @@ -50,10 +50,24 @@ type imp struct { importer.ExtendedOAuth2 } +var baseOAuthConfig = oauth.Config{ + AuthURL: authURL, + TokenURL: tokenURL, + Scope: scopeURL, + + // AccessType needs to be "offline", as the user is not here all the time; + // ApprovalPrompt needs to be "force" to be able to get a RefreshToken + // everytime, even for Re-logins, too. + // + // Source: https://developers.google.com/youtube/v3/guides/authentication#server-side-apps + AccessType: "offline", + ApprovalPrompt: "force", +} + func newImporter() *imp { return &imp{ importer.NewExtendedOAuth2( - oauth.Config{AuthURL: authURL, TokenURL: tokenURL, Scope: scopeURL}, + baseOAuthConfig, func(ctx *context.Context, accessToken string) (*importer.UserInfo, error) { u, err := getUserInfo(ctx, accessToken) if err != nil { @@ -93,8 +107,7 @@ and click "CREATE PROJECT".

// A run is our state for a given run of the importer. type run struct { *importer.RunContext - im *imp - client *http.Client + im *imp } func (im *imp) Run(ctx *importer.RunContext) error { @@ -102,20 +115,16 @@ func (im *imp) Run(ctx *importer.RunContext) error { if err != nil { return err } - client := ctx.Host.HTTPClient() - client.Transport = &oauth.Transport{ - Config: &oauth.Config{ - ClientId: clientId, - ClientSecret: secret, - AuthURL: authURL, - TokenURL: tokenURL, - Scope: scopeURL, - }, - Token: &oauth.Token{ - AccessToken: ctx.AccountNode().Attr(importer.AcctAttrAccessToken), - }, + ocfg := baseOAuthConfig + ocfg.ClientId, ocfg.ClientSecret = clientId, secret + token := importer.DecodeToken(ctx.AccountNode().Attr(importer.AcctAttrOAuthToken)) + transport := &oauth.Transport{ + Config: &ocfg, + Token: &token, + Transport: importer.NotOAuthTransport(ctx.HTTPClient()), } - r := &run{RunContext: ctx, im: im, client: client} + ctx.Context = ctx.Context.New(context.WithHTTPClient(transport.Client())) + r := &run{RunContext: ctx, im: im} if err := r.importAlbums(); err != nil { return err } @@ -123,16 +132,16 @@ func (im *imp) Run(ctx *importer.RunContext) error { } func (r *run) importAlbums() error { - albums, err := picago.GetAlbums(r.client, "default") + albums, err := picago.GetAlbums(r.HTTPClient(), "default") if err != nil { - return err + return fmt.Errorf("importAlbums: error listing albums: %v", err) } albumsNode, err := r.getTopLevelNode("albums", "Albums") for _, album := range albums { if r.Context.IsCanceled() { return context.ErrCanceled } - if err := r.importAlbum(albumsNode, album, r.client); err != nil { + if err := r.importAlbum(albumsNode, album, r.HTTPClient()); err != nil { return fmt.Errorf("picasa importer: error importing album %s: %v", album, err) } } @@ -142,7 +151,7 @@ func (r *run) importAlbums() error { func (r *run) importAlbum(albumsNode *importer.Object, album picago.Album, client *http.Client) error { albumNode, err := albumsNode.ChildPathObject(album.Name) if err != nil { - return err + return fmt.Errorf("importAlbum: error listing album: %v", err) } // Data reference: https://developers.google.com/picasa-web/docs/2.0/reference @@ -222,7 +231,7 @@ func (r *run) importAlbum(albumsNode *importer.Object, album picago.Album, clien func (r *run) importPhoto(albumNode *importer.Object, photo picago.Photo, client *http.Client) (*importer.Object, error) { body, err := picago.DownloadPhoto(client, photo.URL) if err != nil { - return nil, err + return nil, fmt.Errorf("importPhoto: DownloadPhoto error: %v", err) } fileRef, err := schema.WriteFileFromReader( r.Host.Target(),