diff --git a/cmd/uber/main.go b/cmd/uber/main.go new file mode 100644 index 0000000..1f16450 --- /dev/null +++ b/cmd/uber/main.go @@ -0,0 +1,84 @@ +// Copyright 2017 orijtech. All Rights Reserved. +// +// 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 main + +import ( + "encoding/json" + "flag" + "log" + "os" + "path/filepath" + + "github.com/orijtech/uber/oauth2" +) + +func main() { + var init bool + flag.BoolVar(&init, "init", false, "allow a user to authorize this app to make requests on their behalf") + flag.Parse() + + // Make log not print out time info in its prefix. + log.SetFlags(0) + + switch { + case init: + authorize() + } +} + +func authorize() { + uberCredsDirPath, err := ensureUberCredsDirExists() + if err != nil { + log.Fatal(err) + } + + scopes := []string{ + oauth2.ScopeProfile, oauth2.ScopeRequest, + oauth2.ScopeHistory, oauth2.ScopePlaces, + oauth2.ScopeRequestReceipt, + } + + token, err := oauth2.AuthorizeByEnvApp(scopes...) + if err != nil { + log.Fatal(err) + } + + blob, err := json.Marshal(token) + if err != nil { + log.Fatal(err) + } + + credsPath := filepath.Join(uberCredsDirPath, "credentials.json") + f, err := os.Create(credsPath) + if err != nil { + log.Fatal(err) + } + + f.Write(blob) + log.Printf("Successfully saved your OAuth2.0 token to %q", credsPath) +} + +func ensureUberCredsDirExists() (string, error) { + wdir, err := os.Getwd() + if err != nil { + return "", err + } + + curDirPath := filepath.Join(wdir, ".uber") + if err := os.MkdirAll(curDirPath, 0777); err != nil { + return "", err + } + return curDirPath, nil +} diff --git a/oauth2/oauth2.go b/oauth2/oauth2.go new file mode 100644 index 0000000..bf2b028 --- /dev/null +++ b/oauth2/oauth2.go @@ -0,0 +1,200 @@ +// Copyright 2017 orijtech. All Rights Reserved. +// +// 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 oauth2 + +import ( + "context" + "errors" + "fmt" + "log" + "math/rand" + "net/http" + "net/url" + "os" + "strings" + "sync" + "time" + + "golang.org/x/oauth2" +) + +type OAuth2AppConfig struct { + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` +} + +var ( + envOAuth2ClientIDKey = "UBER_APP_OAUTH2_CLIENT_ID" + envOAuth2ClientSecretKey = "UBER_APP_OAUTH2_CLIENT_SECRET" +) + +func Transport(token *oauth2.Token) *oauth2.Transport { + // Once we have the token we can now make the TokenSource + ts := &tokenSourcer{token: token} + return &oauth2.Transport{Source: ts} +} + +type tokenSourcer struct { + sync.RWMutex + token *oauth2.Token +} + +var _ oauth2.TokenSource = (*tokenSourcer)(nil) + +func (ts *tokenSourcer) Token() (*oauth2.Token, error) { + ts.RLock() + defer ts.RUnlock() + + return ts.token, nil +} + +const ( + OAuth2AuthURL = "https://login.uber.com/oauth/v2/authorize" + OAuth2TokenURL = "https://login.uber.com/oauth/v2/token" +) + +// OAuth2ConfigFromEnv retrieves your app's client id and client +// secret from your environment, with the purpose of later being +// able to perform application functions on behalf of users. +func OAuth2ConfigFromEnv() (*OAuth2AppConfig, error) { + var errsList []string + oauth2ClientID := strings.TrimSpace(os.Getenv(envOAuth2ClientIDKey)) + if oauth2ClientID == "" { + errsList = append(errsList, fmt.Sprintf("%q was not set", envOAuth2ClientIDKey)) + } + oauth2ClientSecret := strings.TrimSpace(os.Getenv(envOAuth2ClientSecretKey)) + if oauth2ClientSecret == "" { + errsList = append(errsList, fmt.Sprintf("%q was not set", envOAuth2ClientSecretKey)) + } + + if len(errsList) > 0 { + return nil, errors.New(strings.Join(errsList, "\n")) + } + + config := &OAuth2AppConfig{ + ClientID: oauth2ClientID, + ClientSecret: oauth2ClientSecret, + } + + return config, nil +} + +const ( + // Access the user's basic profile information + // on a user's Uber account including their + // firstname, email address and profile picture. + ScopeProfile = "profile" + + // Pull trip data including times, product + // type andd city information of a user's + // historical pickups and drop-offs. + ScopeHistory = "history" + + // ScopeHistoryLite is the same as + // ScopeHistory but without city information. + ScopeHistoryLite = "history_lite" + + // Access to get and update your saved places. + // This includes your home and work addresses if + // you have saved them with Uber. + ScopePlaces = "places" + + // Allows developers to provide a complete + // Uber ride experience inside their app + // using the widget. Enables users to access + // trip information for rides requested through + // the app and the current ride, available promos, + // and payment methods (last two digits only) + // using the widget. Uber's charges, terms + // and policies will apply. + ScopeRideWidgets = "ride_widgets" + + // ScopeRequest is a privileged scope that + // allows your application to make requests + // for Uber products on behalf of users. + ScopeRequest = "request" + + // ScopeRequestReceipt is a privileged scope that + // allows your application to get receipt details + // for requests made by the application. + // Restrictions: This scope is only granted to apps + // that request Uber rides directly and receipts + // as part of the trip lifecycle. We do not allow + // apps to aggregate receipt information. The receipt + // endpoint will only provide receipts for ride requests + // origination from your application. It is not + // currently possible to receive receipt + // data for all trips, as of: (Fri 12 May 2017 18:18:42 MDT). + ScopeRequestReceipt = "request_receipt" + + // ScopeAllTrips is a privileged scope that allows + // access to trip details about all future Uber trips, + // including pickup, destination and real-time + // location for all of your future rides. + ScopeAllTrips = "all_trips" +) + +func AuthorizeByEnvApp(scopes ...string) (*oauth2.Token, error) { + oconfig, err := OAuth2ConfigFromEnv() + if err != nil { + return nil, err + } + return Authorize(oconfig, scopes...) +} + +func Authorize(oconfig *OAuth2AppConfig, scopes ...string) (*oauth2.Token, error) { + config := &oauth2.Config{ + ClientID: oconfig.ClientID, + ClientSecret: oconfig.ClientSecret, + Scopes: scopes, + Endpoint: oauth2.Endpoint{ + AuthURL: OAuth2AuthURL, + TokenURL: OAuth2TokenURL, + }, + } + + state := fmt.Sprintf("%v%s", time.Now().Unix(), rand.Float32()) + urlToVisit := config.AuthCodeURL(state, oauth2.AccessTypeOffline) + fmt.Printf("Please visit this URL for the auth dialog: %v\n", urlToVisit) + + callbackURLChan := make(chan url.Values) + go func() { + http.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { + query := req.URL.Query() + callbackURLChan <- query + fmt.Fprintf(rw, "Received the token successfully. Please return to your terminal") + }) + + defer close(callbackURLChan) + addr := ":8889" + if err := http.ListenAndServe(addr, nil); err != nil { + log.Fatal(err) + } + }() + + urlValues := <-callbackURLChan + gotState, wantState := urlValues.Get("state"), state + if gotState != wantState { + return nil, fmt.Errorf("states do not match: got: %q want: %q", gotState, wantState) + } + code := urlValues.Get("code") + + ctx := context.Background() + token, err := config.Exchange(ctx, code) + if err != nil { + return nil, err + } + return token, nil +} diff --git a/v1/client.go b/v1/client.go new file mode 100644 index 0000000..8a8d8b6 --- /dev/null +++ b/v1/client.go @@ -0,0 +1,147 @@ +// Copyright 2017 orijtech. All Rights Reserved. +// +// 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 uber + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "reflect" + "strings" + "sync" + + "golang.org/x/oauth2" + + "github.com/orijtech/otils" + uberOAuth2 "github.com/orijtech/uber/oauth2" +) + +const baseURL = "https://api.uber.com/v1.2" + +const envUberTokenKey = "UBER_TOKEN_KEY" + +var errUnsetTokenEnvKey = fmt.Errorf("could not find %q in your environment", envUberTokenKey) + +type Client struct { + sync.RWMutex + + rt http.RoundTripper + token string +} + +func NewClient(tokens ...string) (*Client, error) { + if token := otils.FirstNonEmptyString(tokens...); token != "" { + return &Client{token: token}, nil + } + + // Otherwise fallback to retrieving it from the environment + return NewClientFromEnv() +} + +func NewClientFromEnv() (*Client, error) { + retrToken := strings.TrimSpace(os.Getenv(envUberTokenKey)) + if retrToken == "" { + return nil, errUnsetTokenEnvKey + } + + return &Client{token: retrToken}, nil + +} + +func (c *Client) SetHTTPRoundTripper(rt http.RoundTripper) { + c.Lock() + c.rt = rt + c.Unlock() +} + +func (c *Client) SetBearerToken(token string) { + c.Lock() + defer c.Unlock() + + c.token = token +} + +func (c *Client) httpClient() *http.Client { + c.RLock() + rt := c.rt + c.RUnlock() + + if rt == nil { + rt = http.DefaultTransport + } + + return &http.Client{Transport: rt} +} + +func (c *Client) bearerToken() string { + c.RLock() + defer c.RUnlock() + + return fmt.Sprintf("Bearer %s", c.token) +} + +func (c *Client) tokenToken() string { + c.RLock() + defer c.RUnlock() + + return fmt.Sprintf("Token %s", c.token) +} + +func (c *Client) doAuthAndHTTPReq(req *http.Request) ([]byte, http.Header, error) { + req.Header.Set("Authorization", c.bearerToken()) + return c.doHTTPReq(req) +} + +func (c *Client) doHTTPReq(req *http.Request) ([]byte, http.Header, error) { + res, err := c.httpClient().Do(req) + if err != nil { + return nil, nil, err + } + if res.Body != nil { + defer res.Body.Close() + } + + if !otils.StatusOK(res.StatusCode) { + errMsg := res.Status + var err error + if res.Body != nil { + slurp, _ := ioutil.ReadAll(res.Body) + if len(slurp) > 3 { + ue := new(Error) + plainUE := new(Error) + if jerr := json.Unmarshal(slurp, ue); jerr == nil && !reflect.DeepEqual(ue, plainUE) { + err = ue + } else { + errMsg = string(slurp) + } + } + } + if err == nil { + err = otils.MakeCodedError(errMsg, res.StatusCode) + } + return nil, res.Header, err + } + + blob, err := ioutil.ReadAll(res.Body) + return blob, res.Header, err +} + +func NewClientFromOAuth2Token(token *oauth2.Token) (*Client, error) { + // Once we have the token we can now make the TokenSource + oauth2Transport := uberOAuth2.Transport(token) + return &Client{rt: oauth2Transport}, nil +} diff --git a/v1/uber.go b/v1/uber.go index 1080aed..742abfd 100644 --- a/v1/uber.go +++ b/v1/uber.go @@ -16,123 +16,10 @@ package uber import ( "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "os" - "reflect" "strings" "sync" - - "github.com/orijtech/otils" ) -const baseURL = "https://api.uber.com/v1.2" - -type Client struct { - sync.RWMutex - - rt http.RoundTripper - token string -} - -const envUberTokenKey = "UBER_TOKEN_KEY" - -var errUnsetTokenEnvKey = fmt.Errorf("could not find %q in your environment", envUberTokenKey) - -func NewClient(tokens ...string) (*Client, error) { - if token := otils.FirstNonEmptyString(tokens...); token != "" { - return &Client{token: token}, nil - } - - // Otherwise fallback to retrieving it from the environment - return NewClientFromEnv() -} - -func NewClientFromEnv() (*Client, error) { - retrToken := strings.TrimSpace(os.Getenv(envUberTokenKey)) - if retrToken == "" { - return nil, errUnsetTokenEnvKey - } - - return &Client{token: retrToken}, nil - -} - -func (c *Client) SetHTTPRoundTripper(rt http.RoundTripper) { - c.Lock() - c.rt = rt - c.Unlock() -} - -func (c *Client) SetBearerToken(token string) { - c.Lock() - defer c.Unlock() - - c.token = token -} - -func (c *Client) httpClient() *http.Client { - c.RLock() - rt := c.rt - c.RUnlock() - - if rt == nil { - rt = http.DefaultTransport - } - - return &http.Client{Transport: rt} -} - -func (c *Client) bearerToken() string { - c.RLock() - defer c.RUnlock() - - return fmt.Sprintf("Bearer %s", c.token) -} - -func (c *Client) tokenToken() string { - c.RLock() - defer c.RUnlock() - - return fmt.Sprintf("Token %s", c.token) -} - -func (c *Client) doAuthAndHTTPReq(req *http.Request) ([]byte, http.Header, error) { - req.Header.Set("Authorization", c.bearerToken()) - res, err := c.httpClient().Do(req) - if err != nil { - return nil, nil, err - } - if res.Body != nil { - defer res.Body.Close() - } - - if !otils.StatusOK(res.StatusCode) { - errMsg := res.Status - var err error - if res.Body != nil { - slurp, _ := ioutil.ReadAll(res.Body) - if len(slurp) > 3 { - ue := new(Error) - plainUE := new(Error) - if jerr := json.Unmarshal(slurp, ue); jerr == nil && !reflect.DeepEqual(ue, plainUE) { - err = ue - } else { - errMsg = string(slurp) - } - } - } - if err == nil { - err = otils.MakeCodedError(errMsg, res.StatusCode) - } - return nil, res.Header, err - } - - blob, err := ioutil.ReadAll(res.Body) - return blob, res.Header, err -} - func makeCancelParadigm() (<-chan bool, func()) { var cancelOnce sync.Once cancelChan := make(chan bool, 1)