mirror of https://github.com/perkeep/perkeep.git
Merge "importer: let the implementations build and parse the callback URL"
This commit is contained in:
commit
50afc86843
|
@ -100,6 +100,7 @@ func (*imp) ServeSetup(w http.ResponseWriter, r *http.Request, ctx *importer.Set
|
|||
// to an importer, or when an account is being re-logged into to
|
||||
// refresh its access token.
|
||||
// You typically start the OAuth redirect flow here.
|
||||
// The importer.OAuth2.RedirectURL and importer.OAuth2.RedirectState helpers can be used for OAuth2.
|
||||
http.Redirect(w, r, ctx.CallbackURL(), http.StatusFound)
|
||||
return nil
|
||||
}
|
||||
|
@ -173,3 +174,16 @@ func (im *imp) Run(ctx *importer.RunContext) (err error) {
|
|||
func (im *imp) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
httputil.BadRequestError(w, "Unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
|
||||
func (im *imp) CallbackRequestAccount(r *http.Request) (blob.Ref, error) {
|
||||
// We do not actually use OAuth, but this method works for us anyway.
|
||||
// Even if your importer implementation does not use OAuth, you can
|
||||
// probably just embed importer.OAuth1 in your implementation type.
|
||||
// If OAuth2, embedding importer.OAuth2 should work.
|
||||
return importer.OAuth1{}.CallbackRequestAccount(r)
|
||||
}
|
||||
|
||||
func (im *imp) CallbackURLParameters(acctRef blob.Ref) string {
|
||||
// See comment in CallbackRequestAccount.
|
||||
return importer.OAuth1{}.CallbackURLParameters(acctRef)
|
||||
}
|
||||
|
|
|
@ -70,6 +70,8 @@ var _ importer.ImporterSetupHTMLer = (*imp)(nil)
|
|||
type imp struct {
|
||||
mu sync.Mutex // guards following
|
||||
imageFileRef map[string]blob.Ref // url to file schema blob
|
||||
|
||||
importer.OAuth2 // for CallbackRequestAccount and CallbackURLParameters
|
||||
}
|
||||
|
||||
func (im *imp) NeedsAPIKey() bool { return true }
|
||||
|
@ -432,6 +434,7 @@ func doGet(ctx *context.Context, url string, form url.Values) (*http.Response, e
|
|||
return res, nil
|
||||
}
|
||||
|
||||
// auth returns a new oauth.Config
|
||||
func auth(ctx *importer.SetupContext) (*oauth.Config, error) {
|
||||
clientId, secret, err := ctx.Credentials()
|
||||
if err != nil {
|
||||
|
@ -444,18 +447,20 @@ func auth(ctx *importer.SetupContext) (*oauth.Config, error) {
|
|||
TokenURL: tokenURL,
|
||||
RedirectURL: ctx.CallbackURL(),
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
// possibly common methods for accessing oauth2 sites
|
||||
|
||||
func (im *imp) ServeSetup(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) error {
|
||||
oauthConfig, err := auth(ctx)
|
||||
if err == nil {
|
||||
state := "no_clue_what_this_is" // TODO: ask adg to document this. or send him a CL.
|
||||
http.Redirect(w, r, oauthConfig.AuthCodeURL(state), 302)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
oauthConfig.RedirectURL = im.RedirectURL(im, ctx)
|
||||
state, err := im.RedirectState(im, ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
http.Redirect(w, r, oauthConfig.AuthCodeURL(state), http.StatusFound)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) {
|
||||
|
|
|
@ -76,6 +76,17 @@ type Importer interface {
|
|||
|
||||
ServeSetup(w http.ResponseWriter, r *http.Request, ctx *SetupContext) error
|
||||
ServeCallback(w http.ResponseWriter, r *http.Request, ctx *SetupContext)
|
||||
|
||||
// CallbackRequestAccount extracts the blobref of the importer account from
|
||||
// the callback URL parameters of r. For example, it will be encoded as:
|
||||
// For Twitter (OAuth1), in its own URL parameter: "acct=sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4"
|
||||
// For Picasa: (OAuth2), in the OAuth2 "state" parameter: "state=acct:sha1-97911b1a5887eb5862d1c81666ba839fc1363ea1"
|
||||
CallbackRequestAccount(r *http.Request) (acctRef blob.Ref, err error)
|
||||
|
||||
// CallbackURLParameters uses the input importer account blobRef to build
|
||||
// and return the URL parameters string (including the prefixed "?"), that
|
||||
// will be appended to the callback URL.
|
||||
CallbackURLParameters(acctRef blob.Ref) string
|
||||
}
|
||||
|
||||
// ImporterSetupHTMLer is an optional interface that may be implemented by
|
||||
|
@ -177,7 +188,8 @@ func (sc *SetupContext) Credentials() (clientID, clientSecret string, err error)
|
|||
}
|
||||
|
||||
func (sc *SetupContext) CallbackURL() string {
|
||||
return sc.Host.ImporterBaseURL() + sc.ia.im.name + "/callback?acct=" + sc.AccountNode.PermanodeRef().String()
|
||||
return sc.Host.ImporterBaseURL() + sc.ia.im.name + "/callback" +
|
||||
sc.ia.im.impl.CallbackURLParameters(sc.AccountNode.PermanodeRef())
|
||||
}
|
||||
|
||||
// AccountURL returns the URL to an account of an importer
|
||||
|
@ -354,9 +366,13 @@ func (h *Host) serveImporterAcctCallback(w http.ResponseWriter, r *http.Request,
|
|||
http.Error(w, "invalid method", 400)
|
||||
return
|
||||
}
|
||||
acctRef, ok := blob.Parse(r.FormValue("acct"))
|
||||
if !ok {
|
||||
http.Error(w, "missing 'acct' blobref param", 400)
|
||||
acctRef, err := imp.impl.CallbackRequestAccount(r)
|
||||
if err != nil {
|
||||
httputil.ServeError(w, r, err)
|
||||
return
|
||||
}
|
||||
if !acctRef.Valid() {
|
||||
httputil.ServeError(w, r, errors.New("No valid blobref returned from CallbackRequestAccount(r)"))
|
||||
return
|
||||
}
|
||||
ia, err := imp.account(acctRef)
|
||||
|
|
|
@ -24,7 +24,9 @@ import (
|
|||
|
||||
var TODOImporter Importer = todoImp{}
|
||||
|
||||
type todoImp struct{}
|
||||
type todoImp struct {
|
||||
OAuth1 // for CallbackRequestAccount and CallbackURLParameters
|
||||
}
|
||||
|
||||
func (todoImp) NeedsAPIKey() bool { return false }
|
||||
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
Copyright 2014 The Camlistore Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package importer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"camlistore.org/pkg/blob"
|
||||
)
|
||||
|
||||
// OAuth1 provides methods that the importer implementations can use to
|
||||
// help with OAuth authentication.
|
||||
type OAuth1 struct{}
|
||||
|
||||
func (OAuth1) CallbackRequestAccount(r *http.Request) (blob.Ref, error) {
|
||||
acctRef, ok := blob.Parse(r.FormValue("acct"))
|
||||
if !ok {
|
||||
return blob.Ref{}, errors.New("missing 'acct=' blobref param")
|
||||
}
|
||||
return acctRef, nil
|
||||
}
|
||||
|
||||
func (OAuth1) CallbackURLParameters(acctRef blob.Ref) string {
|
||||
return "?acct=" + acctRef.String()
|
||||
}
|
||||
|
||||
// OAuth2 provides methods that the importer implementations can use to
|
||||
// help with OAuth2 authentication.
|
||||
type OAuth2 struct{}
|
||||
|
||||
func (OAuth2) CallbackRequestAccount(r *http.Request) (blob.Ref, error) {
|
||||
state := r.FormValue("state")
|
||||
if state == "" {
|
||||
return blob.Ref{}, errors.New("missing 'state' parameter")
|
||||
}
|
||||
if !strings.HasPrefix(state, "acct:") {
|
||||
return blob.Ref{}, errors.New("wrong 'state' parameter value, missing 'acct:' prefix.")
|
||||
}
|
||||
acctRef, ok := blob.Parse(strings.TrimPrefix(state, "acct:"))
|
||||
if !ok {
|
||||
return blob.Ref{}, errors.New("invalid account blobref in 'state' parameter")
|
||||
}
|
||||
return acctRef, nil
|
||||
}
|
||||
|
||||
func (OAuth2) CallbackURLParameters(acctRef blob.Ref) string {
|
||||
return "?state=acct:" + acctRef.String()
|
||||
}
|
||||
|
||||
// RedirectURL returns the redirect URI that imp should set in an oauth.Config
|
||||
// for the authorization phase of OAuth2 authentication.
|
||||
func (OAuth2) RedirectURL(imp Importer, ctx *SetupContext) string {
|
||||
// We strip our callback URL of its query component, because the Redirect URI
|
||||
// we send during authorization has to match exactly the registered redirect
|
||||
// URI(s). This query component should be stored in the "state" paremeter instead.
|
||||
// See http://tools.ietf.org/html/rfc6749#section-3.1.2.2
|
||||
fullCallback := ctx.CallbackURL()
|
||||
queryPart := imp.CallbackURLParameters(ctx.AccountNode.PermanodeRef())
|
||||
log.Printf("WARNING: callback URL %q has no query component", fullCallback)
|
||||
return strings.TrimSuffix(fullCallback, queryPart)
|
||||
}
|
||||
|
||||
// RedirectState returns the "state" query parameter that should be used for the authorization
|
||||
// phase of OAuth2 authentication. This parameter contains the query component of the redirection
|
||||
// URI. See http://tools.ietf.org/html/rfc6749#section-3.1.2.2
|
||||
func (OAuth2) RedirectState(imp Importer, ctx *SetupContext) (state string, err error) {
|
||||
m, err := url.ParseQuery(strings.TrimPrefix(imp.CallbackURLParameters(ctx.AccountNode.PermanodeRef()), "?"))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not parse callback parameters string as a query: %q", imp.CallbackURLParameters(ctx.AccountNode.PermanodeRef()))
|
||||
}
|
||||
state = m.Get("state")
|
||||
if state == "" {
|
||||
return "", errors.New("\"state\" not found in callback parameters")
|
||||
}
|
||||
return state, nil
|
||||
}
|
|
@ -61,7 +61,9 @@ func init() {
|
|||
|
||||
var _ importer.ImporterSetupHTMLer = (*imp)(nil)
|
||||
|
||||
type imp struct{}
|
||||
type imp struct {
|
||||
importer.OAuth1 // for CallbackRequestAccount and CallbackURLParameters
|
||||
}
|
||||
|
||||
func (im *imp) NeedsAPIKey() bool { return true }
|
||||
|
||||
|
|
Loading…
Reference in New Issue