diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 44d001a9..5f84785f 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -15,11 +15,6 @@ "Comment": "null-75", "Rev": "bd8df7009305d6ada223ea3c95b94c0f38bfa119" }, - { - "ImportPath": "code.google.com/p/goauth2/oauth", - "Comment": "weekly-56", - "Rev": "afe77d958c701557ec5dc56f6936fcc194d15520" - }, { "ImportPath": "code.google.com/p/gomock/gomock", "Rev": "5d0b59c9f277f888d2c70b00908236a30ab90f45" @@ -151,6 +146,26 @@ "ImportPath": "github.com/stretchr/testify/require", "Rev": "8ce79b9f0b77745113f82c17d0756771456ccbd3" }, + { + "ImportPath": "golang.org/x/net/context", + "Rev": "7dbad50ab5b31073856416cdcfeb2796d682f844" + }, + { + "ImportPath": "golang.org/x/oauth2", + "Rev": "ca8a464d23d55afd32571475db223e065ffd8a52" + }, + { + "ImportPath": "golang.org/x/oauth2/internal", + "Rev": "ca8a464d23d55afd32571475db223e065ffd8a52" + }, + { + "ImportPath": "golang.org/x/oauth2/jwt", + "Rev": "ca8a464d23d55afd32571475db223e065ffd8a52" + }, + { + "ImportPath": "golang.org/x/oauth2/jws", + "Rev": "ca8a464d23d55afd32571475db223e065ffd8a52" + }, { "ImportPath": "google.golang.org/cloud/internal", "Rev": "542bfb014d8e28df6e27be847dfdc40c510dab1a" diff --git a/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/example/oauthreq.go b/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/example/oauthreq.go deleted file mode 100644 index f9651bd0..00000000 --- a/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/example/oauthreq.go +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2011 The goauth2 Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// This program makes a call to the specified API, authenticated with OAuth2. -// a list of example APIs can be found at https://code.google.com/oauthplayground/ -package main - -import ( - "flag" - "fmt" - "io" - "log" - "os" - - "code.google.com/p/goauth2/oauth" -) - -var ( - clientId = flag.String("id", "", "Client ID") - clientSecret = flag.String("secret", "", "Client Secret") - scope = flag.String("scope", "https://www.googleapis.com/auth/userinfo.profile", "OAuth scope") - redirectURL = flag.String("redirect_url", "oob", "Redirect URL") - authURL = flag.String("auth_url", "https://accounts.google.com/o/oauth2/auth", "Authentication URL") - tokenURL = flag.String("token_url", "https://accounts.google.com/o/oauth2/token", "Token URL") - requestURL = flag.String("request_url", "https://www.googleapis.com/oauth2/v1/userinfo", "API request") - code = flag.String("code", "", "Authorization Code") - cachefile = flag.String("cache", "cache.json", "Token cache file") -) - -const usageMsg = ` -To obtain a request token you must specify both -id and -secret. - -To obtain Client ID and Secret, see the "OAuth 2 Credentials" section under -the "API Access" tab on this page: https://code.google.com/apis/console/ - -Once you have completed the OAuth flow, the credentials should be stored inside -the file specified by -cache and you may run without the -id and -secret flags. -` - -func main() { - flag.Parse() - - // Set up a configuration. - config := &oauth.Config{ - ClientId: *clientId, - ClientSecret: *clientSecret, - RedirectURL: *redirectURL, - Scope: *scope, - AuthURL: *authURL, - TokenURL: *tokenURL, - TokenCache: oauth.CacheFile(*cachefile), - } - - // Set up a Transport using the config. - transport := &oauth.Transport{Config: config} - - // Try to pull the token from the cache; if this fails, we need to get one. - token, err := config.TokenCache.Token() - if err != nil { - if *clientId == "" || *clientSecret == "" { - flag.Usage() - fmt.Fprint(os.Stderr, usageMsg) - os.Exit(2) - } - if *code == "" { - // Get an authorization code from the data provider. - // ("Please ask the user if I can access this resource.") - url := config.AuthCodeURL("") - fmt.Print("Visit this URL to get a code, then run again with -code=YOUR_CODE\n\n") - fmt.Println(url) - return - } - // Exchange the authorization code for an access token. - // ("Here's the code you gave the user, now give me a token!") - token, err = transport.Exchange(*code) - if err != nil { - log.Fatal("Exchange:", err) - } - // (The Exchange method will automatically cache the token.) - fmt.Printf("Token is cached in %v\n", config.TokenCache) - } - - // Make the actual request using the cached token to authenticate. - // ("Here's the token, let me in!") - transport.Token = token - - // Make the request. - r, err := transport.Client().Get(*requestURL) - if err != nil { - log.Fatal("Get:", err) - } - defer r.Body.Close() - - // Write the response to standard output. - io.Copy(os.Stdout, r.Body) - - // Send final carriage return, just to be neat. - fmt.Println() -} diff --git a/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/jwt/example/example.client_secrets.json b/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/jwt/example/example.client_secrets.json deleted file mode 100644 index 2ea86f2f..00000000 --- a/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/jwt/example/example.client_secrets.json +++ /dev/null @@ -1 +0,0 @@ -{"web":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","client_email":"XXXXXXXXXXXX@developer.gserviceaccount.com","client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/XXXXXXXXXXXX@developer.gserviceaccount.com","client_id":"XXXXXXXXXXXX.apps.googleusercontent.com","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"}} diff --git a/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/jwt/example/example.pem b/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/jwt/example/example.pem deleted file mode 100644 index 8f78b922..00000000 --- a/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/jwt/example/example.pem +++ /dev/null @@ -1,20 +0,0 @@ -Bag Attributes - friendlyName: privatekey - localKeyID: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 -Key Attributes: ------BEGIN PRIVATE KEY----- -XXXXxyXXXXXXXxxyxxxX9y0XXYXXXXYXXxXyxxXxXxXXXyXXXXx4yx1xy1xyYxxY -1XxYy38YxXxxxyXxyyxx+xxxxyx1Y1xYx7yx2/Y1XyyXYYYxY5YXxX0xY/Y642yX -zYYxYXzXYxY0Y8y9YxyYXxxX40YyXxxXX4XXxx7XxXxxXyXxYYXxXyxX5XY0Yy2X -1YX0XXyy6YXyXx9XxXxyXX9XXYXxXxXXXXXXxYXYY3Y8Yy311XYYY81XyY14Xyyx -xXyx7xxXXXxxxxyyyX4YYYXyYyYXyxX4XYXYyxXYyx9xy23xXYyXyxYxXxx1XXXY -y98yX6yYxyyyX4Xyx1Xy/0yxxYxXxYYx2xx7yYXXXxYXXXxyXyyYYxx5XX2xxyxy -y6Yyyx0XX3YYYyx9YYXXXX7y0yxXXy+90XYz1y2xyx7yXxX+8X0xYxXXYxxyxYYy -YXx8Yy4yX0Xyxxx6yYX92yxy1YYYzyyyyxy55x/yyXXXYYXYXXzXXxYYxyXY8XXX -+y9+yXxX7XxxyYYxxXYxyY623xxXxYX59x5Y6yYyXYY4YxXXYXXXYxXYxXxXXx6x -YXX7XxXX2X0XY7YXyYy1XXxYXxXxYY1xXXxxxyy+07zXYxYxxXyyxxyxXx1XYy5X -5XYzyxYxXXYyX9XX7xX8xXxx+XXYyYXXXX5YY1x8Yxyx54Xy/1XXyyYXY5YxYyxY -XyyxXyX/YxxXXXxXXYXxyxx63xX/xxyYXXyYzx0XY+YxX5xyYyyxxxXXYX/94XXy -Xx63xYxXyXY3/XXxyyXX15XXXyz08XYY5YYXY/YXy/96x68XyyXXxYyXy4xYXx5x -7yxxyxxYxXxyx3y= ------END PRIVATE KEY----- diff --git a/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/jwt/example/main.go b/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/jwt/example/main.go deleted file mode 100644 index 2256e9c6..00000000 --- a/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/jwt/example/main.go +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2011 The goauth2 Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// This program makes a read only call to the Google Cloud Storage API, -// authenticated with OAuth2. A list of example APIs can be found at -// https://code.google.com/oauthplayground/ -package main - -import ( - "encoding/json" - "flag" - "fmt" - "io/ioutil" - "log" - "net/http" - "strings" - - "code.google.com/p/goauth2/oauth/jwt" -) - -const scope = "https://www.googleapis.com/auth/devstorage.read_only" - -var ( - secretsFile = flag.String("s", "", "JSON encoded secrets for the service account") - pemFile = flag.String("k", "", "private pem key file for the service account") -) - -const usageMsg = ` -You must specify -k and -s. - -To obtain client secrets and pem, see the "OAuth 2 Credentials" section under -the "API Access" tab on this page: https://code.google.com/apis/console/ - -Google Cloud Storage must also be turned on in the API console. -` - -func main() { - flag.Parse() - - if *secretsFile == "" || *pemFile == "" { - flag.Usage() - fmt.Println(usageMsg) - return - } - - // Read the secret file bytes into the config. - secretBytes, err := ioutil.ReadFile(*secretsFile) - if err != nil { - log.Fatal("error reading secerets file:", err) - } - var config struct { - Web struct { - ClientEmail string `json:"client_email"` - ClientID string `json:"client_id"` - TokenURI string `json:"token_uri"` - } - } - err = json.Unmarshal(secretBytes, &config) - if err != nil { - log.Fatal("error unmarshalling secerets:", err) - } - - // Get the project ID from the client ID. - projectID := strings.SplitN(config.Web.ClientID, "-", 2)[0] - - // Read the pem file bytes for the private key. - keyBytes, err := ioutil.ReadFile(*pemFile) - if err != nil { - log.Fatal("error reading private key file:", err) - } - - // Craft the ClaimSet and JWT token. - t := jwt.NewToken(config.Web.ClientEmail, scope, keyBytes) - t.ClaimSet.Aud = config.Web.TokenURI - - // We need to provide a client. - c := &http.Client{} - - // Get the access token. - o, err := t.Assert(c) - if err != nil { - log.Fatal("assertion error:", err) - } - - // Refresh token will be missing, but this access_token will be good - // for one hour. - fmt.Printf("access_token = %v\n", o.AccessToken) - fmt.Printf("refresh_token = %v\n", o.RefreshToken) - fmt.Printf("expires %v\n", o.Expiry) - - // Form the request to list Google Cloud Storage buckets. - req, err := http.NewRequest("GET", "https://storage.googleapis.com/", nil) - if err != nil { - log.Fatal("http.NewRequest:", err) - } - req.Header.Set("Authorization", "OAuth "+o.AccessToken) - req.Header.Set("x-goog-api-version", "2") - req.Header.Set("x-goog-project-id", projectID) - - // Make the request. - r, err := c.Do(req) - if err != nil { - log.Fatal("API request error:", err) - } - defer r.Body.Close() - - // Write the response to standard output. - res, err := ioutil.ReadAll(r.Body) - if err != nil { - log.Fatal("error reading API request results:", err) - } - fmt.Printf("\nRESULT:\n%s\n", res) -} diff --git a/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/jwt/jwt.go b/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/jwt/jwt.go deleted file mode 100644 index 61bf5ce9..00000000 --- a/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/jwt/jwt.go +++ /dev/null @@ -1,511 +0,0 @@ -// Copyright 2012 The goauth2 Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// The jwt package provides support for creating credentials for OAuth2 service -// account requests. -// -// For examples of the package usage please see jwt_test.go. -// Example usage (error handling omitted for brevity): -// -// // Craft the ClaimSet and JWT token. -// iss := "XXXXXXXXXXXX@developer.gserviceaccount.com" -// scope := "https://www.googleapis.com/auth/devstorage.read_only" -// t := jwt.NewToken(iss, scope, pemKeyBytes) -// -// // We need to provide a client. -// c := &http.Client{} -// -// // Get the access token. -// o, _ := t.Assert(c) -// -// // Form the request to the service. -// req, _ := http.NewRequest("GET", "https://storage.googleapis.com/", nil) -// req.Header.Set("Authorization", "OAuth "+o.AccessToken) -// req.Header.Set("x-goog-api-version", "2") -// req.Header.Set("x-goog-project-id", "XXXXXXXXXXXX") -// -// // Make the request. -// result, _ := c.Do(req) -// -// For info on OAuth2 service accounts please see the online documentation. -// https://developers.google.com/accounts/docs/OAuth2ServiceAccount -// -package jwt - -import ( - "bytes" - "crypto" - "crypto/rand" - "crypto/rsa" - "crypto/sha256" - "crypto/x509" - "encoding/base64" - "encoding/json" - "encoding/pem" - "errors" - "fmt" - "net/http" - "net/url" - "strings" - "time" - - "code.google.com/p/goauth2/oauth" -) - -// These are the default/standard values for this to work for Google service accounts. -const ( - stdAlgorithm = "RS256" - stdType = "JWT" - stdAssertionType = "http://oauth.net/grant_type/jwt/1.0/bearer" - stdGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" - stdAud = "https://accounts.google.com/o/oauth2/token" -) - -var ( - ErrInvalidKey = errors.New("Invalid Key") -) - -// base64Encode returns and Base64url encoded version of the input string with any -// trailing "=" stripped. -func base64Encode(b []byte) string { - return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=") -} - -// base64Decode decodes the Base64url encoded string -func base64Decode(s string) ([]byte, error) { - // add back missing padding - switch len(s) % 4 { - case 2: - s += "==" - case 3: - s += "=" - } - return base64.URLEncoding.DecodeString(s) -} - -// The JWT claim set contains information about the JWT including the -// permissions being requested (scopes), the target of the token, the issuer, -// the time the token was issued, and the lifetime of the token. -// -// Aud is usually https://accounts.google.com/o/oauth2/token -type ClaimSet struct { - Iss string `json:"iss"` // email address of the client_id of the application making the access token request - Scope string `json:"scope,omitempty"` // space-delimited list of the permissions the application requests - Aud string `json:"aud"` // descriptor of the intended target of the assertion (Optional). - Prn string `json:"prn,omitempty"` // email for which the application is requesting delegated access (Optional). - Exp int64 `json:"exp"` - Iat int64 `json:"iat"` - Typ string `json:"typ,omitempty"` - Sub string `json:"sub,omitempty"` // Add support for googleapi delegation support - - // See http://tools.ietf.org/html/draft-jones-json-web-token-10#section-4.3 - // This array is marshalled using custom code (see (c *ClaimSet) encode()). - PrivateClaims map[string]interface{} `json:"-"` - - exp time.Time - iat time.Time -} - -// setTimes sets iat and exp to time.Now() and iat.Add(time.Hour) respectively. -// -// Note that these times have nothing to do with the expiration time for the -// access_token returned by the server. These have to do with the lifetime of -// the encoded JWT. -// -// A JWT can be re-used for up to one hour after it was encoded. The access -// token that is granted will also be good for one hour so there is little point -// in trying to use the JWT a second time. -func (c *ClaimSet) setTimes(t time.Time) { - c.iat = t - c.exp = c.iat.Add(time.Hour) -} - -var ( - jsonStart = []byte{'{'} - jsonEnd = []byte{'}'} -) - -// encode returns the Base64url encoded form of the Signature. -func (c *ClaimSet) encode() string { - if c.exp.IsZero() || c.iat.IsZero() { - c.setTimes(time.Now()) - } - if c.Aud == "" { - c.Aud = stdAud - } - c.Exp = c.exp.Unix() - c.Iat = c.iat.Unix() - - b, err := json.Marshal(c) - if err != nil { - panic(err) - } - - if len(c.PrivateClaims) == 0 { - return base64Encode(b) - } - - // Marshal private claim set and then append it to b. - prv, err := json.Marshal(c.PrivateClaims) - if err != nil { - panic(fmt.Errorf("Invalid map of private claims %v", c.PrivateClaims)) - } - - // Concatenate public and private claim JSON objects. - if !bytes.HasSuffix(b, jsonEnd) { - panic(fmt.Errorf("Invalid JSON %s", b)) - } - if !bytes.HasPrefix(prv, jsonStart) { - panic(fmt.Errorf("Invalid JSON %s", prv)) - } - b[len(b)-1] = ',' // Replace closing curly brace with a comma. - b = append(b, prv[1:]...) // Append private claims. - - return base64Encode(b) -} - -// Header describes the algorithm and type of token being generated, -// and optionally a KeyID describing additional parameters for the -// signature. -type Header struct { - Algorithm string `json:"alg"` - Type string `json:"typ"` - KeyId string `json:"kid,omitempty"` -} - -func (h *Header) encode() string { - b, err := json.Marshal(h) - if err != nil { - panic(err) - } - return base64Encode(b) -} - -// A JWT is composed of three parts: a header, a claim set, and a signature. -// The well formed and encoded JWT can then be exchanged for an access token. -// -// The Token is not a JWT, but is is encoded to produce a well formed JWT. -// -// When obtaining a key from the Google API console it will be downloaded in a -// PKCS12 encoding. To use this key you will need to convert it to a PEM file. -// This can be achieved with openssl. -// -// $ openssl pkcs12 -in -nocerts -passin pass:notasecret -nodes -out -// -// The contents of this file can then be used as the Key. -type Token struct { - ClaimSet *ClaimSet // claim set used to construct the JWT - Header *Header // header used to construct the JWT - Key []byte // PEM printable encoding of the private key - pKey *rsa.PrivateKey - - header string - claim string - sig string - - useExternalSigner bool - signer Signer -} - -// NewToken returns a filled in *Token based on the standard header, -// and sets the Iat and Exp times based on when the call to Assert is -// made. -func NewToken(iss, scope string, key []byte) *Token { - c := &ClaimSet{ - Iss: iss, - Scope: scope, - Aud: stdAud, - } - h := &Header{ - Algorithm: stdAlgorithm, - Type: stdType, - } - t := &Token{ - ClaimSet: c, - Header: h, - Key: key, - } - return t -} - -// Signer is an interface that given a JWT token, returns the header & -// claim (serialized and urlEncoded to a byte slice), along with the -// signature and an error (if any occured). It could modify any data -// to sign (typically the KeyID). -// -// Example usage where a SHA256 hash of the original url-encoded token -// with an added KeyID and secret data is used as a signature: -// -// var privateData = "secret data added to hash, indexed by KeyID" -// -// type SigningService struct{} -// -// func (ss *SigningService) Sign(in *jwt.Token) (newTokenData, sig []byte, err error) { -// in.Header.KeyID = "signing service" -// newTokenData = in.EncodeWithoutSignature() -// dataToSign := fmt.Sprintf("%s.%s", newTokenData, privateData) -// h := sha256.New() -// _, err := h.Write([]byte(dataToSign)) -// sig = h.Sum(nil) -// return -// } -type Signer interface { - Sign(in *Token) (tokenData, signature []byte, err error) -} - -// NewSignerToken returns a *Token, using an external signer function -func NewSignerToken(iss, scope string, signer Signer) *Token { - t := NewToken(iss, scope, nil) - t.useExternalSigner = true - t.signer = signer - return t -} - -// Expired returns a boolean value letting us know if the token has expired. -func (t *Token) Expired() bool { - return t.ClaimSet.exp.Before(time.Now()) -} - -// Encode constructs and signs a Token returning a JWT ready to use for -// requesting an access token. -func (t *Token) Encode() (string, error) { - var tok string - t.header = t.Header.encode() - t.claim = t.ClaimSet.encode() - err := t.sign() - if err != nil { - return tok, err - } - tok = fmt.Sprintf("%s.%s.%s", t.header, t.claim, t.sig) - return tok, nil -} - -// EncodeWithoutSignature returns the url-encoded value of the Token -// before signing has occured (typically for use by external signers). -func (t *Token) EncodeWithoutSignature() string { - t.header = t.Header.encode() - t.claim = t.ClaimSet.encode() - return fmt.Sprintf("%s.%s", t.header, t.claim) -} - -// sign computes the signature for a Token. The details for this can be found -// in the OAuth2 Service Account documentation. -// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#computingsignature -func (t *Token) sign() error { - if t.useExternalSigner { - fulldata, sig, err := t.signer.Sign(t) - if err != nil { - return err - } - split := strings.Split(string(fulldata), ".") - if len(split) != 2 { - return errors.New("no token returned") - } - t.header = split[0] - t.claim = split[1] - t.sig = base64Encode(sig) - return err - } - ss := fmt.Sprintf("%s.%s", t.header, t.claim) - if t.pKey == nil { - err := t.parsePrivateKey() - if err != nil { - return err - } - } - h := sha256.New() - h.Write([]byte(ss)) - b, err := rsa.SignPKCS1v15(rand.Reader, t.pKey, crypto.SHA256, h.Sum(nil)) - t.sig = base64Encode(b) - return err -} - -// parsePrivateKey converts the Token's Key ([]byte) into a parsed -// rsa.PrivateKey. If the key is not well formed this method will return an -// ErrInvalidKey error. -func (t *Token) parsePrivateKey() error { - block, _ := pem.Decode(t.Key) - if block == nil { - return ErrInvalidKey - } - parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) - if err != nil { - parsedKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return err - } - } - var ok bool - t.pKey, ok = parsedKey.(*rsa.PrivateKey) - if !ok { - return ErrInvalidKey - } - return nil -} - -// Assert obtains an *oauth.Token from the remote server by encoding and sending -// a JWT. The access_token will expire in one hour (3600 seconds) and cannot be -// refreshed (no refresh_token is returned with the response). Once this token -// expires call this method again to get a fresh one. -func (t *Token) Assert(c *http.Client) (*oauth.Token, error) { - var o *oauth.Token - t.ClaimSet.setTimes(time.Now()) - u, v, err := t.buildRequest() - if err != nil { - return o, err - } - resp, err := c.PostForm(u, v) - if err != nil { - return o, err - } - o, err = handleResponse(resp) - return o, err -} - -// buildRequest sets up the URL values and the proper URL string for making our -// access_token request. -func (t *Token) buildRequest() (string, url.Values, error) { - v := url.Values{} - j, err := t.Encode() - if err != nil { - return t.ClaimSet.Aud, v, err - } - v.Set("grant_type", stdGrantType) - v.Set("assertion", j) - return t.ClaimSet.Aud, v, nil -} - -// Used for decoding the response body. -type respBody struct { - IdToken string `json:"id_token"` - Access string `json:"access_token"` - Type string `json:"token_type"` - ExpiresIn time.Duration `json:"expires_in"` -} - -// handleResponse returns a filled in *oauth.Token given the *http.Response from -// a *http.Request created by buildRequest. -func handleResponse(r *http.Response) (*oauth.Token, error) { - o := &oauth.Token{} - defer r.Body.Close() - if r.StatusCode != 200 { - return o, errors.New("invalid response: " + r.Status) - } - b := &respBody{} - err := json.NewDecoder(r.Body).Decode(b) - if err != nil { - return o, err - } - o.AccessToken = b.Access - if b.IdToken != "" { - // decode returned id token to get expiry - o.AccessToken = b.IdToken - s := strings.Split(b.IdToken, ".") - if len(s) < 2 { - return nil, errors.New("invalid token received") - } - d, err := base64Decode(s[1]) - if err != nil { - return o, err - } - c := &ClaimSet{} - err = json.NewDecoder(bytes.NewBuffer(d)).Decode(c) - if err != nil { - return o, err - } - o.Expiry = time.Unix(c.Exp, 0) - return o, nil - } - o.Expiry = time.Now().Add(b.ExpiresIn * time.Second) - return o, nil -} - -// Transport implements http.RoundTripper. When configured with a valid -// JWT and OAuth tokens it can be used to make authenticated HTTP requests. -// -// t := &jwt.Transport{jwtToken, oauthToken} -// r, _, err := t.Client().Get("http://example.org/url/requiring/auth") -// -// It will automatically refresh the OAuth token if it can, updating in place. -type Transport struct { - JWTToken *Token - OAuthToken *oauth.Token - - // Transport is the HTTP transport to use when making requests. - // It will default to http.DefaultTransport if nil. - Transport http.RoundTripper -} - -// Creates a new authenticated transport. -func NewTransport(token *Token) (*Transport, error) { - oa, err := token.Assert(new(http.Client)) - if err != nil { - return nil, err - } - return &Transport{ - JWTToken: token, - OAuthToken: oa, - }, nil -} - -// Client returns an *http.Client that makes OAuth-authenticated requests. -func (t *Transport) Client() *http.Client { - return &http.Client{Transport: t} -} - -// Fetches the internal transport. -func (t *Transport) transport() http.RoundTripper { - if t.Transport != nil { - return t.Transport - } - return http.DefaultTransport -} - -// RoundTrip executes a single HTTP transaction using the Transport's -// OAuthToken as authorization headers. -// -// This method will attempt to renew the token if it has expired and may return -// an error related to that token renewal before attempting the client request. -// If the token cannot be renewed a non-nil os.Error value will be returned. -// If the token is invalid callers should expect HTTP-level errors, -// as indicated by the Response's StatusCode. -func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { - // Sanity check the two tokens - if t.JWTToken == nil { - return nil, fmt.Errorf("no JWT token supplied") - } - if t.OAuthToken == nil { - return nil, fmt.Errorf("no OAuth token supplied") - } - // Refresh the OAuth token if it has expired - if t.OAuthToken.Expired() { - if oa, err := t.JWTToken.Assert(new(http.Client)); err != nil { - return nil, err - } else { - t.OAuthToken = oa - } - } - // To set the Authorization header, we must make a copy of the Request - // so that we don't modify the Request we were given. - // This is required by the specification of http.RoundTripper. - req = cloneRequest(req) - req.Header.Set("Authorization", "Bearer "+t.OAuthToken.AccessToken) - - // Make the HTTP request. - return t.transport().RoundTrip(req) -} - -// cloneRequest returns a clone of the provided *http.Request. -// The clone is a shallow copy of the struct and its Header map. -func cloneRequest(r *http.Request) *http.Request { - // shallow copy of the struct - r2 := new(http.Request) - *r2 = *r - // deep copy of the Header - r2.Header = make(http.Header) - for k, s := range r.Header { - r2.Header[k] = s - } - return r2 -} diff --git a/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/jwt/jwt_test.go b/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/jwt/jwt_test.go deleted file mode 100644 index 622843e1..00000000 --- a/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/jwt/jwt_test.go +++ /dev/null @@ -1,486 +0,0 @@ -// Copyright 2012 The goauth2 Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// For package documentation please see jwt.go. -// -package jwt - -import ( - "bytes" - "crypto" - "crypto/rand" - "crypto/rsa" - "crypto/sha256" - "crypto/x509" - "encoding/json" - "encoding/pem" - "io/ioutil" - "net/http" - "testing" - "time" -) - -const ( - stdHeaderStr = `{"alg":"RS256","typ":"JWT"}` - iss = "761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com" - scope = "https://www.googleapis.com/auth/prediction" - exp = 1328554385 - iat = 1328550785 // exp + 1 hour -) - -// Base64url encoded Header -const headerEnc = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9" - -// Base64url encoded ClaimSet -const claimSetEnc = "eyJpc3MiOiI3NjEzMjY3OTgwNjktcjVtbGpsbG4xcmQ0bHJiaGc3NWVmZ2lncDM2bTc4ajVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvcHJlZGljdGlvbiIsImF1ZCI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsImV4cCI6MTMyODU1NDM4NSwiaWF0IjoxMzI4NTUwNzg1fQ" - -// Base64url encoded Signature -const sigEnc = "olukbHreNiYrgiGCTEmY3eWGeTvYDSUHYoE84Jz3BRPBSaMdZMNOn_0CYK7UHPO7OdvUofjwft1dH59UxE9GWS02pjFti1uAQoImaqjLZoTXr8qiF6O_kDa9JNoykklWlRAIwGIZkDupCS-8cTAnM_ksSymiH1coKJrLDUX_BM0x2f4iMFQzhL5vT1ll-ZipJ0lNlxb5QsyXxDYcxtHYguF12-vpv3ItgT0STfcXoWzIGQoEbhwB9SBp9JYcQ8Ygz6pYDjm0rWX9LrchmTyDArCodpKLFtutNgcIFUP9fWxvwd1C2dNw5GjLcKr9a_SAERyoJ2WnCR1_j9N0wD2o0g" - -// Base64url encoded Token -const tokEnc = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI3NjEzMjY3OTgwNjktcjVtbGpsbG4xcmQ0bHJiaGc3NWVmZ2lncDM2bTc4ajVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvcHJlZGljdGlvbiIsImF1ZCI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsImV4cCI6MTMyODU1NDM4NSwiaWF0IjoxMzI4NTUwNzg1fQ.olukbHreNiYrgiGCTEmY3eWGeTvYDSUHYoE84Jz3BRPBSaMdZMNOn_0CYK7UHPO7OdvUofjwft1dH59UxE9GWS02pjFti1uAQoImaqjLZoTXr8qiF6O_kDa9JNoykklWlRAIwGIZkDupCS-8cTAnM_ksSymiH1coKJrLDUX_BM0x2f4iMFQzhL5vT1ll-ZipJ0lNlxb5QsyXxDYcxtHYguF12-vpv3ItgT0STfcXoWzIGQoEbhwB9SBp9JYcQ8Ygz6pYDjm0rWX9LrchmTyDArCodpKLFtutNgcIFUP9fWxvwd1C2dNw5GjLcKr9a_SAERyoJ2WnCR1_j9N0wD2o0g" - -// Private key for testing -const privateKeyPem = `-----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj -7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/ -xmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs -SliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18 -pe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk -SBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk -nQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq -HD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y -nHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9 -IisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2 -YCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU -Z422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ -vzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP -B8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl -aLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2 -eCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI -aqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk -klORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ -CFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu -UqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg -soBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28 -bvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH -504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL -YXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx -BeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg== ------END RSA PRIVATE KEY-----` - -// Public key to go with the private key for testing -const publicKeyPem = `-----BEGIN CERTIFICATE----- -MIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV -BAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV -MRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB -CgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM -7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer -uQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp -gyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4 -+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3 -ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O -gN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh -GaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD -AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr -odJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk -+JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9 -ovNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql -ybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT -cDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB ------END CERTIFICATE-----` - -var ( - privateKeyPemBytes = []byte(privateKeyPem) - publicKeyPemBytes = []byte(publicKeyPem) - stdHeader = &Header{Algorithm: stdAlgorithm, Type: stdType} -) - -// Testing the urlEncode function. -func TestUrlEncode(t *testing.T) { - enc := base64Encode([]byte(stdHeaderStr)) - b := []byte(enc) - if b[len(b)-1] == 61 { - t.Error("TestUrlEncode: last chat == \"=\"") - } - if enc != headerEnc { - t.Error("TestUrlEncode: enc != headerEnc") - t.Errorf(" enc = %s", enc) - t.Errorf(" headerEnc = %s", headerEnc) - } -} - -// Test that the times are set properly. -func TestClaimSetSetTimes(t *testing.T) { - c := &ClaimSet{ - Iss: iss, - Scope: scope, - } - iat := time.Unix(iat, 0) - c.setTimes(iat) - if c.exp.Unix() != exp { - t.Error("TestClaimSetSetTimes: c.exp != exp") - t.Errorf(" c.Exp = %d", c.exp.Unix()) - t.Errorf(" exp = %d", exp) - } -} - -// Given a well formed ClaimSet, test for proper encoding. -func TestClaimSetEncode(t *testing.T) { - c := &ClaimSet{ - Iss: iss, - Scope: scope, - exp: time.Unix(exp, 0), - iat: time.Unix(iat, 0), - } - enc := c.encode() - re, err := base64Decode(enc) - if err != nil { - t.Fatalf("error decoding encoded claim set: %v", err) - } - - wa, err := base64Decode(claimSetEnc) - if err != nil { - t.Fatalf("error decoding encoded expected claim set: %v", err) - } - - if enc != claimSetEnc { - t.Error("TestClaimSetEncode: enc != claimSetEnc") - t.Errorf(" enc = %s", string(re)) - t.Errorf(" claimSetEnc = %s", string(wa)) - } -} - -// Test that claim sets with private claim names are encoded correctly. -func TestClaimSetWithPrivateNameEncode(t *testing.T) { - iatT := time.Unix(iat, 0) - expT := time.Unix(exp, 0) - - i, err := json.Marshal(iatT.Unix()) - if err != nil { - t.Fatalf("error marshaling iatT value of %v: %v", iatT.Unix(), err) - } - iatStr := string(i) - e, err := json.Marshal(expT.Unix()) - if err != nil { - t.Fatalf("error marshaling expT value of %v: %v", expT.Unix(), err) - } - - expStr := string(e) - - testCases := []struct { - desc string - input map[string]interface{} - want string - }{ - // Test a simple int field. - { - "single simple field", - map[string]interface{}{"amount": 22}, - `{` + - `"iss":"` + iss + `",` + - `"scope":"` + scope + `",` + - `"aud":"` + stdAud + `",` + - `"exp":` + expStr + `,` + - `"iat":` + iatStr + `,` + - `"amount":22` + - `}`, - }, - { - "multiple simple fields", - map[string]interface{}{"tracking_code": "axZf", "amount": 22}, - `{` + - `"iss":"` + iss + `",` + - `"scope":"` + scope + `",` + - `"aud":"` + stdAud + `",` + - `"exp":` + expStr + `,` + - `"iat":` + iatStr + `,` + - `"amount":22,` + - `"tracking_code":"axZf"` + - `}`, - }, - { - "nested struct fields", - map[string]interface{}{ - "tracking_code": "axZf", - "purchase": struct { - Description string `json:"desc"` - Quantity int32 `json:"q"` - Time int64 `json:"t"` - }{ - "toaster", - 5, - iat, - }, - }, - `{` + - `"iss":"` + iss + `",` + - `"scope":"` + scope + `",` + - `"aud":"` + stdAud + `",` + - `"exp":` + expStr + `,` + - `"iat":` + iatStr + `,` + - `"purchase":{"desc":"toaster","q":5,"t":` + iatStr + `},` + - `"tracking_code":"axZf"` + - `}`, - }, - } - - for _, testCase := range testCases { - c := &ClaimSet{ - Iss: iss, - Scope: scope, - Aud: stdAud, - iat: iatT, - exp: expT, - PrivateClaims: testCase.input, - } - cJSON, err := base64Decode(c.encode()) - if err != nil { - t.Fatalf("error decoding claim set: %v", err) - } - if string(cJSON) != testCase.want { - t.Errorf("TestClaimSetWithPrivateNameEncode: enc != want in case %s", testCase.desc) - t.Errorf(" enc = %s", cJSON) - t.Errorf(" want = %s", testCase.want) - } - } -} - -// Test the NewToken constructor. -func TestNewToken(t *testing.T) { - tok := NewToken(iss, scope, privateKeyPemBytes) - if tok.ClaimSet.Iss != iss { - t.Error("TestNewToken: tok.ClaimSet.Iss != iss") - t.Errorf(" tok.ClaimSet.Iss = %s", tok.ClaimSet.Iss) - t.Errorf(" iss = %s", iss) - } - if tok.ClaimSet.Scope != scope { - t.Error("TestNewToken: tok.ClaimSet.Scope != scope") - t.Errorf(" tok.ClaimSet.Scope = %s", tok.ClaimSet.Scope) - t.Errorf(" scope = %s", scope) - } - if tok.ClaimSet.Aud != stdAud { - t.Error("TestNewToken: tok.ClaimSet.Aud != stdAud") - t.Errorf(" tok.ClaimSet.Aud = %s", tok.ClaimSet.Aud) - t.Errorf(" stdAud = %s", stdAud) - } - if !bytes.Equal(tok.Key, privateKeyPemBytes) { - t.Error("TestNewToken: tok.Key != privateKeyPemBytes") - t.Errorf(" tok.Key = %s", tok.Key) - t.Errorf(" privateKeyPemBytes = %s", privateKeyPemBytes) - } -} - -// Make sure the private key parsing functions work. -func TestParsePrivateKey(t *testing.T) { - tok := &Token{ - Key: privateKeyPemBytes, - } - err := tok.parsePrivateKey() - if err != nil { - t.Errorf("TestParsePrivateKey:tok.parsePrivateKey: %v", err) - } -} - -// Test that the token signature generated matches the golden standard. -func TestTokenSign(t *testing.T) { - tok := &Token{ - Key: privateKeyPemBytes, - claim: claimSetEnc, - header: headerEnc, - } - err := tok.parsePrivateKey() - if err != nil { - t.Errorf("TestTokenSign:tok.parsePrivateKey: %v", err) - } - err = tok.sign() - if err != nil { - t.Errorf("TestTokenSign:tok.sign: %v", err) - } - if tok.sig != sigEnc { - t.Error("TestTokenSign: tok.sig != sigEnc") - t.Errorf(" tok.sig = %s", tok.sig) - t.Errorf(" sigEnc = %s", sigEnc) - } -} - -// Test that the token expiration function is working. -func TestTokenExpired(t *testing.T) { - c := &ClaimSet{} - tok := &Token{ - ClaimSet: c, - } - now := time.Now() - c.setTimes(now) - if tok.Expired() != false { - t.Error("TestTokenExpired: tok.Expired != false") - } - // Set the times as if they were set 2 hours ago. - c.setTimes(now.Add(-2 * time.Hour)) - if tok.Expired() != true { - t.Error("TestTokenExpired: tok.Expired != true") - } -} - -// Given a well formed Token, test for proper encoding. -func TestTokenEncode(t *testing.T) { - c := &ClaimSet{ - Iss: iss, - Scope: scope, - exp: time.Unix(exp, 0), - iat: time.Unix(iat, 0), - } - tok := &Token{ - ClaimSet: c, - Header: stdHeader, - Key: privateKeyPemBytes, - } - enc, err := tok.Encode() - if err != nil { - t.Errorf("TestTokenEncode:tok.Assertion: %v", err) - } - if enc != tokEnc { - t.Error("TestTokenEncode: enc != tokEnc") - t.Errorf(" enc = %s", enc) - t.Errorf(" tokEnc = %s", tokEnc) - } -} - -// Given a well formed Token we should get back a well formed request. -func TestBuildRequest(t *testing.T) { - c := &ClaimSet{ - Iss: iss, - Scope: scope, - exp: time.Unix(exp, 0), - iat: time.Unix(iat, 0), - } - tok := &Token{ - ClaimSet: c, - Header: stdHeader, - Key: privateKeyPemBytes, - } - u, v, err := tok.buildRequest() - if err != nil { - t.Errorf("TestBuildRequest:BuildRequest: %v", err) - } - if u != c.Aud { - t.Error("TestBuildRequest: u != c.Aud") - t.Errorf(" u = %s", u) - t.Errorf(" c.Aud = %s", c.Aud) - } - if v.Get("grant_type") != stdGrantType { - t.Error("TestBuildRequest: grant_type != stdGrantType") - t.Errorf(" grant_type = %s", v.Get("grant_type")) - t.Errorf(" stdGrantType = %s", stdGrantType) - } - if v.Get("assertion") != tokEnc { - t.Error("TestBuildRequest: assertion != tokEnc") - t.Errorf(" assertion = %s", v.Get("assertion")) - t.Errorf(" tokEnc = %s", tokEnc) - } -} - -// Given a well formed access request response we should get back a oauth.Token. -func TestHandleResponse(t *testing.T) { - rb := &respBody{ - Access: "1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M", - Type: "Bearer", - ExpiresIn: 3600, - } - b, err := json.Marshal(rb) - if err != nil { - t.Errorf("TestHandleResponse:json.Marshal: %v", err) - } - r := &http.Response{ - Status: "200 OK", - StatusCode: 200, - Body: ioutil.NopCloser(bytes.NewReader(b)), - } - o, err := handleResponse(r) - if err != nil { - t.Errorf("TestHandleResponse:handleResponse: %v", err) - } - if o.AccessToken != rb.Access { - t.Error("TestHandleResponse: o.AccessToken != rb.Access") - t.Errorf(" o.AccessToken = %s", o.AccessToken) - t.Errorf(" rb.Access = %s", rb.Access) - } - if o.Expired() { - t.Error("TestHandleResponse: o.Expired == true") - } -} - -// passthrough signature for test -type FakeSigner struct{} - -func (f FakeSigner) Sign(tok *Token) ([]byte, []byte, error) { - block, _ := pem.Decode(privateKeyPemBytes) - pKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes) - ss := headerEnc + "." + claimSetEnc - h := sha256.New() - h.Write([]byte(ss)) - b, _ := rsa.SignPKCS1v15(rand.Reader, pKey, crypto.SHA256, h.Sum(nil)) - return []byte(ss), b, nil -} - -// Given an external signer, get back a valid and signed JWT -func TestExternalSigner(t *testing.T) { - tok := NewSignerToken(iss, scope, FakeSigner{}) - enc, _ := tok.Encode() - if enc != tokEnc { - t.Errorf("TestExternalSigner: enc != tokEnc") - t.Errorf(" enc = %s", enc) - t.Errorf(" tokEnc = %s", tokEnc) - } -} - -func TestHandleResponseWithNewExpiry(t *testing.T) { - rb := &respBody{ - IdToken: tokEnc, - } - b, err := json.Marshal(rb) - if err != nil { - t.Errorf("TestHandleResponse:json.Marshal: %v", err) - } - r := &http.Response{ - Status: "200 OK", - StatusCode: 200, - Body: ioutil.NopCloser(bytes.NewReader(b)), - } - o, err := handleResponse(r) - if err != nil { - t.Errorf("TestHandleResponse:handleResponse: %v", err) - } - if o.Expiry != time.Unix(exp, 0) { - t.Error("TestHandleResponse: o.Expiry != exp") - t.Errorf(" o.Expiry = %s", o.Expiry) - t.Errorf(" exp = %s", time.Unix(exp, 0)) - } -} - -// Placeholder for future Assert tests. -func TestAssert(t *testing.T) { - // Since this method makes a call to BuildRequest, an htttp.Client, and - // finally HandleResponse there is not much more to test. This is here - // as a placeholder if that changes. -} - -// Benchmark for the end-to-end encoding of a well formed token. -func BenchmarkTokenEncode(b *testing.B) { - b.StopTimer() - c := &ClaimSet{ - Iss: iss, - Scope: scope, - exp: time.Unix(exp, 0), - iat: time.Unix(iat, 0), - } - tok := &Token{ - ClaimSet: c, - Key: privateKeyPemBytes, - } - b.StartTimer() - for i := 0; i < b.N; i++ { - tok.Encode() - } -} diff --git a/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/oauth.go b/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/oauth.go deleted file mode 100644 index 6f1b6c18..00000000 --- a/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/oauth.go +++ /dev/null @@ -1,476 +0,0 @@ -// Copyright 2011 The goauth2 Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package oauth supports making OAuth2-authenticated HTTP requests. -// -// Example usage: -// -// // Specify your configuration. (typically as a global variable) -// var config = &oauth.Config{ -// ClientId: YOUR_CLIENT_ID, -// ClientSecret: YOUR_CLIENT_SECRET, -// Scope: "https://www.googleapis.com/auth/buzz", -// AuthURL: "https://accounts.google.com/o/oauth2/auth", -// TokenURL: "https://accounts.google.com/o/oauth2/token", -// RedirectURL: "http://you.example.org/handler", -// } -// -// // A landing page redirects to the OAuth provider to get the auth code. -// func landing(w http.ResponseWriter, r *http.Request) { -// http.Redirect(w, r, config.AuthCodeURL("foo"), http.StatusFound) -// } -// -// // The user will be redirected back to this handler, that takes the -// // "code" query parameter and Exchanges it for an access token. -// func handler(w http.ResponseWriter, r *http.Request) { -// t := &oauth.Transport{Config: config} -// t.Exchange(r.FormValue("code")) -// // The Transport now has a valid Token. Create an *http.Client -// // with which we can make authenticated API requests. -// c := t.Client() -// c.Post(...) -// // ... -// // btw, r.FormValue("state") == "foo" -// } -// -package oauth - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "mime" - "net/http" - "net/url" - "os" - "strconv" - "strings" - "sync" - "time" -) - -// OAuthError is the error type returned by many operations. -// -// In retrospect it should not exist. Don't depend on it. -type OAuthError struct { - prefix string - msg string -} - -func (oe OAuthError) Error() string { - return "OAuthError: " + oe.prefix + ": " + oe.msg -} - -// Cache specifies the methods that implement a Token cache. -type Cache interface { - Token() (*Token, error) - PutToken(*Token) error -} - -// CacheFile implements Cache. Its value is the name of the file in which -// the Token is stored in JSON format. -type CacheFile string - -func (f CacheFile) Token() (*Token, error) { - file, err := os.Open(string(f)) - if err != nil { - return nil, OAuthError{"CacheFile.Token", err.Error()} - } - defer file.Close() - tok := &Token{} - if err := json.NewDecoder(file).Decode(tok); err != nil { - return nil, OAuthError{"CacheFile.Token", err.Error()} - } - return tok, nil -} - -func (f CacheFile) PutToken(tok *Token) error { - file, err := os.OpenFile(string(f), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) - if err != nil { - return OAuthError{"CacheFile.PutToken", err.Error()} - } - if err := json.NewEncoder(file).Encode(tok); err != nil { - file.Close() - return OAuthError{"CacheFile.PutToken", err.Error()} - } - if err := file.Close(); err != nil { - return OAuthError{"CacheFile.PutToken", err.Error()} - } - return nil -} - -// Config is the configuration of an OAuth consumer. -type Config struct { - // ClientId is the OAuth client identifier used when communicating with - // the configured OAuth provider. - ClientId string - - // ClientSecret is the OAuth client secret used when communicating with - // the configured OAuth provider. - ClientSecret string - - // Scope identifies the level of access being requested. Multiple scope - // values should be provided as a space-delimited string. - Scope string - - // AuthURL is the URL the user will be directed to in order to grant - // access. - AuthURL string - - // TokenURL is the URL used to retrieve OAuth tokens. - TokenURL string - - // RedirectURL is the URL to which the user will be returned after - // granting (or denying) access. - RedirectURL string - - // TokenCache allows tokens to be cached for subsequent requests. - TokenCache Cache - - // AccessType is an OAuth extension that gets sent as the - // "access_type" field in the URL from AuthCodeURL. - // See https://developers.google.com/accounts/docs/OAuth2WebServer. - // It may be "online" (the default) or "offline". - // If your application needs to refresh access tokens when the - // user is not present at the browser, then use offline. This - // will result in your application obtaining a refresh token - // the first time your application exchanges an authorization - // code for a user. - AccessType string - - // ApprovalPrompt indicates whether the user should be - // re-prompted for consent. If set to "auto" (default) the - // user will be prompted only if they haven't previously - // granted consent and the code can only be exchanged for an - // access token. - // If set to "force" the user will always be prompted, and the - // code can be exchanged for a refresh token. - ApprovalPrompt string -} - -// Token contains an end-user's tokens. -// This is the data you must store to persist authentication. -type Token struct { - AccessToken string - RefreshToken string - Expiry time.Time // If zero the token has no (known) expiry time. - - // Extra optionally contains extra metadata from the server - // when updating a token. The only current key that may be - // populated is "id_token". It may be nil and will be - // initialized as needed. - Extra map[string]string -} - -// Expired reports whether the token has expired or is invalid. -func (t *Token) Expired() bool { - if t.AccessToken == "" { - return true - } - if t.Expiry.IsZero() { - return false - } - return t.Expiry.Before(time.Now()) -} - -// Transport implements http.RoundTripper. When configured with a valid -// Config and Token it can be used to make authenticated HTTP requests. -// -// t := &oauth.Transport{config} -// t.Exchange(code) -// // t now contains a valid Token -// r, _, err := t.Client().Get("http://example.org/url/requiring/auth") -// -// It will automatically refresh the Token if it can, -// updating the supplied Token in place. -type Transport struct { - *Config - *Token - - // mu guards modifying the token. - mu sync.Mutex - - // Transport is the HTTP transport to use when making requests. - // It will default to http.DefaultTransport if nil. - // (It should never be an oauth.Transport.) - Transport http.RoundTripper -} - -// Client returns an *http.Client that makes OAuth-authenticated requests. -func (t *Transport) Client() *http.Client { - return &http.Client{Transport: t} -} - -func (t *Transport) transport() http.RoundTripper { - if t.Transport != nil { - return t.Transport - } - return http.DefaultTransport -} - -// AuthCodeURL returns a URL that the end-user should be redirected to, -// so that they may obtain an authorization code. -func (c *Config) AuthCodeURL(state string) string { - url_, err := url.Parse(c.AuthURL) - if err != nil { - panic("AuthURL malformed: " + err.Error()) - } - q := url.Values{ - "response_type": {"code"}, - "client_id": {c.ClientId}, - "state": condVal(state), - "scope": condVal(c.Scope), - "redirect_uri": condVal(c.RedirectURL), - "access_type": condVal(c.AccessType), - "approval_prompt": condVal(c.ApprovalPrompt), - }.Encode() - if url_.RawQuery == "" { - url_.RawQuery = q - } else { - url_.RawQuery += "&" + q - } - return url_.String() -} - -func condVal(v string) []string { - if v == "" { - return nil - } - return []string{v} -} - -// Exchange takes a code and gets access Token from the remote server. -func (t *Transport) Exchange(code string) (*Token, error) { - if t.Config == nil { - return nil, OAuthError{"Exchange", "no Config supplied"} - } - - // If the transport or the cache already has a token, it is - // passed to `updateToken` to preserve existing refresh token. - tok := t.Token - if tok == nil && t.TokenCache != nil { - tok, _ = t.TokenCache.Token() - } - if tok == nil { - tok = new(Token) - } - err := t.updateToken(tok, url.Values{ - "grant_type": {"authorization_code"}, - "redirect_uri": {t.RedirectURL}, - "scope": {t.Scope}, - "code": {code}, - }) - if err != nil { - return nil, err - } - t.Token = tok - if t.TokenCache != nil { - return tok, t.TokenCache.PutToken(tok) - } - return tok, nil -} - -// RoundTrip executes a single HTTP transaction using the Transport's -// Token as authorization headers. -// -// This method will attempt to renew the Token if it has expired and may return -// an error related to that Token renewal before attempting the client request. -// If the Token cannot be renewed a non-nil os.Error value will be returned. -// If the Token is invalid callers should expect HTTP-level errors, -// as indicated by the Response's StatusCode. -func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { - accessToken, err := t.getAccessToken() - if err != nil { - return nil, err - } - // To set the Authorization header, we must make a copy of the Request - // so that we don't modify the Request we were given. - // This is required by the specification of http.RoundTripper. - req = cloneRequest(req) - req.Header.Set("Authorization", "Bearer "+accessToken) - - // Make the HTTP request. - return t.transport().RoundTrip(req) -} - -func (t *Transport) getAccessToken() (string, error) { - t.mu.Lock() - defer t.mu.Unlock() - - if t.Token == nil { - if t.Config == nil { - return "", OAuthError{"RoundTrip", "no Config supplied"} - } - if t.TokenCache == nil { - return "", OAuthError{"RoundTrip", "no Token supplied"} - } - var err error - t.Token, err = t.TokenCache.Token() - if err != nil { - return "", err - } - } - - // Refresh the Token if it has expired. - if t.Expired() { - if err := t.Refresh(); err != nil { - return "", err - } - } - if t.AccessToken == "" { - return "", errors.New("no access token obtained from refresh") - } - return t.AccessToken, nil -} - -// cloneRequest returns a clone of the provided *http.Request. -// The clone is a shallow copy of the struct and its Header map. -func cloneRequest(r *http.Request) *http.Request { - // shallow copy of the struct - r2 := new(http.Request) - *r2 = *r - // deep copy of the Header - r2.Header = make(http.Header) - for k, s := range r.Header { - r2.Header[k] = s - } - return r2 -} - -// Refresh renews the Transport's AccessToken using its RefreshToken. -func (t *Transport) Refresh() error { - if t.Token == nil { - return OAuthError{"Refresh", "no existing Token"} - } - if t.RefreshToken == "" { - return OAuthError{"Refresh", "Token expired; no Refresh Token"} - } - if t.Config == nil { - return OAuthError{"Refresh", "no Config supplied"} - } - - err := t.updateToken(t.Token, url.Values{ - "grant_type": {"refresh_token"}, - "refresh_token": {t.RefreshToken}, - }) - if err != nil { - return err - } - if t.TokenCache != nil { - return t.TokenCache.PutToken(t.Token) - } - return nil -} - -// AuthenticateClient gets an access Token using the client_credentials grant -// type. -func (t *Transport) AuthenticateClient() error { - if t.Config == nil { - return OAuthError{"Exchange", "no Config supplied"} - } - if t.Token == nil { - t.Token = &Token{} - } - return t.updateToken(t.Token, url.Values{"grant_type": {"client_credentials"}}) -} - -// providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL -// implements the OAuth2 spec correctly -// See https://code.google.com/p/goauth2/issues/detail?id=31 for background. -// In summary: -// - Reddit only accepts client secret in the Authorization header -// - Dropbox accepts either it in URL param or Auth header, but not both. -// - Google only accepts URL param (not spec compliant?), not Auth header -func providerAuthHeaderWorks(tokenURL string) bool { - if strings.HasPrefix(tokenURL, "https://accounts.google.com/") || - strings.HasPrefix(tokenURL, "https://github.com/") || - strings.HasPrefix(tokenURL, "https://api.instagram.com/") || - strings.HasPrefix(tokenURL, "https://www.douban.com/") { - // Some sites fail to implement the OAuth2 spec fully. - return false - } - - // Assume the provider implements the spec properly - // otherwise. We can add more exceptions as they're - // discovered. We will _not_ be adding configurable hooks - // to this package to let users select server bugs. - return true -} - -// updateToken mutates both tok and v. -func (t *Transport) updateToken(tok *Token, v url.Values) error { - v.Set("client_id", t.ClientId) - bustedAuth := !providerAuthHeaderWorks(t.TokenURL) - if bustedAuth { - v.Set("client_secret", t.ClientSecret) - } - client := &http.Client{Transport: t.transport()} - req, err := http.NewRequest("POST", t.TokenURL, strings.NewReader(v.Encode())) - if err != nil { - return err - } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - if !bustedAuth { - req.SetBasicAuth(t.ClientId, t.ClientSecret) - } - r, err := client.Do(req) - if err != nil { - return err - } - defer r.Body.Close() - if r.StatusCode != 200 { - return OAuthError{"updateToken", "Unexpected HTTP status " + r.Status} - } - var b struct { - Access string `json:"access_token"` - Refresh string `json:"refresh_token"` - ExpiresIn int64 `json:"expires_in"` // seconds - Id string `json:"id_token"` - } - - body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20)) - if err != nil { - return err - } - - content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) - switch content { - case "application/x-www-form-urlencoded", "text/plain": - vals, err := url.ParseQuery(string(body)) - if err != nil { - return err - } - - b.Access = vals.Get("access_token") - b.Refresh = vals.Get("refresh_token") - b.ExpiresIn, _ = strconv.ParseInt(vals.Get("expires_in"), 10, 64) - b.Id = vals.Get("id_token") - default: - if err = json.Unmarshal(body, &b); err != nil { - return fmt.Errorf("got bad response from server: %q", body) - } - } - if b.Access == "" { - return errors.New("received empty access token from authorization server") - } - tok.AccessToken = b.Access - // Don't overwrite `RefreshToken` with an empty value - if b.Refresh != "" { - tok.RefreshToken = b.Refresh - } - if b.ExpiresIn == 0 { - tok.Expiry = time.Time{} - } else { - tok.Expiry = time.Now().Add(time.Duration(b.ExpiresIn) * time.Second) - } - if b.Id != "" { - if tok.Extra == nil { - tok.Extra = make(map[string]string) - } - tok.Extra["id_token"] = b.Id - } - return nil -} diff --git a/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/oauth_test.go b/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/oauth_test.go deleted file mode 100644 index 9fadc619..00000000 --- a/Godeps/_workspace/src/code.google.com/p/goauth2/oauth/oauth_test.go +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright 2011 The goauth2 Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package oauth - -import ( - "io" - "io/ioutil" - "net/http" - "net/http/httptest" - "net/url" - "os" - "path/filepath" - "runtime" - "testing" - "time" -) - -var requests = []struct { - path, query, auth string // request - contenttype, body string // response -}{ - { - path: "/token", - query: "grant_type=authorization_code&code=c0d3&client_id=cl13nt1d", - contenttype: "application/json", - auth: "Basic Y2wxM250MWQ6czNjcjN0", - body: ` - { - "access_token":"token1", - "refresh_token":"refreshtoken1", - "id_token":"idtoken1", - "expires_in":3600 - } - `, - }, - {path: "/secure", auth: "Bearer token1", body: "first payload"}, - { - path: "/token", - query: "grant_type=refresh_token&refresh_token=refreshtoken1&client_id=cl13nt1d", - contenttype: "application/json", - auth: "Basic Y2wxM250MWQ6czNjcjN0", - body: ` - { - "access_token":"token2", - "refresh_token":"refreshtoken2", - "id_token":"idtoken2", - "expires_in":3600 - } - `, - }, - {path: "/secure", auth: "Bearer token2", body: "second payload"}, - { - path: "/token", - query: "grant_type=refresh_token&refresh_token=refreshtoken2&client_id=cl13nt1d", - contenttype: "application/x-www-form-urlencoded", - body: "access_token=token3&refresh_token=refreshtoken3&id_token=idtoken3&expires_in=3600", - auth: "Basic Y2wxM250MWQ6czNjcjN0", - }, - {path: "/secure", auth: "Bearer token3", body: "third payload"}, - { - path: "/token", - query: "grant_type=client_credentials&client_id=cl13nt1d", - contenttype: "application/json", - auth: "Basic Y2wxM250MWQ6czNjcjN0", - body: ` - { - "access_token":"token4", - "expires_in":3600 - } - `, - }, - {path: "/secure", auth: "Bearer token4", body: "fourth payload"}, -} - -func TestOAuth(t *testing.T) { - // Set up test server. - n := 0 - handler := func(w http.ResponseWriter, r *http.Request) { - if n >= len(requests) { - t.Errorf("too many requests: %d", n) - return - } - req := requests[n] - n++ - - // Check request. - if g, w := r.URL.Path, req.path; g != w { - t.Errorf("request[%d] got path %s, want %s", n, g, w) - } - want, _ := url.ParseQuery(req.query) - for k := range want { - if g, w := r.FormValue(k), want.Get(k); g != w { - t.Errorf("query[%s] = %s, want %s", k, g, w) - } - } - if g, w := r.Header.Get("Authorization"), req.auth; w != "" && g != w { - t.Errorf("Authorization: %v, want %v", g, w) - } - - // Send response. - w.Header().Set("Content-Type", req.contenttype) - io.WriteString(w, req.body) - } - server := httptest.NewServer(http.HandlerFunc(handler)) - defer server.Close() - - config := &Config{ - ClientId: "cl13nt1d", - ClientSecret: "s3cr3t", - Scope: "https://example.net/scope", - AuthURL: server.URL + "/auth", - TokenURL: server.URL + "/token", - } - - // TODO(adg): test AuthCodeURL - - transport := &Transport{Config: config} - _, err := transport.Exchange("c0d3") - if err != nil { - t.Fatalf("Exchange: %v", err) - } - checkToken(t, transport.Token, "token1", "refreshtoken1", "idtoken1") - - c := transport.Client() - resp, err := c.Get(server.URL + "/secure") - if err != nil { - t.Fatalf("Get: %v", err) - } - checkBody(t, resp, "first payload") - - // test automatic refresh - transport.Expiry = time.Now().Add(-time.Hour) - resp, err = c.Get(server.URL + "/secure") - if err != nil { - t.Fatalf("Get: %v", err) - } - checkBody(t, resp, "second payload") - checkToken(t, transport.Token, "token2", "refreshtoken2", "idtoken2") - - // refresh one more time, but get URL-encoded token instead of JSON - transport.Expiry = time.Now().Add(-time.Hour) - resp, err = c.Get(server.URL + "/secure") - if err != nil { - t.Fatalf("Get: %v", err) - } - checkBody(t, resp, "third payload") - checkToken(t, transport.Token, "token3", "refreshtoken3", "idtoken3") - - transport.Token = &Token{} - err = transport.AuthenticateClient() - if err != nil { - t.Fatalf("AuthenticateClient: %v", err) - } - checkToken(t, transport.Token, "token4", "", "") - resp, err = c.Get(server.URL + "/secure") - if err != nil { - t.Fatalf("Get: %v", err) - } - checkBody(t, resp, "fourth payload") -} - -func checkToken(t *testing.T, tok *Token, access, refresh, id string) { - if g, w := tok.AccessToken, access; g != w { - t.Errorf("AccessToken = %q, want %q", g, w) - } - if g, w := tok.RefreshToken, refresh; g != w { - t.Errorf("RefreshToken = %q, want %q", g, w) - } - if g, w := tok.Extra["id_token"], id; g != w { - t.Errorf("Extra['id_token'] = %q, want %q", g, w) - } - if tok.Expiry.IsZero() { - t.Errorf("Expiry is zero; want ~1 hour") - } else { - exp := tok.Expiry.Sub(time.Now()) - const slop = 3 * time.Second // time moving during test - if (time.Hour-slop) > exp || exp > time.Hour { - t.Errorf("Expiry = %v, want ~1 hour", exp) - } - } -} - -func checkBody(t *testing.T, r *http.Response, body string) { - b, err := ioutil.ReadAll(r.Body) - if err != nil { - t.Errorf("reading reponse body: %v, want %q", err, body) - } - if g, w := string(b), body; g != w { - t.Errorf("request body mismatch: got %q, want %q", g, w) - } -} - -func TestCachePermissions(t *testing.T) { - if runtime.GOOS == "windows" { - // Windows doesn't support file mode bits. - return - } - - td, err := ioutil.TempDir("", "oauth-test") - if err != nil { - t.Fatalf("ioutil.TempDir: %v", err) - } - defer os.RemoveAll(td) - tempFile := filepath.Join(td, "cache-file") - - cf := CacheFile(tempFile) - if err := cf.PutToken(new(Token)); err != nil { - t.Fatalf("PutToken: %v", err) - } - fi, err := os.Stat(tempFile) - if err != nil { - t.Fatalf("os.Stat: %v", err) - } - if fi.Mode()&0077 != 0 { - t.Errorf("Created cache file has mode %#o, want non-accessible to group+other", fi.Mode()) - } -} - -func TestTokenExpired(t *testing.T) { - tests := []struct { - token Token - expired bool - }{ - {Token{AccessToken: "foo"}, false}, - {Token{AccessToken: ""}, true}, - {Token{AccessToken: "foo", Expiry: time.Now().Add(-1 * time.Hour)}, true}, - {Token{AccessToken: "foo", Expiry: time.Now().Add(1 * time.Hour)}, false}, - } - for _, tt := range tests { - if got := tt.token.Expired(); got != tt.expired { - t.Errorf("token %+v Expired = %v; want %v", tt.token, got, !got) - } - } -} diff --git a/Godeps/_workspace/src/golang.org/x/net/context/context.go b/Godeps/_workspace/src/golang.org/x/net/context/context.go new file mode 100644 index 00000000..e7ee376c --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/net/context/context.go @@ -0,0 +1,447 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package context defines the Context type, which carries deadlines, +// cancelation signals, and other request-scoped values across API boundaries +// and between processes. +// +// Incoming requests to a server should create a Context, and outgoing calls to +// servers should accept a Context. The chain of function calls between must +// propagate the Context, optionally replacing it with a modified copy created +// using WithDeadline, WithTimeout, WithCancel, or WithValue. +// +// Programs that use Contexts should follow these rules to keep interfaces +// consistent across packages and enable static analysis tools to check context +// propagation: +// +// Do not store Contexts inside a struct type; instead, pass a Context +// explicitly to each function that needs it. The Context should be the first +// parameter, typically named ctx: +// +// func DoSomething(ctx context.Context, arg Arg) error { +// // ... use ctx ... +// } +// +// Do not pass a nil Context, even if a function permits it. Pass context.TODO +// if you are unsure about which Context to use. +// +// Use context Values only for request-scoped data that transits processes and +// APIs, not for passing optional parameters to functions. +// +// The same Context may be passed to functions running in different goroutines; +// Contexts are safe for simultaneous use by multiple goroutines. +// +// See http://blog.golang.org/context for example code for a server that uses +// Contexts. +package context // import "golang.org/x/net/context" + +import ( + "errors" + "fmt" + "sync" + "time" +) + +// A Context carries a deadline, a cancelation signal, and other values across +// API boundaries. +// +// Context's methods may be called by multiple goroutines simultaneously. +type Context interface { + // Deadline returns the time when work done on behalf of this context + // should be canceled. Deadline returns ok==false when no deadline is + // set. Successive calls to Deadline return the same results. + Deadline() (deadline time.Time, ok bool) + + // Done returns a channel that's closed when work done on behalf of this + // context should be canceled. Done may return nil if this context can + // never be canceled. Successive calls to Done return the same value. + // + // WithCancel arranges for Done to be closed when cancel is called; + // WithDeadline arranges for Done to be closed when the deadline + // expires; WithTimeout arranges for Done to be closed when the timeout + // elapses. + // + // Done is provided for use in select statements: + // + // // Stream generates values with DoSomething and sends them to out + // // until DoSomething returns an error or ctx.Done is closed. + // func Stream(ctx context.Context, out <-chan Value) error { + // for { + // v, err := DoSomething(ctx) + // if err != nil { + // return err + // } + // select { + // case <-ctx.Done(): + // return ctx.Err() + // case out <- v: + // } + // } + // } + // + // See http://blog.golang.org/pipelines for more examples of how to use + // a Done channel for cancelation. + Done() <-chan struct{} + + // Err returns a non-nil error value after Done is closed. Err returns + // Canceled if the context was canceled or DeadlineExceeded if the + // context's deadline passed. No other values for Err are defined. + // After Done is closed, successive calls to Err return the same value. + Err() error + + // Value returns the value associated with this context for key, or nil + // if no value is associated with key. Successive calls to Value with + // the same key returns the same result. + // + // Use context values only for request-scoped data that transits + // processes and API boundaries, not for passing optional parameters to + // functions. + // + // A key identifies a specific value in a Context. Functions that wish + // to store values in Context typically allocate a key in a global + // variable then use that key as the argument to context.WithValue and + // Context.Value. A key can be any type that supports equality; + // packages should define keys as an unexported type to avoid + // collisions. + // + // Packages that define a Context key should provide type-safe accessors + // for the values stores using that key: + // + // // Package user defines a User type that's stored in Contexts. + // package user + // + // import "golang.org/x/net/context" + // + // // User is the type of value stored in the Contexts. + // type User struct {...} + // + // // key is an unexported type for keys defined in this package. + // // This prevents collisions with keys defined in other packages. + // type key int + // + // // userKey is the key for user.User values in Contexts. It is + // // unexported; clients use user.NewContext and user.FromContext + // // instead of using this key directly. + // var userKey key = 0 + // + // // NewContext returns a new Context that carries value u. + // func NewContext(ctx context.Context, u *User) context.Context { + // return context.WithValue(ctx, userKey, u) + // } + // + // // FromContext returns the User value stored in ctx, if any. + // func FromContext(ctx context.Context) (*User, bool) { + // u, ok := ctx.Value(userKey).(*User) + // return u, ok + // } + Value(key interface{}) interface{} +} + +// Canceled is the error returned by Context.Err when the context is canceled. +var Canceled = errors.New("context canceled") + +// DeadlineExceeded is the error returned by Context.Err when the context's +// deadline passes. +var DeadlineExceeded = errors.New("context deadline exceeded") + +// An emptyCtx is never canceled, has no values, and has no deadline. It is not +// struct{}, since vars of this type must have distinct addresses. +type emptyCtx int + +func (*emptyCtx) Deadline() (deadline time.Time, ok bool) { + return +} + +func (*emptyCtx) Done() <-chan struct{} { + return nil +} + +func (*emptyCtx) Err() error { + return nil +} + +func (*emptyCtx) Value(key interface{}) interface{} { + return nil +} + +func (e *emptyCtx) String() string { + switch e { + case background: + return "context.Background" + case todo: + return "context.TODO" + } + return "unknown empty Context" +} + +var ( + background = new(emptyCtx) + todo = new(emptyCtx) +) + +// Background returns a non-nil, empty Context. It is never canceled, has no +// values, and has no deadline. It is typically used by the main function, +// initialization, and tests, and as the top-level Context for incoming +// requests. +func Background() Context { + return background +} + +// TODO returns a non-nil, empty Context. Code should use context.TODO when +// it's unclear which Context to use or it's is not yet available (because the +// surrounding function has not yet been extended to accept a Context +// parameter). TODO is recognized by static analysis tools that determine +// whether Contexts are propagated correctly in a program. +func TODO() Context { + return todo +} + +// A CancelFunc tells an operation to abandon its work. +// A CancelFunc does not wait for the work to stop. +// After the first call, subsequent calls to a CancelFunc do nothing. +type CancelFunc func() + +// WithCancel returns a copy of parent with a new Done channel. The returned +// context's Done channel is closed when the returned cancel function is called +// or when the parent context's Done channel is closed, whichever happens first. +// +// Canceling this context releases resources associated with it, so code should +// call cancel as soon as the operations running in this Context complete. +func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { + c := newCancelCtx(parent) + propagateCancel(parent, &c) + return &c, func() { c.cancel(true, Canceled) } +} + +// newCancelCtx returns an initialized cancelCtx. +func newCancelCtx(parent Context) cancelCtx { + return cancelCtx{ + Context: parent, + done: make(chan struct{}), + } +} + +// propagateCancel arranges for child to be canceled when parent is. +func propagateCancel(parent Context, child canceler) { + if parent.Done() == nil { + return // parent is never canceled + } + if p, ok := parentCancelCtx(parent); ok { + p.mu.Lock() + if p.err != nil { + // parent has already been canceled + child.cancel(false, p.err) + } else { + if p.children == nil { + p.children = make(map[canceler]bool) + } + p.children[child] = true + } + p.mu.Unlock() + } else { + go func() { + select { + case <-parent.Done(): + child.cancel(false, parent.Err()) + case <-child.Done(): + } + }() + } +} + +// parentCancelCtx follows a chain of parent references until it finds a +// *cancelCtx. This function understands how each of the concrete types in this +// package represents its parent. +func parentCancelCtx(parent Context) (*cancelCtx, bool) { + for { + switch c := parent.(type) { + case *cancelCtx: + return c, true + case *timerCtx: + return &c.cancelCtx, true + case *valueCtx: + parent = c.Context + default: + return nil, false + } + } +} + +// removeChild removes a context from its parent. +func removeChild(parent Context, child canceler) { + p, ok := parentCancelCtx(parent) + if !ok { + return + } + p.mu.Lock() + if p.children != nil { + delete(p.children, child) + } + p.mu.Unlock() +} + +// A canceler is a context type that can be canceled directly. The +// implementations are *cancelCtx and *timerCtx. +type canceler interface { + cancel(removeFromParent bool, err error) + Done() <-chan struct{} +} + +// A cancelCtx can be canceled. When canceled, it also cancels any children +// that implement canceler. +type cancelCtx struct { + Context + + done chan struct{} // closed by the first cancel call. + + mu sync.Mutex + children map[canceler]bool // set to nil by the first cancel call + err error // set to non-nil by the first cancel call +} + +func (c *cancelCtx) Done() <-chan struct{} { + return c.done +} + +func (c *cancelCtx) Err() error { + c.mu.Lock() + defer c.mu.Unlock() + return c.err +} + +func (c *cancelCtx) String() string { + return fmt.Sprintf("%v.WithCancel", c.Context) +} + +// cancel closes c.done, cancels each of c's children, and, if +// removeFromParent is true, removes c from its parent's children. +func (c *cancelCtx) cancel(removeFromParent bool, err error) { + if err == nil { + panic("context: internal error: missing cancel error") + } + c.mu.Lock() + if c.err != nil { + c.mu.Unlock() + return // already canceled + } + c.err = err + close(c.done) + for child := range c.children { + // NOTE: acquiring the child's lock while holding parent's lock. + child.cancel(false, err) + } + c.children = nil + c.mu.Unlock() + + if removeFromParent { + removeChild(c.Context, c) + } +} + +// WithDeadline returns a copy of the parent context with the deadline adjusted +// to be no later than d. If the parent's deadline is already earlier than d, +// WithDeadline(parent, d) is semantically equivalent to parent. The returned +// context's Done channel is closed when the deadline expires, when the returned +// cancel function is called, or when the parent context's Done channel is +// closed, whichever happens first. +// +// Canceling this context releases resources associated with it, so code should +// call cancel as soon as the operations running in this Context complete. +func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) { + if cur, ok := parent.Deadline(); ok && cur.Before(deadline) { + // The current deadline is already sooner than the new one. + return WithCancel(parent) + } + c := &timerCtx{ + cancelCtx: newCancelCtx(parent), + deadline: deadline, + } + propagateCancel(parent, c) + d := deadline.Sub(time.Now()) + if d <= 0 { + c.cancel(true, DeadlineExceeded) // deadline has already passed + return c, func() { c.cancel(true, Canceled) } + } + c.mu.Lock() + defer c.mu.Unlock() + if c.err == nil { + c.timer = time.AfterFunc(d, func() { + c.cancel(true, DeadlineExceeded) + }) + } + return c, func() { c.cancel(true, Canceled) } +} + +// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to +// implement Done and Err. It implements cancel by stopping its timer then +// delegating to cancelCtx.cancel. +type timerCtx struct { + cancelCtx + timer *time.Timer // Under cancelCtx.mu. + + deadline time.Time +} + +func (c *timerCtx) Deadline() (deadline time.Time, ok bool) { + return c.deadline, true +} + +func (c *timerCtx) String() string { + return fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, c.deadline.Sub(time.Now())) +} + +func (c *timerCtx) cancel(removeFromParent bool, err error) { + c.cancelCtx.cancel(false, err) + if removeFromParent { + // Remove this timerCtx from its parent cancelCtx's children. + removeChild(c.cancelCtx.Context, c) + } + c.mu.Lock() + if c.timer != nil { + c.timer.Stop() + c.timer = nil + } + c.mu.Unlock() +} + +// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)). +// +// Canceling this context releases resources associated with it, so code should +// call cancel as soon as the operations running in this Context complete: +// +// func slowOperationWithTimeout(ctx context.Context) (Result, error) { +// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) +// defer cancel() // releases resources if slowOperation completes before timeout elapses +// return slowOperation(ctx) +// } +func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { + return WithDeadline(parent, time.Now().Add(timeout)) +} + +// WithValue returns a copy of parent in which the value associated with key is +// val. +// +// Use context Values only for request-scoped data that transits processes and +// APIs, not for passing optional parameters to functions. +func WithValue(parent Context, key interface{}, val interface{}) Context { + return &valueCtx{parent, key, val} +} + +// A valueCtx carries a key-value pair. It implements Value for that key and +// delegates all other calls to the embedded Context. +type valueCtx struct { + Context + key, val interface{} +} + +func (c *valueCtx) String() string { + return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val) +} + +func (c *valueCtx) Value(key interface{}) interface{} { + if c.key == key { + return c.val + } + return c.Context.Value(key) +} diff --git a/Godeps/_workspace/src/golang.org/x/net/context/context_test.go b/Godeps/_workspace/src/golang.org/x/net/context/context_test.go new file mode 100644 index 00000000..faf67722 --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/net/context/context_test.go @@ -0,0 +1,575 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package context + +import ( + "fmt" + "math/rand" + "runtime" + "strings" + "sync" + "testing" + "time" +) + +// otherContext is a Context that's not one of the types defined in context.go. +// This lets us test code paths that differ based on the underlying type of the +// Context. +type otherContext struct { + Context +} + +func TestBackground(t *testing.T) { + c := Background() + if c == nil { + t.Fatalf("Background returned nil") + } + select { + case x := <-c.Done(): + t.Errorf("<-c.Done() == %v want nothing (it should block)", x) + default: + } + if got, want := fmt.Sprint(c), "context.Background"; got != want { + t.Errorf("Background().String() = %q want %q", got, want) + } +} + +func TestTODO(t *testing.T) { + c := TODO() + if c == nil { + t.Fatalf("TODO returned nil") + } + select { + case x := <-c.Done(): + t.Errorf("<-c.Done() == %v want nothing (it should block)", x) + default: + } + if got, want := fmt.Sprint(c), "context.TODO"; got != want { + t.Errorf("TODO().String() = %q want %q", got, want) + } +} + +func TestWithCancel(t *testing.T) { + c1, cancel := WithCancel(Background()) + + if got, want := fmt.Sprint(c1), "context.Background.WithCancel"; got != want { + t.Errorf("c1.String() = %q want %q", got, want) + } + + o := otherContext{c1} + c2, _ := WithCancel(o) + contexts := []Context{c1, o, c2} + + for i, c := range contexts { + if d := c.Done(); d == nil { + t.Errorf("c[%d].Done() == %v want non-nil", i, d) + } + if e := c.Err(); e != nil { + t.Errorf("c[%d].Err() == %v want nil", i, e) + } + + select { + case x := <-c.Done(): + t.Errorf("<-c.Done() == %v want nothing (it should block)", x) + default: + } + } + + cancel() + time.Sleep(100 * time.Millisecond) // let cancelation propagate + + for i, c := range contexts { + select { + case <-c.Done(): + default: + t.Errorf("<-c[%d].Done() blocked, but shouldn't have", i) + } + if e := c.Err(); e != Canceled { + t.Errorf("c[%d].Err() == %v want %v", i, e, Canceled) + } + } +} + +func TestParentFinishesChild(t *testing.T) { + // Context tree: + // parent -> cancelChild + // parent -> valueChild -> timerChild + parent, cancel := WithCancel(Background()) + cancelChild, stop := WithCancel(parent) + defer stop() + valueChild := WithValue(parent, "key", "value") + timerChild, stop := WithTimeout(valueChild, 10000*time.Hour) + defer stop() + + select { + case x := <-parent.Done(): + t.Errorf("<-parent.Done() == %v want nothing (it should block)", x) + case x := <-cancelChild.Done(): + t.Errorf("<-cancelChild.Done() == %v want nothing (it should block)", x) + case x := <-timerChild.Done(): + t.Errorf("<-timerChild.Done() == %v want nothing (it should block)", x) + case x := <-valueChild.Done(): + t.Errorf("<-valueChild.Done() == %v want nothing (it should block)", x) + default: + } + + // The parent's children should contain the two cancelable children. + pc := parent.(*cancelCtx) + cc := cancelChild.(*cancelCtx) + tc := timerChild.(*timerCtx) + pc.mu.Lock() + if len(pc.children) != 2 || !pc.children[cc] || !pc.children[tc] { + t.Errorf("bad linkage: pc.children = %v, want %v and %v", + pc.children, cc, tc) + } + pc.mu.Unlock() + + if p, ok := parentCancelCtx(cc.Context); !ok || p != pc { + t.Errorf("bad linkage: parentCancelCtx(cancelChild.Context) = %v, %v want %v, true", p, ok, pc) + } + if p, ok := parentCancelCtx(tc.Context); !ok || p != pc { + t.Errorf("bad linkage: parentCancelCtx(timerChild.Context) = %v, %v want %v, true", p, ok, pc) + } + + cancel() + + pc.mu.Lock() + if len(pc.children) != 0 { + t.Errorf("pc.cancel didn't clear pc.children = %v", pc.children) + } + pc.mu.Unlock() + + // parent and children should all be finished. + check := func(ctx Context, name string) { + select { + case <-ctx.Done(): + default: + t.Errorf("<-%s.Done() blocked, but shouldn't have", name) + } + if e := ctx.Err(); e != Canceled { + t.Errorf("%s.Err() == %v want %v", name, e, Canceled) + } + } + check(parent, "parent") + check(cancelChild, "cancelChild") + check(valueChild, "valueChild") + check(timerChild, "timerChild") + + // WithCancel should return a canceled context on a canceled parent. + precanceledChild := WithValue(parent, "key", "value") + select { + case <-precanceledChild.Done(): + default: + t.Errorf("<-precanceledChild.Done() blocked, but shouldn't have") + } + if e := precanceledChild.Err(); e != Canceled { + t.Errorf("precanceledChild.Err() == %v want %v", e, Canceled) + } +} + +func TestChildFinishesFirst(t *testing.T) { + cancelable, stop := WithCancel(Background()) + defer stop() + for _, parent := range []Context{Background(), cancelable} { + child, cancel := WithCancel(parent) + + select { + case x := <-parent.Done(): + t.Errorf("<-parent.Done() == %v want nothing (it should block)", x) + case x := <-child.Done(): + t.Errorf("<-child.Done() == %v want nothing (it should block)", x) + default: + } + + cc := child.(*cancelCtx) + pc, pcok := parent.(*cancelCtx) // pcok == false when parent == Background() + if p, ok := parentCancelCtx(cc.Context); ok != pcok || (ok && pc != p) { + t.Errorf("bad linkage: parentCancelCtx(cc.Context) = %v, %v want %v, %v", p, ok, pc, pcok) + } + + if pcok { + pc.mu.Lock() + if len(pc.children) != 1 || !pc.children[cc] { + t.Errorf("bad linkage: pc.children = %v, cc = %v", pc.children, cc) + } + pc.mu.Unlock() + } + + cancel() + + if pcok { + pc.mu.Lock() + if len(pc.children) != 0 { + t.Errorf("child's cancel didn't remove self from pc.children = %v", pc.children) + } + pc.mu.Unlock() + } + + // child should be finished. + select { + case <-child.Done(): + default: + t.Errorf("<-child.Done() blocked, but shouldn't have") + } + if e := child.Err(); e != Canceled { + t.Errorf("child.Err() == %v want %v", e, Canceled) + } + + // parent should not be finished. + select { + case x := <-parent.Done(): + t.Errorf("<-parent.Done() == %v want nothing (it should block)", x) + default: + } + if e := parent.Err(); e != nil { + t.Errorf("parent.Err() == %v want nil", e) + } + } +} + +func testDeadline(c Context, wait time.Duration, t *testing.T) { + select { + case <-time.After(wait): + t.Fatalf("context should have timed out") + case <-c.Done(): + } + if e := c.Err(); e != DeadlineExceeded { + t.Errorf("c.Err() == %v want %v", e, DeadlineExceeded) + } +} + +func TestDeadline(t *testing.T) { + c, _ := WithDeadline(Background(), time.Now().Add(100*time.Millisecond)) + if got, prefix := fmt.Sprint(c), "context.Background.WithDeadline("; !strings.HasPrefix(got, prefix) { + t.Errorf("c.String() = %q want prefix %q", got, prefix) + } + testDeadline(c, 200*time.Millisecond, t) + + c, _ = WithDeadline(Background(), time.Now().Add(100*time.Millisecond)) + o := otherContext{c} + testDeadline(o, 200*time.Millisecond, t) + + c, _ = WithDeadline(Background(), time.Now().Add(100*time.Millisecond)) + o = otherContext{c} + c, _ = WithDeadline(o, time.Now().Add(300*time.Millisecond)) + testDeadline(c, 200*time.Millisecond, t) +} + +func TestTimeout(t *testing.T) { + c, _ := WithTimeout(Background(), 100*time.Millisecond) + if got, prefix := fmt.Sprint(c), "context.Background.WithDeadline("; !strings.HasPrefix(got, prefix) { + t.Errorf("c.String() = %q want prefix %q", got, prefix) + } + testDeadline(c, 200*time.Millisecond, t) + + c, _ = WithTimeout(Background(), 100*time.Millisecond) + o := otherContext{c} + testDeadline(o, 200*time.Millisecond, t) + + c, _ = WithTimeout(Background(), 100*time.Millisecond) + o = otherContext{c} + c, _ = WithTimeout(o, 300*time.Millisecond) + testDeadline(c, 200*time.Millisecond, t) +} + +func TestCanceledTimeout(t *testing.T) { + c, _ := WithTimeout(Background(), 200*time.Millisecond) + o := otherContext{c} + c, cancel := WithTimeout(o, 400*time.Millisecond) + cancel() + time.Sleep(100 * time.Millisecond) // let cancelation propagate + select { + case <-c.Done(): + default: + t.Errorf("<-c.Done() blocked, but shouldn't have") + } + if e := c.Err(); e != Canceled { + t.Errorf("c.Err() == %v want %v", e, Canceled) + } +} + +type key1 int +type key2 int + +var k1 = key1(1) +var k2 = key2(1) // same int as k1, different type +var k3 = key2(3) // same type as k2, different int + +func TestValues(t *testing.T) { + check := func(c Context, nm, v1, v2, v3 string) { + if v, ok := c.Value(k1).(string); ok == (len(v1) == 0) || v != v1 { + t.Errorf(`%s.Value(k1).(string) = %q, %t want %q, %t`, nm, v, ok, v1, len(v1) != 0) + } + if v, ok := c.Value(k2).(string); ok == (len(v2) == 0) || v != v2 { + t.Errorf(`%s.Value(k2).(string) = %q, %t want %q, %t`, nm, v, ok, v2, len(v2) != 0) + } + if v, ok := c.Value(k3).(string); ok == (len(v3) == 0) || v != v3 { + t.Errorf(`%s.Value(k3).(string) = %q, %t want %q, %t`, nm, v, ok, v3, len(v3) != 0) + } + } + + c0 := Background() + check(c0, "c0", "", "", "") + + c1 := WithValue(Background(), k1, "c1k1") + check(c1, "c1", "c1k1", "", "") + + if got, want := fmt.Sprint(c1), `context.Background.WithValue(1, "c1k1")`; got != want { + t.Errorf("c.String() = %q want %q", got, want) + } + + c2 := WithValue(c1, k2, "c2k2") + check(c2, "c2", "c1k1", "c2k2", "") + + c3 := WithValue(c2, k3, "c3k3") + check(c3, "c2", "c1k1", "c2k2", "c3k3") + + c4 := WithValue(c3, k1, nil) + check(c4, "c4", "", "c2k2", "c3k3") + + o0 := otherContext{Background()} + check(o0, "o0", "", "", "") + + o1 := otherContext{WithValue(Background(), k1, "c1k1")} + check(o1, "o1", "c1k1", "", "") + + o2 := WithValue(o1, k2, "o2k2") + check(o2, "o2", "c1k1", "o2k2", "") + + o3 := otherContext{c4} + check(o3, "o3", "", "c2k2", "c3k3") + + o4 := WithValue(o3, k3, nil) + check(o4, "o4", "", "c2k2", "") +} + +func TestAllocs(t *testing.T) { + bg := Background() + for _, test := range []struct { + desc string + f func() + limit float64 + gccgoLimit float64 + }{ + { + desc: "Background()", + f: func() { Background() }, + limit: 0, + gccgoLimit: 0, + }, + { + desc: fmt.Sprintf("WithValue(bg, %v, nil)", k1), + f: func() { + c := WithValue(bg, k1, nil) + c.Value(k1) + }, + limit: 3, + gccgoLimit: 3, + }, + { + desc: "WithTimeout(bg, 15*time.Millisecond)", + f: func() { + c, _ := WithTimeout(bg, 15*time.Millisecond) + <-c.Done() + }, + limit: 8, + gccgoLimit: 13, + }, + { + desc: "WithCancel(bg)", + f: func() { + c, cancel := WithCancel(bg) + cancel() + <-c.Done() + }, + limit: 5, + gccgoLimit: 8, + }, + { + desc: "WithTimeout(bg, 100*time.Millisecond)", + f: func() { + c, cancel := WithTimeout(bg, 100*time.Millisecond) + cancel() + <-c.Done() + }, + limit: 8, + gccgoLimit: 25, + }, + } { + limit := test.limit + if runtime.Compiler == "gccgo" { + // gccgo does not yet do escape analysis. + // TOOD(iant): Remove this when gccgo does do escape analysis. + limit = test.gccgoLimit + } + if n := testing.AllocsPerRun(100, test.f); n > limit { + t.Errorf("%s allocs = %f want %d", test.desc, n, int(limit)) + } + } +} + +func TestSimultaneousCancels(t *testing.T) { + root, cancel := WithCancel(Background()) + m := map[Context]CancelFunc{root: cancel} + q := []Context{root} + // Create a tree of contexts. + for len(q) != 0 && len(m) < 100 { + parent := q[0] + q = q[1:] + for i := 0; i < 4; i++ { + ctx, cancel := WithCancel(parent) + m[ctx] = cancel + q = append(q, ctx) + } + } + // Start all the cancels in a random order. + var wg sync.WaitGroup + wg.Add(len(m)) + for _, cancel := range m { + go func(cancel CancelFunc) { + cancel() + wg.Done() + }(cancel) + } + // Wait on all the contexts in a random order. + for ctx := range m { + select { + case <-ctx.Done(): + case <-time.After(1 * time.Second): + buf := make([]byte, 10<<10) + n := runtime.Stack(buf, true) + t.Fatalf("timed out waiting for <-ctx.Done(); stacks:\n%s", buf[:n]) + } + } + // Wait for all the cancel functions to return. + done := make(chan struct{}) + go func() { + wg.Wait() + close(done) + }() + select { + case <-done: + case <-time.After(1 * time.Second): + buf := make([]byte, 10<<10) + n := runtime.Stack(buf, true) + t.Fatalf("timed out waiting for cancel functions; stacks:\n%s", buf[:n]) + } +} + +func TestInterlockedCancels(t *testing.T) { + parent, cancelParent := WithCancel(Background()) + child, cancelChild := WithCancel(parent) + go func() { + parent.Done() + cancelChild() + }() + cancelParent() + select { + case <-child.Done(): + case <-time.After(1 * time.Second): + buf := make([]byte, 10<<10) + n := runtime.Stack(buf, true) + t.Fatalf("timed out waiting for child.Done(); stacks:\n%s", buf[:n]) + } +} + +func TestLayersCancel(t *testing.T) { + testLayers(t, time.Now().UnixNano(), false) +} + +func TestLayersTimeout(t *testing.T) { + testLayers(t, time.Now().UnixNano(), true) +} + +func testLayers(t *testing.T, seed int64, testTimeout bool) { + rand.Seed(seed) + errorf := func(format string, a ...interface{}) { + t.Errorf(fmt.Sprintf("seed=%d: %s", seed, format), a...) + } + const ( + timeout = 200 * time.Millisecond + minLayers = 30 + ) + type value int + var ( + vals []*value + cancels []CancelFunc + numTimers int + ctx = Background() + ) + for i := 0; i < minLayers || numTimers == 0 || len(cancels) == 0 || len(vals) == 0; i++ { + switch rand.Intn(3) { + case 0: + v := new(value) + ctx = WithValue(ctx, v, v) + vals = append(vals, v) + case 1: + var cancel CancelFunc + ctx, cancel = WithCancel(ctx) + cancels = append(cancels, cancel) + case 2: + var cancel CancelFunc + ctx, cancel = WithTimeout(ctx, timeout) + cancels = append(cancels, cancel) + numTimers++ + } + } + checkValues := func(when string) { + for _, key := range vals { + if val := ctx.Value(key).(*value); key != val { + errorf("%s: ctx.Value(%p) = %p want %p", when, key, val, key) + } + } + } + select { + case <-ctx.Done(): + errorf("ctx should not be canceled yet") + default: + } + if s, prefix := fmt.Sprint(ctx), "context.Background."; !strings.HasPrefix(s, prefix) { + t.Errorf("ctx.String() = %q want prefix %q", s, prefix) + } + t.Log(ctx) + checkValues("before cancel") + if testTimeout { + select { + case <-ctx.Done(): + case <-time.After(timeout + timeout/10): + errorf("ctx should have timed out") + } + checkValues("after timeout") + } else { + cancel := cancels[rand.Intn(len(cancels))] + cancel() + select { + case <-ctx.Done(): + default: + errorf("ctx should be canceled") + } + checkValues("after cancel") + } +} + +func TestCancelRemoves(t *testing.T) { + checkChildren := func(when string, ctx Context, want int) { + if got := len(ctx.(*cancelCtx).children); got != want { + t.Errorf("%s: context has %d children, want %d", when, got, want) + } + } + + ctx, _ := WithCancel(Background()) + checkChildren("after creation", ctx, 0) + _, cancel := WithCancel(ctx) + checkChildren("with WithCancel child ", ctx, 1) + cancel() + checkChildren("after cancelling WithCancel child", ctx, 0) + + ctx, _ = WithCancel(Background()) + checkChildren("after creation", ctx, 0) + _, cancel = WithTimeout(ctx, 60*time.Minute) + checkChildren("with WithTimeout child ", ctx, 1) + cancel() + checkChildren("after cancelling WithTimeout child", ctx, 0) +} diff --git a/Godeps/_workspace/src/golang.org/x/net/context/withtimeout_test.go b/Godeps/_workspace/src/golang.org/x/net/context/withtimeout_test.go new file mode 100644 index 00000000..a6754dc3 --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/net/context/withtimeout_test.go @@ -0,0 +1,26 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package context_test + +import ( + "fmt" + "time" + + "golang.org/x/net/context" +) + +func ExampleWithTimeout() { + // Pass a context with a timeout to tell a blocking function that it + // should abandon its work after the timeout elapses. + ctx, _ := context.WithTimeout(context.Background(), 100*time.Millisecond) + select { + case <-time.After(200 * time.Millisecond): + fmt.Println("overslept") + case <-ctx.Done(): + fmt.Println(ctx.Err()) // prints "context deadline exceeded" + } + // Output: + // context deadline exceeded +} diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/AUTHORS b/Godeps/_workspace/src/golang.org/x/oauth2/AUTHORS new file mode 100644 index 00000000..15167cd7 --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/oauth2/AUTHORS @@ -0,0 +1,3 @@ +# This source code refers to The Go Authors for copyright purposes. +# The master list of authors is in the main Go distribution, +# visible at http://tip.golang.org/AUTHORS. diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/CONTRIBUTING.md b/Godeps/_workspace/src/golang.org/x/oauth2/CONTRIBUTING.md new file mode 100644 index 00000000..88dff59b --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/oauth2/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing to Go + +Go is an open source project. + +It is the work of hundreds of contributors. We appreciate your help! + + +## Filing issues + +When [filing an issue](https://golang.org/issue/new), make sure to answer these five questions: + +1. What version of Go are you using (`go version`)? +2. What operating system and processor architecture are you using? +3. What did you do? +4. What did you expect to see? +5. What did you see instead? + +General questions should go to the [golang-nuts mailing list](https://groups.google.com/group/golang-nuts) instead of the issue tracker. +The gophers there will answer or ask you to file an issue if you've tripped over a bug. + +## Contributing code + +Please read the [Contribution Guidelines](https://golang.org/doc/contribute.html) +before sending patches. + +**We do not accept GitHub pull requests** +(we use [Gerrit](https://code.google.com/p/gerrit/) instead for code review). + +Unless otherwise noted, the Go source files are distributed under +the BSD-style license found in the LICENSE file. + diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/CONTRIBUTORS b/Godeps/_workspace/src/golang.org/x/oauth2/CONTRIBUTORS new file mode 100644 index 00000000..1c4577e9 --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/oauth2/CONTRIBUTORS @@ -0,0 +1,3 @@ +# This source code was written by the Go contributors. +# The master list of contributors is in the main Go distribution, +# visible at http://tip.golang.org/CONTRIBUTORS. diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/LICENSE b/Godeps/_workspace/src/golang.org/x/oauth2/LICENSE new file mode 100644 index 00000000..d02f24fd --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/oauth2/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The oauth2 Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/README.md b/Godeps/_workspace/src/golang.org/x/oauth2/README.md new file mode 100644 index 00000000..a5afeca2 --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/oauth2/README.md @@ -0,0 +1,64 @@ +# OAuth2 for Go + +[![Build Status](https://travis-ci.org/golang/oauth2.svg?branch=master)](https://travis-ci.org/golang/oauth2) + +oauth2 package contains a client implementation for OAuth 2.0 spec. + +## Installation + +~~~~ +go get golang.org/x/oauth2 +~~~~ + +See godoc for further documentation and examples. + +* [godoc.org/golang.org/x/oauth2](http://godoc.org/golang.org/x/oauth2) +* [godoc.org/golang.org/x/oauth2/google](http://godoc.org/golang.org/x/oauth2/google) + + +## App Engine + +In change 96e89be (March 2015) we removed the `oauth2.Context2` type in favor +of the [`context.Context`](https://golang.org/x/net/context#Context) type from +the `golang.org/x/net/context` package + +This means its no longer possible to use the "Classic App Engine" +`appengine.Context` type with the `oauth2` package. (You're using +Classic App Engine if you import the package `"appengine"`.) + +To work around this, you may use the new `"google.golang.org/appengine"` +package. This package has almost the same API as the `"appengine"` package, +but it can be fetched with `go get` and used on "Managed VMs" and well as +Classic App Engine. + +See the [new `appengine` package's readme](https://github.com/golang/appengine#updating-a-go-app-engine-app) +for information on updating your app. + +If you don't want to update your entire app to use the new App Engine packages, +you may use both sets of packages in parallel, using only the new packages +with the `oauth2` package. + + import ( + "golang.org/x/net/context" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + newappengine "google.golang.org/appengine" + newurlftech "google.golang.org/urlfetch" + + "appengine" + ) + + func handler(w http.ResponseWriter, r *http.Request) { + var c appengine.Context = appengine.NewContext(r) + c.Infof("Logging a message with the old package") + + var ctx context.Context = newappengine.NewContext(r) + client := &http.Client{ + Transport: &oauth2.Transport{ + Source: google.AppEngineTokenSource(ctx, "scope"), + Base: &newurlfetch.Transport{Context: ctx}, + }, + } + client.Get("...") + } + diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/client_appengine.go b/Godeps/_workspace/src/golang.org/x/oauth2/client_appengine.go new file mode 100644 index 00000000..10aaf917 --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/oauth2/client_appengine.go @@ -0,0 +1,24 @@ +// Copyright 2014 The oauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build appengine appenginevm + +// App Engine hooks. + +package oauth2 + +import ( + "net/http" + + "golang.org/x/net/context" + "google.golang.org/appengine/urlfetch" +) + +func init() { + registerContextClientFunc(contextClientAppEngine) +} + +func contextClientAppEngine(ctx context.Context) (*http.Client, error) { + return urlfetch.Client(ctx), nil +} diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/example_test.go b/Godeps/_workspace/src/golang.org/x/oauth2/example_test.go new file mode 100644 index 00000000..8be27885 --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/oauth2/example_test.go @@ -0,0 +1,45 @@ +// Copyright 2014 The oauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package oauth2_test + +import ( + "fmt" + "log" + + "golang.org/x/oauth2" +) + +func ExampleConfig() { + conf := &oauth2.Config{ + ClientID: "YOUR_CLIENT_ID", + ClientSecret: "YOUR_CLIENT_SECRET", + Scopes: []string{"SCOPE1", "SCOPE2"}, + Endpoint: oauth2.Endpoint{ + AuthURL: "https://provider.com/o/oauth2/auth", + TokenURL: "https://provider.com/o/oauth2/token", + }, + } + + // Redirect user to consent page to ask for permission + // for the scopes specified above. + url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline) + fmt.Printf("Visit the URL for the auth dialog: %v", url) + + // Use the authorization code that is pushed to the redirect URL. + // NewTransportWithCode will do the handshake to retrieve + // an access token and initiate a Transport that is + // authorized and authenticated by the retrieved token. + var code string + if _, err := fmt.Scan(&code); err != nil { + log.Fatal(err) + } + tok, err := conf.Exchange(oauth2.NoContext, code) + if err != nil { + log.Fatal(err) + } + + client := conf.Client(oauth2.NoContext, tok) + client.Get("...") +} diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/internal/oauth2.go b/Godeps/_workspace/src/golang.org/x/oauth2/internal/oauth2.go new file mode 100644 index 00000000..37571a12 --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/oauth2/internal/oauth2.go @@ -0,0 +1,69 @@ +// Copyright 2014 The oauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package internal contains support packages for oauth2 package. +package internal + +import ( + "bufio" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "io" + "strings" +) + +// ParseKey converts the binary contents of a private key file +// to an *rsa.PrivateKey. It detects whether the private key is in a +// PEM container or not. If so, it extracts the the private key +// from PEM container before conversion. It only supports PEM +// containers with no passphrase. +func ParseKey(key []byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(key) + if block != nil { + key = block.Bytes + } + parsedKey, err := x509.ParsePKCS8PrivateKey(key) + if err != nil { + parsedKey, err = x509.ParsePKCS1PrivateKey(key) + if err != nil { + return nil, fmt.Errorf("private key should be a PEM or plain PKSC1 or PKCS8; parse error: %v", err) + } + } + parsed, ok := parsedKey.(*rsa.PrivateKey) + if !ok { + return nil, errors.New("private key is invalid") + } + return parsed, nil +} + +func ParseINI(ini io.Reader) (map[string]map[string]string, error) { + result := map[string]map[string]string{ + "": map[string]string{}, // root section + } + scanner := bufio.NewScanner(ini) + currentSection := "" + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(line, ";") { + // comment. + continue + } + if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { + currentSection = strings.TrimSpace(line[1 : len(line)-1]) + result[currentSection] = map[string]string{} + continue + } + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 && parts[0] != "" { + result[currentSection][strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) + } + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error scanning ini: %v", err) + } + return result, nil +} diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/internal/oauth2_test.go b/Godeps/_workspace/src/golang.org/x/oauth2/internal/oauth2_test.go new file mode 100644 index 00000000..014a351e --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/oauth2/internal/oauth2_test.go @@ -0,0 +1,62 @@ +// Copyright 2014 The oauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package internal contains support packages for oauth2 package. +package internal + +import ( + "reflect" + "strings" + "testing" +) + +func TestParseINI(t *testing.T) { + tests := []struct { + ini string + want map[string]map[string]string + }{ + { + `root = toor +[foo] +bar = hop +ini = nin +`, + map[string]map[string]string{ + "": map[string]string{"root": "toor"}, + "foo": map[string]string{"bar": "hop", "ini": "nin"}, + }, + }, + { + `[empty] +[section] +empty= +`, + map[string]map[string]string{ + "": map[string]string{}, + "empty": map[string]string{}, + "section": map[string]string{"empty": ""}, + }, + }, + { + `ignore +[invalid +=stuff +;comment=true +`, + map[string]map[string]string{ + "": map[string]string{}, + }, + }, + } + for _, tt := range tests { + result, err := ParseINI(strings.NewReader(tt.ini)) + if err != nil { + t.Errorf("ParseINI(%q) error %v, want: no error", tt.ini, err) + continue + } + if !reflect.DeepEqual(result, tt.want) { + t.Errorf("ParseINI(%q) = %#v, want: %#v", tt.ini, result, tt.want) + } + } +} diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/jws/jws.go b/Godeps/_workspace/src/golang.org/x/oauth2/jws/jws.go new file mode 100644 index 00000000..362323c4 --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/oauth2/jws/jws.go @@ -0,0 +1,160 @@ +// Copyright 2014 The oauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package jws provides encoding and decoding utilities for +// signed JWS messages. +package jws // import "golang.org/x/oauth2/jws" + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strings" + "time" +) + +// ClaimSet contains information about the JWT signature including the +// permissions being requested (scopes), the target of the token, the issuer, +// the time the token was issued, and the lifetime of the token. +type ClaimSet struct { + Iss string `json:"iss"` // email address of the client_id of the application making the access token request + Scope string `json:"scope,omitempty"` // space-delimited list of the permissions the application requests + Aud string `json:"aud"` // descriptor of the intended target of the assertion (Optional). + Exp int64 `json:"exp"` // the expiration time of the assertion + Iat int64 `json:"iat"` // the time the assertion was issued. + Typ string `json:"typ,omitempty"` // token type (Optional). + + // Email for which the application is requesting delegated access (Optional). + Sub string `json:"sub,omitempty"` + + // The old name of Sub. Client keeps setting Prn to be + // complaint with legacy OAuth 2.0 providers. (Optional) + Prn string `json:"prn,omitempty"` + + // See http://tools.ietf.org/html/draft-jones-json-web-token-10#section-4.3 + // This array is marshalled using custom code (see (c *ClaimSet) encode()). + PrivateClaims map[string]interface{} `json:"-"` + + exp time.Time + iat time.Time +} + +func (c *ClaimSet) encode() (string, error) { + if c.exp.IsZero() || c.iat.IsZero() { + // Reverting time back for machines whose time is not perfectly in sync. + // If client machine's time is in the future according + // to Google servers, an access token will not be issued. + now := time.Now().Add(-10 * time.Second) + c.iat = now + c.exp = now.Add(time.Hour) + } + + c.Exp = c.exp.Unix() + c.Iat = c.iat.Unix() + + b, err := json.Marshal(c) + if err != nil { + return "", err + } + + if len(c.PrivateClaims) == 0 { + return base64Encode(b), nil + } + + // Marshal private claim set and then append it to b. + prv, err := json.Marshal(c.PrivateClaims) + if err != nil { + return "", fmt.Errorf("jws: invalid map of private claims %v", c.PrivateClaims) + } + + // Concatenate public and private claim JSON objects. + if !bytes.HasSuffix(b, []byte{'}'}) { + return "", fmt.Errorf("jws: invalid JSON %s", b) + } + if !bytes.HasPrefix(prv, []byte{'{'}) { + return "", fmt.Errorf("jws: invalid JSON %s", prv) + } + b[len(b)-1] = ',' // Replace closing curly brace with a comma. + b = append(b, prv[1:]...) // Append private claims. + return base64Encode(b), nil +} + +// Header represents the header for the signed JWS payloads. +type Header struct { + // The algorithm used for signature. + Algorithm string `json:"alg"` + + // Represents the token type. + Typ string `json:"typ"` +} + +func (h *Header) encode() (string, error) { + b, err := json.Marshal(h) + if err != nil { + return "", err + } + return base64Encode(b), nil +} + +// Decode decodes a claim set from a JWS payload. +func Decode(payload string) (*ClaimSet, error) { + // decode returned id token to get expiry + s := strings.Split(payload, ".") + if len(s) < 2 { + // TODO(jbd): Provide more context about the error. + return nil, errors.New("jws: invalid token received") + } + decoded, err := base64Decode(s[1]) + if err != nil { + return nil, err + } + c := &ClaimSet{} + err = json.NewDecoder(bytes.NewBuffer(decoded)).Decode(c) + return c, err +} + +// Encode encodes a signed JWS with provided header and claim set. +func Encode(header *Header, c *ClaimSet, signature *rsa.PrivateKey) (string, error) { + head, err := header.encode() + if err != nil { + return "", err + } + cs, err := c.encode() + if err != nil { + return "", err + } + ss := fmt.Sprintf("%s.%s", head, cs) + h := sha256.New() + h.Write([]byte(ss)) + b, err := rsa.SignPKCS1v15(rand.Reader, signature, crypto.SHA256, h.Sum(nil)) + if err != nil { + return "", err + } + sig := base64Encode(b) + return fmt.Sprintf("%s.%s", ss, sig), nil +} + +// base64Encode returns and Base64url encoded version of the input string with any +// trailing "=" stripped. +func base64Encode(b []byte) string { + return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=") +} + +// base64Decode decodes the Base64url encoded string +func base64Decode(s string) ([]byte, error) { + // add back missing padding + switch len(s) % 4 { + case 2: + s += "==" + case 3: + s += "=" + } + return base64.URLEncoding.DecodeString(s) +} diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/jwt/example_test.go b/Godeps/_workspace/src/golang.org/x/oauth2/jwt/example_test.go new file mode 100644 index 00000000..6d618836 --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/oauth2/jwt/example_test.go @@ -0,0 +1,31 @@ +// Copyright 2014 The oauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package jwt_test + +import ( + "golang.org/x/oauth2" + "golang.org/x/oauth2/jwt" +) + +func ExampleJWTConfig() { + conf := &jwt.Config{ + Email: "xxx@developer.com", + // The contents of your RSA private key or your PEM file + // that contains a private key. + // If you have a p12 file instead, you + // can use `openssl` to export the private key into a pem file. + // + // $ openssl pkcs12 -in key.p12 -out key.pem -nodes + // + // It only supports PEM containers with no passphrase. + PrivateKey: []byte("-----BEGIN RSA PRIVATE KEY-----..."), + Subject: "user@example.com", + TokenURL: "https://provider.com/o/oauth2/token", + } + // Initiate an http.Client, the following GET request will be + // authorized and authenticated on the behalf of user@example.com. + client := conf.Client(oauth2.NoContext) + client.Get("...") +} diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/jwt/jwt.go b/Godeps/_workspace/src/golang.org/x/oauth2/jwt/jwt.go new file mode 100644 index 00000000..205d23ed --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/oauth2/jwt/jwt.go @@ -0,0 +1,147 @@ +// Copyright 2014 The oauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package jwt implements the OAuth 2.0 JSON Web Token flow, commonly +// known as "two-legged OAuth 2.0". +// +// See: https://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-12 +package jwt + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" + + "golang.org/x/net/context" + "golang.org/x/oauth2" + "golang.org/x/oauth2/internal" + "golang.org/x/oauth2/jws" +) + +var ( + defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" + defaultHeader = &jws.Header{Algorithm: "RS256", Typ: "JWT"} +) + +// Config is the configuration for using JWT to fetch tokens, +// commonly known as "two-legged OAuth 2.0". +type Config struct { + // Email is the OAuth client identifier used when communicating with + // the configured OAuth provider. + Email string + + // PrivateKey contains the contents of an RSA private key or the + // contents of a PEM file that contains a private key. The provided + // private key is used to sign JWT payloads. + // PEM containers with a passphrase are not supported. + // Use the following command to convert a PKCS 12 file into a PEM. + // + // $ openssl pkcs12 -in key.p12 -out key.pem -nodes + // + PrivateKey []byte + + // Subject is the optional user to impersonate. + Subject string + + // Scopes optionally specifies a list of requested permission scopes. + Scopes []string + + // TokenURL is the endpoint required to complete the 2-legged JWT flow. + TokenURL string +} + +// TokenSource returns a JWT TokenSource using the configuration +// in c and the HTTP client from the provided context. +func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { + return oauth2.ReuseTokenSource(nil, jwtSource{ctx, c}) +} + +// Client returns an HTTP client wrapping the context's +// HTTP transport and adding Authorization headers with tokens +// obtained from c. +// +// The returned client and its Transport should not be modified. +func (c *Config) Client(ctx context.Context) *http.Client { + return oauth2.NewClient(ctx, c.TokenSource(ctx)) +} + +// jwtSource is a source that always does a signed JWT request for a token. +// It should typically be wrapped with a reuseTokenSource. +type jwtSource struct { + ctx context.Context + conf *Config +} + +func (js jwtSource) Token() (*oauth2.Token, error) { + pk, err := internal.ParseKey(js.conf.PrivateKey) + if err != nil { + return nil, err + } + hc := oauth2.NewClient(js.ctx, nil) + claimSet := &jws.ClaimSet{ + Iss: js.conf.Email, + Scope: strings.Join(js.conf.Scopes, " "), + Aud: js.conf.TokenURL, + } + if subject := js.conf.Subject; subject != "" { + claimSet.Sub = subject + // prn is the old name of sub. Keep setting it + // to be compatible with legacy OAuth 2.0 providers. + claimSet.Prn = subject + } + payload, err := jws.Encode(defaultHeader, claimSet, pk) + if err != nil { + return nil, err + } + v := url.Values{} + v.Set("grant_type", defaultGrantType) + v.Set("assertion", payload) + resp, err := hc.PostForm(js.conf.TokenURL, v) + if err != nil { + return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) + } + if c := resp.StatusCode; c < 200 || c > 299 { + return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", resp.Status, body) + } + // tokenRes is the JSON response body. + var tokenRes struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + IDToken string `json:"id_token"` + ExpiresIn int64 `json:"expires_in"` // relative seconds from now + } + if err := json.Unmarshal(body, &tokenRes); err != nil { + return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) + } + token := &oauth2.Token{ + AccessToken: tokenRes.AccessToken, + TokenType: tokenRes.TokenType, + } + raw := make(map[string]interface{}) + json.Unmarshal(body, &raw) // no error checks for optional fields + token = token.WithExtra(raw) + + if secs := tokenRes.ExpiresIn; secs > 0 { + token.Expiry = time.Now().Add(time.Duration(secs) * time.Second) + } + if v := tokenRes.IDToken; v != "" { + // decode returned id token to get expiry + claimSet, err := jws.Decode(v) + if err != nil { + return nil, fmt.Errorf("oauth2: error decoding JWT token: %v", err) + } + token.Expiry = time.Unix(claimSet.Exp, 0) + } + return token, nil +} diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/jwt/jwt_test.go b/Godeps/_workspace/src/golang.org/x/oauth2/jwt/jwt_test.go new file mode 100644 index 00000000..da922c3d --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/oauth2/jwt/jwt_test.go @@ -0,0 +1,134 @@ +// Copyright 2014 The oauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package jwt + +import ( + "net/http" + "net/http/httptest" + "testing" + + "golang.org/x/oauth2" +) + +var dummyPrivateKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAx4fm7dngEmOULNmAs1IGZ9Apfzh+BkaQ1dzkmbUgpcoghucE +DZRnAGd2aPyB6skGMXUytWQvNYav0WTR00wFtX1ohWTfv68HGXJ8QXCpyoSKSSFY +fuP9X36wBSkSX9J5DVgiuzD5VBdzUISSmapjKm+DcbRALjz6OUIPEWi1Tjl6p5RK +1w41qdbmt7E5/kGhKLDuT7+M83g4VWhgIvaAXtnhklDAggilPPa8ZJ1IFe31lNlr +k4DRk38nc6sEutdf3RL7QoH7FBusI7uXV03DC6dwN1kP4GE7bjJhcRb/7jYt7CQ9 +/E9Exz3c0yAp0yrTg0Fwh+qxfH9dKwN52S7SBwIDAQABAoIBAQCaCs26K07WY5Jt +3a2Cw3y2gPrIgTCqX6hJs7O5ByEhXZ8nBwsWANBUe4vrGaajQHdLj5OKfsIDrOvn +2NI1MqflqeAbu/kR32q3tq8/Rl+PPiwUsW3E6Pcf1orGMSNCXxeducF2iySySzh3 +nSIhCG5uwJDWI7a4+9KiieFgK1pt/Iv30q1SQS8IEntTfXYwANQrfKUVMmVF9aIK +6/WZE2yd5+q3wVVIJ6jsmTzoDCX6QQkkJICIYwCkglmVy5AeTckOVwcXL0jqw5Kf +5/soZJQwLEyBoQq7Kbpa26QHq+CJONetPP8Ssy8MJJXBT+u/bSseMb3Zsr5cr43e +DJOhwsThAoGBAPY6rPKl2NT/K7XfRCGm1sbWjUQyDShscwuWJ5+kD0yudnT/ZEJ1 +M3+KS/iOOAoHDdEDi9crRvMl0UfNa8MAcDKHflzxg2jg/QI+fTBjPP5GOX0lkZ9g +z6VePoVoQw2gpPFVNPPTxKfk27tEzbaffvOLGBEih0Kb7HTINkW8rIlzAoGBAM9y +1yr+jvfS1cGFtNU+Gotoihw2eMKtIqR03Yn3n0PK1nVCDKqwdUqCypz4+ml6cxRK +J8+Pfdh7D+ZJd4LEG6Y4QRDLuv5OA700tUoSHxMSNn3q9As4+T3MUyYxWKvTeu3U +f2NWP9ePU0lV8ttk7YlpVRaPQmc1qwooBA/z/8AdAoGAW9x0HWqmRICWTBnpjyxx +QGlW9rQ9mHEtUotIaRSJ6K/F3cxSGUEkX1a3FRnp6kPLcckC6NlqdNgNBd6rb2rA +cPl/uSkZP42Als+9YMoFPU/xrrDPbUhu72EDrj3Bllnyb168jKLa4VBOccUvggxr +Dm08I1hgYgdN5huzs7y6GeUCgYEAj+AZJSOJ6o1aXS6rfV3mMRve9bQ9yt8jcKXw +5HhOCEmMtaSKfnOF1Ziih34Sxsb7O2428DiX0mV/YHtBnPsAJidL0SdLWIapBzeg +KHArByIRkwE6IvJvwpGMdaex1PIGhx5i/3VZL9qiq/ElT05PhIb+UXgoWMabCp84 +OgxDK20CgYAeaFo8BdQ7FmVX2+EEejF+8xSge6WVLtkaon8bqcn6P0O8lLypoOhd +mJAYH8WU+UAy9pecUnDZj14LAGNVmYcse8HFX71MoshnvCTFEPVo4rZxIAGwMpeJ +5jgQ3slYLpqrGlcbLgUXBUgzEO684Wk/UV9DFPlHALVqCfXQ9dpJPg== +-----END RSA PRIVATE KEY-----`) + +func TestJWTFetch_JSONResponse(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "access_token": "90d64460d14870c08c81352a05dedd3465940a7c", + "scope": "user", + "token_type": "bearer", + "expires_in": 3600 + }`)) + })) + defer ts.Close() + + conf := &Config{ + Email: "aaa@xxx.com", + PrivateKey: dummyPrivateKey, + TokenURL: ts.URL, + } + tok, err := conf.TokenSource(oauth2.NoContext).Token() + if err != nil { + t.Fatal(err) + } + if !tok.Valid() { + t.Errorf("Token invalid") + } + if tok.AccessToken != "90d64460d14870c08c81352a05dedd3465940a7c" { + t.Errorf("Unexpected access token, %#v", tok.AccessToken) + } + if tok.TokenType != "bearer" { + t.Errorf("Unexpected token type, %#v", tok.TokenType) + } + if tok.Expiry.IsZero() { + t.Errorf("Unexpected token expiry, %#v", tok.Expiry) + } + scope := tok.Extra("scope") + if scope != "user" { + t.Errorf("Unexpected value for scope: %v", scope) + } +} + +func TestJWTFetch_BadResponse(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"scope": "user", "token_type": "bearer"}`)) + })) + defer ts.Close() + + conf := &Config{ + Email: "aaa@xxx.com", + PrivateKey: dummyPrivateKey, + TokenURL: ts.URL, + } + tok, err := conf.TokenSource(oauth2.NoContext).Token() + if err != nil { + t.Fatal(err) + } + if tok == nil { + t.Fatalf("token is nil") + } + if tok.Valid() { + t.Errorf("token is valid. want invalid.") + } + if tok.AccessToken != "" { + t.Errorf("Unexpected non-empty access token %q.", tok.AccessToken) + } + if want := "bearer"; tok.TokenType != want { + t.Errorf("TokenType = %q; want %q", tok.TokenType, want) + } + scope := tok.Extra("scope") + if want := "user"; scope != want { + t.Errorf("token scope = %q; want %q", scope, want) + } +} + +func TestJWTFetch_BadResponseType(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"access_token":123, "scope": "user", "token_type": "bearer"}`)) + })) + defer ts.Close() + conf := &Config{ + Email: "aaa@xxx.com", + PrivateKey: dummyPrivateKey, + TokenURL: ts.URL, + } + tok, err := conf.TokenSource(oauth2.NoContext).Token() + if err == nil { + t.Error("got a token; expected error") + if tok.AccessToken != "" { + t.Errorf("Unexpected access token, %#v.", tok.AccessToken) + } + } +} diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/oauth2.go b/Godeps/_workspace/src/golang.org/x/oauth2/oauth2.go new file mode 100644 index 00000000..dae0066b --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/oauth2/oauth2.go @@ -0,0 +1,517 @@ +// Copyright 2014 The oauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package oauth2 provides support for making +// OAuth2 authorized and authenticated HTTP requests. +// It can additionally grant authorization with Bearer JWT. +package oauth2 // import "golang.org/x/oauth2" + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "mime" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + "time" + + "golang.org/x/net/context" +) + +// NoContext is the default context you should supply if not using +// your own context.Context (see https://golang.org/x/net/context). +var NoContext = context.TODO() + +// Config describes a typical 3-legged OAuth2 flow, with both the +// client application information and the server's endpoint URLs. +type Config struct { + // ClientID is the application's ID. + ClientID string + + // ClientSecret is the application's secret. + ClientSecret string + + // Endpoint contains the resource server's token endpoint + // URLs. These are constants specific to each server and are + // often available via site-specific packages, such as + // google.Endpoint or github.Endpoint. + Endpoint Endpoint + + // RedirectURL is the URL to redirect users going through + // the OAuth flow, after the resource owner's URLs. + RedirectURL string + + // Scope specifies optional requested permissions. + Scopes []string +} + +// A TokenSource is anything that can return a token. +type TokenSource interface { + // Token returns a token or an error. + // Token must be safe for concurrent use by multiple goroutines. + // The returned Token must not be modified. + Token() (*Token, error) +} + +// Endpoint contains the OAuth 2.0 provider's authorization and token +// endpoint URLs. +type Endpoint struct { + AuthURL string + TokenURL string +} + +var ( + // AccessTypeOnline and AccessTypeOffline are options passed + // to the Options.AuthCodeURL method. They modify the + // "access_type" field that gets sent in the URL returned by + // AuthCodeURL. + // + // Online is the default if neither is specified. If your + // application needs to refresh access tokens when the user + // is not present at the browser, then use offline. This will + // result in your application obtaining a refresh token the + // first time your application exchanges an authorization + // code for a user. + AccessTypeOnline AuthCodeOption = setParam{"access_type", "online"} + AccessTypeOffline AuthCodeOption = setParam{"access_type", "offline"} + + // ApprovalForce forces the users to view the consent dialog + // and confirm the permissions request at the URL returned + // from AuthCodeURL, even if they've already done so. + ApprovalForce AuthCodeOption = setParam{"approval_prompt", "force"} +) + +type setParam struct{ k, v string } + +func (p setParam) setValue(m url.Values) { m.Set(p.k, p.v) } + +// An AuthCodeOption is passed to Config.AuthCodeURL. +type AuthCodeOption interface { + setValue(url.Values) +} + +// AuthCodeURL returns a URL to OAuth 2.0 provider's consent page +// that asks for permissions for the required scopes explicitly. +// +// State is a token to protect the user from CSRF attacks. You must +// always provide a non-zero string and validate that it matches the +// the state query parameter on your redirect callback. +// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info. +// +// Opts may include AccessTypeOnline or AccessTypeOffline, as well +// as ApprovalForce. +func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string { + var buf bytes.Buffer + buf.WriteString(c.Endpoint.AuthURL) + v := url.Values{ + "response_type": {"code"}, + "client_id": {c.ClientID}, + "redirect_uri": condVal(c.RedirectURL), + "scope": condVal(strings.Join(c.Scopes, " ")), + "state": condVal(state), + } + for _, opt := range opts { + opt.setValue(v) + } + if strings.Contains(c.Endpoint.AuthURL, "?") { + buf.WriteByte('&') + } else { + buf.WriteByte('?') + } + buf.WriteString(v.Encode()) + return buf.String() +} + +// PasswordCredentialsToken converts a resource owner username and password +// pair into a token. +// +// Per the RFC, this grant type should only be used "when there is a high +// degree of trust between the resource owner and the client (e.g., the client +// is part of the device operating system or a highly privileged application), +// and when other authorization grant types are not available." +// See https://tools.ietf.org/html/rfc6749#section-4.3 for more info. +// +// The HTTP client to use is derived from the context. +// If nil, http.DefaultClient is used. +func (c *Config) PasswordCredentialsToken(ctx context.Context, username, password string) (*Token, error) { + return retrieveToken(ctx, c, url.Values{ + "grant_type": {"password"}, + "username": {username}, + "password": {password}, + "scope": condVal(strings.Join(c.Scopes, " ")), + }) +} + +// Exchange converts an authorization code into a token. +// +// It is used after a resource provider redirects the user back +// to the Redirect URI (the URL obtained from AuthCodeURL). +// +// The HTTP client to use is derived from the context. +// If a client is not provided via the context, http.DefaultClient is used. +// +// The code will be in the *http.Request.FormValue("code"). Before +// calling Exchange, be sure to validate FormValue("state"). +func (c *Config) Exchange(ctx context.Context, code string) (*Token, error) { + return retrieveToken(ctx, c, url.Values{ + "grant_type": {"authorization_code"}, + "code": {code}, + "redirect_uri": condVal(c.RedirectURL), + "scope": condVal(strings.Join(c.Scopes, " ")), + }) +} + +// contextClientFunc is a func which tries to return an *http.Client +// given a Context value. If it returns an error, the search stops +// with that error. If it returns (nil, nil), the search continues +// down the list of registered funcs. +type contextClientFunc func(context.Context) (*http.Client, error) + +var contextClientFuncs []contextClientFunc + +func registerContextClientFunc(fn contextClientFunc) { + contextClientFuncs = append(contextClientFuncs, fn) +} + +func contextClient(ctx context.Context) (*http.Client, error) { + for _, fn := range contextClientFuncs { + c, err := fn(ctx) + if err != nil { + return nil, err + } + if c != nil { + return c, nil + } + } + if hc, ok := ctx.Value(HTTPClient).(*http.Client); ok { + return hc, nil + } + return http.DefaultClient, nil +} + +func contextTransport(ctx context.Context) http.RoundTripper { + hc, err := contextClient(ctx) + if err != nil { + // This is a rare error case (somebody using nil on App Engine), + // so I'd rather not everybody do an error check on this Client + // method. They can get the error that they're doing it wrong + // later, at client.Get/PostForm time. + return errorTransport{err} + } + return hc.Transport +} + +// Client returns an HTTP client using the provided token. +// The token will auto-refresh as necessary. The underlying +// HTTP transport will be obtained using the provided context. +// The returned client and its Transport should not be modified. +func (c *Config) Client(ctx context.Context, t *Token) *http.Client { + return NewClient(ctx, c.TokenSource(ctx, t)) +} + +// TokenSource returns a TokenSource that returns t until t expires, +// automatically refreshing it as necessary using the provided context. +// +// Most users will use Config.Client instead. +func (c *Config) TokenSource(ctx context.Context, t *Token) TokenSource { + tkr := &tokenRefresher{ + ctx: ctx, + conf: c, + } + if t != nil { + tkr.refreshToken = t.RefreshToken + } + return &reuseTokenSource{ + t: t, + new: tkr, + } +} + +// tokenRefresher is a TokenSource that makes "grant_type"=="refresh_token" +// HTTP requests to renew a token using a RefreshToken. +type tokenRefresher struct { + ctx context.Context // used to get HTTP requests + conf *Config + refreshToken string +} + +// WARNING: Token is not safe for concurrent access, as it +// updates the tokenRefresher's refreshToken field. +// Within this package, it is used by reuseTokenSource which +// synchronizes calls to this method with its own mutex. +func (tf *tokenRefresher) Token() (*Token, error) { + if tf.refreshToken == "" { + return nil, errors.New("oauth2: token expired and refresh token is not set") + } + + tk, err := retrieveToken(tf.ctx, tf.conf, url.Values{ + "grant_type": {"refresh_token"}, + "refresh_token": {tf.refreshToken}, + }) + + if err != nil { + return nil, err + } + if tf.refreshToken != tk.RefreshToken { + tf.refreshToken = tk.RefreshToken + } + return tk, err +} + +// reuseTokenSource is a TokenSource that holds a single token in memory +// and validates its expiry before each call to retrieve it with +// Token. If it's expired, it will be auto-refreshed using the +// new TokenSource. +type reuseTokenSource struct { + new TokenSource // called when t is expired. + + mu sync.Mutex // guards t + t *Token +} + +// Token returns the current token if it's still valid, else will +// refresh the current token (using r.Context for HTTP client +// information) and return the new one. +func (s *reuseTokenSource) Token() (*Token, error) { + s.mu.Lock() + defer s.mu.Unlock() + if s.t.Valid() { + return s.t, nil + } + t, err := s.new.Token() + if err != nil { + return nil, err + } + s.t = t + return t, nil +} + +func retrieveToken(ctx context.Context, c *Config, v url.Values) (*Token, error) { + hc, err := contextClient(ctx) + if err != nil { + return nil, err + } + v.Set("client_id", c.ClientID) + bustedAuth := !providerAuthHeaderWorks(c.Endpoint.TokenURL) + if bustedAuth && c.ClientSecret != "" { + v.Set("client_secret", c.ClientSecret) + } + req, err := http.NewRequest("POST", c.Endpoint.TokenURL, strings.NewReader(v.Encode())) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + if !bustedAuth { + req.SetBasicAuth(c.ClientID, c.ClientSecret) + } + r, err := hc.Do(req) + if err != nil { + return nil, err + } + defer r.Body.Close() + body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20)) + if err != nil { + return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) + } + if code := r.StatusCode; code < 200 || code > 299 { + return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", r.Status, body) + } + + var token *Token + content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) + switch content { + case "application/x-www-form-urlencoded", "text/plain": + vals, err := url.ParseQuery(string(body)) + if err != nil { + return nil, err + } + token = &Token{ + AccessToken: vals.Get("access_token"), + TokenType: vals.Get("token_type"), + RefreshToken: vals.Get("refresh_token"), + raw: vals, + } + e := vals.Get("expires_in") + if e == "" { + // TODO(jbd): Facebook's OAuth2 implementation is broken and + // returns expires_in field in expires. Remove the fallback to expires, + // when Facebook fixes their implementation. + e = vals.Get("expires") + } + expires, _ := strconv.Atoi(e) + if expires != 0 { + token.Expiry = time.Now().Add(time.Duration(expires) * time.Second) + } + default: + var tj tokenJSON + if err = json.Unmarshal(body, &tj); err != nil { + return nil, err + } + token = &Token{ + AccessToken: tj.AccessToken, + TokenType: tj.TokenType, + RefreshToken: tj.RefreshToken, + Expiry: tj.expiry(), + raw: make(map[string]interface{}), + } + json.Unmarshal(body, &token.raw) // no error checks for optional fields + } + // Don't overwrite `RefreshToken` with an empty value + // if this was a token refreshing request. + if token.RefreshToken == "" { + token.RefreshToken = v.Get("refresh_token") + } + return token, nil +} + +// tokenJSON is the struct representing the HTTP response from OAuth2 +// providers returning a token in JSON form. +type tokenJSON struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + RefreshToken string `json:"refresh_token"` + ExpiresIn expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number + Expires expirationTime `json:"expires"` // broken Facebook spelling of expires_in +} + +func (e *tokenJSON) expiry() (t time.Time) { + if v := e.ExpiresIn; v != 0 { + return time.Now().Add(time.Duration(v) * time.Second) + } + if v := e.Expires; v != 0 { + return time.Now().Add(time.Duration(v) * time.Second) + } + return +} + +type expirationTime int32 + +func (e *expirationTime) UnmarshalJSON(b []byte) error { + var n json.Number + err := json.Unmarshal(b, &n) + if err != nil { + return err + } + i, err := n.Int64() + if err != nil { + return err + } + *e = expirationTime(i) + return nil +} + +func condVal(v string) []string { + if v == "" { + return nil + } + return []string{v} +} + +var brokenAuthHeaderProviders = []string{ + "https://accounts.google.com/", + "https://www.googleapis.com/", + "https://github.com/", + "https://api.instagram.com/", + "https://www.douban.com/", + "https://api.dropbox.com/", + "https://api.soundcloud.com/", + "https://www.linkedin.com/", + "https://api.twitch.tv/", + "https://oauth.vk.com/", + "https://api.odnoklassniki.ru/", + "https://connect.stripe.com/", + "https://api.pushbullet.com/", + "https://oauth.sandbox.trainingpeaks.com/", + "https://oauth.trainingpeaks.com/", + "https://www.strava.com/oauth/", +} + +// providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL +// implements the OAuth2 spec correctly +// See https://code.google.com/p/goauth2/issues/detail?id=31 for background. +// In summary: +// - Reddit only accepts client secret in the Authorization header +// - Dropbox accepts either it in URL param or Auth header, but not both. +// - Google only accepts URL param (not spec compliant?), not Auth header +// - Stripe only accepts client secret in Auth header with Bearer method, not Basic +func providerAuthHeaderWorks(tokenURL string) bool { + for _, s := range brokenAuthHeaderProviders { + if strings.HasPrefix(tokenURL, s) { + // Some sites fail to implement the OAuth2 spec fully. + return false + } + } + + // Assume the provider implements the spec properly + // otherwise. We can add more exceptions as they're + // discovered. We will _not_ be adding configurable hooks + // to this package to let users select server bugs. + return true +} + +// HTTPClient is the context key to use with golang.org/x/net/context's +// WithValue function to associate an *http.Client value with a context. +var HTTPClient contextKey + +// contextKey is just an empty struct. It exists so HTTPClient can be +// an immutable public variable with a unique type. It's immutable +// because nobody else can create a contextKey, being unexported. +type contextKey struct{} + +// NewClient creates an *http.Client from a Context and TokenSource. +// The returned client is not valid beyond the lifetime of the context. +// +// As a special case, if src is nil, a non-OAuth2 client is returned +// using the provided context. This exists to support related OAuth2 +// packages. +func NewClient(ctx context.Context, src TokenSource) *http.Client { + if src == nil { + c, err := contextClient(ctx) + if err != nil { + return &http.Client{Transport: errorTransport{err}} + } + return c + } + return &http.Client{ + Transport: &Transport{ + Base: contextTransport(ctx), + Source: ReuseTokenSource(nil, src), + }, + } +} + +// ReuseTokenSource returns a TokenSource which repeatedly returns the +// same token as long as it's valid, starting with t. +// When its cached token is invalid, a new token is obtained from src. +// +// ReuseTokenSource is typically used to reuse tokens from a cache +// (such as a file on disk) between runs of a program, rather than +// obtaining new tokens unnecessarily. +// +// The initial token t may be nil, in which case the TokenSource is +// wrapped in a caching version if it isn't one already. This also +// means it's always safe to wrap ReuseTokenSource around any other +// TokenSource without adverse effects. +func ReuseTokenSource(t *Token, src TokenSource) TokenSource { + // Don't wrap a reuseTokenSource in itself. That would work, + // but cause an unnecessary number of mutex operations. + // Just build the equivalent one. + if rt, ok := src.(*reuseTokenSource); ok { + if t == nil { + // Just use it directly. + return rt + } + src = rt.new + } + return &reuseTokenSource{ + t: t, + new: src, + } +} diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/oauth2_test.go b/Godeps/_workspace/src/golang.org/x/oauth2/oauth2_test.go new file mode 100644 index 00000000..df40ee9a --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/oauth2/oauth2_test.go @@ -0,0 +1,426 @@ +// Copyright 2014 The oauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package oauth2 + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "reflect" + "strconv" + "testing" + "time" + + "golang.org/x/net/context" +) + +type mockTransport struct { + rt func(req *http.Request) (resp *http.Response, err error) +} + +func (t *mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { + return t.rt(req) +} + +type mockCache struct { + token *Token + readErr error +} + +func (c *mockCache) ReadToken() (*Token, error) { + return c.token, c.readErr +} + +func (c *mockCache) WriteToken(*Token) { + // do nothing +} + +func newConf(url string) *Config { + return &Config{ + ClientID: "CLIENT_ID", + ClientSecret: "CLIENT_SECRET", + RedirectURL: "REDIRECT_URL", + Scopes: []string{"scope1", "scope2"}, + Endpoint: Endpoint{ + AuthURL: url + "/auth", + TokenURL: url + "/token", + }, + } +} + +func TestAuthCodeURL(t *testing.T) { + conf := newConf("server") + url := conf.AuthCodeURL("foo", AccessTypeOffline, ApprovalForce) + if url != "server/auth?access_type=offline&approval_prompt=force&client_id=CLIENT_ID&redirect_uri=REDIRECT_URL&response_type=code&scope=scope1+scope2&state=foo" { + t.Errorf("Auth code URL doesn't match the expected, found: %v", url) + } +} + +func TestAuthCodeURL_Optional(t *testing.T) { + conf := &Config{ + ClientID: "CLIENT_ID", + Endpoint: Endpoint{ + AuthURL: "/auth-url", + TokenURL: "/token-url", + }, + } + url := conf.AuthCodeURL("") + if url != "/auth-url?client_id=CLIENT_ID&response_type=code" { + t.Fatalf("Auth code URL doesn't match the expected, found: %v", url) + } +} + +func TestExchangeRequest(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.String() != "/token" { + t.Errorf("Unexpected exchange request URL, %v is found.", r.URL) + } + headerAuth := r.Header.Get("Authorization") + if headerAuth != "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=" { + t.Errorf("Unexpected authorization header, %v is found.", headerAuth) + } + headerContentType := r.Header.Get("Content-Type") + if headerContentType != "application/x-www-form-urlencoded" { + t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType) + } + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Failed reading request body: %s.", err) + } + if string(body) != "client_id=CLIENT_ID&code=exchange-code&grant_type=authorization_code&redirect_uri=REDIRECT_URL&scope=scope1+scope2" { + t.Errorf("Unexpected exchange payload, %v is found.", string(body)) + } + w.Header().Set("Content-Type", "application/x-www-form-urlencoded") + w.Write([]byte("access_token=90d64460d14870c08c81352a05dedd3465940a7c&scope=user&token_type=bearer")) + })) + defer ts.Close() + conf := newConf(ts.URL) + tok, err := conf.Exchange(NoContext, "exchange-code") + if err != nil { + t.Error(err) + } + if !tok.Valid() { + t.Fatalf("Token invalid. Got: %#v", tok) + } + if tok.AccessToken != "90d64460d14870c08c81352a05dedd3465940a7c" { + t.Errorf("Unexpected access token, %#v.", tok.AccessToken) + } + if tok.TokenType != "bearer" { + t.Errorf("Unexpected token type, %#v.", tok.TokenType) + } + scope := tok.Extra("scope") + if scope != "user" { + t.Errorf("Unexpected value for scope: %v", scope) + } +} + +func TestExchangeRequest_JSONResponse(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.String() != "/token" { + t.Errorf("Unexpected exchange request URL, %v is found.", r.URL) + } + headerAuth := r.Header.Get("Authorization") + if headerAuth != "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=" { + t.Errorf("Unexpected authorization header, %v is found.", headerAuth) + } + headerContentType := r.Header.Get("Content-Type") + if headerContentType != "application/x-www-form-urlencoded" { + t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType) + } + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Failed reading request body: %s.", err) + } + if string(body) != "client_id=CLIENT_ID&code=exchange-code&grant_type=authorization_code&redirect_uri=REDIRECT_URL&scope=scope1+scope2" { + t.Errorf("Unexpected exchange payload, %v is found.", string(body)) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"access_token": "90d64460d14870c08c81352a05dedd3465940a7c", "scope": "user", "token_type": "bearer", "expires_in": 86400}`)) + })) + defer ts.Close() + conf := newConf(ts.URL) + tok, err := conf.Exchange(NoContext, "exchange-code") + if err != nil { + t.Error(err) + } + if !tok.Valid() { + t.Fatalf("Token invalid. Got: %#v", tok) + } + if tok.AccessToken != "90d64460d14870c08c81352a05dedd3465940a7c" { + t.Errorf("Unexpected access token, %#v.", tok.AccessToken) + } + if tok.TokenType != "bearer" { + t.Errorf("Unexpected token type, %#v.", tok.TokenType) + } + scope := tok.Extra("scope") + if scope != "user" { + t.Errorf("Unexpected value for scope: %v", scope) + } +} + +const day = 24 * time.Hour + +func TestExchangeRequest_JSONResponse_Expiry(t *testing.T) { + seconds := int32(day.Seconds()) + jsonNumberType := reflect.TypeOf(json.Number("0")) + for _, c := range []struct { + expires string + expect error + }{ + {fmt.Sprintf(`"expires_in": %d`, seconds), nil}, + {fmt.Sprintf(`"expires_in": "%d"`, seconds), nil}, // PayPal case + {fmt.Sprintf(`"expires": %d`, seconds), nil}, // Facebook case + {`"expires": false`, &json.UnmarshalTypeError{"bool", jsonNumberType}}, // wrong type + {`"expires": {}`, &json.UnmarshalTypeError{"object", jsonNumberType}}, // wrong type + {`"expires": "zzz"`, &strconv.NumError{"ParseInt", "zzz", strconv.ErrSyntax}}, // wrong value + } { + testExchangeRequest_JSONResponse_expiry(t, c.expires, c.expect) + } +} + +func testExchangeRequest_JSONResponse_expiry(t *testing.T, exp string, expect error) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(fmt.Sprintf(`{"access_token": "90d", "scope": "user", "token_type": "bearer", %s}`, exp))) + })) + defer ts.Close() + conf := newConf(ts.URL) + t1 := time.Now().Add(day) + tok, err := conf.Exchange(NoContext, "exchange-code") + t2 := time.Now().Add(day) + if err == nil && expect != nil { + t.Errorf("Incorrect state, conf.Exchange() should return an error: %v", expect) + } else if err != nil { + if reflect.DeepEqual(err, expect) { + t.Logf("Expected error: %v", err) + return + } else { + t.Error(err) + } + + } + if !tok.Valid() { + t.Fatalf("Token invalid. Got: %#v", tok) + } + expiry := tok.Expiry + if expiry.Before(t1) || expiry.After(t2) { + t.Errorf("Unexpected value for Expiry: %v (shold be between %v and %v)", expiry, t1, t2) + } +} + +func TestExchangeRequest_BadResponse(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"scope": "user", "token_type": "bearer"}`)) + })) + defer ts.Close() + conf := newConf(ts.URL) + tok, err := conf.Exchange(NoContext, "code") + if err != nil { + t.Fatal(err) + } + if tok.AccessToken != "" { + t.Errorf("Unexpected access token, %#v.", tok.AccessToken) + } +} + +func TestExchangeRequest_BadResponseType(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"access_token":123, "scope": "user", "token_type": "bearer"}`)) + })) + defer ts.Close() + conf := newConf(ts.URL) + _, err := conf.Exchange(NoContext, "exchange-code") + if err == nil { + t.Error("expected error from invalid access_token type") + } +} + +func TestExchangeRequest_NonBasicAuth(t *testing.T) { + tr := &mockTransport{ + rt: func(r *http.Request) (w *http.Response, err error) { + headerAuth := r.Header.Get("Authorization") + if headerAuth != "" { + t.Errorf("Unexpected authorization header, %v is found.", headerAuth) + } + return nil, errors.New("no response") + }, + } + c := &http.Client{Transport: tr} + conf := &Config{ + ClientID: "CLIENT_ID", + Endpoint: Endpoint{ + AuthURL: "https://accounts.google.com/auth", + TokenURL: "https://accounts.google.com/token", + }, + } + + ctx := context.WithValue(context.Background(), HTTPClient, c) + conf.Exchange(ctx, "code") +} + +func TestPasswordCredentialsTokenRequest(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + expected := "/token" + if r.URL.String() != expected { + t.Errorf("URL = %q; want %q", r.URL, expected) + } + headerAuth := r.Header.Get("Authorization") + expected = "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=" + if headerAuth != expected { + t.Errorf("Authorization header = %q; want %q", headerAuth, expected) + } + headerContentType := r.Header.Get("Content-Type") + expected = "application/x-www-form-urlencoded" + if headerContentType != expected { + t.Errorf("Content-Type header = %q; want %q", headerContentType, expected) + } + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Failed reading request body: %s.", err) + } + expected = "client_id=CLIENT_ID&grant_type=password&password=password1&scope=scope1+scope2&username=user1" + if string(body) != expected { + t.Errorf("res.Body = %q; want %q", string(body), expected) + } + w.Header().Set("Content-Type", "application/x-www-form-urlencoded") + w.Write([]byte("access_token=90d64460d14870c08c81352a05dedd3465940a7c&scope=user&token_type=bearer")) + })) + defer ts.Close() + conf := newConf(ts.URL) + tok, err := conf.PasswordCredentialsToken(NoContext, "user1", "password1") + if err != nil { + t.Error(err) + } + if !tok.Valid() { + t.Fatalf("Token invalid. Got: %#v", tok) + } + expected := "90d64460d14870c08c81352a05dedd3465940a7c" + if tok.AccessToken != expected { + t.Errorf("AccessToken = %q; want %q", tok.AccessToken, expected) + } + expected = "bearer" + if tok.TokenType != expected { + t.Errorf("TokenType = %q; want %q", tok.TokenType, expected) + } +} + +func TestTokenRefreshRequest(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.String() == "/somethingelse" { + return + } + if r.URL.String() != "/token" { + t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL) + } + headerContentType := r.Header.Get("Content-Type") + if headerContentType != "application/x-www-form-urlencoded" { + t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType) + } + body, _ := ioutil.ReadAll(r.Body) + if string(body) != "client_id=CLIENT_ID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN" { + t.Errorf("Unexpected refresh token payload, %v is found.", string(body)) + } + })) + defer ts.Close() + conf := newConf(ts.URL) + c := conf.Client(NoContext, &Token{RefreshToken: "REFRESH_TOKEN"}) + c.Get(ts.URL + "/somethingelse") +} + +func TestFetchWithNoRefreshToken(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.String() == "/somethingelse" { + return + } + if r.URL.String() != "/token" { + t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL) + } + headerContentType := r.Header.Get("Content-Type") + if headerContentType != "application/x-www-form-urlencoded" { + t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType) + } + body, _ := ioutil.ReadAll(r.Body) + if string(body) != "client_id=CLIENT_ID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN" { + t.Errorf("Unexpected refresh token payload, %v is found.", string(body)) + } + })) + defer ts.Close() + conf := newConf(ts.URL) + c := conf.Client(NoContext, nil) + _, err := c.Get(ts.URL + "/somethingelse") + if err == nil { + t.Errorf("Fetch should return an error if no refresh token is set") + } +} + +func TestRefreshToken_RefreshTokenReplacement(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"access_token":"ACCESS TOKEN", "scope": "user", "token_type": "bearer", "refresh_token": "NEW REFRESH TOKEN"}`)) + return + })) + defer ts.Close() + conf := newConf(ts.URL) + tkr := tokenRefresher{ + conf: conf, + ctx: NoContext, + refreshToken: "OLD REFRESH TOKEN", + } + tk, err := tkr.Token() + if err != nil { + t.Errorf("Unexpected refreshToken error returned: %v", err) + return + } + if tk.RefreshToken != tkr.refreshToken { + t.Errorf("tokenRefresher.refresh_token = %s; want %s", tkr.refreshToken, tk.RefreshToken) + } +} + +func TestConfigClientWithToken(t *testing.T) { + tok := &Token{ + AccessToken: "abc123", + } + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("Authorization"), fmt.Sprintf("Bearer %s", tok.AccessToken); got != want { + t.Errorf("Authorization header = %q; want %q", got, want) + } + return + })) + defer ts.Close() + conf := newConf(ts.URL) + + c := conf.Client(NoContext, tok) + req, err := http.NewRequest("GET", ts.URL, nil) + if err != nil { + t.Error(err) + } + _, err = c.Do(req) + if err != nil { + t.Error(err) + } +} + +func Test_providerAuthHeaderWorks(t *testing.T) { + for _, p := range brokenAuthHeaderProviders { + if providerAuthHeaderWorks(p) { + t.Errorf("URL: %s not found in list", p) + } + p := fmt.Sprintf("%ssomesuffix", p) + if providerAuthHeaderWorks(p) { + t.Errorf("URL: %s not found in list", p) + } + } + p := "https://api.not-in-the-list-example.com/" + if !providerAuthHeaderWorks(p) { + t.Errorf("URL: %s found in list", p) + } + +} diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/token.go b/Godeps/_workspace/src/golang.org/x/oauth2/token.go new file mode 100644 index 00000000..9852ceb4 --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/oauth2/token.go @@ -0,0 +1,104 @@ +// Copyright 2014 The oauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package oauth2 + +import ( + "net/http" + "net/url" + "time" +) + +// expiryDelta determines how earlier a token should be considered +// expired than its actual expiration time. It is used to avoid late +// expirations due to client-server time mismatches. +const expiryDelta = 10 * time.Second + +// Token represents the crendentials used to authorize +// the requests to access protected resources on the OAuth 2.0 +// provider's backend. +// +// Most users of this package should not access fields of Token +// directly. They're exported mostly for use by related packages +// implementing derivative OAuth2 flows. +type Token struct { + // AccessToken is the token that authorizes and authenticates + // the requests. + AccessToken string `json:"access_token"` + + // TokenType is the type of token. + // The Type method returns either this or "Bearer", the default. + TokenType string `json:"token_type,omitempty"` + + // RefreshToken is a token that's used by the application + // (as opposed to the user) to refresh the access token + // if it expires. + RefreshToken string `json:"refresh_token,omitempty"` + + // Expiry is the optional expiration time of the access token. + // + // If zero, TokenSource implementations will reuse the same + // token forever and RefreshToken or equivalent + // mechanisms for that TokenSource will not be used. + Expiry time.Time `json:"expiry,omitempty"` + + // raw optionally contains extra metadata from the server + // when updating a token. + raw interface{} +} + +// Type returns t.TokenType if non-empty, else "Bearer". +func (t *Token) Type() string { + if t.TokenType != "" { + return t.TokenType + } + return "Bearer" +} + +// SetAuthHeader sets the Authorization header to r using the access +// token in t. +// +// This method is unnecessary when using Transport or an HTTP Client +// returned by this package. +func (t *Token) SetAuthHeader(r *http.Request) { + r.Header.Set("Authorization", t.Type()+" "+t.AccessToken) +} + +// WithExtra returns a new Token that's a clone of t, but using the +// provided raw extra map. This is only intended for use by packages +// implementing derivative OAuth2 flows. +func (t *Token) WithExtra(extra interface{}) *Token { + t2 := new(Token) + *t2 = *t + t2.raw = extra + return t2 +} + +// Extra returns an extra field. +// Extra fields are key-value pairs returned by the server as a +// part of the token retrieval response. +func (t *Token) Extra(key string) interface{} { + if vals, ok := t.raw.(url.Values); ok { + // TODO(jbd): Cast numeric values to int64 or float64. + return vals.Get(key) + } + if raw, ok := t.raw.(map[string]interface{}); ok { + return raw[key] + } + return nil +} + +// expired reports whether the token is expired. +// t must be non-nil. +func (t *Token) expired() bool { + if t.Expiry.IsZero() { + return false + } + return t.Expiry.Add(-expiryDelta).Before(time.Now()) +} + +// Valid reports whether t is non-nil, has an AccessToken, and is not expired. +func (t *Token) Valid() bool { + return t != nil && t.AccessToken != "" && !t.expired() +} diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/token_test.go b/Godeps/_workspace/src/golang.org/x/oauth2/token_test.go new file mode 100644 index 00000000..739eeb2a --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/oauth2/token_test.go @@ -0,0 +1,50 @@ +// Copyright 2014 The oauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package oauth2 + +import ( + "testing" + "time" +) + +func TestTokenExtra(t *testing.T) { + type testCase struct { + key string + val interface{} + want interface{} + } + const key = "extra-key" + cases := []testCase{ + {key: key, val: "abc", want: "abc"}, + {key: key, val: 123, want: 123}, + {key: key, val: "", want: ""}, + {key: "other-key", val: "def", want: nil}, + } + for _, tc := range cases { + extra := make(map[string]interface{}) + extra[tc.key] = tc.val + tok := &Token{raw: extra} + if got, want := tok.Extra(key), tc.want; got != want { + t.Errorf("Extra(%q) = %q; want %q", key, got, want) + } + } +} + +func TestTokenExpiry(t *testing.T) { + now := time.Now() + cases := []struct { + name string + tok *Token + want bool + }{ + {name: "12 seconds", tok: &Token{Expiry: now.Add(12 * time.Second)}, want: false}, + {name: "10 seconds", tok: &Token{Expiry: now.Add(expiryDelta)}, want: true}, + } + for _, tc := range cases { + if got, want := tc.tok.expired(), tc.want; got != want { + t.Errorf("expired (%q) = %v; want %v", tc.name, got, want) + } + } +} diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/transport.go b/Godeps/_workspace/src/golang.org/x/oauth2/transport.go new file mode 100644 index 00000000..10339a0b --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/oauth2/transport.go @@ -0,0 +1,138 @@ +// Copyright 2014 The oauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package oauth2 + +import ( + "errors" + "io" + "net/http" + "sync" +) + +// Transport is an http.RoundTripper that makes OAuth 2.0 HTTP requests, +// wrapping a base RoundTripper and adding an Authorization header +// with a token from the supplied Sources. +// +// Transport is a low-level mechanism. Most code will use the +// higher-level Config.Client method instead. +type Transport struct { + // Source supplies the token to add to outgoing requests' + // Authorization headers. + Source TokenSource + + // Base is the base RoundTripper used to make HTTP requests. + // If nil, http.DefaultTransport is used. + Base http.RoundTripper + + mu sync.Mutex // guards modReq + modReq map[*http.Request]*http.Request // original -> modified +} + +// RoundTrip authorizes and authenticates the request with an +// access token. If no token exists or token is expired, +// tries to refresh/fetch a new token. +func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { + if t.Source == nil { + return nil, errors.New("oauth2: Transport's Source is nil") + } + token, err := t.Source.Token() + if err != nil { + return nil, err + } + + req2 := cloneRequest(req) // per RoundTripper contract + token.SetAuthHeader(req2) + t.setModReq(req, req2) + res, err := t.base().RoundTrip(req2) + if err != nil { + t.setModReq(req, nil) + return nil, err + } + res.Body = &onEOFReader{ + rc: res.Body, + fn: func() { t.setModReq(req, nil) }, + } + return res, nil +} + +// CancelRequest cancels an in-flight request by closing its connection. +func (t *Transport) CancelRequest(req *http.Request) { + type canceler interface { + CancelRequest(*http.Request) + } + if cr, ok := t.base().(canceler); ok { + t.mu.Lock() + modReq := t.modReq[req] + delete(t.modReq, req) + t.mu.Unlock() + cr.CancelRequest(modReq) + } +} + +func (t *Transport) base() http.RoundTripper { + if t.Base != nil { + return t.Base + } + return http.DefaultTransport +} + +func (t *Transport) setModReq(orig, mod *http.Request) { + t.mu.Lock() + defer t.mu.Unlock() + if t.modReq == nil { + t.modReq = make(map[*http.Request]*http.Request) + } + if mod == nil { + delete(t.modReq, orig) + } else { + t.modReq[orig] = mod + } +} + +// cloneRequest returns a clone of the provided *http.Request. +// The clone is a shallow copy of the struct and its Header map. +func cloneRequest(r *http.Request) *http.Request { + // shallow copy of the struct + r2 := new(http.Request) + *r2 = *r + // deep copy of the Header + r2.Header = make(http.Header, len(r.Header)) + for k, s := range r.Header { + r2.Header[k] = append([]string(nil), s...) + } + return r2 +} + +type onEOFReader struct { + rc io.ReadCloser + fn func() +} + +func (r *onEOFReader) Read(p []byte) (n int, err error) { + n, err = r.rc.Read(p) + if err == io.EOF { + r.runFunc() + } + return +} + +func (r *onEOFReader) Close() error { + err := r.rc.Close() + r.runFunc() + return err +} + +func (r *onEOFReader) runFunc() { + if fn := r.fn; fn != nil { + fn() + r.fn = nil + } +} + +type errorTransport struct{ err error } + +func (t errorTransport) RoundTrip(*http.Request) (*http.Response, error) { + return nil, t.err +} diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/transport_test.go b/Godeps/_workspace/src/golang.org/x/oauth2/transport_test.go new file mode 100644 index 00000000..efb8232a --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/oauth2/transport_test.go @@ -0,0 +1,53 @@ +package oauth2 + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" +) + +type tokenSource struct{ token *Token } + +func (t *tokenSource) Token() (*Token, error) { + return t.token, nil +} + +func TestTransportTokenSource(t *testing.T) { + ts := &tokenSource{ + token: &Token{ + AccessToken: "abc", + }, + } + tr := &Transport{ + Source: ts, + } + server := newMockServer(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != "Bearer abc" { + t.Errorf("Transport doesn't set the Authorization header from the fetched token") + } + }) + defer server.Close() + client := http.Client{Transport: tr} + client.Get(server.URL) +} + +func TestTokenValidNoAccessToken(t *testing.T) { + token := &Token{} + if token.Valid() { + t.Errorf("Token should not be valid with no access token") + } +} + +func TestExpiredWithExpiry(t *testing.T) { + token := &Token{ + Expiry: time.Now().Add(-5 * time.Hour), + } + if token.Valid() { + t.Errorf("Token should not be valid if it expired in the past") + } +} + +func newMockServer(handler func(w http.ResponseWriter, r *http.Request)) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(handler)) +}