Home Download Docs Code Community
     1	/*
     2	Copyright 2014 The Perkeep Authors
     3	
     4	Licensed under the Apache License, Version 2.0 (the "License");
     5	you may not use this file except in compliance with the License.
     6	You may obtain a copy of the License at
     7	
     8	     http://www.apache.org/licenses/LICENSE-2.0
     9	
    10	Unless required by applicable law or agreed to in writing, software
    11	distributed under the License is distributed on an "AS IS" BASIS,
    12	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13	See the License for the specific language governing permissions and
    14	limitations under the License.
    15	*/
    16	
    17	package importer
    18	
    19	import (
    20		"context"
    21		"errors"
    22		"fmt"
    23		"log"
    24		"net/http"
    25		"net/url"
    26		"strings"
    27	
    28		"github.com/garyburd/go-oauth/oauth"
    29		"perkeep.org/internal/httputil"
    30		"perkeep.org/pkg/blob"
    31	
    32		"go4.org/ctxutil"
    33	)
    34	
    35	const (
    36		AcctAttrTempToken         = "oauthTempToken"
    37		AcctAttrTempSecret        = "oauthTempSecret"
    38		AcctAttrAccessToken       = "oauthAccessToken"
    39		AcctAttrAccessTokenSecret = "oauthAccessTokenSecret"
    40	)
    41	
    42	// OAuth1 provides methods that the importer implementations can use to
    43	// help with OAuth authentication.
    44	type OAuth1 struct{}
    45	
    46	func (OAuth1) CallbackRequestAccount(r *http.Request) (blob.Ref, error) {
    47		acctRef, ok := blob.Parse(r.FormValue("acct"))
    48		if !ok {
    49			return blob.Ref{}, errors.New("missing 'acct=' blobref param")
    50		}
    51		return acctRef, nil
    52	}
    53	
    54	func (OAuth1) CallbackURLParameters(acctRef blob.Ref) url.Values {
    55		v := url.Values{}
    56		v.Add("acct", acctRef.String())
    57		return v
    58	}
    59	
    60	// OAuth2 provides methods that the importer implementations can use to
    61	// help with OAuth2 authentication.
    62	type OAuth2 struct{}
    63	
    64	func (OAuth2) CallbackRequestAccount(r *http.Request) (blob.Ref, error) {
    65		state := r.FormValue("state")
    66		if state == "" {
    67			return blob.Ref{}, errors.New("missing 'state' parameter")
    68		}
    69		if !strings.HasPrefix(state, "acct:") {
    70			return blob.Ref{}, errors.New("wrong 'state' parameter value, missing 'acct:' prefix")
    71		}
    72		acctRef, ok := blob.Parse(strings.TrimPrefix(state, "acct:"))
    73		if !ok {
    74			return blob.Ref{}, errors.New("invalid account blobref in 'state' parameter")
    75		}
    76		return acctRef, nil
    77	}
    78	
    79	func (OAuth2) CallbackURLParameters(acctRef blob.Ref) url.Values {
    80		v := url.Values{}
    81		v.Set("state", "acct:"+acctRef.String())
    82		return v
    83	}
    84	
    85	// RedirectURL returns the redirect URI that imp should set in an oauth.Config
    86	// for the authorization phase of OAuth2 authentication.
    87	func (OAuth2) RedirectURL(imp Importer, ctx *SetupContext) string {
    88		// We strip our callback URL of its query component, because the Redirect URI
    89		// we send during authorization has to match exactly the registered redirect
    90		// URI(s). This query component should be stored in the "state" parameter instead.
    91		// See http://tools.ietf.org/html/rfc6749#section-3.1.2.2
    92		fullCallback := ctx.CallbackURL()
    93		queryPart := imp.CallbackURLParameters(ctx.AccountNode.PermanodeRef())
    94		if len(queryPart) == 0 {
    95			log.Printf("WARNING: callback URL %q has no query component", fullCallback)
    96		}
    97		u, _ := url.Parse(fullCallback)
    98		v := u.Query()
    99		// remove query params in CallbackURLParameters
   100		for k := range queryPart {
   101			v.Del(k)
   102		}
   103		u.RawQuery = v.Encode()
   104		return u.String()
   105	}
   106	
   107	// RedirectState returns the "state" query parameter that should be used for the authorization
   108	// phase of OAuth2 authentication. This parameter contains the query component of the redirection
   109	// URI. See http://tools.ietf.org/html/rfc6749#section-3.1.2.2
   110	func (OAuth2) RedirectState(imp Importer, ctx *SetupContext) (state string, err error) {
   111		m := imp.CallbackURLParameters(ctx.AccountNode.PermanodeRef())
   112		state = m.Get("state")
   113		if state == "" {
   114			return "", errors.New("\"state\" not found in callback parameters")
   115		}
   116		return state, nil
   117	}
   118	
   119	// IsAccountReady returns whether the account has been properly configured
   120	// - whether the user ID and access token has been stored in the given account node.
   121	func (OAuth2) IsAccountReady(acctNode *Object) (ok bool, err error) {
   122		if acctNode.Attr(AcctAttrUserID) != "" &&
   123			acctNode.Attr(AcctAttrAccessToken) != "" {
   124			return true, nil
   125		}
   126		return false, nil
   127	}
   128	
   129	// SummarizeAccount returns a summary for the account if it is configured,
   130	// or an error string otherwise.
   131	func (im OAuth2) SummarizeAccount(acct *Object) string {
   132		ok, err := im.IsAccountReady(acct)
   133		if err != nil {
   134			return ""
   135		}
   136		if !ok {
   137			return ""
   138		}
   139		if acct.Attr(AcctAttrGivenName) == "" &&
   140			acct.Attr(AcctAttrFamilyName) == "" {
   141			return fmt.Sprintf("userid %s", acct.Attr(AcctAttrUserID))
   142		}
   143		return fmt.Sprintf("userid %s (%s %s)",
   144			acct.Attr(AcctAttrUserID),
   145			acct.Attr(AcctAttrGivenName),
   146			acct.Attr(AcctAttrFamilyName))
   147	}
   148	
   149	// OAuthContext wraps the OAuth1 state needed to perform API calls.
   150	//
   151	// It is used as a value type.
   152	type OAuthContext struct {
   153		Ctx    context.Context
   154		Client *oauth.Client
   155		Creds  *oauth.Credentials
   156	}
   157	
   158	// Do sends through octx the request defined by url and the values in form.
   159	func (octx OAuthContext) do(method string, url string, form url.Values) (*http.Response, error) {
   160		if octx.Creds == nil {
   161			return nil, errors.New("no OAuth credentials. Not logged in?")
   162		}
   163		if octx.Client == nil {
   164			return nil, errors.New("no OAuth client")
   165		}
   166		var (
   167			res *http.Response
   168			err error
   169		)
   170		if method == http.MethodPost {
   171			res, err = octx.Client.Post(ctxutil.Client(octx.Ctx), octx.Creds, url, form)
   172		} else {
   173			res, err = octx.Client.Get(ctxutil.Client(octx.Ctx), octx.Creds, url, form)
   174		}
   175		if err != nil {
   176			return nil, fmt.Errorf("error fetching %s: %v", url, err)
   177		}
   178		if res.StatusCode != http.StatusOK {
   179			return res, fmt.Errorf("%s request on %s failed with: %s", method, url, res.Status)
   180		}
   181		return res, nil
   182	}
   183	
   184	func (octx OAuthContext) Get(url string, form url.Values) (*http.Response, error) {
   185		return octx.do("GET", url, form)
   186	}
   187	
   188	func (octx OAuthContext) POST(url string, form url.Values) (*http.Response, error) {
   189		return octx.do("POST", url, form)
   190	}
   191	
   192	// PopulateJSONFromURL makes a POST or GET call at apiURL, using keyval as parameters of
   193	// the associated form. The JSON response is decoded into result.
   194	func (octx OAuthContext) PopulateJSONFromURL(result interface{}, method string, apiURL string, keyval ...string) error {
   195		if method != http.MethodGet && method != http.MethodPost {
   196			return fmt.Errorf("only HTTP Get or Post supported: found %v", method)
   197		}
   198		if len(keyval)%2 == 1 {
   199			return errors.New("incorrect number of keyval arguments. must be even")
   200		}
   201		form := url.Values{}
   202		for i := 0; i < len(keyval); i += 2 {
   203			form.Set(keyval[i], keyval[i+1])
   204		}
   205		hres, err := octx.do(method, apiURL, form)
   206		if err != nil {
   207			return err
   208		}
   209		err = httputil.DecodeJSON(hres, result)
   210		if err != nil {
   211			return fmt.Errorf("could not parse response for %s: %v", apiURL, err)
   212		}
   213		return err
   214	}
   215	
   216	// OAuthURIs holds the URIs needed to initialize an OAuth 1 client.
   217	type OAuthURIs struct {
   218		TemporaryCredentialRequestURI string
   219		ResourceOwnerAuthorizationURI string
   220		TokenRequestURI               string
   221	}
   222	
   223	// NewOAuthClient returns an oauth Client configured with uris and the
   224	// credentials obtained from ctx.
   225	func (ctx *SetupContext) NewOAuthClient(uris OAuthURIs) (*oauth.Client, error) {
   226		clientID, secret, err := ctx.Credentials()
   227		if err != nil {
   228			return nil, err
   229		}
   230		return &oauth.Client{
   231			TemporaryCredentialRequestURI: uris.TemporaryCredentialRequestURI,
   232			ResourceOwnerAuthorizationURI: uris.ResourceOwnerAuthorizationURI,
   233			TokenRequestURI:               uris.TokenRequestURI,
   234			Credentials: oauth.Credentials{
   235				Token:  clientID,
   236				Secret: secret,
   237			},
   238		}, nil
   239	}
Website layout inspired by memcached.
Content by the authors.