1 2 3 4 5 6 7 8 9 10 11 12 13 14 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
43
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
61
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
86
87 func (OAuth2) RedirectURL(imp Importer, ctx *SetupContext) string {
88
89
90
91
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
100 for k := range queryPart {
101 v.Del(k)
102 }
103 u.RawQuery = v.Encode()
104 return u.String()
105 }
106
107
108
109
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
120
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
130
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
150
151
152 type OAuthContext struct {
153 Ctx context.Context
154 Client *oauth.Client
155 Creds *oauth.Credentials
156 }
157
158
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
193
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
217 type OAuthURIs struct {
218 TemporaryCredentialRequestURI string
219 ResourceOwnerAuthorizationURI string
220 TokenRequestURI string
221 }
222
223
224
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 }