From ded3513ab74cdf7b2d8a6f94fc615b2f256cee91 Mon Sep 17 00:00:00 2001 From: willemvd Date: Tue, 10 Jan 2017 14:38:53 +0100 Subject: [PATCH 01/40] initial stuff for oauth2 login, fails on: import cycle not allowed package main imports code.gitea.io/gitea/cmd imports code.gitea.io/gitea/models imports code.gitea.io/gitea/modules/auth/oauth2 imports code.gitea.io/gitea/modules/context imports code.gitea.io/gitea/models --- cmd/web.go | 2 + models/login_source.go | 87 ++- modules/auth/auth_form.go | 54 +- modules/auth/oauth2/oauth2.go | 132 ++++ routers/admin/auths.go | 15 + vendor/github.com/gorilla/context/LICENSE | 27 + vendor/github.com/gorilla/context/README.md | 10 + vendor/github.com/gorilla/context/context.go | 143 ++++ vendor/github.com/gorilla/context/doc.go | 88 +++ vendor/github.com/gorilla/mux/LICENSE | 27 + vendor/github.com/gorilla/mux/README.md | 299 ++++++++ .../github.com/gorilla/mux/context_gorilla.go | 26 + .../github.com/gorilla/mux/context_native.go | 24 + vendor/github.com/gorilla/mux/doc.go | 235 +++++++ vendor/github.com/gorilla/mux/mux.go | 542 +++++++++++++++ vendor/github.com/gorilla/mux/regexp.go | 316 +++++++++ vendor/github.com/gorilla/mux/route.go | 636 +++++++++++++++++ .../github.com/gorilla/securecookie/LICENSE | 27 + .../github.com/gorilla/securecookie/README.md | 78 +++ vendor/github.com/gorilla/securecookie/doc.go | 61 ++ .../github.com/gorilla/securecookie/fuzz.go | 25 + .../gorilla/securecookie/securecookie.go | 646 ++++++++++++++++++ vendor/github.com/gorilla/sessions/LICENSE | 27 + vendor/github.com/gorilla/sessions/README.md | 81 +++ vendor/github.com/gorilla/sessions/doc.go | 199 ++++++ vendor/github.com/gorilla/sessions/lex.go | 102 +++ .../github.com/gorilla/sessions/sessions.go | 241 +++++++ vendor/github.com/gorilla/sessions/store.go | 295 ++++++++ vendor/github.com/markbates/goth/LICENSE.txt | 22 + vendor/github.com/markbates/goth/README.md | 113 +++ vendor/github.com/markbates/goth/doc.go | 10 + .../markbates/goth/gothic/gothic.go | 190 ++++++ vendor/github.com/markbates/goth/provider.go | 51 ++ .../markbates/goth/providers/github/github.go | 207 ++++++ .../goth/providers/github/session.go | 56 ++ vendor/github.com/markbates/goth/session.go | 21 + vendor/github.com/markbates/goth/user.go | 30 + vendor/golang.org/x/net/context/context.go | 156 +++++ vendor/golang.org/x/net/context/go17.go | 72 ++ vendor/golang.org/x/net/context/pre_go17.go | 300 ++++++++ vendor/golang.org/x/oauth2/AUTHORS | 3 + vendor/golang.org/x/oauth2/CONTRIBUTING.md | 31 + vendor/golang.org/x/oauth2/CONTRIBUTORS | 3 + vendor/golang.org/x/oauth2/LICENSE | 27 + vendor/golang.org/x/oauth2/README.md | 65 ++ .../golang.org/x/oauth2/client_appengine.go | 25 + vendor/golang.org/x/oauth2/internal/oauth2.go | 76 +++ vendor/golang.org/x/oauth2/internal/token.go | 227 ++++++ .../golang.org/x/oauth2/internal/transport.go | 69 ++ vendor/golang.org/x/oauth2/oauth2.go | 341 +++++++++ vendor/golang.org/x/oauth2/token.go | 158 +++++ vendor/golang.org/x/oauth2/transport.go | 132 ++++ vendor/vendor.json | 24 + 53 files changed, 6822 insertions(+), 32 deletions(-) create mode 100644 modules/auth/oauth2/oauth2.go create mode 100644 vendor/github.com/gorilla/context/LICENSE create mode 100644 vendor/github.com/gorilla/context/README.md create mode 100644 vendor/github.com/gorilla/context/context.go create mode 100644 vendor/github.com/gorilla/context/doc.go create mode 100644 vendor/github.com/gorilla/mux/LICENSE create mode 100644 vendor/github.com/gorilla/mux/README.md create mode 100644 vendor/github.com/gorilla/mux/context_gorilla.go create mode 100644 vendor/github.com/gorilla/mux/context_native.go create mode 100644 vendor/github.com/gorilla/mux/doc.go create mode 100644 vendor/github.com/gorilla/mux/mux.go create mode 100644 vendor/github.com/gorilla/mux/regexp.go create mode 100644 vendor/github.com/gorilla/mux/route.go create mode 100644 vendor/github.com/gorilla/securecookie/LICENSE create mode 100644 vendor/github.com/gorilla/securecookie/README.md create mode 100644 vendor/github.com/gorilla/securecookie/doc.go create mode 100644 vendor/github.com/gorilla/securecookie/fuzz.go create mode 100644 vendor/github.com/gorilla/securecookie/securecookie.go create mode 100644 vendor/github.com/gorilla/sessions/LICENSE create mode 100644 vendor/github.com/gorilla/sessions/README.md create mode 100644 vendor/github.com/gorilla/sessions/doc.go create mode 100644 vendor/github.com/gorilla/sessions/lex.go create mode 100644 vendor/github.com/gorilla/sessions/sessions.go create mode 100644 vendor/github.com/gorilla/sessions/store.go create mode 100644 vendor/github.com/markbates/goth/LICENSE.txt create mode 100644 vendor/github.com/markbates/goth/README.md create mode 100644 vendor/github.com/markbates/goth/doc.go create mode 100644 vendor/github.com/markbates/goth/gothic/gothic.go create mode 100644 vendor/github.com/markbates/goth/provider.go create mode 100644 vendor/github.com/markbates/goth/providers/github/github.go create mode 100644 vendor/github.com/markbates/goth/providers/github/session.go create mode 100644 vendor/github.com/markbates/goth/session.go create mode 100644 vendor/github.com/markbates/goth/user.go create mode 100644 vendor/golang.org/x/net/context/context.go create mode 100644 vendor/golang.org/x/net/context/go17.go create mode 100644 vendor/golang.org/x/net/context/pre_go17.go create mode 100644 vendor/golang.org/x/oauth2/AUTHORS create mode 100644 vendor/golang.org/x/oauth2/CONTRIBUTING.md create mode 100644 vendor/golang.org/x/oauth2/CONTRIBUTORS create mode 100644 vendor/golang.org/x/oauth2/LICENSE create mode 100644 vendor/golang.org/x/oauth2/README.md create mode 100644 vendor/golang.org/x/oauth2/client_appengine.go create mode 100644 vendor/golang.org/x/oauth2/internal/oauth2.go create mode 100644 vendor/golang.org/x/oauth2/internal/token.go create mode 100644 vendor/golang.org/x/oauth2/internal/transport.go create mode 100644 vendor/golang.org/x/oauth2/oauth2.go create mode 100644 vendor/golang.org/x/oauth2/token.go create mode 100644 vendor/golang.org/x/oauth2/transport.go diff --git a/cmd/web.go b/cmd/web.go index bbf386d43f3e..2658a4bd6119 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -40,6 +40,7 @@ import ( "github.com/go-macaron/toolbox" "github.com/urfave/cli" macaron "gopkg.in/macaron.v1" + "code.gitea.io/gitea/modules/auth/oauth2" ) // CmdWeb represents the available web sub-command. @@ -193,6 +194,7 @@ func runWeb(ctx *cli.Context) error { m.Post("/sign_up", bindIgnErr(auth.RegisterForm{}), user.SignUpPost) m.Get("/reset_password", user.ResetPasswd) m.Post("/reset_password", user.ResetPasswdPost) + m.Get("/oauth2/:provider/callback", oauth2.Callback) }, reqSignOut) m.Group("/user/settings", func() { diff --git a/models/login_source.go b/models/login_source.go index 3fe5e172e3cc..cd1a818b21a7 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -21,6 +21,7 @@ import ( "code.gitea.io/gitea/modules/auth/ldap" "code.gitea.io/gitea/modules/auth/pam" + "code.gitea.io/gitea/modules/auth/oauth2" "code.gitea.io/gitea/modules/log" ) @@ -35,14 +36,16 @@ const ( LoginSMTP // 3 LoginPAM // 4 LoginDLDAP // 5 + LoginOAuth2 // 6 ) // LoginNames contains the name of LoginType values. var LoginNames = map[LoginType]string{ - LoginLDAP: "LDAP (via BindDN)", - LoginDLDAP: "LDAP (simple auth)", // Via direct bind - LoginSMTP: "SMTP", - LoginPAM: "PAM", + LoginLDAP: "LDAP (via BindDN)", + LoginDLDAP: "LDAP (simple auth)", // Via direct bind + LoginSMTP: "SMTP", + LoginPAM: "PAM", + LoginOAuth2: "OAuth2", } // SecurityProtocolNames contains the name of SecurityProtocol values. @@ -57,6 +60,7 @@ var ( _ core.Conversion = &LDAPConfig{} _ core.Conversion = &SMTPConfig{} _ core.Conversion = &PAMConfig{} + _ core.Conversion = &OAuth2Config{} ) // LDAPConfig holds configuration for LDAP login source. @@ -115,6 +119,25 @@ func (cfg *PAMConfig) ToDB() ([]byte, error) { return json.Marshal(cfg) } +// OAuth2 holds configuration for the OAuth2 login source. +type OAuth2Config struct { + Provider string + ClientId string + ClientSecret string + TLS bool + SkipVerify bool +} + +// FromDB fills up an OAuth2Config from serialized format. +func (cfg *OAuth2Config) FromDB(bs []byte) error { + return json.Unmarshal(bs, cfg) +} + +// ToDB exports an SMTPConfig to a serialized format. +func (cfg *OAuth2Config) ToDB() ([]byte, error) { + return json.Marshal(cfg) +} + // LoginSource represents an external way for authorizing users. type LoginSource struct { ID int64 `xorm:"pk autoincr"` @@ -162,6 +185,8 @@ func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) { source.Cfg = new(SMTPConfig) case LoginPAM: source.Cfg = new(PAMConfig) + case LoginOAuth2: + source.Cfg = new(OAuth2Config) default: panic("unrecognized login source type: " + com.ToStr(*val)) } @@ -203,11 +228,16 @@ func (source *LoginSource) IsPAM() bool { return source.Type == LoginPAM } +// IsOAuth2 returns true of this source is of the OAuth2 type. +func (source *LoginSource) IsOAuth2() bool { + return source.Type == LoginOAuth2 +} + // HasTLS returns true of this source supports TLS. func (source *LoginSource) HasTLS() bool { return ((source.IsLDAP() || source.IsDLDAP()) && source.LDAP().SecurityProtocol > ldap.SecurityProtocolUnencrypted) || - source.IsSMTP() + source.IsSMTP() || source.IsOAuth2() } // UseTLS returns true of this source is configured to use TLS. @@ -217,6 +247,8 @@ func (source *LoginSource) UseTLS() bool { return source.LDAP().SecurityProtocol != ldap.SecurityProtocolUnencrypted case LoginSMTP: return source.SMTP().TLS + case LoginOAuth2: + return source.OAuth2().TLS } return false @@ -230,6 +262,8 @@ func (source *LoginSource) SkipVerify() bool { return source.LDAP().SkipVerify case LoginSMTP: return source.SMTP().SkipVerify + case LoginOAuth2: + return source.OAuth2().SkipVerify } return false @@ -250,6 +284,11 @@ func (source *LoginSource) PAM() *PAMConfig { return source.Cfg.(*PAMConfig) } +// OAuth2 returns OAuth2Config for this source, if of OAuth2 type. +func (source *LoginSource) OAuth2() *OAuth2Config { + return source.Cfg.(*OAuth2Config) +} + // CreateLoginSource inserts a LoginSource in the DB if not already // existing with the given name. func CreateLoginSource(source *LoginSource) error { @@ -266,7 +305,7 @@ func CreateLoginSource(source *LoginSource) error { // LoginSources returns a slice of all login sources found in DB. func LoginSources() ([]*LoginSource, error) { - auths := make([]*LoginSource, 0, 5) + auths := make([]*LoginSource, 0, 6) return auths, x.Find(&auths) } @@ -526,6 +565,38 @@ func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMCon return user, CreateUser(user) } +// ________ _____ __ .__ ________ +// \_____ \ / _ \ __ ___/ |_| |__ \_____ \ +// / | \ / /_\ \| | \ __\ | \ / ____/ +// / | \/ | \ | /| | | Y \/ \ +// \_______ /\____|__ /____/ |__| |___| /\_______ \ +// \/ \/ \/ \/ + +// LoginViaOAuth2 queries if login/password is valid against the OAuth2.0 provider, +// and create a local user if success when enabled. +func LoginViaOAuth2(user *User, login, password string, sourceID int64, cfg *OAuth2Config, autoRegister bool) (*User, error) { + email, err := oauth2.Auth(cfg.Provider, cfg.ClientId, cfg.ClientSecret, login, password) + if err != nil { + return nil, err + } + + if !autoRegister { + return user, nil + } + + user = &User{ + LowerName: strings.ToLower(login), + Name: login, + Email: email, + Passwd: password, + LoginType: LoginOAuth2, + LoginSource: sourceID, + LoginName: login, + IsActive: true, + } + return user, CreateUser(user) +} + // ExternalUserLogin attempts a login using external source types. func ExternalUserLogin(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) { if !source.IsActived { @@ -539,6 +610,8 @@ func ExternalUserLogin(user *User, login, password string, source *LoginSource, return LoginViaSMTP(user, login, password, source.ID, source.Cfg.(*SMTPConfig), autoRegister) case LoginPAM: return LoginViaPAM(user, login, password, source.ID, source.Cfg.(*PAMConfig), autoRegister) + case LoginOAuth2: + return LoginViaOAuth2(user, login, password, source.ID, source.Cfg.(*OAuth2Config), autoRegister) } return nil, ErrUnsupportedLoginType @@ -580,7 +653,7 @@ func UserSignIn(username, password string) (*User, error) { } } - sources := make([]*LoginSource, 0, 3) + sources := make([]*LoginSource, 0, 6) if err = x.UseBool().Find(&sources, &LoginSource{IsActived: true}); err != nil { return nil, err } diff --git a/modules/auth/auth_form.go b/modules/auth/auth_form.go index 78ba2b899788..c403a9ce8021 100644 --- a/modules/auth/auth_form.go +++ b/modules/auth/auth_form.go @@ -7,35 +7,39 @@ package auth import ( "github.com/go-macaron/binding" "gopkg.in/macaron.v1" + "golang.org/x/oauth2" ) // AuthenticationForm form for authentication type AuthenticationForm struct { - ID int64 - Type int `binding:"Range(2,5)"` - Name string `binding:"Required;MaxSize(30)"` - Host string - Port int - BindDN string - BindPassword string - UserBase string - UserDN string - AttributeUsername string - AttributeName string - AttributeSurname string - AttributeMail string - AttributesInBind bool - Filter string - AdminFilter string - IsActive bool - SMTPAuth string - SMTPHost string - SMTPPort int - AllowedDomains string - SecurityProtocol int `binding:"Range(0,2)"` - TLS bool - SkipVerify bool - PAMServiceName string + ID int64 + Type int `binding:"Range(2,5)"` + Name string `binding:"Required;MaxSize(30)"` + Host string + Port int + BindDN string + BindPassword string + UserBase string + UserDN string + AttributeUsername string + AttributeName string + AttributeSurname string + AttributeMail string + AttributesInBind bool + Filter string + AdminFilter string + IsActive bool + SMTPAuth string + SMTPHost string + SMTPPort int + AllowedDomains string + SecurityProtocol int `binding:"Range(0,2)"` + TLS bool + SkipVerify bool + PAMServiceName string + OAuth2Provider string + OAuth2ClientId string + OAuth2ClientSecret string } // Validate validates fields diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go new file mode 100644 index 000000000000..d9de6cbefa2f --- /dev/null +++ b/modules/auth/oauth2/oauth2.go @@ -0,0 +1,132 @@ +package oauth2 + +import ( + "github.com/markbates/goth" + "github.com/markbates/goth/gothic" + "github.com/gorilla/sessions" + "os" + "github.com/markbates/goth/providers/github" + "code.gitea.io/gitea/modules/setting" + "net/http" + "fmt" + "code.gitea.io/gitea/modules/context" + "github.com/go-macaron/session" + "github.com/satori/go.uuid" + "encoding/base64" + "strings" +) + +var ( + oAuthUserSessionKey = "oauth2-user" + oAuthStateUniqueIdSessionKey = "oauth2-state-key" +) + +func init() { + gothic.Store = sessions.NewFilesystemStore(os.TempDir(), []byte("gitea-oauth-sessions")) +} + +// Auth OAuth2 auth service +func Auth(provider, clientId, clientSecret, userName, passwd string) (string, error) { + goth.UseProviders( + github.New(clientId, clientSecret, setting.AppURL + "user/oauth2/github/callback"), + ) + + ctx := context.Context{} + request := ctx.Req.Request + session := ctx.Session + + oauth2User := session.Get(oAuthUserSessionKey) + if oauth2User != nil { + // already authenticated, stop OAuth2 flow + session.Delete(oAuthUserSessionKey) + return oauth2User.(goth.User).Email, nil + } + + gothic.SetState = func(req *http.Request) string { + state, uniqueId := encodeState(req.URL.String()) + + session.Set(oAuthStateUniqueIdSessionKey, uniqueId) + + return state + } + gothic.GetProviderName = func(r *http.Request) (string, error) { + provider := ctx.Params(":provider") + if provider == "" { + provider = "github" + } + return provider, nil + } + + gothic.BeginAuthHandler(ctx.Resp, request) + + + return nil, nil +} + +// handle OAuth callback, resolve to a goth user and send back to original url +// this will trigger a new authentication request, but because we save it in the session we can use that +func Callback(ctx *context.Context, session session.Store) { + request := ctx.Req.Request + + res := ctx.Resp + + user, err := gothic.CompleteUserAuth(res, request) + if err != nil { + fmt.Fprintln(res, err) + return + } + + redirectUrl := "" + + state := gothic.GetState(request) + if state == "" { + redirectUrl = "/TROUBLE" + } + + decodedState, uniqueId, err := decodeState(state) + if err != nil { + redirectUrl = "/TROUBLE2" + } else { + uniqueIdSession := session.Get(oAuthStateUniqueIdSessionKey) + session.Delete(oAuthStateUniqueIdSessionKey) + + if uniqueIdSession != nil && uniqueIdSession == uniqueId { + redirectUrl = decodedState + session.Set(oAuthUserSessionKey, user) + } else { + redirectUrl = "/TROUBLE3" + } + } + + ctx.Redirect(redirectUrl) +} + +func encodeState(state string) (string, string) { + uniqueId := uuid.NewV4().String() + data := []byte(uniqueId + "|" + state) + + return base64.URLEncoding.EncodeToString(data), uniqueId +} + +func decodeState(encodedState string) (string, string, error) { + data, err := base64.URLEncoding.DecodeString(encodedState) + + if err != nil { + return nil, nil, err + } + + decodedState := string(data[:]) + slices := strings.Split(decodedState, "|") + + uniqueId := slices[0] + + // validate uuid + _, err = uuid.FromString(uniqueId) + if err != nil { + return nil, nil, err + } + + state := slices[1] + + return state, uniqueId, nil +} diff --git a/routers/admin/auths.go b/routers/admin/auths.go index d437dc4b80e6..e90dee6b613d 100644 --- a/routers/admin/auths.go +++ b/routers/admin/auths.go @@ -53,6 +53,7 @@ var ( {models.LoginNames[models.LoginDLDAP], models.LoginDLDAP}, {models.LoginNames[models.LoginSMTP], models.LoginSMTP}, {models.LoginNames[models.LoginPAM], models.LoginPAM}, + {models.LoginNames[models.LoginOAuth2], models.LoginOAuth2}, } securityProtocols = []dropdownItem{ {models.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted}, @@ -113,6 +114,16 @@ func parseSMTPConfig(form auth.AuthenticationForm) *models.SMTPConfig { } } +func parseOAuth2Config(form auth.AuthenticationForm) *models.OAuth2Config { + return &models.OAuth2Config{ + Provider: form.OAuth2Provider, + ClientId: form.OAuth2ClientId, + ClientSecret: form.OAuth2ClientSecret, + TLS: form.TLS, + SkipVerify: form.SkipVerify, + } +} + // NewAuthSourcePost response for adding an auth source func NewAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) { ctx.Data["Title"] = ctx.Tr("admin.auths.new") @@ -138,6 +149,8 @@ func NewAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) { config = &models.PAMConfig{ ServiceName: form.PAMServiceName, } + case models.LoginOAuth2: + config = parseOAuth2Config(form) default: ctx.Error(400) return @@ -221,6 +234,8 @@ func EditAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) { config = &models.PAMConfig{ ServiceName: form.PAMServiceName, } + case models.LoginOAuth2: + config = parseOAuth2Config(form) default: ctx.Error(400) return diff --git a/vendor/github.com/gorilla/context/LICENSE b/vendor/github.com/gorilla/context/LICENSE new file mode 100644 index 000000000000..0e5fb872800d --- /dev/null +++ b/vendor/github.com/gorilla/context/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 Rodrigo Moraes. 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/vendor/github.com/gorilla/context/README.md b/vendor/github.com/gorilla/context/README.md new file mode 100644 index 000000000000..08f86693bcd8 --- /dev/null +++ b/vendor/github.com/gorilla/context/README.md @@ -0,0 +1,10 @@ +context +======= +[![Build Status](https://travis-ci.org/gorilla/context.png?branch=master)](https://travis-ci.org/gorilla/context) + +gorilla/context is a general purpose registry for global request variables. + +> Note: gorilla/context, having been born well before `context.Context` existed, does not play well +> with the shallow copying of the request that [`http.Request.WithContext`](https://golang.org/pkg/net/http/#Request.WithContext) (added to net/http Go 1.7 onwards) performs. You should either use *just* gorilla/context, or moving forward, the new `http.Request.Context()`. + +Read the full documentation here: http://www.gorillatoolkit.org/pkg/context diff --git a/vendor/github.com/gorilla/context/context.go b/vendor/github.com/gorilla/context/context.go new file mode 100644 index 000000000000..81cb128b19ca --- /dev/null +++ b/vendor/github.com/gorilla/context/context.go @@ -0,0 +1,143 @@ +// Copyright 2012 The Gorilla 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 ( + "net/http" + "sync" + "time" +) + +var ( + mutex sync.RWMutex + data = make(map[*http.Request]map[interface{}]interface{}) + datat = make(map[*http.Request]int64) +) + +// Set stores a value for a given key in a given request. +func Set(r *http.Request, key, val interface{}) { + mutex.Lock() + if data[r] == nil { + data[r] = make(map[interface{}]interface{}) + datat[r] = time.Now().Unix() + } + data[r][key] = val + mutex.Unlock() +} + +// Get returns a value stored for a given key in a given request. +func Get(r *http.Request, key interface{}) interface{} { + mutex.RLock() + if ctx := data[r]; ctx != nil { + value := ctx[key] + mutex.RUnlock() + return value + } + mutex.RUnlock() + return nil +} + +// GetOk returns stored value and presence state like multi-value return of map access. +func GetOk(r *http.Request, key interface{}) (interface{}, bool) { + mutex.RLock() + if _, ok := data[r]; ok { + value, ok := data[r][key] + mutex.RUnlock() + return value, ok + } + mutex.RUnlock() + return nil, false +} + +// GetAll returns all stored values for the request as a map. Nil is returned for invalid requests. +func GetAll(r *http.Request) map[interface{}]interface{} { + mutex.RLock() + if context, ok := data[r]; ok { + result := make(map[interface{}]interface{}, len(context)) + for k, v := range context { + result[k] = v + } + mutex.RUnlock() + return result + } + mutex.RUnlock() + return nil +} + +// GetAllOk returns all stored values for the request as a map and a boolean value that indicates if +// the request was registered. +func GetAllOk(r *http.Request) (map[interface{}]interface{}, bool) { + mutex.RLock() + context, ok := data[r] + result := make(map[interface{}]interface{}, len(context)) + for k, v := range context { + result[k] = v + } + mutex.RUnlock() + return result, ok +} + +// Delete removes a value stored for a given key in a given request. +func Delete(r *http.Request, key interface{}) { + mutex.Lock() + if data[r] != nil { + delete(data[r], key) + } + mutex.Unlock() +} + +// Clear removes all values stored for a given request. +// +// This is usually called by a handler wrapper to clean up request +// variables at the end of a request lifetime. See ClearHandler(). +func Clear(r *http.Request) { + mutex.Lock() + clear(r) + mutex.Unlock() +} + +// clear is Clear without the lock. +func clear(r *http.Request) { + delete(data, r) + delete(datat, r) +} + +// Purge removes request data stored for longer than maxAge, in seconds. +// It returns the amount of requests removed. +// +// If maxAge <= 0, all request data is removed. +// +// This is only used for sanity check: in case context cleaning was not +// properly set some request data can be kept forever, consuming an increasing +// amount of memory. In case this is detected, Purge() must be called +// periodically until the problem is fixed. +func Purge(maxAge int) int { + mutex.Lock() + count := 0 + if maxAge <= 0 { + count = len(data) + data = make(map[*http.Request]map[interface{}]interface{}) + datat = make(map[*http.Request]int64) + } else { + min := time.Now().Unix() - int64(maxAge) + for r := range data { + if datat[r] < min { + clear(r) + count++ + } + } + } + mutex.Unlock() + return count +} + +// ClearHandler wraps an http.Handler and clears request values at the end +// of a request lifetime. +func ClearHandler(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer Clear(r) + h.ServeHTTP(w, r) + }) +} diff --git a/vendor/github.com/gorilla/context/doc.go b/vendor/github.com/gorilla/context/doc.go new file mode 100644 index 000000000000..448d1bfcac6e --- /dev/null +++ b/vendor/github.com/gorilla/context/doc.go @@ -0,0 +1,88 @@ +// Copyright 2012 The Gorilla 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 stores values shared during a request lifetime. + +Note: gorilla/context, having been born well before `context.Context` existed, +does not play well > with the shallow copying of the request that +[`http.Request.WithContext`](https://golang.org/pkg/net/http/#Request.WithContext) +(added to net/http Go 1.7 onwards) performs. You should either use *just* +gorilla/context, or moving forward, the new `http.Request.Context()`. + +For example, a router can set variables extracted from the URL and later +application handlers can access those values, or it can be used to store +sessions values to be saved at the end of a request. There are several +others common uses. + +The idea was posted by Brad Fitzpatrick to the go-nuts mailing list: + + http://groups.google.com/group/golang-nuts/msg/e2d679d303aa5d53 + +Here's the basic usage: first define the keys that you will need. The key +type is interface{} so a key can be of any type that supports equality. +Here we define a key using a custom int type to avoid name collisions: + + package foo + + import ( + "github.com/gorilla/context" + ) + + type key int + + const MyKey key = 0 + +Then set a variable. Variables are bound to an http.Request object, so you +need a request instance to set a value: + + context.Set(r, MyKey, "bar") + +The application can later access the variable using the same key you provided: + + func MyHandler(w http.ResponseWriter, r *http.Request) { + // val is "bar". + val := context.Get(r, foo.MyKey) + + // returns ("bar", true) + val, ok := context.GetOk(r, foo.MyKey) + // ... + } + +And that's all about the basic usage. We discuss some other ideas below. + +Any type can be stored in the context. To enforce a given type, make the key +private and wrap Get() and Set() to accept and return values of a specific +type: + + type key int + + const mykey key = 0 + + // GetMyKey returns a value for this package from the request values. + func GetMyKey(r *http.Request) SomeType { + if rv := context.Get(r, mykey); rv != nil { + return rv.(SomeType) + } + return nil + } + + // SetMyKey sets a value for this package in the request values. + func SetMyKey(r *http.Request, val SomeType) { + context.Set(r, mykey, val) + } + +Variables must be cleared at the end of a request, to remove all values +that were stored. This can be done in an http.Handler, after a request was +served. Just call Clear() passing the request: + + context.Clear(r) + +...or use ClearHandler(), which conveniently wraps an http.Handler to clear +variables at the end of a request lifetime. + +The Routers from the packages gorilla/mux and gorilla/pat call Clear() +so if you are using either of them you don't need to clear the context manually. +*/ +package context diff --git a/vendor/github.com/gorilla/mux/LICENSE b/vendor/github.com/gorilla/mux/LICENSE new file mode 100644 index 000000000000..0e5fb872800d --- /dev/null +++ b/vendor/github.com/gorilla/mux/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 Rodrigo Moraes. 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/vendor/github.com/gorilla/mux/README.md b/vendor/github.com/gorilla/mux/README.md new file mode 100644 index 000000000000..fa79a6bc3aee --- /dev/null +++ b/vendor/github.com/gorilla/mux/README.md @@ -0,0 +1,299 @@ +gorilla/mux +=== +[![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) +[![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux) + +![Gorilla Logo](http://www.gorillatoolkit.org/static/images/gorilla-icon-64.png) + +http://www.gorillatoolkit.org/pkg/mux + +Package `gorilla/mux` implements a request router and dispatcher for matching incoming requests to +their respective handler. + +The name mux stands for "HTTP request multiplexer". Like the standard `http.ServeMux`, `mux.Router` matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are: + +* It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`. +* Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers. +* URL hosts and paths can have variables with an optional regular expression. +* Registered URLs can be built, or "reversed", which helps maintaining references to resources. +* Routes can be used as subrouters: nested routes are only tested if the parent route matches. This is useful to define groups of routes that share common conditions like a host, a path prefix or other repeated attributes. As a bonus, this optimizes request matching. + +--- + +* [Install](#install) +* [Examples](#examples) +* [Matching Routes](#matching-routes) +* [Static Files](#static-files) +* [Registered URLs](#registered-urls) +* [Full Example](#full-example) + +--- + +## Install + +With a [correctly configured](https://golang.org/doc/install#testing) Go toolchain: + +```sh +go get -u github.com/gorilla/mux +``` + +## Examples + +Let's start registering a couple of URL paths and handlers: + +```go +func main() { + r := mux.NewRouter() + r.HandleFunc("/", HomeHandler) + r.HandleFunc("/products", ProductsHandler) + r.HandleFunc("/articles", ArticlesHandler) + http.Handle("/", r) +} +``` + +Here we register three routes mapping URL paths to handlers. This is equivalent to how `http.HandleFunc()` works: if an incoming request URL matches one of the paths, the corresponding handler is called passing (`http.ResponseWriter`, `*http.Request`) as parameters. + +Paths can have variables. They are defined using the format `{name}` or `{name:pattern}`. If a regular expression pattern is not defined, the matched variable will be anything until the next slash. For example: + +```go +r := mux.NewRouter() +r.HandleFunc("/products/{key}", ProductHandler) +r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) +r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) +``` + +The names are used to create a map of route variables which can be retrieved calling `mux.Vars()`: + +```go +vars := mux.Vars(request) +category := vars["category"] +``` + +And this is all you need to know about the basic usage. More advanced options are explained below. + +### Matching Routes + +Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables: + +```go +r := mux.NewRouter() +// Only matches if domain is "www.example.com". +r.Host("www.example.com") +// Matches a dynamic subdomain. +r.Host("{subdomain:[a-z]+}.domain.com") +``` + +There are several other matchers that can be added. To match path prefixes: + +```go +r.PathPrefix("/products/") +``` + +...or HTTP methods: + +```go +r.Methods("GET", "POST") +``` + +...or URL schemes: + +```go +r.Schemes("https") +``` + +...or header values: + +```go +r.Headers("X-Requested-With", "XMLHttpRequest") +``` + +...or query values: + +```go +r.Queries("key", "value") +``` + +...or to use a custom matcher function: + +```go +r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { + return r.ProtoMajor == 0 +}) +``` + +...and finally, it is possible to combine several matchers in a single route: + +```go +r.HandleFunc("/products", ProductsHandler). + Host("www.example.com"). + Methods("GET"). + Schemes("http") +``` + +Setting the same matching conditions again and again can be boring, so we have a way to group several routes that share the same requirements. We call it "subrouting". + +For example, let's say we have several URLs that should only match when the host is `www.example.com`. Create a route for that host and get a "subrouter" from it: + +```go +r := mux.NewRouter() +s := r.Host("www.example.com").Subrouter() +``` + +Then register routes in the subrouter: + +```go +s.HandleFunc("/products/", ProductsHandler) +s.HandleFunc("/products/{key}", ProductHandler) +s.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) +``` + +The three URL paths we registered above will only be tested if the domain is `www.example.com`, because the subrouter is tested first. This is not only convenient, but also optimizes request matching. You can create subrouters combining any attribute matchers accepted by a route. + +Subrouters can be used to create domain or path "namespaces": you define subrouters in a central place and then parts of the app can register its paths relatively to a given subrouter. + +There's one more thing about subroutes. When a subrouter has a path prefix, the inner routes use it as base for their paths: + +```go +r := mux.NewRouter() +s := r.PathPrefix("/products").Subrouter() +// "/products/" +s.HandleFunc("/", ProductsHandler) +// "/products/{key}/" +s.HandleFunc("/{key}/", ProductHandler) +// "/products/{key}/details" +s.HandleFunc("/{key}/details", ProductDetailsHandler) +``` + +### Static Files + +Note that the path provided to `PathPrefix()` represents a "wildcard": calling +`PathPrefix("/static/").Handler(...)` means that the handler will be passed any +request that matches "/static/*". This makes it easy to serve static files with mux: + +```go +func main() { + var dir string + + flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir") + flag.Parse() + r := mux.NewRouter() + + // This will serve files under http://localhost:8000/static/ + r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) + + srv := &http.Server{ + Handler: r, + Addr: "127.0.0.1:8000", + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + log.Fatal(srv.ListenAndServe()) +} +``` + +### Registered URLs + +Now let's see how to build registered URLs. + +Routes can be named. All routes that define a name can have their URLs built, or "reversed". We define a name calling `Name()` on a route. For example: + +```go +r := mux.NewRouter() +r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). + Name("article") +``` + +To build a URL, get the route and call the `URL()` method, passing a sequence of key/value pairs for the route variables. For the previous route, we would do: + +```go +url, err := r.Get("article").URL("category", "technology", "id", "42") +``` + +...and the result will be a `url.URL` with the following path: + +``` +"/articles/technology/42" +``` + +This also works for host variables: + +```go +r := mux.NewRouter() +r.Host("{subdomain}.domain.com"). + Path("/articles/{category}/{id:[0-9]+}"). + HandlerFunc(ArticleHandler). + Name("article") + +// url.String() will be "http://news.domain.com/articles/technology/42" +url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42") +``` + +All variables defined in the route are required, and their values must conform to the corresponding patterns. These requirements guarantee that a generated URL will always match a registered route -- the only exception is for explicitly defined "build-only" routes which never match. + +Regex support also exists for matching Headers within a route. For example, we could do: + +```go +r.HeadersRegexp("Content-Type", "application/(text|json)") +``` + +...and the route will match both requests with a Content-Type of `application/json` as well as `application/text` + +There's also a way to build only the URL host or path for a route: use the methods `URLHost()` or `URLPath()` instead. For the previous route, we would do: + +```go +// "http://news.domain.com/" +host, err := r.Get("article").URLHost("subdomain", "news") + +// "/articles/technology/42" +path, err := r.Get("article").URLPath("category", "technology", "id", "42") +``` + +And if you use subrouters, host and path defined separately can be built as well: + +```go +r := mux.NewRouter() +s := r.Host("{subdomain}.domain.com").Subrouter() +s.Path("/articles/{category}/{id:[0-9]+}"). + HandlerFunc(ArticleHandler). + Name("article") + +// "http://news.domain.com/articles/technology/42" +url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42") +``` + +## Full Example + +Here's a complete, runnable example of a small `mux` based server: + +```go +package main + +import ( + "net/http" + "log" + "github.com/gorilla/mux" +) + +func YourHandler(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Gorilla!\n")) +} + +func main() { + r := mux.NewRouter() + // Routes consist of a path and a handler function. + r.HandleFunc("/", YourHandler) + + // Bind to a port and pass our router in + log.Fatal(http.ListenAndServe(":8000", r)) +} +``` + +## License + +BSD licensed. See the LICENSE file for details. diff --git a/vendor/github.com/gorilla/mux/context_gorilla.go b/vendor/github.com/gorilla/mux/context_gorilla.go new file mode 100644 index 000000000000..d7adaa8fad4f --- /dev/null +++ b/vendor/github.com/gorilla/mux/context_gorilla.go @@ -0,0 +1,26 @@ +// +build !go1.7 + +package mux + +import ( + "net/http" + + "github.com/gorilla/context" +) + +func contextGet(r *http.Request, key interface{}) interface{} { + return context.Get(r, key) +} + +func contextSet(r *http.Request, key, val interface{}) *http.Request { + if val == nil { + return r + } + + context.Set(r, key, val) + return r +} + +func contextClear(r *http.Request) { + context.Clear(r) +} diff --git a/vendor/github.com/gorilla/mux/context_native.go b/vendor/github.com/gorilla/mux/context_native.go new file mode 100644 index 000000000000..209cbea7d661 --- /dev/null +++ b/vendor/github.com/gorilla/mux/context_native.go @@ -0,0 +1,24 @@ +// +build go1.7 + +package mux + +import ( + "context" + "net/http" +) + +func contextGet(r *http.Request, key interface{}) interface{} { + return r.Context().Value(key) +} + +func contextSet(r *http.Request, key, val interface{}) *http.Request { + if val == nil { + return r + } + + return r.WithContext(context.WithValue(r.Context(), key, val)) +} + +func contextClear(r *http.Request) { + return +} diff --git a/vendor/github.com/gorilla/mux/doc.go b/vendor/github.com/gorilla/mux/doc.go new file mode 100644 index 000000000000..e9573dd8ad17 --- /dev/null +++ b/vendor/github.com/gorilla/mux/doc.go @@ -0,0 +1,235 @@ +// Copyright 2012 The Gorilla 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 mux implements a request router and dispatcher. + +The name mux stands for "HTTP request multiplexer". Like the standard +http.ServeMux, mux.Router matches incoming requests against a list of +registered routes and calls a handler for the route that matches the URL +or other conditions. The main features are: + + * Requests can be matched based on URL host, path, path prefix, schemes, + header and query values, HTTP methods or using custom matchers. + * URL hosts and paths can have variables with an optional regular + expression. + * Registered URLs can be built, or "reversed", which helps maintaining + references to resources. + * Routes can be used as subrouters: nested routes are only tested if the + parent route matches. This is useful to define groups of routes that + share common conditions like a host, a path prefix or other repeated + attributes. As a bonus, this optimizes request matching. + * It implements the http.Handler interface so it is compatible with the + standard http.ServeMux. + +Let's start registering a couple of URL paths and handlers: + + func main() { + r := mux.NewRouter() + r.HandleFunc("/", HomeHandler) + r.HandleFunc("/products", ProductsHandler) + r.HandleFunc("/articles", ArticlesHandler) + http.Handle("/", r) + } + +Here we register three routes mapping URL paths to handlers. This is +equivalent to how http.HandleFunc() works: if an incoming request URL matches +one of the paths, the corresponding handler is called passing +(http.ResponseWriter, *http.Request) as parameters. + +Paths can have variables. They are defined using the format {name} or +{name:pattern}. If a regular expression pattern is not defined, the matched +variable will be anything until the next slash. For example: + + r := mux.NewRouter() + r.HandleFunc("/products/{key}", ProductHandler) + r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) + r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) + +Groups can be used inside patterns, as long as they are non-capturing (?:re). For example: + + r.HandleFunc("/articles/{category}/{sort:(?:asc|desc|new)}", ArticlesCategoryHandler) + +The names are used to create a map of route variables which can be retrieved +calling mux.Vars(): + + vars := mux.Vars(request) + category := vars["category"] + +And this is all you need to know about the basic usage. More advanced options +are explained below. + +Routes can also be restricted to a domain or subdomain. Just define a host +pattern to be matched. They can also have variables: + + r := mux.NewRouter() + // Only matches if domain is "www.example.com". + r.Host("www.example.com") + // Matches a dynamic subdomain. + r.Host("{subdomain:[a-z]+}.domain.com") + +There are several other matchers that can be added. To match path prefixes: + + r.PathPrefix("/products/") + +...or HTTP methods: + + r.Methods("GET", "POST") + +...or URL schemes: + + r.Schemes("https") + +...or header values: + + r.Headers("X-Requested-With", "XMLHttpRequest") + +...or query values: + + r.Queries("key", "value") + +...or to use a custom matcher function: + + r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { + return r.ProtoMajor == 0 + }) + +...and finally, it is possible to combine several matchers in a single route: + + r.HandleFunc("/products", ProductsHandler). + Host("www.example.com"). + Methods("GET"). + Schemes("http") + +Setting the same matching conditions again and again can be boring, so we have +a way to group several routes that share the same requirements. +We call it "subrouting". + +For example, let's say we have several URLs that should only match when the +host is "www.example.com". Create a route for that host and get a "subrouter" +from it: + + r := mux.NewRouter() + s := r.Host("www.example.com").Subrouter() + +Then register routes in the subrouter: + + s.HandleFunc("/products/", ProductsHandler) + s.HandleFunc("/products/{key}", ProductHandler) + s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) + +The three URL paths we registered above will only be tested if the domain is +"www.example.com", because the subrouter is tested first. This is not +only convenient, but also optimizes request matching. You can create +subrouters combining any attribute matchers accepted by a route. + +Subrouters can be used to create domain or path "namespaces": you define +subrouters in a central place and then parts of the app can register its +paths relatively to a given subrouter. + +There's one more thing about subroutes. When a subrouter has a path prefix, +the inner routes use it as base for their paths: + + r := mux.NewRouter() + s := r.PathPrefix("/products").Subrouter() + // "/products/" + s.HandleFunc("/", ProductsHandler) + // "/products/{key}/" + s.HandleFunc("/{key}/", ProductHandler) + // "/products/{key}/details" + s.HandleFunc("/{key}/details", ProductDetailsHandler) + +Note that the path provided to PathPrefix() represents a "wildcard": calling +PathPrefix("/static/").Handler(...) means that the handler will be passed any +request that matches "/static/*". This makes it easy to serve static files with mux: + + func main() { + var dir string + + flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir") + flag.Parse() + r := mux.NewRouter() + + // This will serve files under http://localhost:8000/static/ + r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) + + srv := &http.Server{ + Handler: r, + Addr: "127.0.0.1:8000", + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + log.Fatal(srv.ListenAndServe()) + } + +Now let's see how to build registered URLs. + +Routes can be named. All routes that define a name can have their URLs built, +or "reversed". We define a name calling Name() on a route. For example: + + r := mux.NewRouter() + r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). + Name("article") + +To build a URL, get the route and call the URL() method, passing a sequence of +key/value pairs for the route variables. For the previous route, we would do: + + url, err := r.Get("article").URL("category", "technology", "id", "42") + +...and the result will be a url.URL with the following path: + + "/articles/technology/42" + +This also works for host variables: + + r := mux.NewRouter() + r.Host("{subdomain}.domain.com"). + Path("/articles/{category}/{id:[0-9]+}"). + HandlerFunc(ArticleHandler). + Name("article") + + // url.String() will be "http://news.domain.com/articles/technology/42" + url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42") + +All variables defined in the route are required, and their values must +conform to the corresponding patterns. These requirements guarantee that a +generated URL will always match a registered route -- the only exception is +for explicitly defined "build-only" routes which never match. + +Regex support also exists for matching Headers within a route. For example, we could do: + + r.HeadersRegexp("Content-Type", "application/(text|json)") + +...and the route will match both requests with a Content-Type of `application/json` as well as +`application/text` + +There's also a way to build only the URL host or path for a route: +use the methods URLHost() or URLPath() instead. For the previous route, +we would do: + + // "http://news.domain.com/" + host, err := r.Get("article").URLHost("subdomain", "news") + + // "/articles/technology/42" + path, err := r.Get("article").URLPath("category", "technology", "id", "42") + +And if you use subrouters, host and path defined separately can be built +as well: + + r := mux.NewRouter() + s := r.Host("{subdomain}.domain.com").Subrouter() + s.Path("/articles/{category}/{id:[0-9]+}"). + HandlerFunc(ArticleHandler). + Name("article") + + // "http://news.domain.com/articles/technology/42" + url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42") +*/ +package mux diff --git a/vendor/github.com/gorilla/mux/mux.go b/vendor/github.com/gorilla/mux/mux.go new file mode 100644 index 000000000000..d66ec38415fd --- /dev/null +++ b/vendor/github.com/gorilla/mux/mux.go @@ -0,0 +1,542 @@ +// Copyright 2012 The Gorilla 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 mux + +import ( + "errors" + "fmt" + "net/http" + "path" + "regexp" + "strings" +) + +// NewRouter returns a new router instance. +func NewRouter() *Router { + return &Router{namedRoutes: make(map[string]*Route), KeepContext: false} +} + +// Router registers routes to be matched and dispatches a handler. +// +// It implements the http.Handler interface, so it can be registered to serve +// requests: +// +// var router = mux.NewRouter() +// +// func main() { +// http.Handle("/", router) +// } +// +// Or, for Google App Engine, register it in a init() function: +// +// func init() { +// http.Handle("/", router) +// } +// +// This will send all incoming requests to the router. +type Router struct { + // Configurable Handler to be used when no route matches. + NotFoundHandler http.Handler + // Parent route, if this is a subrouter. + parent parentRoute + // Routes to be matched, in order. + routes []*Route + // Routes by name for URL building. + namedRoutes map[string]*Route + // See Router.StrictSlash(). This defines the flag for new routes. + strictSlash bool + // See Router.SkipClean(). This defines the flag for new routes. + skipClean bool + // If true, do not clear the request context after handling the request. + // This has no effect when go1.7+ is used, since the context is stored + // on the request itself. + KeepContext bool + // see Router.UseEncodedPath(). This defines a flag for all routes. + useEncodedPath bool +} + +// Match matches registered routes against the request. +func (r *Router) Match(req *http.Request, match *RouteMatch) bool { + for _, route := range r.routes { + if route.Match(req, match) { + return true + } + } + + // Closest match for a router (includes sub-routers) + if r.NotFoundHandler != nil { + match.Handler = r.NotFoundHandler + return true + } + return false +} + +// ServeHTTP dispatches the handler registered in the matched route. +// +// When there is a match, the route variables can be retrieved calling +// mux.Vars(request). +func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if !r.skipClean { + path := req.URL.Path + if r.useEncodedPath { + path = getPath(req) + } + // Clean path to canonical form and redirect. + if p := cleanPath(path); p != path { + + // Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query. + // This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue: + // http://code.google.com/p/go/issues/detail?id=5252 + url := *req.URL + url.Path = p + p = url.String() + + w.Header().Set("Location", p) + w.WriteHeader(http.StatusMovedPermanently) + return + } + } + var match RouteMatch + var handler http.Handler + if r.Match(req, &match) { + handler = match.Handler + req = setVars(req, match.Vars) + req = setCurrentRoute(req, match.Route) + } + if handler == nil { + handler = http.NotFoundHandler() + } + if !r.KeepContext { + defer contextClear(req) + } + handler.ServeHTTP(w, req) +} + +// Get returns a route registered with the given name. +func (r *Router) Get(name string) *Route { + return r.getNamedRoutes()[name] +} + +// GetRoute returns a route registered with the given name. This method +// was renamed to Get() and remains here for backwards compatibility. +func (r *Router) GetRoute(name string) *Route { + return r.getNamedRoutes()[name] +} + +// StrictSlash defines the trailing slash behavior for new routes. The initial +// value is false. +// +// When true, if the route path is "/path/", accessing "/path" will redirect +// to the former and vice versa. In other words, your application will always +// see the path as specified in the route. +// +// When false, if the route path is "/path", accessing "/path/" will not match +// this route and vice versa. +// +// Special case: when a route sets a path prefix using the PathPrefix() method, +// strict slash is ignored for that route because the redirect behavior can't +// be determined from a prefix alone. However, any subrouters created from that +// route inherit the original StrictSlash setting. +func (r *Router) StrictSlash(value bool) *Router { + r.strictSlash = value + return r +} + +// SkipClean defines the path cleaning behaviour for new routes. The initial +// value is false. Users should be careful about which routes are not cleaned +// +// When true, if the route path is "/path//to", it will remain with the double +// slash. This is helpful if you have a route like: /fetch/http://xkcd.com/534/ +// +// When false, the path will be cleaned, so /fetch/http://xkcd.com/534/ will +// become /fetch/http/xkcd.com/534 +func (r *Router) SkipClean(value bool) *Router { + r.skipClean = value + return r +} + +// UseEncodedPath tells the router to match the encoded original path +// to the routes. +// For eg. "/path/foo%2Fbar/to" will match the path "/path/{var}/to". +// This behavior has the drawback of needing to match routes against +// r.RequestURI instead of r.URL.Path. Any modifications (such as http.StripPrefix) +// to r.URL.Path will not affect routing when this flag is on and thus may +// induce unintended behavior. +// +// If not called, the router will match the unencoded path to the routes. +// For eg. "/path/foo%2Fbar/to" will match the path "/path/foo/bar/to" +func (r *Router) UseEncodedPath() *Router { + r.useEncodedPath = true + return r +} + +// ---------------------------------------------------------------------------- +// parentRoute +// ---------------------------------------------------------------------------- + +// getNamedRoutes returns the map where named routes are registered. +func (r *Router) getNamedRoutes() map[string]*Route { + if r.namedRoutes == nil { + if r.parent != nil { + r.namedRoutes = r.parent.getNamedRoutes() + } else { + r.namedRoutes = make(map[string]*Route) + } + } + return r.namedRoutes +} + +// getRegexpGroup returns regexp definitions from the parent route, if any. +func (r *Router) getRegexpGroup() *routeRegexpGroup { + if r.parent != nil { + return r.parent.getRegexpGroup() + } + return nil +} + +func (r *Router) buildVars(m map[string]string) map[string]string { + if r.parent != nil { + m = r.parent.buildVars(m) + } + return m +} + +// ---------------------------------------------------------------------------- +// Route factories +// ---------------------------------------------------------------------------- + +// NewRoute registers an empty route. +func (r *Router) NewRoute() *Route { + route := &Route{parent: r, strictSlash: r.strictSlash, skipClean: r.skipClean, useEncodedPath: r.useEncodedPath} + r.routes = append(r.routes, route) + return route +} + +// Handle registers a new route with a matcher for the URL path. +// See Route.Path() and Route.Handler(). +func (r *Router) Handle(path string, handler http.Handler) *Route { + return r.NewRoute().Path(path).Handler(handler) +} + +// HandleFunc registers a new route with a matcher for the URL path. +// See Route.Path() and Route.HandlerFunc(). +func (r *Router) HandleFunc(path string, f func(http.ResponseWriter, + *http.Request)) *Route { + return r.NewRoute().Path(path).HandlerFunc(f) +} + +// Headers registers a new route with a matcher for request header values. +// See Route.Headers(). +func (r *Router) Headers(pairs ...string) *Route { + return r.NewRoute().Headers(pairs...) +} + +// Host registers a new route with a matcher for the URL host. +// See Route.Host(). +func (r *Router) Host(tpl string) *Route { + return r.NewRoute().Host(tpl) +} + +// MatcherFunc registers a new route with a custom matcher function. +// See Route.MatcherFunc(). +func (r *Router) MatcherFunc(f MatcherFunc) *Route { + return r.NewRoute().MatcherFunc(f) +} + +// Methods registers a new route with a matcher for HTTP methods. +// See Route.Methods(). +func (r *Router) Methods(methods ...string) *Route { + return r.NewRoute().Methods(methods...) +} + +// Path registers a new route with a matcher for the URL path. +// See Route.Path(). +func (r *Router) Path(tpl string) *Route { + return r.NewRoute().Path(tpl) +} + +// PathPrefix registers a new route with a matcher for the URL path prefix. +// See Route.PathPrefix(). +func (r *Router) PathPrefix(tpl string) *Route { + return r.NewRoute().PathPrefix(tpl) +} + +// Queries registers a new route with a matcher for URL query values. +// See Route.Queries(). +func (r *Router) Queries(pairs ...string) *Route { + return r.NewRoute().Queries(pairs...) +} + +// Schemes registers a new route with a matcher for URL schemes. +// See Route.Schemes(). +func (r *Router) Schemes(schemes ...string) *Route { + return r.NewRoute().Schemes(schemes...) +} + +// BuildVarsFunc registers a new route with a custom function for modifying +// route variables before building a URL. +func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route { + return r.NewRoute().BuildVarsFunc(f) +} + +// Walk walks the router and all its sub-routers, calling walkFn for each route +// in the tree. The routes are walked in the order they were added. Sub-routers +// are explored depth-first. +func (r *Router) Walk(walkFn WalkFunc) error { + return r.walk(walkFn, []*Route{}) +} + +// SkipRouter is used as a return value from WalkFuncs to indicate that the +// router that walk is about to descend down to should be skipped. +var SkipRouter = errors.New("skip this router") + +// WalkFunc is the type of the function called for each route visited by Walk. +// At every invocation, it is given the current route, and the current router, +// and a list of ancestor routes that lead to the current route. +type WalkFunc func(route *Route, router *Router, ancestors []*Route) error + +func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error { + for _, t := range r.routes { + if t.regexp == nil || t.regexp.path == nil || t.regexp.path.template == "" { + continue + } + + err := walkFn(t, r, ancestors) + if err == SkipRouter { + continue + } + if err != nil { + return err + } + for _, sr := range t.matchers { + if h, ok := sr.(*Router); ok { + err := h.walk(walkFn, ancestors) + if err != nil { + return err + } + } + } + if h, ok := t.handler.(*Router); ok { + ancestors = append(ancestors, t) + err := h.walk(walkFn, ancestors) + if err != nil { + return err + } + ancestors = ancestors[:len(ancestors)-1] + } + } + return nil +} + +// ---------------------------------------------------------------------------- +// Context +// ---------------------------------------------------------------------------- + +// RouteMatch stores information about a matched route. +type RouteMatch struct { + Route *Route + Handler http.Handler + Vars map[string]string +} + +type contextKey int + +const ( + varsKey contextKey = iota + routeKey +) + +// Vars returns the route variables for the current request, if any. +func Vars(r *http.Request) map[string]string { + if rv := contextGet(r, varsKey); rv != nil { + return rv.(map[string]string) + } + return nil +} + +// CurrentRoute returns the matched route for the current request, if any. +// This only works when called inside the handler of the matched route +// because the matched route is stored in the request context which is cleared +// after the handler returns, unless the KeepContext option is set on the +// Router. +func CurrentRoute(r *http.Request) *Route { + if rv := contextGet(r, routeKey); rv != nil { + return rv.(*Route) + } + return nil +} + +func setVars(r *http.Request, val interface{}) *http.Request { + return contextSet(r, varsKey, val) +} + +func setCurrentRoute(r *http.Request, val interface{}) *http.Request { + return contextSet(r, routeKey, val) +} + +// ---------------------------------------------------------------------------- +// Helpers +// ---------------------------------------------------------------------------- + +// getPath returns the escaped path if possible; doing what URL.EscapedPath() +// which was added in go1.5 does +func getPath(req *http.Request) string { + if req.RequestURI != "" { + // Extract the path from RequestURI (which is escaped unlike URL.Path) + // as detailed here as detailed in https://golang.org/pkg/net/url/#URL + // for < 1.5 server side workaround + // http://localhost/path/here?v=1 -> /path/here + path := req.RequestURI + path = strings.TrimPrefix(path, req.URL.Scheme+`://`) + path = strings.TrimPrefix(path, req.URL.Host) + if i := strings.LastIndex(path, "?"); i > -1 { + path = path[:i] + } + if i := strings.LastIndex(path, "#"); i > -1 { + path = path[:i] + } + return path + } + return req.URL.Path +} + +// cleanPath returns the canonical path for p, eliminating . and .. elements. +// Borrowed from the net/http package. +func cleanPath(p string) string { + if p == "" { + return "/" + } + if p[0] != '/' { + p = "/" + p + } + np := path.Clean(p) + // path.Clean removes trailing slash except for root; + // put the trailing slash back if necessary. + if p[len(p)-1] == '/' && np != "/" { + np += "/" + } + + return np +} + +// uniqueVars returns an error if two slices contain duplicated strings. +func uniqueVars(s1, s2 []string) error { + for _, v1 := range s1 { + for _, v2 := range s2 { + if v1 == v2 { + return fmt.Errorf("mux: duplicated route variable %q", v2) + } + } + } + return nil +} + +// checkPairs returns the count of strings passed in, and an error if +// the count is not an even number. +func checkPairs(pairs ...string) (int, error) { + length := len(pairs) + if length%2 != 0 { + return length, fmt.Errorf( + "mux: number of parameters must be multiple of 2, got %v", pairs) + } + return length, nil +} + +// mapFromPairsToString converts variadic string parameters to a +// string to string map. +func mapFromPairsToString(pairs ...string) (map[string]string, error) { + length, err := checkPairs(pairs...) + if err != nil { + return nil, err + } + m := make(map[string]string, length/2) + for i := 0; i < length; i += 2 { + m[pairs[i]] = pairs[i+1] + } + return m, nil +} + +// mapFromPairsToRegex converts variadic string paramers to a +// string to regex map. +func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) { + length, err := checkPairs(pairs...) + if err != nil { + return nil, err + } + m := make(map[string]*regexp.Regexp, length/2) + for i := 0; i < length; i += 2 { + regex, err := regexp.Compile(pairs[i+1]) + if err != nil { + return nil, err + } + m[pairs[i]] = regex + } + return m, nil +} + +// matchInArray returns true if the given string value is in the array. +func matchInArray(arr []string, value string) bool { + for _, v := range arr { + if v == value { + return true + } + } + return false +} + +// matchMapWithString returns true if the given key/value pairs exist in a given map. +func matchMapWithString(toCheck map[string]string, toMatch map[string][]string, canonicalKey bool) bool { + for k, v := range toCheck { + // Check if key exists. + if canonicalKey { + k = http.CanonicalHeaderKey(k) + } + if values := toMatch[k]; values == nil { + return false + } else if v != "" { + // If value was defined as an empty string we only check that the + // key exists. Otherwise we also check for equality. + valueExists := false + for _, value := range values { + if v == value { + valueExists = true + break + } + } + if !valueExists { + return false + } + } + } + return true +} + +// matchMapWithRegex returns true if the given key/value pairs exist in a given map compiled against +// the given regex +func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]string, canonicalKey bool) bool { + for k, v := range toCheck { + // Check if key exists. + if canonicalKey { + k = http.CanonicalHeaderKey(k) + } + if values := toMatch[k]; values == nil { + return false + } else if v != nil { + // If value was defined as an empty string we only check that the + // key exists. Otherwise we also check for equality. + valueExists := false + for _, value := range values { + if v.MatchString(value) { + valueExists = true + break + } + } + if !valueExists { + return false + } + } + } + return true +} diff --git a/vendor/github.com/gorilla/mux/regexp.go b/vendor/github.com/gorilla/mux/regexp.go new file mode 100644 index 000000000000..fd8fe39560b2 --- /dev/null +++ b/vendor/github.com/gorilla/mux/regexp.go @@ -0,0 +1,316 @@ +// Copyright 2012 The Gorilla 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 mux + +import ( + "bytes" + "fmt" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" +) + +// newRouteRegexp parses a route template and returns a routeRegexp, +// used to match a host, a path or a query string. +// +// It will extract named variables, assemble a regexp to be matched, create +// a "reverse" template to build URLs and compile regexps to validate variable +// values used in URL building. +// +// Previously we accepted only Python-like identifiers for variable +// names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that +// name and pattern can't be empty, and names can't contain a colon. +func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash, useEncodedPath bool) (*routeRegexp, error) { + // Check if it is well-formed. + idxs, errBraces := braceIndices(tpl) + if errBraces != nil { + return nil, errBraces + } + // Backup the original. + template := tpl + // Now let's parse it. + defaultPattern := "[^/]+" + if matchQuery { + defaultPattern = "[^?&]*" + } else if matchHost { + defaultPattern = "[^.]+" + matchPrefix = false + } + // Only match strict slash if not matching + if matchPrefix || matchHost || matchQuery { + strictSlash = false + } + // Set a flag for strictSlash. + endSlash := false + if strictSlash && strings.HasSuffix(tpl, "/") { + tpl = tpl[:len(tpl)-1] + endSlash = true + } + varsN := make([]string, len(idxs)/2) + varsR := make([]*regexp.Regexp, len(idxs)/2) + pattern := bytes.NewBufferString("") + pattern.WriteByte('^') + reverse := bytes.NewBufferString("") + var end int + var err error + for i := 0; i < len(idxs); i += 2 { + // Set all values we are interested in. + raw := tpl[end:idxs[i]] + end = idxs[i+1] + parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2) + name := parts[0] + patt := defaultPattern + if len(parts) == 2 { + patt = parts[1] + } + // Name or pattern can't be empty. + if name == "" || patt == "" { + return nil, fmt.Errorf("mux: missing name or pattern in %q", + tpl[idxs[i]:end]) + } + // Build the regexp pattern. + fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt) + + // Build the reverse template. + fmt.Fprintf(reverse, "%s%%s", raw) + + // Append variable name and compiled pattern. + varsN[i/2] = name + varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt)) + if err != nil { + return nil, err + } + } + // Add the remaining. + raw := tpl[end:] + pattern.WriteString(regexp.QuoteMeta(raw)) + if strictSlash { + pattern.WriteString("[/]?") + } + if matchQuery { + // Add the default pattern if the query value is empty + if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" { + pattern.WriteString(defaultPattern) + } + } + if !matchPrefix { + pattern.WriteByte('$') + } + reverse.WriteString(raw) + if endSlash { + reverse.WriteByte('/') + } + // Compile full regexp. + reg, errCompile := regexp.Compile(pattern.String()) + if errCompile != nil { + return nil, errCompile + } + // Done! + return &routeRegexp{ + template: template, + matchHost: matchHost, + matchQuery: matchQuery, + strictSlash: strictSlash, + useEncodedPath: useEncodedPath, + regexp: reg, + reverse: reverse.String(), + varsN: varsN, + varsR: varsR, + }, nil +} + +// routeRegexp stores a regexp to match a host or path and information to +// collect and validate route variables. +type routeRegexp struct { + // The unmodified template. + template string + // True for host match, false for path or query string match. + matchHost bool + // True for query string match, false for path and host match. + matchQuery bool + // The strictSlash value defined on the route, but disabled if PathPrefix was used. + strictSlash bool + // Determines whether to use encoded path from getPath function or unencoded + // req.URL.Path for path matching + useEncodedPath bool + // Expanded regexp. + regexp *regexp.Regexp + // Reverse template. + reverse string + // Variable names. + varsN []string + // Variable regexps (validators). + varsR []*regexp.Regexp +} + +// Match matches the regexp against the URL host or path. +func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { + if !r.matchHost { + if r.matchQuery { + return r.matchQueryString(req) + } + path := req.URL.Path + if r.useEncodedPath { + path = getPath(req) + } + return r.regexp.MatchString(path) + } + + return r.regexp.MatchString(getHost(req)) +} + +// url builds a URL part using the given values. +func (r *routeRegexp) url(values map[string]string) (string, error) { + urlValues := make([]interface{}, len(r.varsN)) + for k, v := range r.varsN { + value, ok := values[v] + if !ok { + return "", fmt.Errorf("mux: missing route variable %q", v) + } + urlValues[k] = value + } + rv := fmt.Sprintf(r.reverse, urlValues...) + if !r.regexp.MatchString(rv) { + // The URL is checked against the full regexp, instead of checking + // individual variables. This is faster but to provide a good error + // message, we check individual regexps if the URL doesn't match. + for k, v := range r.varsN { + if !r.varsR[k].MatchString(values[v]) { + return "", fmt.Errorf( + "mux: variable %q doesn't match, expected %q", values[v], + r.varsR[k].String()) + } + } + } + return rv, nil +} + +// getURLQuery returns a single query parameter from a request URL. +// For a URL with foo=bar&baz=ding, we return only the relevant key +// value pair for the routeRegexp. +func (r *routeRegexp) getURLQuery(req *http.Request) string { + if !r.matchQuery { + return "" + } + templateKey := strings.SplitN(r.template, "=", 2)[0] + for key, vals := range req.URL.Query() { + if key == templateKey && len(vals) > 0 { + return key + "=" + vals[0] + } + } + return "" +} + +func (r *routeRegexp) matchQueryString(req *http.Request) bool { + return r.regexp.MatchString(r.getURLQuery(req)) +} + +// braceIndices returns the first level curly brace indices from a string. +// It returns an error in case of unbalanced braces. +func braceIndices(s string) ([]int, error) { + var level, idx int + var idxs []int + for i := 0; i < len(s); i++ { + switch s[i] { + case '{': + if level++; level == 1 { + idx = i + } + case '}': + if level--; level == 0 { + idxs = append(idxs, idx, i+1) + } else if level < 0 { + return nil, fmt.Errorf("mux: unbalanced braces in %q", s) + } + } + } + if level != 0 { + return nil, fmt.Errorf("mux: unbalanced braces in %q", s) + } + return idxs, nil +} + +// varGroupName builds a capturing group name for the indexed variable. +func varGroupName(idx int) string { + return "v" + strconv.Itoa(idx) +} + +// ---------------------------------------------------------------------------- +// routeRegexpGroup +// ---------------------------------------------------------------------------- + +// routeRegexpGroup groups the route matchers that carry variables. +type routeRegexpGroup struct { + host *routeRegexp + path *routeRegexp + queries []*routeRegexp +} + +// setMatch extracts the variables from the URL once a route matches. +func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { + // Store host variables. + if v.host != nil { + host := getHost(req) + matches := v.host.regexp.FindStringSubmatchIndex(host) + if len(matches) > 0 { + extractVars(host, matches, v.host.varsN, m.Vars) + } + } + path := req.URL.Path + if r.useEncodedPath { + path = getPath(req) + } + // Store path variables. + if v.path != nil { + matches := v.path.regexp.FindStringSubmatchIndex(path) + if len(matches) > 0 { + extractVars(path, matches, v.path.varsN, m.Vars) + // Check if we should redirect. + if v.path.strictSlash { + p1 := strings.HasSuffix(path, "/") + p2 := strings.HasSuffix(v.path.template, "/") + if p1 != p2 { + u, _ := url.Parse(req.URL.String()) + if p1 { + u.Path = u.Path[:len(u.Path)-1] + } else { + u.Path += "/" + } + m.Handler = http.RedirectHandler(u.String(), 301) + } + } + } + } + // Store query string variables. + for _, q := range v.queries { + queryURL := q.getURLQuery(req) + matches := q.regexp.FindStringSubmatchIndex(queryURL) + if len(matches) > 0 { + extractVars(queryURL, matches, q.varsN, m.Vars) + } + } +} + +// getHost tries its best to return the request host. +func getHost(r *http.Request) string { + if r.URL.IsAbs() { + return r.URL.Host + } + host := r.Host + // Slice off any port information. + if i := strings.Index(host, ":"); i != -1 { + host = host[:i] + } + return host + +} + +func extractVars(input string, matches []int, names []string, output map[string]string) { + for i, name := range names { + output[name] = input[matches[2*i+2]:matches[2*i+3]] + } +} diff --git a/vendor/github.com/gorilla/mux/route.go b/vendor/github.com/gorilla/mux/route.go new file mode 100644 index 000000000000..293b6d49386f --- /dev/null +++ b/vendor/github.com/gorilla/mux/route.go @@ -0,0 +1,636 @@ +// Copyright 2012 The Gorilla 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 mux + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "regexp" + "strings" +) + +// Route stores information to match a request and build URLs. +type Route struct { + // Parent where the route was registered (a Router). + parent parentRoute + // Request handler for the route. + handler http.Handler + // List of matchers. + matchers []matcher + // Manager for the variables from host and path. + regexp *routeRegexpGroup + // If true, when the path pattern is "/path/", accessing "/path" will + // redirect to the former and vice versa. + strictSlash bool + // If true, when the path pattern is "/path//to", accessing "/path//to" + // will not redirect + skipClean bool + // If true, "/path/foo%2Fbar/to" will match the path "/path/{var}/to" + useEncodedPath bool + // If true, this route never matches: it is only used to build URLs. + buildOnly bool + // The name used to build URLs. + name string + // Error resulted from building a route. + err error + + buildVarsFunc BuildVarsFunc +} + +func (r *Route) SkipClean() bool { + return r.skipClean +} + +// Match matches the route against the request. +func (r *Route) Match(req *http.Request, match *RouteMatch) bool { + if r.buildOnly || r.err != nil { + return false + } + // Match everything. + for _, m := range r.matchers { + if matched := m.Match(req, match); !matched { + return false + } + } + // Yay, we have a match. Let's collect some info about it. + if match.Route == nil { + match.Route = r + } + if match.Handler == nil { + match.Handler = r.handler + } + if match.Vars == nil { + match.Vars = make(map[string]string) + } + // Set variables. + if r.regexp != nil { + r.regexp.setMatch(req, match, r) + } + return true +} + +// ---------------------------------------------------------------------------- +// Route attributes +// ---------------------------------------------------------------------------- + +// GetError returns an error resulted from building the route, if any. +func (r *Route) GetError() error { + return r.err +} + +// BuildOnly sets the route to never match: it is only used to build URLs. +func (r *Route) BuildOnly() *Route { + r.buildOnly = true + return r +} + +// Handler -------------------------------------------------------------------- + +// Handler sets a handler for the route. +func (r *Route) Handler(handler http.Handler) *Route { + if r.err == nil { + r.handler = handler + } + return r +} + +// HandlerFunc sets a handler function for the route. +func (r *Route) HandlerFunc(f func(http.ResponseWriter, *http.Request)) *Route { + return r.Handler(http.HandlerFunc(f)) +} + +// GetHandler returns the handler for the route, if any. +func (r *Route) GetHandler() http.Handler { + return r.handler +} + +// Name ----------------------------------------------------------------------- + +// Name sets the name for the route, used to build URLs. +// If the name was registered already it will be overwritten. +func (r *Route) Name(name string) *Route { + if r.name != "" { + r.err = fmt.Errorf("mux: route already has name %q, can't set %q", + r.name, name) + } + if r.err == nil { + r.name = name + r.getNamedRoutes()[name] = r + } + return r +} + +// GetName returns the name for the route, if any. +func (r *Route) GetName() string { + return r.name +} + +// ---------------------------------------------------------------------------- +// Matchers +// ---------------------------------------------------------------------------- + +// matcher types try to match a request. +type matcher interface { + Match(*http.Request, *RouteMatch) bool +} + +// addMatcher adds a matcher to the route. +func (r *Route) addMatcher(m matcher) *Route { + if r.err == nil { + r.matchers = append(r.matchers, m) + } + return r +} + +// addRegexpMatcher adds a host or path matcher and builder to a route. +func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery bool) error { + if r.err != nil { + return r.err + } + r.regexp = r.getRegexpGroup() + if !matchHost && !matchQuery { + if len(tpl) == 0 || tpl[0] != '/' { + return fmt.Errorf("mux: path must start with a slash, got %q", tpl) + } + if r.regexp.path != nil { + tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl + } + } + rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, matchQuery, r.strictSlash, r.useEncodedPath) + if err != nil { + return err + } + for _, q := range r.regexp.queries { + if err = uniqueVars(rr.varsN, q.varsN); err != nil { + return err + } + } + if matchHost { + if r.regexp.path != nil { + if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil { + return err + } + } + r.regexp.host = rr + } else { + if r.regexp.host != nil { + if err = uniqueVars(rr.varsN, r.regexp.host.varsN); err != nil { + return err + } + } + if matchQuery { + r.regexp.queries = append(r.regexp.queries, rr) + } else { + r.regexp.path = rr + } + } + r.addMatcher(rr) + return nil +} + +// Headers -------------------------------------------------------------------- + +// headerMatcher matches the request against header values. +type headerMatcher map[string]string + +func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchMapWithString(m, r.Header, true) +} + +// Headers adds a matcher for request header values. +// It accepts a sequence of key/value pairs to be matched. For example: +// +// r := mux.NewRouter() +// r.Headers("Content-Type", "application/json", +// "X-Requested-With", "XMLHttpRequest") +// +// The above route will only match if both request header values match. +// If the value is an empty string, it will match any value if the key is set. +func (r *Route) Headers(pairs ...string) *Route { + if r.err == nil { + var headers map[string]string + headers, r.err = mapFromPairsToString(pairs...) + return r.addMatcher(headerMatcher(headers)) + } + return r +} + +// headerRegexMatcher matches the request against the route given a regex for the header +type headerRegexMatcher map[string]*regexp.Regexp + +func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchMapWithRegex(m, r.Header, true) +} + +// HeadersRegexp accepts a sequence of key/value pairs, where the value has regex +// support. For example: +// +// r := mux.NewRouter() +// r.HeadersRegexp("Content-Type", "application/(text|json)", +// "X-Requested-With", "XMLHttpRequest") +// +// The above route will only match if both the request header matches both regular expressions. +// It the value is an empty string, it will match any value if the key is set. +func (r *Route) HeadersRegexp(pairs ...string) *Route { + if r.err == nil { + var headers map[string]*regexp.Regexp + headers, r.err = mapFromPairsToRegex(pairs...) + return r.addMatcher(headerRegexMatcher(headers)) + } + return r +} + +// Host ----------------------------------------------------------------------- + +// Host adds a matcher for the URL host. +// It accepts a template with zero or more URL variables enclosed by {}. +// Variables can define an optional regexp pattern to be matched: +// +// - {name} matches anything until the next dot. +// +// - {name:pattern} matches the given regexp pattern. +// +// For example: +// +// r := mux.NewRouter() +// r.Host("www.example.com") +// r.Host("{subdomain}.domain.com") +// r.Host("{subdomain:[a-z]+}.domain.com") +// +// Variable names must be unique in a given route. They can be retrieved +// calling mux.Vars(request). +func (r *Route) Host(tpl string) *Route { + r.err = r.addRegexpMatcher(tpl, true, false, false) + return r +} + +// MatcherFunc ---------------------------------------------------------------- + +// MatcherFunc is the function signature used by custom matchers. +type MatcherFunc func(*http.Request, *RouteMatch) bool + +// Match returns the match for a given request. +func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool { + return m(r, match) +} + +// MatcherFunc adds a custom function to be used as request matcher. +func (r *Route) MatcherFunc(f MatcherFunc) *Route { + return r.addMatcher(f) +} + +// Methods -------------------------------------------------------------------- + +// methodMatcher matches the request against HTTP methods. +type methodMatcher []string + +func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchInArray(m, r.Method) +} + +// Methods adds a matcher for HTTP methods. +// It accepts a sequence of one or more methods to be matched, e.g.: +// "GET", "POST", "PUT". +func (r *Route) Methods(methods ...string) *Route { + for k, v := range methods { + methods[k] = strings.ToUpper(v) + } + return r.addMatcher(methodMatcher(methods)) +} + +// Path ----------------------------------------------------------------------- + +// Path adds a matcher for the URL path. +// It accepts a template with zero or more URL variables enclosed by {}. The +// template must start with a "/". +// Variables can define an optional regexp pattern to be matched: +// +// - {name} matches anything until the next slash. +// +// - {name:pattern} matches the given regexp pattern. +// +// For example: +// +// r := mux.NewRouter() +// r.Path("/products/").Handler(ProductsHandler) +// r.Path("/products/{key}").Handler(ProductsHandler) +// r.Path("/articles/{category}/{id:[0-9]+}"). +// Handler(ArticleHandler) +// +// Variable names must be unique in a given route. They can be retrieved +// calling mux.Vars(request). +func (r *Route) Path(tpl string) *Route { + r.err = r.addRegexpMatcher(tpl, false, false, false) + return r +} + +// PathPrefix ----------------------------------------------------------------- + +// PathPrefix adds a matcher for the URL path prefix. This matches if the given +// template is a prefix of the full URL path. See Route.Path() for details on +// the tpl argument. +// +// Note that it does not treat slashes specially ("/foobar/" will be matched by +// the prefix "/foo") so you may want to use a trailing slash here. +// +// Also note that the setting of Router.StrictSlash() has no effect on routes +// with a PathPrefix matcher. +func (r *Route) PathPrefix(tpl string) *Route { + r.err = r.addRegexpMatcher(tpl, false, true, false) + return r +} + +// Query ---------------------------------------------------------------------- + +// Queries adds a matcher for URL query values. +// It accepts a sequence of key/value pairs. Values may define variables. +// For example: +// +// r := mux.NewRouter() +// r.Queries("foo", "bar", "id", "{id:[0-9]+}") +// +// The above route will only match if the URL contains the defined queries +// values, e.g.: ?foo=bar&id=42. +// +// It the value is an empty string, it will match any value if the key is set. +// +// Variables can define an optional regexp pattern to be matched: +// +// - {name} matches anything until the next slash. +// +// - {name:pattern} matches the given regexp pattern. +func (r *Route) Queries(pairs ...string) *Route { + length := len(pairs) + if length%2 != 0 { + r.err = fmt.Errorf( + "mux: number of parameters must be multiple of 2, got %v", pairs) + return nil + } + for i := 0; i < length; i += 2 { + if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], false, false, true); r.err != nil { + return r + } + } + + return r +} + +// Schemes -------------------------------------------------------------------- + +// schemeMatcher matches the request against URL schemes. +type schemeMatcher []string + +func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchInArray(m, r.URL.Scheme) +} + +// Schemes adds a matcher for URL schemes. +// It accepts a sequence of schemes to be matched, e.g.: "http", "https". +func (r *Route) Schemes(schemes ...string) *Route { + for k, v := range schemes { + schemes[k] = strings.ToLower(v) + } + return r.addMatcher(schemeMatcher(schemes)) +} + +// BuildVarsFunc -------------------------------------------------------------- + +// BuildVarsFunc is the function signature used by custom build variable +// functions (which can modify route variables before a route's URL is built). +type BuildVarsFunc func(map[string]string) map[string]string + +// BuildVarsFunc adds a custom function to be used to modify build variables +// before a route's URL is built. +func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route { + r.buildVarsFunc = f + return r +} + +// Subrouter ------------------------------------------------------------------ + +// Subrouter creates a subrouter for the route. +// +// It will test the inner routes only if the parent route matched. For example: +// +// r := mux.NewRouter() +// s := r.Host("www.example.com").Subrouter() +// s.HandleFunc("/products/", ProductsHandler) +// s.HandleFunc("/products/{key}", ProductHandler) +// s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) +// +// Here, the routes registered in the subrouter won't be tested if the host +// doesn't match. +func (r *Route) Subrouter() *Router { + router := &Router{parent: r, strictSlash: r.strictSlash} + r.addMatcher(router) + return router +} + +// ---------------------------------------------------------------------------- +// URL building +// ---------------------------------------------------------------------------- + +// URL builds a URL for the route. +// +// It accepts a sequence of key/value pairs for the route variables. For +// example, given this route: +// +// r := mux.NewRouter() +// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). +// Name("article") +// +// ...a URL for it can be built using: +// +// url, err := r.Get("article").URL("category", "technology", "id", "42") +// +// ...which will return an url.URL with the following path: +// +// "/articles/technology/42" +// +// This also works for host variables: +// +// r := mux.NewRouter() +// r.Host("{subdomain}.domain.com"). +// HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). +// Name("article") +// +// // url.String() will be "http://news.domain.com/articles/technology/42" +// url, err := r.Get("article").URL("subdomain", "news", +// "category", "technology", +// "id", "42") +// +// All variables defined in the route are required, and their values must +// conform to the corresponding patterns. +func (r *Route) URL(pairs ...string) (*url.URL, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp == nil { + return nil, errors.New("mux: route doesn't have a host or path") + } + values, err := r.prepareVars(pairs...) + if err != nil { + return nil, err + } + var scheme, host, path string + if r.regexp.host != nil { + // Set a default scheme. + scheme = "http" + if host, err = r.regexp.host.url(values); err != nil { + return nil, err + } + } + if r.regexp.path != nil { + if path, err = r.regexp.path.url(values); err != nil { + return nil, err + } + } + return &url.URL{ + Scheme: scheme, + Host: host, + Path: path, + }, nil +} + +// URLHost builds the host part of the URL for a route. See Route.URL(). +// +// The route must have a host defined. +func (r *Route) URLHost(pairs ...string) (*url.URL, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp == nil || r.regexp.host == nil { + return nil, errors.New("mux: route doesn't have a host") + } + values, err := r.prepareVars(pairs...) + if err != nil { + return nil, err + } + host, err := r.regexp.host.url(values) + if err != nil { + return nil, err + } + return &url.URL{ + Scheme: "http", + Host: host, + }, nil +} + +// URLPath builds the path part of the URL for a route. See Route.URL(). +// +// The route must have a path defined. +func (r *Route) URLPath(pairs ...string) (*url.URL, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp == nil || r.regexp.path == nil { + return nil, errors.New("mux: route doesn't have a path") + } + values, err := r.prepareVars(pairs...) + if err != nil { + return nil, err + } + path, err := r.regexp.path.url(values) + if err != nil { + return nil, err + } + return &url.URL{ + Path: path, + }, nil +} + +// GetPathTemplate returns the template used to build the +// route match. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define a path. +func (r *Route) GetPathTemplate() (string, error) { + if r.err != nil { + return "", r.err + } + if r.regexp == nil || r.regexp.path == nil { + return "", errors.New("mux: route doesn't have a path") + } + return r.regexp.path.template, nil +} + +// GetHostTemplate returns the template used to build the +// route match. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define a host. +func (r *Route) GetHostTemplate() (string, error) { + if r.err != nil { + return "", r.err + } + if r.regexp == nil || r.regexp.host == nil { + return "", errors.New("mux: route doesn't have a host") + } + return r.regexp.host.template, nil +} + +// prepareVars converts the route variable pairs into a map. If the route has a +// BuildVarsFunc, it is invoked. +func (r *Route) prepareVars(pairs ...string) (map[string]string, error) { + m, err := mapFromPairsToString(pairs...) + if err != nil { + return nil, err + } + return r.buildVars(m), nil +} + +func (r *Route) buildVars(m map[string]string) map[string]string { + if r.parent != nil { + m = r.parent.buildVars(m) + } + if r.buildVarsFunc != nil { + m = r.buildVarsFunc(m) + } + return m +} + +// ---------------------------------------------------------------------------- +// parentRoute +// ---------------------------------------------------------------------------- + +// parentRoute allows routes to know about parent host and path definitions. +type parentRoute interface { + getNamedRoutes() map[string]*Route + getRegexpGroup() *routeRegexpGroup + buildVars(map[string]string) map[string]string +} + +// getNamedRoutes returns the map where named routes are registered. +func (r *Route) getNamedRoutes() map[string]*Route { + if r.parent == nil { + // During tests router is not always set. + r.parent = NewRouter() + } + return r.parent.getNamedRoutes() +} + +// getRegexpGroup returns regexp definitions from this route. +func (r *Route) getRegexpGroup() *routeRegexpGroup { + if r.regexp == nil { + if r.parent == nil { + // During tests router is not always set. + r.parent = NewRouter() + } + regexp := r.parent.getRegexpGroup() + if regexp == nil { + r.regexp = new(routeRegexpGroup) + } else { + // Copy. + r.regexp = &routeRegexpGroup{ + host: regexp.host, + path: regexp.path, + queries: regexp.queries, + } + } + } + return r.regexp +} diff --git a/vendor/github.com/gorilla/securecookie/LICENSE b/vendor/github.com/gorilla/securecookie/LICENSE new file mode 100644 index 000000000000..0e5fb872800d --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 Rodrigo Moraes. 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/vendor/github.com/gorilla/securecookie/README.md b/vendor/github.com/gorilla/securecookie/README.md new file mode 100644 index 000000000000..da112e4d0881 --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/README.md @@ -0,0 +1,78 @@ +securecookie +============ +[![GoDoc](https://godoc.org/github.com/gorilla/securecookie?status.svg)](https://godoc.org/github.com/gorilla/securecookie) [![Build Status](https://travis-ci.org/gorilla/securecookie.png?branch=master)](https://travis-ci.org/gorilla/securecookie) + +securecookie encodes and decodes authenticated and optionally encrypted +cookie values. + +Secure cookies can't be forged, because their values are validated using HMAC. +When encrypted, the content is also inaccessible to malicious eyes. It is still +recommended that sensitive data not be stored in cookies, and that HTTPS be used +to prevent cookie [replay attacks](https://en.wikipedia.org/wiki/Replay_attack). + +## Examples + +To use it, first create a new SecureCookie instance: + +```go +// Hash keys should be at least 32 bytes long +var hashKey = []byte("very-secret") +// Block keys should be 16 bytes (AES-128) or 32 bytes (AES-256) long. +// Shorter keys may weaken the encryption used. +var blockKey = []byte("a-lot-secret") +var s = securecookie.New(hashKey, blockKey) +``` + +The hashKey is required, used to authenticate the cookie value using HMAC. +It is recommended to use a key with 32 or 64 bytes. + +The blockKey is optional, used to encrypt the cookie value -- set it to nil +to not use encryption. If set, the length must correspond to the block size +of the encryption algorithm. For AES, used by default, valid lengths are +16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. + +Strong keys can be created using the convenience function GenerateRandomKey(). + +Once a SecureCookie instance is set, use it to encode a cookie value: + +```go +func SetCookieHandler(w http.ResponseWriter, r *http.Request) { + value := map[string]string{ + "foo": "bar", + } + if encoded, err := s.Encode("cookie-name", value); err == nil { + cookie := &http.Cookie{ + Name: "cookie-name", + Value: encoded, + Path: "/", + Secure: true, + HttpOnly: true, + } + http.SetCookie(w, cookie) + } +} +``` + +Later, use the same SecureCookie instance to decode and validate a cookie +value: + +```go +func ReadCookieHandler(w http.ResponseWriter, r *http.Request) { + if cookie, err := r.Cookie("cookie-name"); err == nil { + value := make(map[string]string) + if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil { + fmt.Fprintf(w, "The value of foo is %q", value["foo"]) + } + } +} +``` + +We stored a map[string]string, but secure cookies can hold any value that +can be encoded using `encoding/gob`. To store custom types, they must be +registered first using gob.Register(). For basic types this is not needed; +it works out of the box. An optional JSON encoder that uses `encoding/json` is +available for types compatible with JSON. + +## License + +BSD licensed. See the LICENSE file for details. diff --git a/vendor/github.com/gorilla/securecookie/doc.go b/vendor/github.com/gorilla/securecookie/doc.go new file mode 100644 index 000000000000..ae89408d9d56 --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/doc.go @@ -0,0 +1,61 @@ +// Copyright 2012 The Gorilla 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 securecookie encodes and decodes authenticated and optionally +encrypted cookie values. + +Secure cookies can't be forged, because their values are validated using HMAC. +When encrypted, the content is also inaccessible to malicious eyes. + +To use it, first create a new SecureCookie instance: + + var hashKey = []byte("very-secret") + var blockKey = []byte("a-lot-secret") + var s = securecookie.New(hashKey, blockKey) + +The hashKey is required, used to authenticate the cookie value using HMAC. +It is recommended to use a key with 32 or 64 bytes. + +The blockKey is optional, used to encrypt the cookie value -- set it to nil +to not use encryption. If set, the length must correspond to the block size +of the encryption algorithm. For AES, used by default, valid lengths are +16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. + +Strong keys can be created using the convenience function GenerateRandomKey(). + +Once a SecureCookie instance is set, use it to encode a cookie value: + + func SetCookieHandler(w http.ResponseWriter, r *http.Request) { + value := map[string]string{ + "foo": "bar", + } + if encoded, err := s.Encode("cookie-name", value); err == nil { + cookie := &http.Cookie{ + Name: "cookie-name", + Value: encoded, + Path: "/", + } + http.SetCookie(w, cookie) + } + } + +Later, use the same SecureCookie instance to decode and validate a cookie +value: + + func ReadCookieHandler(w http.ResponseWriter, r *http.Request) { + if cookie, err := r.Cookie("cookie-name"); err == nil { + value := make(map[string]string) + if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil { + fmt.Fprintf(w, "The value of foo is %q", value["foo"]) + } + } + } + +We stored a map[string]string, but secure cookies can hold any value that +can be encoded using encoding/gob. To store custom types, they must be +registered first using gob.Register(). For basic types this is not needed; +it works out of the box. +*/ +package securecookie diff --git a/vendor/github.com/gorilla/securecookie/fuzz.go b/vendor/github.com/gorilla/securecookie/fuzz.go new file mode 100644 index 000000000000..e4d0534e41bf --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/fuzz.go @@ -0,0 +1,25 @@ +// +build gofuzz + +package securecookie + +var hashKey = []byte("very-secret12345") +var blockKey = []byte("a-lot-secret1234") +var s = New(hashKey, blockKey) + +type Cookie struct { + B bool + I int + S string +} + +func Fuzz(data []byte) int { + datas := string(data) + var c Cookie + if err := s.Decode("fuzz", datas, &c); err != nil { + return 0 + } + if _, err := s.Encode("fuzz", c); err != nil { + panic(err) + } + return 1 +} diff --git a/vendor/github.com/gorilla/securecookie/securecookie.go b/vendor/github.com/gorilla/securecookie/securecookie.go new file mode 100644 index 000000000000..cd4e0976d663 --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/securecookie.go @@ -0,0 +1,646 @@ +// Copyright 2012 The Gorilla 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 securecookie + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "encoding/gob" + "encoding/json" + "fmt" + "hash" + "io" + "strconv" + "strings" + "time" +) + +// Error is the interface of all errors returned by functions in this library. +type Error interface { + error + + // IsUsage returns true for errors indicating the client code probably + // uses this library incorrectly. For example, the client may have + // failed to provide a valid hash key, or may have failed to configure + // the Serializer adequately for encoding value. + IsUsage() bool + + // IsDecode returns true for errors indicating that a cookie could not + // be decoded and validated. Since cookies are usually untrusted + // user-provided input, errors of this type should be expected. + // Usually, the proper action is simply to reject the request. + IsDecode() bool + + // IsInternal returns true for unexpected errors occurring in the + // securecookie implementation. + IsInternal() bool + + // Cause, if it returns a non-nil value, indicates that this error was + // propagated from some underlying library. If this method returns nil, + // this error was raised directly by this library. + // + // Cause is provided principally for debugging/logging purposes; it is + // rare that application logic should perform meaningfully different + // logic based on Cause. See, for example, the caveats described on + // (MultiError).Cause(). + Cause() error +} + +// errorType is a bitmask giving the error type(s) of an cookieError value. +type errorType int + +const ( + usageError = errorType(1 << iota) + decodeError + internalError +) + +type cookieError struct { + typ errorType + msg string + cause error +} + +func (e cookieError) IsUsage() bool { return (e.typ & usageError) != 0 } +func (e cookieError) IsDecode() bool { return (e.typ & decodeError) != 0 } +func (e cookieError) IsInternal() bool { return (e.typ & internalError) != 0 } + +func (e cookieError) Cause() error { return e.cause } + +func (e cookieError) Error() string { + parts := []string{"securecookie: "} + if e.msg == "" { + parts = append(parts, "error") + } else { + parts = append(parts, e.msg) + } + if c := e.Cause(); c != nil { + parts = append(parts, " - caused by: ", c.Error()) + } + return strings.Join(parts, "") +} + +var ( + errGeneratingIV = cookieError{typ: internalError, msg: "failed to generate random iv"} + + errNoCodecs = cookieError{typ: usageError, msg: "no codecs provided"} + errHashKeyNotSet = cookieError{typ: usageError, msg: "hash key is not set"} + errBlockKeyNotSet = cookieError{typ: usageError, msg: "block key is not set"} + errEncodedValueTooLong = cookieError{typ: usageError, msg: "the value is too long"} + + errValueToDecodeTooLong = cookieError{typ: decodeError, msg: "the value is too long"} + errTimestampInvalid = cookieError{typ: decodeError, msg: "invalid timestamp"} + errTimestampTooNew = cookieError{typ: decodeError, msg: "timestamp is too new"} + errTimestampExpired = cookieError{typ: decodeError, msg: "expired timestamp"} + errDecryptionFailed = cookieError{typ: decodeError, msg: "the value could not be decrypted"} + errValueNotByte = cookieError{typ: decodeError, msg: "value not a []byte."} + errValueNotBytePtr = cookieError{typ: decodeError, msg: "value not a pointer to []byte."} + + // ErrMacInvalid indicates that cookie decoding failed because the HMAC + // could not be extracted and verified. Direct use of this error + // variable is deprecated; it is public only for legacy compatibility, + // and may be privatized in the future, as it is rarely useful to + // distinguish between this error and other Error implementations. + ErrMacInvalid = cookieError{typ: decodeError, msg: "the value is not valid"} +) + +// Codec defines an interface to encode and decode cookie values. +type Codec interface { + Encode(name string, value interface{}) (string, error) + Decode(name, value string, dst interface{}) error +} + +// New returns a new SecureCookie. +// +// hashKey is required, used to authenticate values using HMAC. Create it using +// GenerateRandomKey(). It is recommended to use a key with 32 or 64 bytes. +// +// blockKey is optional, used to encrypt values. Create it using +// GenerateRandomKey(). The key length must correspond to the block size +// of the encryption algorithm. For AES, used by default, valid lengths are +// 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. +// The default encoder used for cookie serialization is encoding/gob. +// +// Note that keys created using GenerateRandomKey() are not automatically +// persisted. New keys will be created when the application is restarted, and +// previously issued cookies will not be able to be decoded. +func New(hashKey, blockKey []byte) *SecureCookie { + s := &SecureCookie{ + hashKey: hashKey, + blockKey: blockKey, + hashFunc: sha256.New, + maxAge: 86400 * 30, + maxLength: 4096, + sz: GobEncoder{}, + } + if hashKey == nil { + s.err = errHashKeyNotSet + } + if blockKey != nil { + s.BlockFunc(aes.NewCipher) + } + return s +} + +// SecureCookie encodes and decodes authenticated and optionally encrypted +// cookie values. +type SecureCookie struct { + hashKey []byte + hashFunc func() hash.Hash + blockKey []byte + block cipher.Block + maxLength int + maxAge int64 + minAge int64 + err error + sz Serializer + // For testing purposes, the function that returns the current timestamp. + // If not set, it will use time.Now().UTC().Unix(). + timeFunc func() int64 +} + +// Serializer provides an interface for providing custom serializers for cookie +// values. +type Serializer interface { + Serialize(src interface{}) ([]byte, error) + Deserialize(src []byte, dst interface{}) error +} + +// GobEncoder encodes cookie values using encoding/gob. This is the simplest +// encoder and can handle complex types via gob.Register. +type GobEncoder struct{} + +// JSONEncoder encodes cookie values using encoding/json. Users who wish to +// encode complex types need to satisfy the json.Marshaller and +// json.Unmarshaller interfaces. +type JSONEncoder struct{} + +// NopEncoder does not encode cookie values, and instead simply accepts a []byte +// (as an interface{}) and returns a []byte. This is particularly useful when +// you encoding an object upstream and do not wish to re-encode it. +type NopEncoder struct{} + +// MaxLength restricts the maximum length, in bytes, for the cookie value. +// +// Default is 4096, which is the maximum value accepted by Internet Explorer. +func (s *SecureCookie) MaxLength(value int) *SecureCookie { + s.maxLength = value + return s +} + +// MaxAge restricts the maximum age, in seconds, for the cookie value. +// +// Default is 86400 * 30. Set it to 0 for no restriction. +func (s *SecureCookie) MaxAge(value int) *SecureCookie { + s.maxAge = int64(value) + return s +} + +// MinAge restricts the minimum age, in seconds, for the cookie value. +// +// Default is 0 (no restriction). +func (s *SecureCookie) MinAge(value int) *SecureCookie { + s.minAge = int64(value) + return s +} + +// HashFunc sets the hash function used to create HMAC. +// +// Default is crypto/sha256.New. +func (s *SecureCookie) HashFunc(f func() hash.Hash) *SecureCookie { + s.hashFunc = f + return s +} + +// BlockFunc sets the encryption function used to create a cipher.Block. +// +// Default is crypto/aes.New. +func (s *SecureCookie) BlockFunc(f func([]byte) (cipher.Block, error)) *SecureCookie { + if s.blockKey == nil { + s.err = errBlockKeyNotSet + } else if block, err := f(s.blockKey); err == nil { + s.block = block + } else { + s.err = cookieError{cause: err, typ: usageError} + } + return s +} + +// Encoding sets the encoding/serialization method for cookies. +// +// Default is encoding/gob. To encode special structures using encoding/gob, +// they must be registered first using gob.Register(). +func (s *SecureCookie) SetSerializer(sz Serializer) *SecureCookie { + s.sz = sz + + return s +} + +// Encode encodes a cookie value. +// +// It serializes, optionally encrypts, signs with a message authentication code, +// and finally encodes the value. +// +// The name argument is the cookie name. It is stored with the encoded value. +// The value argument is the value to be encoded. It can be any value that can +// be encoded using the currently selected serializer; see SetSerializer(). +// +// It is the client's responsibility to ensure that value, when encoded using +// the current serialization/encryption settings on s and then base64-encoded, +// is shorter than the maximum permissible length. +func (s *SecureCookie) Encode(name string, value interface{}) (string, error) { + if s.err != nil { + return "", s.err + } + if s.hashKey == nil { + s.err = errHashKeyNotSet + return "", s.err + } + var err error + var b []byte + // 1. Serialize. + if b, err = s.sz.Serialize(value); err != nil { + return "", cookieError{cause: err, typ: usageError} + } + // 2. Encrypt (optional). + if s.block != nil { + if b, err = encrypt(s.block, b); err != nil { + return "", cookieError{cause: err, typ: usageError} + } + } + b = encode(b) + // 3. Create MAC for "name|date|value". Extra pipe to be used later. + b = []byte(fmt.Sprintf("%s|%d|%s|", name, s.timestamp(), b)) + mac := createMac(hmac.New(s.hashFunc, s.hashKey), b[:len(b)-1]) + // Append mac, remove name. + b = append(b, mac...)[len(name)+1:] + // 4. Encode to base64. + b = encode(b) + // 5. Check length. + if s.maxLength != 0 && len(b) > s.maxLength { + return "", errEncodedValueTooLong + } + // Done. + return string(b), nil +} + +// Decode decodes a cookie value. +// +// It decodes, verifies a message authentication code, optionally decrypts and +// finally deserializes the value. +// +// The name argument is the cookie name. It must be the same name used when +// it was stored. The value argument is the encoded cookie value. The dst +// argument is where the cookie will be decoded. It must be a pointer. +func (s *SecureCookie) Decode(name, value string, dst interface{}) error { + if s.err != nil { + return s.err + } + if s.hashKey == nil { + s.err = errHashKeyNotSet + return s.err + } + // 1. Check length. + if s.maxLength != 0 && len(value) > s.maxLength { + return errValueToDecodeTooLong + } + // 2. Decode from base64. + b, err := decode([]byte(value)) + if err != nil { + return err + } + // 3. Verify MAC. Value is "date|value|mac". + parts := bytes.SplitN(b, []byte("|"), 3) + if len(parts) != 3 { + return ErrMacInvalid + } + h := hmac.New(s.hashFunc, s.hashKey) + b = append([]byte(name+"|"), b[:len(b)-len(parts[2])-1]...) + if err = verifyMac(h, b, parts[2]); err != nil { + return err + } + // 4. Verify date ranges. + var t1 int64 + if t1, err = strconv.ParseInt(string(parts[0]), 10, 64); err != nil { + return errTimestampInvalid + } + t2 := s.timestamp() + if s.minAge != 0 && t1 > t2-s.minAge { + return errTimestampTooNew + } + if s.maxAge != 0 && t1 < t2-s.maxAge { + return errTimestampExpired + } + // 5. Decrypt (optional). + b, err = decode(parts[1]) + if err != nil { + return err + } + if s.block != nil { + if b, err = decrypt(s.block, b); err != nil { + return err + } + } + // 6. Deserialize. + if err = s.sz.Deserialize(b, dst); err != nil { + return cookieError{cause: err, typ: decodeError} + } + // Done. + return nil +} + +// timestamp returns the current timestamp, in seconds. +// +// For testing purposes, the function that generates the timestamp can be +// overridden. If not set, it will return time.Now().UTC().Unix(). +func (s *SecureCookie) timestamp() int64 { + if s.timeFunc == nil { + return time.Now().UTC().Unix() + } + return s.timeFunc() +} + +// Authentication ------------------------------------------------------------- + +// createMac creates a message authentication code (MAC). +func createMac(h hash.Hash, value []byte) []byte { + h.Write(value) + return h.Sum(nil) +} + +// verifyMac verifies that a message authentication code (MAC) is valid. +func verifyMac(h hash.Hash, value []byte, mac []byte) error { + mac2 := createMac(h, value) + // Check that both MACs are of equal length, as subtle.ConstantTimeCompare + // does not do this prior to Go 1.4. + if len(mac) == len(mac2) && subtle.ConstantTimeCompare(mac, mac2) == 1 { + return nil + } + return ErrMacInvalid +} + +// Encryption ----------------------------------------------------------------- + +// encrypt encrypts a value using the given block in counter mode. +// +// A random initialization vector (http://goo.gl/zF67k) with the length of the +// block size is prepended to the resulting ciphertext. +func encrypt(block cipher.Block, value []byte) ([]byte, error) { + iv := GenerateRandomKey(block.BlockSize()) + if iv == nil { + return nil, errGeneratingIV + } + // Encrypt it. + stream := cipher.NewCTR(block, iv) + stream.XORKeyStream(value, value) + // Return iv + ciphertext. + return append(iv, value...), nil +} + +// decrypt decrypts a value using the given block in counter mode. +// +// The value to be decrypted must be prepended by a initialization vector +// (http://goo.gl/zF67k) with the length of the block size. +func decrypt(block cipher.Block, value []byte) ([]byte, error) { + size := block.BlockSize() + if len(value) > size { + // Extract iv. + iv := value[:size] + // Extract ciphertext. + value = value[size:] + // Decrypt it. + stream := cipher.NewCTR(block, iv) + stream.XORKeyStream(value, value) + return value, nil + } + return nil, errDecryptionFailed +} + +// Serialization -------------------------------------------------------------- + +// Serialize encodes a value using gob. +func (e GobEncoder) Serialize(src interface{}) ([]byte, error) { + buf := new(bytes.Buffer) + enc := gob.NewEncoder(buf) + if err := enc.Encode(src); err != nil { + return nil, cookieError{cause: err, typ: usageError} + } + return buf.Bytes(), nil +} + +// Deserialize decodes a value using gob. +func (e GobEncoder) Deserialize(src []byte, dst interface{}) error { + dec := gob.NewDecoder(bytes.NewBuffer(src)) + if err := dec.Decode(dst); err != nil { + return cookieError{cause: err, typ: decodeError} + } + return nil +} + +// Serialize encodes a value using encoding/json. +func (e JSONEncoder) Serialize(src interface{}) ([]byte, error) { + buf := new(bytes.Buffer) + enc := json.NewEncoder(buf) + if err := enc.Encode(src); err != nil { + return nil, cookieError{cause: err, typ: usageError} + } + return buf.Bytes(), nil +} + +// Deserialize decodes a value using encoding/json. +func (e JSONEncoder) Deserialize(src []byte, dst interface{}) error { + dec := json.NewDecoder(bytes.NewReader(src)) + if err := dec.Decode(dst); err != nil { + return cookieError{cause: err, typ: decodeError} + } + return nil +} + +// Serialize passes a []byte through as-is. +func (e NopEncoder) Serialize(src interface{}) ([]byte, error) { + if b, ok := src.([]byte); ok { + return b, nil + } + + return nil, errValueNotByte +} + +// Deserialize passes a []byte through as-is. +func (e NopEncoder) Deserialize(src []byte, dst interface{}) error { + if dat, ok := dst.(*[]byte); ok { + *dat = src + return nil + } + return errValueNotBytePtr +} + +// Encoding ------------------------------------------------------------------- + +// encode encodes a value using base64. +func encode(value []byte) []byte { + encoded := make([]byte, base64.URLEncoding.EncodedLen(len(value))) + base64.URLEncoding.Encode(encoded, value) + return encoded +} + +// decode decodes a cookie using base64. +func decode(value []byte) ([]byte, error) { + decoded := make([]byte, base64.URLEncoding.DecodedLen(len(value))) + b, err := base64.URLEncoding.Decode(decoded, value) + if err != nil { + return nil, cookieError{cause: err, typ: decodeError, msg: "base64 decode failed"} + } + return decoded[:b], nil +} + +// Helpers -------------------------------------------------------------------- + +// GenerateRandomKey creates a random key with the given length in bytes. +// On failure, returns nil. +// +// Callers should explicitly check for the possibility of a nil return, treat +// it as a failure of the system random number generator, and not continue. +func GenerateRandomKey(length int) []byte { + k := make([]byte, length) + if _, err := io.ReadFull(rand.Reader, k); err != nil { + return nil + } + return k +} + +// CodecsFromPairs returns a slice of SecureCookie instances. +// +// It is a convenience function to create a list of codecs for key rotation. Note +// that the generated Codecs will have the default options applied: callers +// should iterate over each Codec and type-assert the underlying *SecureCookie to +// change these. +// +// Example: +// +// codecs := securecookie.CodecsFromPairs( +// []byte("new-hash-key"), +// []byte("new-block-key"), +// []byte("old-hash-key"), +// []byte("old-block-key"), +// ) +// +// // Modify each instance. +// for _, s := range codecs { +// if cookie, ok := s.(*securecookie.SecureCookie); ok { +// cookie.MaxAge(86400 * 7) +// cookie.SetSerializer(securecookie.JSONEncoder{}) +// cookie.HashFunc(sha512.New512_256) +// } +// } +// +func CodecsFromPairs(keyPairs ...[]byte) []Codec { + codecs := make([]Codec, len(keyPairs)/2+len(keyPairs)%2) + for i := 0; i < len(keyPairs); i += 2 { + var blockKey []byte + if i+1 < len(keyPairs) { + blockKey = keyPairs[i+1] + } + codecs[i/2] = New(keyPairs[i], blockKey) + } + return codecs +} + +// EncodeMulti encodes a cookie value using a group of codecs. +// +// The codecs are tried in order. Multiple codecs are accepted to allow +// key rotation. +// +// On error, may return a MultiError. +func EncodeMulti(name string, value interface{}, codecs ...Codec) (string, error) { + if len(codecs) == 0 { + return "", errNoCodecs + } + + var errors MultiError + for _, codec := range codecs { + encoded, err := codec.Encode(name, value) + if err == nil { + return encoded, nil + } + errors = append(errors, err) + } + return "", errors +} + +// DecodeMulti decodes a cookie value using a group of codecs. +// +// The codecs are tried in order. Multiple codecs are accepted to allow +// key rotation. +// +// On error, may return a MultiError. +func DecodeMulti(name string, value string, dst interface{}, codecs ...Codec) error { + if len(codecs) == 0 { + return errNoCodecs + } + + var errors MultiError + for _, codec := range codecs { + err := codec.Decode(name, value, dst) + if err == nil { + return nil + } + errors = append(errors, err) + } + return errors +} + +// MultiError groups multiple errors. +type MultiError []error + +func (m MultiError) IsUsage() bool { return m.any(func(e Error) bool { return e.IsUsage() }) } +func (m MultiError) IsDecode() bool { return m.any(func(e Error) bool { return e.IsDecode() }) } +func (m MultiError) IsInternal() bool { return m.any(func(e Error) bool { return e.IsInternal() }) } + +// Cause returns nil for MultiError; there is no unique underlying cause in the +// general case. +// +// Note: we could conceivably return a non-nil Cause only when there is exactly +// one child error with a Cause. However, it would be brittle for client code +// to rely on the arity of causes inside a MultiError, so we have opted not to +// provide this functionality. Clients which really wish to access the Causes +// of the underlying errors are free to iterate through the errors themselves. +func (m MultiError) Cause() error { return nil } + +func (m MultiError) Error() string { + s, n := "", 0 + for _, e := range m { + if e != nil { + if n == 0 { + s = e.Error() + } + n++ + } + } + switch n { + case 0: + return "(0 errors)" + case 1: + return s + case 2: + return s + " (and 1 other error)" + } + return fmt.Sprintf("%s (and %d other errors)", s, n-1) +} + +// any returns true if any element of m is an Error for which pred returns true. +func (m MultiError) any(pred func(Error) bool) bool { + for _, e := range m { + if ourErr, ok := e.(Error); ok && pred(ourErr) { + return true + } + } + return false +} diff --git a/vendor/github.com/gorilla/sessions/LICENSE b/vendor/github.com/gorilla/sessions/LICENSE new file mode 100644 index 000000000000..0e5fb872800d --- /dev/null +++ b/vendor/github.com/gorilla/sessions/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 Rodrigo Moraes. 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/vendor/github.com/gorilla/sessions/README.md b/vendor/github.com/gorilla/sessions/README.md new file mode 100644 index 000000000000..5bb310704190 --- /dev/null +++ b/vendor/github.com/gorilla/sessions/README.md @@ -0,0 +1,81 @@ +sessions +======== +[![GoDoc](https://godoc.org/github.com/gorilla/sessions?status.svg)](https://godoc.org/github.com/gorilla/sessions) [![Build Status](https://travis-ci.org/gorilla/sessions.png?branch=master)](https://travis-ci.org/gorilla/sessions) + +gorilla/sessions provides cookie and filesystem sessions and infrastructure for +custom session backends. + +The key features are: + +* Simple API: use it as an easy way to set signed (and optionally + encrypted) cookies. +* Built-in backends to store sessions in cookies or the filesystem. +* Flash messages: session values that last until read. +* Convenient way to switch session persistency (aka "remember me") and set + other attributes. +* Mechanism to rotate authentication and encryption keys. +* Multiple sessions per request, even using different backends. +* Interfaces and infrastructure for custom session backends: sessions from + different stores can be retrieved and batch-saved using a common API. + +Let's start with an example that shows the sessions API in a nutshell: + +```go + import ( + "net/http" + "github.com/gorilla/sessions" + ) + + var store = sessions.NewCookieStore([]byte("something-very-secret")) + + func MyHandler(w http.ResponseWriter, r *http.Request) { + // Get a session. We're ignoring the error resulted from decoding an + // existing session: Get() always returns a session, even if empty. + session, _ := store.Get(r, "session-name") + // Set some session values. + session.Values["foo"] = "bar" + session.Values[42] = 43 + // Save it before we write to the response/return from the handler. + session.Save(r, w) + } +``` + +First we initialize a session store calling `NewCookieStore()` and passing a +secret key used to authenticate the session. Inside the handler, we call +`store.Get()` to retrieve an existing session or a new one. Then we set some +session values in session.Values, which is a `map[interface{}]interface{}`. +And finally we call `session.Save()` to save the session in the response. + +Important Note: If you aren't using gorilla/mux, you need to wrap your handlers +with +[`context.ClearHandler`](http://www.gorillatoolkit.org/pkg/context#ClearHandler) +as or else you will leak memory! An easy way to do this is to wrap the top-level +mux when calling http.ListenAndServe: + +More examples are available [on the Gorilla +website](http://www.gorillatoolkit.org/pkg/sessions). + +## Store Implementations + +Other implementations of the `sessions.Store` interface: + +* [github.com/starJammer/gorilla-sessions-arangodb](https://github.com/starJammer/gorilla-sessions-arangodb) - ArangoDB +* [github.com/yosssi/boltstore](https://github.com/yosssi/boltstore) - Bolt +* [github.com/srinathgs/couchbasestore](https://github.com/srinathgs/couchbasestore) - Couchbase +* [github.com/denizeren/dynamostore](https://github.com/denizeren/dynamostore) - Dynamodb on AWS +* [github.com/bradleypeabody/gorilla-sessions-memcache](https://github.com/bradleypeabody/gorilla-sessions-memcache) - Memcache +* [github.com/dsoprea/go-appengine-sessioncascade](https://github.com/dsoprea/go-appengine-sessioncascade) - Memcache/Datastore/Context in AppEngine +* [github.com/kidstuff/mongostore](https://github.com/kidstuff/mongostore) - MongoDB +* [github.com/srinathgs/mysqlstore](https://github.com/srinathgs/mysqlstore) - MySQL +* [github.com/EnumApps/clustersqlstore](https://github.com/EnumApps/clustersqlstore) - MySQL Cluster +* [github.com/antonlindstrom/pgstore](https://github.com/antonlindstrom/pgstore) - PostgreSQL +* [github.com/boj/redistore](https://github.com/boj/redistore) - Redis +* [github.com/boj/rethinkstore](https://github.com/boj/rethinkstore) - RethinkDB +* [github.com/boj/riakstore](https://github.com/boj/riakstore) - Riak +* [github.com/michaeljs1990/sqlitestore](https://github.com/michaeljs1990/sqlitestore) - SQLite +* [github.com/wader/gormstore](https://github.com/wader/gormstore) - GORM (MySQL, PostgreSQL, SQLite) +* [github.com/gernest/qlstore](https://github.com/gernest/qlstore) - ql + +## License + +BSD licensed. See the LICENSE file for details. diff --git a/vendor/github.com/gorilla/sessions/doc.go b/vendor/github.com/gorilla/sessions/doc.go new file mode 100644 index 000000000000..668e05e4555c --- /dev/null +++ b/vendor/github.com/gorilla/sessions/doc.go @@ -0,0 +1,199 @@ +// Copyright 2012 The Gorilla 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 sessions provides cookie and filesystem sessions and +infrastructure for custom session backends. + +The key features are: + + * Simple API: use it as an easy way to set signed (and optionally + encrypted) cookies. + * Built-in backends to store sessions in cookies or the filesystem. + * Flash messages: session values that last until read. + * Convenient way to switch session persistency (aka "remember me") and set + other attributes. + * Mechanism to rotate authentication and encryption keys. + * Multiple sessions per request, even using different backends. + * Interfaces and infrastructure for custom session backends: sessions from + different stores can be retrieved and batch-saved using a common API. + +Let's start with an example that shows the sessions API in a nutshell: + + import ( + "net/http" + "github.com/gorilla/sessions" + ) + + var store = sessions.NewCookieStore([]byte("something-very-secret")) + + func MyHandler(w http.ResponseWriter, r *http.Request) { + // Get a session. We're ignoring the error resulted from decoding an + // existing session: Get() always returns a session, even if empty. + session, err := store.Get(r, "session-name") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Set some session values. + session.Values["foo"] = "bar" + session.Values[42] = 43 + // Save it before we write to the response/return from the handler. + session.Save(r, w) + } + +First we initialize a session store calling NewCookieStore() and passing a +secret key used to authenticate the session. Inside the handler, we call +store.Get() to retrieve an existing session or a new one. Then we set some +session values in session.Values, which is a map[interface{}]interface{}. +And finally we call session.Save() to save the session in the response. + +Note that in production code, we should check for errors when calling +session.Save(r, w), and either display an error message or otherwise handle it. + +Save must be called before writing to the response, otherwise the session +cookie will not be sent to the client. + +Important Note: If you aren't using gorilla/mux, you need to wrap your handlers +with context.ClearHandler as or else you will leak memory! An easy way to do this +is to wrap the top-level mux when calling http.ListenAndServe: + + http.ListenAndServe(":8080", context.ClearHandler(http.DefaultServeMux)) + +The ClearHandler function is provided by the gorilla/context package. + +That's all you need to know for the basic usage. Let's take a look at other +options, starting with flash messages. + +Flash messages are session values that last until read. The term appeared with +Ruby On Rails a few years back. When we request a flash message, it is removed +from the session. To add a flash, call session.AddFlash(), and to get all +flashes, call session.Flashes(). Here is an example: + + func MyHandler(w http.ResponseWriter, r *http.Request) { + // Get a session. + session, err := store.Get(r, "session-name") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Get the previously flashes, if any. + if flashes := session.Flashes(); len(flashes) > 0 { + // Use the flash values. + } else { + // Set a new flash. + session.AddFlash("Hello, flash messages world!") + } + session.Save(r, w) + } + +Flash messages are useful to set information to be read after a redirection, +like after form submissions. + +There may also be cases where you want to store a complex datatype within a +session, such as a struct. Sessions are serialised using the encoding/gob package, +so it is easy to register new datatypes for storage in sessions: + + import( + "encoding/gob" + "github.com/gorilla/sessions" + ) + + type Person struct { + FirstName string + LastName string + Email string + Age int + } + + type M map[string]interface{} + + func init() { + + gob.Register(&Person{}) + gob.Register(&M{}) + } + +As it's not possible to pass a raw type as a parameter to a function, gob.Register() +relies on us passing it a value of the desired type. In the example above we've passed +it a pointer to a struct and a pointer to a custom type representing a +map[string]interface. (We could have passed non-pointer values if we wished.) This will +then allow us to serialise/deserialise values of those types to and from our sessions. + +Note that because session values are stored in a map[string]interface{}, there's +a need to type-assert data when retrieving it. We'll use the Person struct we registered above: + + func MyHandler(w http.ResponseWriter, r *http.Request) { + session, err := store.Get(r, "session-name") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Retrieve our struct and type-assert it + val := session.Values["person"] + var person = &Person{} + if person, ok := val.(*Person); !ok { + // Handle the case that it's not an expected type + } + + // Now we can use our person object + } + +By default, session cookies last for a month. This is probably too long for +some cases, but it is easy to change this and other attributes during +runtime. Sessions can be configured individually or the store can be +configured and then all sessions saved using it will use that configuration. +We access session.Options or store.Options to set a new configuration. The +fields are basically a subset of http.Cookie fields. Let's change the +maximum age of a session to one week: + + session.Options = &sessions.Options{ + Path: "/", + MaxAge: 86400 * 7, + HttpOnly: true, + } + +Sometimes we may want to change authentication and/or encryption keys without +breaking existing sessions. The CookieStore supports key rotation, and to use +it you just need to set multiple authentication and encryption keys, in pairs, +to be tested in order: + + var store = sessions.NewCookieStore( + []byte("new-authentication-key"), + []byte("new-encryption-key"), + []byte("old-authentication-key"), + []byte("old-encryption-key"), + ) + +New sessions will be saved using the first pair. Old sessions can still be +read because the first pair will fail, and the second will be tested. This +makes it easy to "rotate" secret keys and still be able to validate existing +sessions. Note: for all pairs the encryption key is optional; set it to nil +or omit it and and encryption won't be used. + +Multiple sessions can be used in the same request, even with different +session backends. When this happens, calling Save() on each session +individually would be cumbersome, so we have a way to save all sessions +at once: it's sessions.Save(). Here's an example: + + var store = sessions.NewCookieStore([]byte("something-very-secret")) + + func MyHandler(w http.ResponseWriter, r *http.Request) { + // Get a session and set a value. + session1, _ := store.Get(r, "session-one") + session1.Values["foo"] = "bar" + // Get another session and set another value. + session2, _ := store.Get(r, "session-two") + session2.Values[42] = 43 + // Save all sessions. + sessions.Save(r, w) + } + +This is possible because when we call Get() from a session store, it adds the +session to a common registry. Save() uses it to save all registered sessions. +*/ +package sessions diff --git a/vendor/github.com/gorilla/sessions/lex.go b/vendor/github.com/gorilla/sessions/lex.go new file mode 100644 index 000000000000..4bbbe1096de0 --- /dev/null +++ b/vendor/github.com/gorilla/sessions/lex.go @@ -0,0 +1,102 @@ +// This file contains code adapted from the Go standard library +// https://github.com/golang/go/blob/39ad0fd0789872f9469167be7fe9578625ff246e/src/net/http/lex.go + +package sessions + +import "strings" + +var isTokenTable = [127]bool{ + '!': true, + '#': true, + '$': true, + '%': true, + '&': true, + '\'': true, + '*': true, + '+': true, + '-': true, + '.': true, + '0': true, + '1': true, + '2': true, + '3': true, + '4': true, + '5': true, + '6': true, + '7': true, + '8': true, + '9': true, + 'A': true, + 'B': true, + 'C': true, + 'D': true, + 'E': true, + 'F': true, + 'G': true, + 'H': true, + 'I': true, + 'J': true, + 'K': true, + 'L': true, + 'M': true, + 'N': true, + 'O': true, + 'P': true, + 'Q': true, + 'R': true, + 'S': true, + 'T': true, + 'U': true, + 'W': true, + 'V': true, + 'X': true, + 'Y': true, + 'Z': true, + '^': true, + '_': true, + '`': true, + 'a': true, + 'b': true, + 'c': true, + 'd': true, + 'e': true, + 'f': true, + 'g': true, + 'h': true, + 'i': true, + 'j': true, + 'k': true, + 'l': true, + 'm': true, + 'n': true, + 'o': true, + 'p': true, + 'q': true, + 'r': true, + 's': true, + 't': true, + 'u': true, + 'v': true, + 'w': true, + 'x': true, + 'y': true, + 'z': true, + '|': true, + '~': true, +} + +func isToken(r rune) bool { + i := int(r) + return i < len(isTokenTable) && isTokenTable[i] +} + +func isNotToken(r rune) bool { + return !isToken(r) +} + +func isCookieNameValid(raw string) bool { + if raw == "" { + return false + } + return strings.IndexFunc(raw, isNotToken) < 0 +} diff --git a/vendor/github.com/gorilla/sessions/sessions.go b/vendor/github.com/gorilla/sessions/sessions.go new file mode 100644 index 000000000000..fe0d2bc8fa4a --- /dev/null +++ b/vendor/github.com/gorilla/sessions/sessions.go @@ -0,0 +1,241 @@ +// Copyright 2012 The Gorilla 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 sessions + +import ( + "encoding/gob" + "fmt" + "net/http" + "time" + + "github.com/gorilla/context" +) + +// Default flashes key. +const flashesKey = "_flash" + +// Options -------------------------------------------------------------------- + +// Options stores configuration for a session or session store. +// +// Fields are a subset of http.Cookie fields. +type Options struct { + Path string + Domain string + // MaxAge=0 means no 'Max-Age' attribute specified. + // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'. + // MaxAge>0 means Max-Age attribute present and given in seconds. + MaxAge int + Secure bool + HttpOnly bool +} + +// Session -------------------------------------------------------------------- + +// NewSession is called by session stores to create a new session instance. +func NewSession(store Store, name string) *Session { + return &Session{ + Values: make(map[interface{}]interface{}), + store: store, + name: name, + } +} + +// Session stores the values and optional configuration for a session. +type Session struct { + // The ID of the session, generated by stores. It should not be used for + // user data. + ID string + // Values contains the user-data for the session. + Values map[interface{}]interface{} + Options *Options + IsNew bool + store Store + name string +} + +// Flashes returns a slice of flash messages from the session. +// +// A single variadic argument is accepted, and it is optional: it defines +// the flash key. If not defined "_flash" is used by default. +func (s *Session) Flashes(vars ...string) []interface{} { + var flashes []interface{} + key := flashesKey + if len(vars) > 0 { + key = vars[0] + } + if v, ok := s.Values[key]; ok { + // Drop the flashes and return it. + delete(s.Values, key) + flashes = v.([]interface{}) + } + return flashes +} + +// AddFlash adds a flash message to the session. +// +// A single variadic argument is accepted, and it is optional: it defines +// the flash key. If not defined "_flash" is used by default. +func (s *Session) AddFlash(value interface{}, vars ...string) { + key := flashesKey + if len(vars) > 0 { + key = vars[0] + } + var flashes []interface{} + if v, ok := s.Values[key]; ok { + flashes = v.([]interface{}) + } + s.Values[key] = append(flashes, value) +} + +// Save is a convenience method to save this session. It is the same as calling +// store.Save(request, response, session). You should call Save before writing to +// the response or returning from the handler. +func (s *Session) Save(r *http.Request, w http.ResponseWriter) error { + return s.store.Save(r, w, s) +} + +// Name returns the name used to register the session. +func (s *Session) Name() string { + return s.name +} + +// Store returns the session store used to register the session. +func (s *Session) Store() Store { + return s.store +} + +// Registry ------------------------------------------------------------------- + +// sessionInfo stores a session tracked by the registry. +type sessionInfo struct { + s *Session + e error +} + +// contextKey is the type used to store the registry in the context. +type contextKey int + +// registryKey is the key used to store the registry in the context. +const registryKey contextKey = 0 + +// GetRegistry returns a registry instance for the current request. +func GetRegistry(r *http.Request) *Registry { + registry := context.Get(r, registryKey) + if registry != nil { + return registry.(*Registry) + } + newRegistry := &Registry{ + request: r, + sessions: make(map[string]sessionInfo), + } + context.Set(r, registryKey, newRegistry) + return newRegistry +} + +// Registry stores sessions used during a request. +type Registry struct { + request *http.Request + sessions map[string]sessionInfo +} + +// Get registers and returns a session for the given name and session store. +// +// It returns a new session if there are no sessions registered for the name. +func (s *Registry) Get(store Store, name string) (session *Session, err error) { + if !isCookieNameValid(name) { + return nil, fmt.Errorf("sessions: invalid character in cookie name: %s", name) + } + if info, ok := s.sessions[name]; ok { + session, err = info.s, info.e + } else { + session, err = store.New(s.request, name) + session.name = name + s.sessions[name] = sessionInfo{s: session, e: err} + } + session.store = store + return +} + +// Save saves all sessions registered for the current request. +func (s *Registry) Save(w http.ResponseWriter) error { + var errMulti MultiError + for name, info := range s.sessions { + session := info.s + if session.store == nil { + errMulti = append(errMulti, fmt.Errorf( + "sessions: missing store for session %q", name)) + } else if err := session.store.Save(s.request, w, session); err != nil { + errMulti = append(errMulti, fmt.Errorf( + "sessions: error saving session %q -- %v", name, err)) + } + } + if errMulti != nil { + return errMulti + } + return nil +} + +// Helpers -------------------------------------------------------------------- + +func init() { + gob.Register([]interface{}{}) +} + +// Save saves all sessions used during the current request. +func Save(r *http.Request, w http.ResponseWriter) error { + return GetRegistry(r).Save(w) +} + +// NewCookie returns an http.Cookie with the options set. It also sets +// the Expires field calculated based on the MaxAge value, for Internet +// Explorer compatibility. +func NewCookie(name, value string, options *Options) *http.Cookie { + cookie := &http.Cookie{ + Name: name, + Value: value, + Path: options.Path, + Domain: options.Domain, + MaxAge: options.MaxAge, + Secure: options.Secure, + HttpOnly: options.HttpOnly, + } + if options.MaxAge > 0 { + d := time.Duration(options.MaxAge) * time.Second + cookie.Expires = time.Now().Add(d) + } else if options.MaxAge < 0 { + // Set it to the past to expire now. + cookie.Expires = time.Unix(1, 0) + } + return cookie +} + +// Error ---------------------------------------------------------------------- + +// MultiError stores multiple errors. +// +// Borrowed from the App Engine SDK. +type MultiError []error + +func (m MultiError) Error() string { + s, n := "", 0 + for _, e := range m { + if e != nil { + if n == 0 { + s = e.Error() + } + n++ + } + } + switch n { + case 0: + return "(0 errors)" + case 1: + return s + case 2: + return s + " (and 1 other error)" + } + return fmt.Sprintf("%s (and %d other errors)", s, n-1) +} diff --git a/vendor/github.com/gorilla/sessions/store.go b/vendor/github.com/gorilla/sessions/store.go new file mode 100644 index 000000000000..4ff6b6c322c1 --- /dev/null +++ b/vendor/github.com/gorilla/sessions/store.go @@ -0,0 +1,295 @@ +// Copyright 2012 The Gorilla 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 sessions + +import ( + "encoding/base32" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/gorilla/securecookie" +) + +// Store is an interface for custom session stores. +// +// See CookieStore and FilesystemStore for examples. +type Store interface { + // Get should return a cached session. + Get(r *http.Request, name string) (*Session, error) + + // New should create and return a new session. + // + // Note that New should never return a nil session, even in the case of + // an error if using the Registry infrastructure to cache the session. + New(r *http.Request, name string) (*Session, error) + + // Save should persist session to the underlying store implementation. + Save(r *http.Request, w http.ResponseWriter, s *Session) error +} + +// CookieStore ---------------------------------------------------------------- + +// NewCookieStore returns a new CookieStore. +// +// Keys are defined in pairs to allow key rotation, but the common case is +// to set a single authentication key and optionally an encryption key. +// +// The first key in a pair is used for authentication and the second for +// encryption. The encryption key can be set to nil or omitted in the last +// pair, but the authentication key is required in all pairs. +// +// It is recommended to use an authentication key with 32 or 64 bytes. +// The encryption key, if set, must be either 16, 24, or 32 bytes to select +// AES-128, AES-192, or AES-256 modes. +// +// Use the convenience function securecookie.GenerateRandomKey() to create +// strong keys. +func NewCookieStore(keyPairs ...[]byte) *CookieStore { + cs := &CookieStore{ + Codecs: securecookie.CodecsFromPairs(keyPairs...), + Options: &Options{ + Path: "/", + MaxAge: 86400 * 30, + }, + } + + cs.MaxAge(cs.Options.MaxAge) + return cs +} + +// CookieStore stores sessions using secure cookies. +type CookieStore struct { + Codecs []securecookie.Codec + Options *Options // default configuration +} + +// Get returns a session for the given name after adding it to the registry. +// +// It returns a new session if the sessions doesn't exist. Access IsNew on +// the session to check if it is an existing session or a new one. +// +// It returns a new session and an error if the session exists but could +// not be decoded. +func (s *CookieStore) Get(r *http.Request, name string) (*Session, error) { + return GetRegistry(r).Get(s, name) +} + +// New returns a session for the given name without adding it to the registry. +// +// The difference between New() and Get() is that calling New() twice will +// decode the session data twice, while Get() registers and reuses the same +// decoded session after the first call. +func (s *CookieStore) New(r *http.Request, name string) (*Session, error) { + session := NewSession(s, name) + opts := *s.Options + session.Options = &opts + session.IsNew = true + var err error + if c, errCookie := r.Cookie(name); errCookie == nil { + err = securecookie.DecodeMulti(name, c.Value, &session.Values, + s.Codecs...) + if err == nil { + session.IsNew = false + } + } + return session, err +} + +// Save adds a single session to the response. +func (s *CookieStore) Save(r *http.Request, w http.ResponseWriter, + session *Session) error { + encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, + s.Codecs...) + if err != nil { + return err + } + http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options)) + return nil +} + +// MaxAge sets the maximum age for the store and the underlying cookie +// implementation. Individual sessions can be deleted by setting Options.MaxAge +// = -1 for that session. +func (s *CookieStore) MaxAge(age int) { + s.Options.MaxAge = age + + // Set the maxAge for each securecookie instance. + for _, codec := range s.Codecs { + if sc, ok := codec.(*securecookie.SecureCookie); ok { + sc.MaxAge(age) + } + } +} + +// FilesystemStore ------------------------------------------------------------ + +var fileMutex sync.RWMutex + +// NewFilesystemStore returns a new FilesystemStore. +// +// The path argument is the directory where sessions will be saved. If empty +// it will use os.TempDir(). +// +// See NewCookieStore() for a description of the other parameters. +func NewFilesystemStore(path string, keyPairs ...[]byte) *FilesystemStore { + if path == "" { + path = os.TempDir() + } + fs := &FilesystemStore{ + Codecs: securecookie.CodecsFromPairs(keyPairs...), + Options: &Options{ + Path: "/", + MaxAge: 86400 * 30, + }, + path: path, + } + + fs.MaxAge(fs.Options.MaxAge) + return fs +} + +// FilesystemStore stores sessions in the filesystem. +// +// It also serves as a reference for custom stores. +// +// This store is still experimental and not well tested. Feedback is welcome. +type FilesystemStore struct { + Codecs []securecookie.Codec + Options *Options // default configuration + path string +} + +// MaxLength restricts the maximum length of new sessions to l. +// If l is 0 there is no limit to the size of a session, use with caution. +// The default for a new FilesystemStore is 4096. +func (s *FilesystemStore) MaxLength(l int) { + for _, c := range s.Codecs { + if codec, ok := c.(*securecookie.SecureCookie); ok { + codec.MaxLength(l) + } + } +} + +// Get returns a session for the given name after adding it to the registry. +// +// See CookieStore.Get(). +func (s *FilesystemStore) Get(r *http.Request, name string) (*Session, error) { + return GetRegistry(r).Get(s, name) +} + +// New returns a session for the given name without adding it to the registry. +// +// See CookieStore.New(). +func (s *FilesystemStore) New(r *http.Request, name string) (*Session, error) { + session := NewSession(s, name) + opts := *s.Options + session.Options = &opts + session.IsNew = true + var err error + if c, errCookie := r.Cookie(name); errCookie == nil { + err = securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...) + if err == nil { + err = s.load(session) + if err == nil { + session.IsNew = false + } + } + } + return session, err +} + +// Save adds a single session to the response. +// +// If the Options.MaxAge of the session is <= 0 then the session file will be +// deleted from the store path. With this process it enforces the properly +// session cookie handling so no need to trust in the cookie management in the +// web browser. +func (s *FilesystemStore) Save(r *http.Request, w http.ResponseWriter, + session *Session) error { + // Delete if max-age is <= 0 + if session.Options.MaxAge <= 0 { + if err := s.erase(session); err != nil { + return err + } + http.SetCookie(w, NewCookie(session.Name(), "", session.Options)) + return nil + } + + if session.ID == "" { + // Because the ID is used in the filename, encode it to + // use alphanumeric characters only. + session.ID = strings.TrimRight( + base32.StdEncoding.EncodeToString( + securecookie.GenerateRandomKey(32)), "=") + } + if err := s.save(session); err != nil { + return err + } + encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, + s.Codecs...) + if err != nil { + return err + } + http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options)) + return nil +} + +// MaxAge sets the maximum age for the store and the underlying cookie +// implementation. Individual sessions can be deleted by setting Options.MaxAge +// = -1 for that session. +func (s *FilesystemStore) MaxAge(age int) { + s.Options.MaxAge = age + + // Set the maxAge for each securecookie instance. + for _, codec := range s.Codecs { + if sc, ok := codec.(*securecookie.SecureCookie); ok { + sc.MaxAge(age) + } + } +} + +// save writes encoded session.Values to a file. +func (s *FilesystemStore) save(session *Session) error { + encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, + s.Codecs...) + if err != nil { + return err + } + filename := filepath.Join(s.path, "session_"+session.ID) + fileMutex.Lock() + defer fileMutex.Unlock() + return ioutil.WriteFile(filename, []byte(encoded), 0600) +} + +// load reads a file and decodes its content into session.Values. +func (s *FilesystemStore) load(session *Session) error { + filename := filepath.Join(s.path, "session_"+session.ID) + fileMutex.RLock() + defer fileMutex.RUnlock() + fdata, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + if err = securecookie.DecodeMulti(session.Name(), string(fdata), + &session.Values, s.Codecs...); err != nil { + return err + } + return nil +} + +// delete session file +func (s *FilesystemStore) erase(session *Session) error { + filename := filepath.Join(s.path, "session_"+session.ID) + + fileMutex.RLock() + defer fileMutex.RUnlock() + + err := os.Remove(filename) + return err +} diff --git a/vendor/github.com/markbates/goth/LICENSE.txt b/vendor/github.com/markbates/goth/LICENSE.txt new file mode 100644 index 000000000000..f8e6d5b27fa6 --- /dev/null +++ b/vendor/github.com/markbates/goth/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2014 Mark Bates + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/markbates/goth/README.md b/vendor/github.com/markbates/goth/README.md new file mode 100644 index 000000000000..a002ff8fddf0 --- /dev/null +++ b/vendor/github.com/markbates/goth/README.md @@ -0,0 +1,113 @@ +# Goth: Multi-Provider Authentication for Go [![GoDoc](https://godoc.org/github.com/markbates/goth?status.svg)](https://godoc.org/github.com/markbates/goth) [![Build Status](https://travis-ci.org/markbates/goth.svg)](https://travis-ci.org/markbates/goth) + +Package goth provides a simple, clean, and idiomatic way to write authentication +packages for Go web applications. + +Unlike other similar packages, Goth, lets you write OAuth, OAuth2, or any other +protocol providers, as long as they implement the `Provider` and `Session` interfaces. + +This package was inspired by [https://github.com/intridea/omniauth](https://github.com/intridea/omniauth). + +## Installation + +```text +$ go get github.com/markbates/goth +``` + +## Supported Providers + +* Amazon +* Bitbucket +* Box +* Cloud Foundry +* Dailymotion +* Deezer +* Digital Ocean +* Dropbox +* Facebook +* Fitbit +* GitHub +* Gitlab +* Google+ +* Heroku +* InfluxCloud +* Instagram +* Intercom +* Lastfm +* Linkedin +* OneDrive +* Paypal +* SalesForce +* Slack +* Soundcloud +* Spotify +* Steam +* Stripe +* Twitch +* Twitter +* Uber +* Wepay +* Yahoo +* Yammer + +## Examples + +See the [examples](examples) folder for a working application that lets users authenticate +through Twitter, Facebook, Google Plus etc. + +## Issues + +Issues always stand a significantly better chance of getting fixed if the are accompanied by a +pull request. + +## Contributing + +Would I love to see more providers? Certainly! Would you love to contribute one? Hopefully, yes! + +1. Fork it +2. Create your feature branch (git checkout -b my-new-feature) +3. Write Tests! +4. Commit your changes (git commit -am 'Add some feature') +5. Push to the branch (git push origin my-new-feature) +6. Create new Pull Request + +## Contributors + +* Mark Bates +* Tyler Bunnell +* Corey McGrillis +* Rakesh Goyal +* Andy Grunwald +* Kevin Fitzpatrick +* Sharad Ganapathy +* Glenn Walker +* Ben Tranter +* sharadgana +* Geoff Franks +* Craig P Jolicoeur +* Zac Bergquist +* Aurorae +* Rafael Quintela +* oov +* Tyler +* Noah Shibley +* DenSm +* Jacob Walker +* Samy KACIMI +* Roy +* dante gray +* Raphael Geronimi +* Noah +* Jerome Touffe-Blin +* Johnny Boursiquot +* Omni Adams +* Masanobu YOSHIOKA +* Jonathan Hall +* HaiMing.Yin +* Albin Gilles +* Dante Swift +* Felix Lamouroux +* Syed Zubairuddin +* dkhamsing +* Sasa Brankovic +* bryanl diff --git a/vendor/github.com/markbates/goth/doc.go b/vendor/github.com/markbates/goth/doc.go new file mode 100644 index 000000000000..d0bec281c1a1 --- /dev/null +++ b/vendor/github.com/markbates/goth/doc.go @@ -0,0 +1,10 @@ +/* +Package goth provides a simple, clean, and idiomatic way to write authentication +packages for Go web applications. + +This package was inspired by https://github.com/intridea/omniauth. + +See the examples folder for a working application that lets users authenticate +through Twitter or Facebook. +*/ +package goth diff --git a/vendor/github.com/markbates/goth/gothic/gothic.go b/vendor/github.com/markbates/goth/gothic/gothic.go new file mode 100644 index 000000000000..1ac31f3af02d --- /dev/null +++ b/vendor/github.com/markbates/goth/gothic/gothic.go @@ -0,0 +1,190 @@ +/* +Package gothic wraps common behaviour when using Goth. This makes it quick, and easy, to get up +and running with Goth. Of course, if you want complete control over how things flow, in regards +to the authentication process, feel free and use Goth directly. + +See https://github.com/markbates/goth/examples/main.go to see this in action. +*/ +package gothic + +import ( + "errors" + "fmt" + "net/http" + "os" + + "github.com/gorilla/mux" + "github.com/gorilla/sessions" + "github.com/markbates/goth" +) + +// SessionName is the key used to access the session store. +const SessionName = "_gothic_session" + +// Store can/should be set by applications using gothic. The default is a cookie store. +var Store sessions.Store +var defaultStore sessions.Store + +var keySet = false + +func init() { + key := []byte(os.Getenv("SESSION_SECRET")) + keySet = len(key) != 0 + Store = sessions.NewCookieStore([]byte(key)) + defaultStore = Store +} + +/* +BeginAuthHandler is a convienence handler for starting the authentication process. +It expects to be able to get the name of the provider from the query parameters +as either "provider" or ":provider". + +BeginAuthHandler will redirect the user to the appropriate authentication end-point +for the requested provider. + +See https://github.com/markbates/goth/examples/main.go to see this in action. +*/ +func BeginAuthHandler(res http.ResponseWriter, req *http.Request) { + url, err := GetAuthURL(res, req) + if err != nil { + res.WriteHeader(http.StatusBadRequest) + fmt.Fprintln(res, err) + return + } + + http.Redirect(res, req, url, http.StatusTemporaryRedirect) +} + +// SetState sets the state string associated with the given request. +// If no state string is associated with the request, one will be generated. +// This state is sent to the provider and can be retrieved during the +// callback. +var SetState = func(req *http.Request) string { + state := req.URL.Query().Get("state") + if len(state) > 0 { + return state + } + + return "state" + +} + +// GetState gets the state returned by the provider during the callback. +// This is used to prevent CSRF attacks, see +// http://tools.ietf.org/html/rfc6749#section-10.12 +var GetState = func(req *http.Request) string { + return req.URL.Query().Get("state") +} + +/* +GetAuthURL starts the authentication process with the requested provided. +It will return a URL that should be used to send users to. + +It expects to be able to get the name of the provider from the query parameters +as either "provider" or ":provider". + +I would recommend using the BeginAuthHandler instead of doing all of these steps +yourself, but that's entirely up to you. +*/ +func GetAuthURL(res http.ResponseWriter, req *http.Request) (string, error) { + + if !keySet && defaultStore == Store { + fmt.Println("goth/gothic: no SESSION_SECRET environment variable is set. The default cookie store is not available and any calls will fail. Ignore this warning if you are using a different store.") + } + + providerName, err := GetProviderName(req) + if err != nil { + return "", err + } + + provider, err := goth.GetProvider(providerName) + if err != nil { + return "", err + } + sess, err := provider.BeginAuth(SetState(req)) + if err != nil { + return "", err + } + + url, err := sess.GetAuthURL() + if err != nil { + return "", err + } + + session, _ := Store.Get(req, SessionName) + session.Values[SessionName] = sess.Marshal() + err = session.Save(req, res) + if err != nil { + return "", err + } + + return url, err +} + +/* +CompleteUserAuth does what it says on the tin. It completes the authentication +process and fetches all of the basic information about the user from the provider. + +It expects to be able to get the name of the provider from the query parameters +as either "provider" or ":provider". + +See https://github.com/markbates/goth/examples/main.go to see this in action. +*/ +var CompleteUserAuth = func(res http.ResponseWriter, req *http.Request) (goth.User, error) { + + if !keySet && defaultStore == Store { + fmt.Println("goth/gothic: no SESSION_SECRET environment variable is set. The default cookie store is not available and any calls will fail. Ignore this warning if you are using a different store.") + } + + providerName, err := GetProviderName(req) + if err != nil { + return goth.User{}, err + } + + provider, err := goth.GetProvider(providerName) + if err != nil { + return goth.User{}, err + } + + session, _ := Store.Get(req, SessionName) + + if session.Values[SessionName] == nil { + return goth.User{}, errors.New("could not find a matching session for this request") + } + + sess, err := provider.UnmarshalSession(session.Values[SessionName].(string)) + if err != nil { + return goth.User{}, err + } + + _, err = sess.Authorize(provider, req.URL.Query()) + + if err != nil { + return goth.User{}, err + } + + return provider.FetchUser(sess) +} + +// GetProviderName is a function used to get the name of a provider +// for a given request. By default, this provider is fetched from +// the URL query string. If you provide it in a different way, +// assign your own function to this variable that returns the provider +// name for your request. +var GetProviderName = getProviderName + +func getProviderName(req *http.Request) (string, error) { + provider := req.URL.Query().Get("provider") + if provider == "" { + if p, ok := mux.Vars(req)["provider"]; ok { + return p, nil + } + } + if provider == "" { + provider = req.URL.Query().Get(":provider") + } + if provider == "" { + return provider, errors.New("you must select a provider") + } + return provider, nil +} diff --git a/vendor/github.com/markbates/goth/provider.go b/vendor/github.com/markbates/goth/provider.go new file mode 100644 index 000000000000..ff83143135a5 --- /dev/null +++ b/vendor/github.com/markbates/goth/provider.go @@ -0,0 +1,51 @@ +package goth + +import "fmt" +import "golang.org/x/oauth2" + +// Provider needs to be implemented for each 3rd party authentication provider +// e.g. Facebook, Twitter, etc... +type Provider interface { + Name() string + BeginAuth(state string) (Session, error) + UnmarshalSession(string) (Session, error) + FetchUser(Session) (User, error) + Debug(bool) + RefreshToken(refreshToken string) (*oauth2.Token, error) //Get new access token based on the refresh token + RefreshTokenAvailable() bool //Refresh token is provided by auth provider or not +} + +// Providers is list of known/available providers. +type Providers map[string]Provider + +var providers = Providers{} + +// UseProviders adds a list of available providers for use with Goth. +// Can be called multiple times. If you pass the same provider more +// than once, the last will be used. +func UseProviders(viders ...Provider) { + for _, provider := range viders { + providers[provider.Name()] = provider + } +} + +// GetProviders returns a list of all the providers currently in use. +func GetProviders() Providers { + return providers +} + +// GetProvider returns a previously created provider. If Goth has not +// been told to use the named provider it will return an error. +func GetProvider(name string) (Provider, error) { + provider := providers[name] + if provider == nil { + return nil, fmt.Errorf("no provider for %s exists", name) + } + return provider, nil +} + +// ClearProviders will remove all providers currently in use. +// This is useful, mostly, for testing purposes. +func ClearProviders() { + providers = Providers{} +} diff --git a/vendor/github.com/markbates/goth/providers/github/github.go b/vendor/github.com/markbates/goth/providers/github/github.go new file mode 100644 index 000000000000..46d810d3c31b --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/github/github.go @@ -0,0 +1,207 @@ +// Package github implements the OAuth2 protocol for authenticating users through Github. +// This package can be used as a reference implementation of an OAuth2 provider for Goth. +package github + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/markbates/goth" + "golang.org/x/oauth2" +) + +// These vars define the Authentication, Token, and API URLS for GitHub. If +// using GitHub enterprise you should change these values before calling New. +// +// Examples: +// github.AuthURL = "https://github.acme.com/login/oauth/authorize +// github.TokenURL = "https://github.acme.com/login/oauth/access_token +// github.ProfileURL = "https://github.acme.com/api/v3/user +// github.EmailURL = "https://github.acme.com/api/v3/user/emails +var ( + AuthURL = "https://github.com/login/oauth/authorize" + TokenURL = "https://github.com/login/oauth/access_token" + ProfileURL = "https://api.github.com/user" + EmailURL = "https://api.github.com/user/emails" +) + +// New creates a new Github provider, and sets up important connection details. +// You should always call `github.New` to get a new Provider. Never try to create +// one manually. +func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { + p := &Provider{ + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + } + p.config = newConfig(p, scopes) + return p +} + +// Provider is the implementation of `goth.Provider` for accessing Github. +type Provider struct { + ClientKey string + Secret string + CallbackURL string + config *oauth2.Config +} + +// Name is the name used to retrieve this provider later. +func (p *Provider) Name() string { + return "github" +} + +// Debug is a no-op for the github package. +func (p *Provider) Debug(debug bool) {} + +// BeginAuth asks Github for an authentication end-point. +func (p *Provider) BeginAuth(state string) (goth.Session, error) { + url := p.config.AuthCodeURL(state) + session := &Session{ + AuthURL: url, + } + return session, nil +} + +// FetchUser will go to Github and access basic information about the user. +func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { + sess := session.(*Session) + user := goth.User{ + AccessToken: sess.AccessToken, + Provider: p.Name(), + } + + response, err := http.Get(ProfileURL + "?access_token=" + url.QueryEscape(sess.AccessToken)) + if err != nil { + return user, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return user, fmt.Errorf("GitHub API responded with a %d trying to fetch user information", response.StatusCode) + } + + bits, err := ioutil.ReadAll(response.Body) + if err != nil { + return user, err + } + + err = json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData) + if err != nil { + return user, err + } + + err = userFromReader(bytes.NewReader(bits), &user) + if err != nil { + return user, err + } + + if user.Email == "" { + for _, scope := range p.config.Scopes { + if strings.TrimSpace(scope) == "user" || strings.TrimSpace(scope) == "user:email" { + user.Email, err = getPrivateMail(p, sess) + if err != nil { + return user, err + } + break + } + } + } + return user, err +} + +func userFromReader(reader io.Reader, user *goth.User) error { + u := struct { + ID int `json:"id"` + Email string `json:"email"` + Bio string `json:"bio"` + Name string `json:"name"` + Login string `json:"login"` + Picture string `json:"avatar_url"` + Location string `json:"location"` + }{} + + err := json.NewDecoder(reader).Decode(&u) + if err != nil { + return err + } + + user.Name = u.Name + user.NickName = u.Login + user.Email = u.Email + user.Description = u.Bio + user.AvatarURL = u.Picture + user.UserID = strconv.Itoa(u.ID) + user.Location = u.Location + + return err +} + +func getPrivateMail(p *Provider, sess *Session) (email string, err error) { + response, err := http.Get(EmailURL + "?access_token=" + url.QueryEscape(sess.AccessToken)) + if err != nil { + if response != nil { + response.Body.Close() + } + return email, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return email, fmt.Errorf("GitHub API responded with a %d trying to fetch user email", response.StatusCode) + } + + var mailList = []struct { + Email string `json:"email"` + Primary bool `json:"primary"` + Verified bool `json:"verified"` + }{} + err = json.NewDecoder(response.Body).Decode(&mailList) + if err != nil { + return email, err + } + for _, v := range mailList { + if v.Primary && v.Verified { + return v.Email, nil + } + } + // can't get primary email - shouldn't be possible + return +} + +func newConfig(provider *Provider, scopes []string) *oauth2.Config { + c := &oauth2.Config{ + ClientID: provider.ClientKey, + ClientSecret: provider.Secret, + RedirectURL: provider.CallbackURL, + Endpoint: oauth2.Endpoint{ + AuthURL: AuthURL, + TokenURL: TokenURL, + }, + Scopes: []string{}, + } + + for _, scope := range scopes { + c.Scopes = append(c.Scopes, scope) + } + + return c +} + +//RefreshToken refresh token is not provided by github +func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + return nil, errors.New("Refresh token is not provided by github") +} + +//RefreshTokenAvailable refresh token is not provided by github +func (p *Provider) RefreshTokenAvailable() bool { + return false +} diff --git a/vendor/github.com/markbates/goth/providers/github/session.go b/vendor/github.com/markbates/goth/providers/github/session.go new file mode 100644 index 000000000000..fd7a8481bed1 --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/github/session.go @@ -0,0 +1,56 @@ +package github + +import ( + "encoding/json" + "errors" + "github.com/markbates/goth" + "golang.org/x/oauth2" + "strings" +) + +// Session stores data during the auth process with Github. +type Session struct { + AuthURL string + AccessToken string +} + +// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Github provider. +func (s Session) GetAuthURL() (string, error) { + if s.AuthURL == "" { + return "", errors.New("an AuthURL has not be set") + } + return s.AuthURL, nil +} + +// Authorize the session with Github and return the access token to be stored for future use. +func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { + p := provider.(*Provider) + token, err := p.config.Exchange(oauth2.NoContext, params.Get("code")) + if err != nil { + return "", err + } + + if !token.Valid() { + return "", errors.New("Invalid token received from provider") + } + + s.AccessToken = token.AccessToken + return token.AccessToken, err +} + +// Marshal the session into a string +func (s Session) Marshal() string { + b, _ := json.Marshal(s) + return string(b) +} + +func (s Session) String() string { + return s.Marshal() +} + +// UnmarshalSession will unmarshal a JSON string into a session. +func (p *Provider) UnmarshalSession(data string) (goth.Session, error) { + sess := &Session{} + err := json.NewDecoder(strings.NewReader(data)).Decode(sess) + return sess, err +} diff --git a/vendor/github.com/markbates/goth/session.go b/vendor/github.com/markbates/goth/session.go new file mode 100644 index 000000000000..2d40b50bb46b --- /dev/null +++ b/vendor/github.com/markbates/goth/session.go @@ -0,0 +1,21 @@ +package goth + +// Params is used to pass data to sessions for authorization. An existing +// implementation, and the one most likely to be used, is `url.Values`. +type Params interface { + Get(string) string +} + +// Session needs to be implemented as part of the provider package. +// It will be marshaled and persisted between requests to "tie" +// the start and the end of the authorization process with a +// 3rd party provider. +type Session interface { + // GetAuthURL returns the URL for the authentication end-point for the provider. + GetAuthURL() (string, error) + // Marshal generates a string representation of the Session for storing between requests. + Marshal() string + // Authorize should validate the data from the provider and return an access token + // that can be stored for later access to the provider. + Authorize(Provider, Params) (string, error) +} diff --git a/vendor/github.com/markbates/goth/user.go b/vendor/github.com/markbates/goth/user.go new file mode 100644 index 000000000000..1d6a419632fc --- /dev/null +++ b/vendor/github.com/markbates/goth/user.go @@ -0,0 +1,30 @@ +package goth + +import ( + "encoding/gob" + "time" +) + +func init() { + gob.Register(User{}) +} + +// User contains the information common amongst most OAuth and OAuth2 providers. +// All of the "raw" datafrom the provider can be found in the `RawData` field. +type User struct { + RawData map[string]interface{} + Provider string + Email string + Name string + FirstName string + LastName string + NickName string + Description string + UserID string + AvatarURL string + Location string + AccessToken string + AccessTokenSecret string + RefreshToken string + ExpiresAt time.Time +} diff --git a/vendor/golang.org/x/net/context/context.go b/vendor/golang.org/x/net/context/context.go new file mode 100644 index 000000000000..134654cf7e2b --- /dev/null +++ b/vendor/golang.org/x/net/context/context.go @@ -0,0 +1,156 @@ +// 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 "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{} +} + +// 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 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() diff --git a/vendor/golang.org/x/net/context/go17.go b/vendor/golang.org/x/net/context/go17.go new file mode 100644 index 000000000000..f8cda19adaee --- /dev/null +++ b/vendor/golang.org/x/net/context/go17.go @@ -0,0 +1,72 @@ +// Copyright 2016 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. + +// +build go1.7 + +package context + +import ( + "context" // standard library's context, as of Go 1.7 + "time" +) + +var ( + todo = context.TODO() + background = context.Background() +) + +// Canceled is the error returned by Context.Err when the context is canceled. +var Canceled = context.Canceled + +// DeadlineExceeded is the error returned by Context.Err when the context's +// deadline passes. +var DeadlineExceeded = context.DeadlineExceeded + +// 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) { + ctx, f := context.WithCancel(parent) + return ctx, CancelFunc(f) +} + +// 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) { + ctx, f := context.WithDeadline(parent, deadline) + return ctx, CancelFunc(f) +} + +// 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 context.WithValue(parent, key, val) +} diff --git a/vendor/golang.org/x/net/context/pre_go17.go b/vendor/golang.org/x/net/context/pre_go17.go new file mode 100644 index 000000000000..5a30acabd03c --- /dev/null +++ b/vendor/golang.org/x/net/context/pre_go17.go @@ -0,0 +1,300 @@ +// 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. + +// +build !go1.7 + +package context + +import ( + "errors" + "fmt" + "sync" + "time" +) + +// 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) +) + +// 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") + +// 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/vendor/golang.org/x/oauth2/AUTHORS b/vendor/golang.org/x/oauth2/AUTHORS new file mode 100644 index 000000000000..15167cd746c5 --- /dev/null +++ b/vendor/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/vendor/golang.org/x/oauth2/CONTRIBUTING.md b/vendor/golang.org/x/oauth2/CONTRIBUTING.md new file mode 100644 index 000000000000..46aa2b12dda8 --- /dev/null +++ b/vendor/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://github.com/golang/oauth2/issues), 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/vendor/golang.org/x/oauth2/CONTRIBUTORS b/vendor/golang.org/x/oauth2/CONTRIBUTORS new file mode 100644 index 000000000000..1c4577e96806 --- /dev/null +++ b/vendor/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/vendor/golang.org/x/oauth2/LICENSE b/vendor/golang.org/x/oauth2/LICENSE new file mode 100644 index 000000000000..d02f24fd5288 --- /dev/null +++ b/vendor/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/vendor/golang.org/x/oauth2/README.md b/vendor/golang.org/x/oauth2/README.md new file mode 100644 index 000000000000..1643c08ef876 --- /dev/null +++ b/vendor/golang.org/x/oauth2/README.md @@ -0,0 +1,65 @@ +# OAuth2 for Go + +[![Build Status](https://travis-ci.org/golang/oauth2.svg?branch=master)](https://travis-ci.org/golang/oauth2) +[![GoDoc](https://godoc.org/golang.org/x/oauth2?status.svg)](https://godoc.org/golang.org/x/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" + newurlfetch "google.golang.org/appengine/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/vendor/golang.org/x/oauth2/client_appengine.go b/vendor/golang.org/x/oauth2/client_appengine.go new file mode 100644 index 000000000000..8962c49d1deb --- /dev/null +++ b/vendor/golang.org/x/oauth2/client_appengine.go @@ -0,0 +1,25 @@ +// 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. + +// +build appengine + +// App Engine hooks. + +package oauth2 + +import ( + "net/http" + + "golang.org/x/net/context" + "golang.org/x/oauth2/internal" + "google.golang.org/appengine/urlfetch" +) + +func init() { + internal.RegisterContextClientFunc(contextClientAppEngine) +} + +func contextClientAppEngine(ctx context.Context) (*http.Client, error) { + return urlfetch.Client(ctx), nil +} diff --git a/vendor/golang.org/x/oauth2/internal/oauth2.go b/vendor/golang.org/x/oauth2/internal/oauth2.go new file mode 100644 index 000000000000..fbe1028d64e5 --- /dev/null +++ b/vendor/golang.org/x/oauth2/internal/oauth2.go @@ -0,0 +1,76 @@ +// 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 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 +} + +func CondVal(v string) []string { + if v == "" { + return nil + } + return []string{v} +} diff --git a/vendor/golang.org/x/oauth2/internal/token.go b/vendor/golang.org/x/oauth2/internal/token.go new file mode 100644 index 000000000000..1c0ec76d0444 --- /dev/null +++ b/vendor/golang.org/x/oauth2/internal/token.go @@ -0,0 +1,227 @@ +// 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 internal contains support packages for oauth2 package. +package internal + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "mime" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "golang.org/x/net/context" +) + +// Token represents the crendentials used to authorize +// the requests to access protected resources on the OAuth 2.0 +// provider's backend. +// +// This type is a mirror of oauth2.Token and exists to break +// an otherwise-circular dependency. Other internal packages +// should convert this Token into an oauth2.Token before use. +type Token struct { + // AccessToken is the token that authorizes and authenticates + // the requests. + AccessToken string + + // TokenType is the type of token. + // The Type method returns either this or "Bearer", the default. + TokenType string + + // 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 + + // 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 + + // Raw optionally contains extra metadata from the server + // when updating a token. + Raw interface{} +} + +// 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 +} + +var brokenAuthHeaderProviders = []string{ + "https://accounts.google.com/", + "https://api.dropbox.com/", + "https://api.dropboxapi.com/", + "https://api.instagram.com/", + "https://api.netatmo.net/", + "https://api.odnoklassniki.ru/", + "https://api.pushbullet.com/", + "https://api.soundcloud.com/", + "https://api.twitch.tv/", + "https://app.box.com/", + "https://connect.stripe.com/", + "https://login.microsoftonline.com/", + "https://login.salesforce.com/", + "https://oauth.sandbox.trainingpeaks.com/", + "https://oauth.trainingpeaks.com/", + "https://oauth.vk.com/", + "https://openapi.baidu.com/", + "https://slack.com/", + "https://test-sandbox.auth.corp.google.com", + "https://test.salesforce.com/", + "https://user.gini.net/", + "https://www.douban.com/", + "https://www.googleapis.com/", + "https://www.linkedin.com/", + "https://www.strava.com/oauth/", + "https://www.wunderlist.com/oauth/", + "https://api.patreon.com/", + "https://sandbox.codeswholesale.com/oauth/token", + "https://api.codeswholesale.com/oauth/token", +} + +func RegisterBrokenAuthHeaderProvider(tokenURL string) { + brokenAuthHeaderProviders = append(brokenAuthHeaderProviders, tokenURL) +} + +// 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 +} + +func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values) (*Token, error) { + hc, err := ContextClient(ctx) + if err != nil { + return nil, err + } + v.Set("client_id", clientID) + bustedAuth := !providerAuthHeaderWorks(tokenURL) + if bustedAuth && clientSecret != "" { + v.Set("client_secret", clientSecret) + } + req, err := http.NewRequest("POST", 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(clientID, 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 +} diff --git a/vendor/golang.org/x/oauth2/internal/transport.go b/vendor/golang.org/x/oauth2/internal/transport.go new file mode 100644 index 000000000000..f1f173e345db --- /dev/null +++ b/vendor/golang.org/x/oauth2/internal/transport.go @@ -0,0 +1,69 @@ +// 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 internal contains support packages for oauth2 package. +package internal + +import ( + "net/http" + + "golang.org/x/net/context" +) + +// 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{} + +// 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) { + if ctx != nil { + if hc, ok := ctx.Value(HTTPClient).(*http.Client); ok { + return hc, nil + } + } + for _, fn := range contextClientFuncs { + c, err := fn(ctx) + if err != nil { + return nil, err + } + if c != nil { + return c, nil + } + } + return http.DefaultClient, nil +} + +func ContextTransport(ctx context.Context) http.RoundTripper { + hc, err := ContextClient(ctx) + // This is a rare error case (somebody using nil on App Engine). + if err != nil { + return ErrorTransport{err} + } + return hc.Transport +} + +// ErrorTransport returns the specified error on RoundTrip. +// This RoundTripper should be used in rare error cases where +// error handling can be postponed to response handling time. +type ErrorTransport struct{ Err error } + +func (t ErrorTransport) RoundTrip(*http.Request) (*http.Response, error) { + return nil, t.Err +} diff --git a/vendor/golang.org/x/oauth2/oauth2.go b/vendor/golang.org/x/oauth2/oauth2.go new file mode 100644 index 000000000000..7b06bfe1ef14 --- /dev/null +++ b/vendor/golang.org/x/oauth2/oauth2.go @@ -0,0 +1,341 @@ +// 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 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" + "errors" + "net/http" + "net/url" + "strings" + "sync" + + "golang.org/x/net/context" + "golang.org/x/oauth2/internal" +) + +// NoContext is the default context you should supply if not using +// your own context.Context (see https://golang.org/x/net/context). +// +// Deprecated: Use context.Background() or context.TODO() instead. +var NoContext = context.TODO() + +// RegisterBrokenAuthHeaderProvider registers an OAuth2 server +// identified by the tokenURL prefix as an OAuth2 implementation +// which doesn't support the HTTP Basic authentication +// scheme to authenticate with the authorization server. +// Once a server is registered, credentials (client_id and client_secret) +// will be passed as query parameters rather than being present +// in the Authorization header. +// See https://code.google.com/p/goauth2/issues/detail?id=31 for background. +func RegisterBrokenAuthHeaderProvider(tokenURL string) { + internal.RegisterBrokenAuthHeaderProvider(tokenURL) +} + +// Config describes a typical 3-legged OAuth2 flow, with both the +// client application information and the server's endpoint URLs. +// For the client credentials 2-legged OAuth2 flow, see the clientcredentials +// package (https://golang.org/x/oauth2/clientcredentials). +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 = SetAuthURLParam("access_type", "online") + AccessTypeOffline AuthCodeOption = SetAuthURLParam("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 = SetAuthURLParam("approval_prompt", "force") +) + +// An AuthCodeOption is passed to Config.AuthCodeURL. +type AuthCodeOption interface { + setValue(url.Values) +} + +type setParam struct{ k, v string } + +func (p setParam) setValue(m url.Values) { m.Set(p.k, p.v) } + +// SetAuthURLParam builds an AuthCodeOption which passes key/value parameters +// to a provider's authorization endpoint. +func SetAuthURLParam(key, value string) AuthCodeOption { + return setParam{key, value} +} + +// 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": internal.CondVal(c.RedirectURL), + "scope": internal.CondVal(strings.Join(c.Scopes, " ")), + "state": internal.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": internal.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": internal.CondVal(c.RedirectURL), + "scope": internal.CondVal(strings.Join(c.Scopes, " ")), + }) +} + +// 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 +} + +// StaticTokenSource returns a TokenSource that always returns the same token. +// Because the provided token t is never refreshed, StaticTokenSource is only +// useful for tokens that never expire. +func StaticTokenSource(t *Token) TokenSource { + return staticTokenSource{t} +} + +// staticTokenSource is a TokenSource that always returns the same Token. +type staticTokenSource struct { + t *Token +} + +func (s staticTokenSource) Token() (*Token, error) { + return s.t, nil +} + +// 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 internal.ContextKey + +// 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 := internal.ContextClient(ctx) + if err != nil { + return &http.Client{Transport: internal.ErrorTransport{Err: err}} + } + return c + } + return &http.Client{ + Transport: &Transport{ + Base: internal.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/vendor/golang.org/x/oauth2/token.go b/vendor/golang.org/x/oauth2/token.go new file mode 100644 index 000000000000..7a3167f15b04 --- /dev/null +++ b/vendor/golang.org/x/oauth2/token.go @@ -0,0 +1,158 @@ +// 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 oauth2 + +import ( + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "golang.org/x/net/context" + "golang.org/x/oauth2/internal" +) + +// 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 strings.EqualFold(t.TokenType, "bearer") { + return "Bearer" + } + if strings.EqualFold(t.TokenType, "mac") { + return "MAC" + } + if strings.EqualFold(t.TokenType, "basic") { + return "Basic" + } + 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 raw, ok := t.raw.(map[string]interface{}); ok { + return raw[key] + } + + vals, ok := t.raw.(url.Values) + if !ok { + return nil + } + + v := vals.Get(key) + switch s := strings.TrimSpace(v); strings.Count(s, ".") { + case 0: // Contains no "."; try to parse as int + if i, err := strconv.ParseInt(s, 10, 64); err == nil { + return i + } + case 1: // Contains a single "."; try to parse as float + if f, err := strconv.ParseFloat(s, 64); err == nil { + return f + } + } + + return v +} + +// 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() +} + +// tokenFromInternal maps an *internal.Token struct into +// a *Token struct. +func tokenFromInternal(t *internal.Token) *Token { + if t == nil { + return nil + } + return &Token{ + AccessToken: t.AccessToken, + TokenType: t.TokenType, + RefreshToken: t.RefreshToken, + Expiry: t.Expiry, + raw: t.Raw, + } +} + +// retrieveToken takes a *Config and uses that to retrieve an *internal.Token. +// This token is then mapped from *internal.Token into an *oauth2.Token which is returned along +// with an error.. +func retrieveToken(ctx context.Context, c *Config, v url.Values) (*Token, error) { + tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.TokenURL, v) + if err != nil { + return nil, err + } + return tokenFromInternal(tk), nil +} diff --git a/vendor/golang.org/x/oauth2/transport.go b/vendor/golang.org/x/oauth2/transport.go new file mode 100644 index 000000000000..92ac7e2531f4 --- /dev/null +++ b/vendor/golang.org/x/oauth2/transport.go @@ -0,0 +1,132 @@ +// 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 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 + } +} diff --git a/vendor/vendor.json b/vendor/vendor.json index d34874edd657..36e87562f66b 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -326,6 +326,12 @@ "revision": "d8eeeb8bae8896dd8e1b7e514ab0d396c4f12a1b", "revisionTime": "2016-11-03T02:43:54Z" }, + { + "checksumSHA1": "ompMQ/S/j3TdR7MhlODhbv0KHzk=", + "path": "github.com/markbates/goth", + "revision": "6652f6789160467f9d34f447302071ed8d1285db", + "revisionTime": "2017-01-09T01:05:47Z" + }, { "checksumSHA1": "9FJUwn3EIgASVki+p8IHgWVC5vQ=", "path": "github.com/mattn/go-sqlite3", @@ -938,6 +944,12 @@ "revision": "9477e0b78b9ac3d0b03822fd95422e2fe07627cd", "revisionTime": "2016-10-31T15:37:30Z" }, + { + "checksumSHA1": "9jjO5GjLa0XF/nfWihF02RoH4qc=", + "path": "golang.org/x/net/context", + "revision": "da2b4fa28524a3baf148c1b94df4440267063c88", + "revisionTime": "2017-01-07T14:28:43Z" + }, { "checksumSHA1": "vqc3a+oTUGX8PmD0TS+qQ7gmN8I=", "path": "golang.org/x/net/html", @@ -956,6 +968,18 @@ "revision": "569280fa63be4e201b975e5411e30a92178f0118", "revisionTime": "2016-11-03T00:14:07Z" }, + { + "checksumSHA1": "XH7CgbL5Z8COUc+MKrYqS3FFosY=", + "path": "golang.org/x/oauth2", + "revision": "314dd2c0bf3ebd592ec0d20847d27e79d0dbe8dd", + "revisionTime": "2016-12-14T09:25:55Z" + }, + { + "checksumSHA1": "Wqm34oALxi3GkTSHIBa/EcfE37Y=", + "path": "golang.org/x/oauth2/internal", + "revision": "314dd2c0bf3ebd592ec0d20847d27e79d0dbe8dd", + "revisionTime": "2016-12-14T09:25:55Z" + }, { "checksumSHA1": "8fD/im5Kwvy3JgmxulDTambmE8w=", "path": "golang.org/x/sys/unix", From 1f94c85c57d4c01bfa5fb90c65b757c046b4fec6 Mon Sep 17 00:00:00 2001 From: willemvd Date: Sun, 15 Jan 2017 14:48:46 +0100 Subject: [PATCH 02/40] login button on the signIn page to start the OAuth2 flow and a callback for each provider Only GitHub is implemented for now --- cmd/web.go | 4 +- models/login_source.go | 150 ++++++++++++++++++++++---------- modules/auth/auth.go | 2 +- modules/auth/auth_form.go | 57 ++++++------ modules/auth/oauth2/oauth2.go | 106 +++++++++++----------- options/locale/locale_en-US.ini | 3 + public/js/index.js | 4 + routers/admin/auths.go | 17 ++-- routers/org/setting.go | 2 +- routers/repo/http.go | 2 +- routers/user/auth.go | 41 ++++++++- routers/user/setting.go | 2 +- templates/admin/auth/edit.tmpl | 26 ++++++ templates/admin/auth/new.tmpl | 25 ++++++ templates/user/auth/signin.tmpl | 8 ++ 15 files changed, 310 insertions(+), 139 deletions(-) diff --git a/cmd/web.go b/cmd/web.go index 2658a4bd6119..c187f9896a1a 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -40,7 +40,6 @@ import ( "github.com/go-macaron/toolbox" "github.com/urfave/cli" macaron "gopkg.in/macaron.v1" - "code.gitea.io/gitea/modules/auth/oauth2" ) // CmdWeb represents the available web sub-command. @@ -194,7 +193,8 @@ func runWeb(ctx *cli.Context) error { m.Post("/sign_up", bindIgnErr(auth.RegisterForm{}), user.SignUpPost) m.Get("/reset_password", user.ResetPasswd) m.Post("/reset_password", user.ResetPasswdPost) - m.Get("/oauth2/:provider/callback", oauth2.Callback) + m.Get("/oauth2/:provider", user.SignInOAuth) + m.Get("/oauth2/:provider/callback", user.SignInOAuthCallback) }, reqSignOut) m.Group("/user/settings", func() { diff --git a/models/login_source.go b/models/login_source.go index cd1a818b21a7..2a7f39ce328a 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -23,6 +23,8 @@ import ( "code.gitea.io/gitea/modules/auth/pam" "code.gitea.io/gitea/modules/auth/oauth2" "code.gitea.io/gitea/modules/log" + "gopkg.in/macaron.v1" + "github.com/go-macaron/session" ) // LoginType represents an login type. @@ -31,12 +33,12 @@ type LoginType int // Note: new type must append to the end of list to maintain compatibility. const ( LoginNoType LoginType = iota - LoginPlain // 1 - LoginLDAP // 2 - LoginSMTP // 3 - LoginPAM // 4 - LoginDLDAP // 5 - LoginOAuth2 // 6 + LoginPlain // 1 + LoginLDAP // 2 + LoginSMTP // 3 + LoginPAM // 4 + LoginDLDAP // 5 + LoginOAuth2 // 6 ) // LoginNames contains the name of LoginType values. @@ -119,13 +121,11 @@ func (cfg *PAMConfig) ToDB() ([]byte, error) { return json.Marshal(cfg) } -// OAuth2 holds configuration for the OAuth2 login source. +// OAuth2Config holds configuration for the OAuth2 login source. type OAuth2Config struct { - Provider string - ClientId string - ClientSecret string - TLS bool - SkipVerify bool + Provider string + ClientID string + ClientSecret string } // FromDB fills up an OAuth2Config from serialized format. @@ -237,7 +237,7 @@ func (source *LoginSource) IsOAuth2() bool { func (source *LoginSource) HasTLS() bool { return ((source.IsLDAP() || source.IsDLDAP()) && source.LDAP().SecurityProtocol > ldap.SecurityProtocolUnencrypted) || - source.IsSMTP() || source.IsOAuth2() + source.IsSMTP() } // UseTLS returns true of this source is configured to use TLS. @@ -248,7 +248,7 @@ func (source *LoginSource) UseTLS() bool { case LoginSMTP: return source.SMTP().TLS case LoginOAuth2: - return source.OAuth2().TLS + return true } return false @@ -262,8 +262,6 @@ func (source *LoginSource) SkipVerify() bool { return source.LDAP().SkipVerify case LoginSMTP: return source.SMTP().SkipVerify - case LoginOAuth2: - return source.OAuth2().SkipVerify } return false @@ -483,7 +481,7 @@ func LoginViaSMTP(user *User, login, password string, sourceID int64, cfg *SMTPC idx := strings.Index(login, "@") if idx == -1 { return nil, ErrUserNotExist{0, login, 0} - } else if !com.IsSliceContainsStr(strings.Split(cfg.AllowedDomains, ","), login[idx+1:]) { + } else if !com.IsSliceContainsStr(strings.Split(cfg.AllowedDomains, ","), login[idx + 1:]) { return nil, ErrUserNotExist{0, login, 0} } } @@ -574,31 +572,12 @@ func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMCon // LoginViaOAuth2 queries if login/password is valid against the OAuth2.0 provider, // and create a local user if success when enabled. -func LoginViaOAuth2(user *User, login, password string, sourceID int64, cfg *OAuth2Config, autoRegister bool) (*User, error) { - email, err := oauth2.Auth(cfg.Provider, cfg.ClientId, cfg.ClientSecret, login, password) - if err != nil { - return nil, err - } - - if !autoRegister { - return user, nil - } - - user = &User{ - LowerName: strings.ToLower(login), - Name: login, - Email: email, - Passwd: password, - LoginType: LoginOAuth2, - LoginSource: sourceID, - LoginName: login, - IsActive: true, - } - return user, CreateUser(user) +func LoginViaOAuth2(cfg *OAuth2Config, ctx *macaron.Context, sess session.Store) { + oauth2.Auth(cfg.Provider, cfg.ClientID, cfg.ClientSecret, ctx, sess) } // ExternalUserLogin attempts a login using external source types. -func ExternalUserLogin(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) { +func ExternalUserLogin(user *User, login, password string, source *LoginSource, autoRegister bool, ctx *macaron.Context, sess session.Store) (*User, error) { if !source.IsActived { return nil, ErrLoginSourceNotActived } @@ -611,14 +590,15 @@ func ExternalUserLogin(user *User, login, password string, source *LoginSource, case LoginPAM: return LoginViaPAM(user, login, password, source.ID, source.Cfg.(*PAMConfig), autoRegister) case LoginOAuth2: - return LoginViaOAuth2(user, login, password, source.ID, source.Cfg.(*OAuth2Config), autoRegister) + LoginViaOAuth2(source.Cfg.(*OAuth2Config), ctx, sess) + return nil, nil } return nil, ErrUnsupportedLoginType } // UserSignIn validates user name and password. -func UserSignIn(username, password string) (*User, error) { +func UserSignIn(username, password string, ctx *macaron.Context, sess session.Store) (*User, error) { var user *User if strings.Contains(username, "@") { user = &User{Email: strings.ToLower(strings.TrimSpace(username))} @@ -649,17 +629,17 @@ func UserSignIn(username, password string) (*User, error) { return nil, ErrLoginSourceNotExist{user.LoginSource} } - return ExternalUserLogin(user, user.LoginName, password, &source, false) + return ExternalUserLogin(user, user.LoginName, password, &source, false, ctx, sess) } } - sources := make([]*LoginSource, 0, 6) + sources := make([]*LoginSource, 0, 5) if err = x.UseBool().Find(&sources, &LoginSource{IsActived: true}); err != nil { return nil, err } for _, source := range sources { - authUser, err := ExternalUserLogin(nil, username, password, source, true) + authUser, err := ExternalUserLogin(nil, username, password, source, true, ctx, sess) if err == nil { return authUser, nil } @@ -669,3 +649,85 @@ func UserSignIn(username, password string) (*User, error) { return nil, ErrUserNotExist{user.ID, user.Name, 0} } + + +// OAuth2UserLogin attempts a login using a OAuth2 source type +func OAuth2UserLogin(provider string, ctx *macaron.Context, sess session.Store) (*User, error) { + sources := make([]*LoginSource, 0, 1) + if err := x.UseBool().Find(&sources, &LoginSource{IsActived: true, Type: LoginOAuth2}); err != nil { + return nil, err + } + + for _, source := range sources { + // TODO how to put this in the xorm Find ? + if source.Cfg.(*OAuth2Config).Provider == provider { + LoginViaOAuth2(source.Cfg.(*OAuth2Config), ctx, sess) + return nil, nil + } + } + + return nil, errors.New("No valid provider found") +} + +// OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful +// login the user +func OAuth2UserLoginCallback(ctx *macaron.Context, sess session.Store) (*User, string, error) { + provider := ctx.Params(":provider") + + gothUser, redirectURL, error := oauth2.ProviderCallback(provider, ctx, sess) + + if error != nil { + return nil, redirectURL, error + } + + sources := make([]*LoginSource, 0, 1) + if err := x.UseBool().Find(&sources, &LoginSource{IsActived: true, Type: LoginOAuth2}); err != nil { + return nil, "", err + } + + for _, source := range sources { + // TODO how to put this in the xorm Find ? + if source.Cfg.(*OAuth2Config).Provider == provider { + user := &User{ + LoginName: gothUser.UserID, + LoginType: LoginOAuth2, + LoginSource: sources[0].ID, + } + + hasUser, err := x.Get(user) + if err != nil { + return nil, "", err + } + + if !hasUser { + user = &User{ + LowerName: strings.ToLower(gothUser.NickName), + Name: gothUser.NickName, + Email: gothUser.Email, + LoginType: LoginOAuth2, + LoginSource: sources[0].ID, + LoginName: gothUser.NickName, + IsActive: true, + // TODO should OAuth2 imported emails be private? + KeepEmailPrivate: true, + } + return user, redirectURL, CreateUser(user) + } + + return user, redirectURL, nil + } + } + + return nil, "", errors.New("No valid provider found") +} + +// GetOAuth2Providers returns the map of registered OAuth2 providers +// key is used as technical name (like in the callbackURL) +// value is used to display +func GetOAuth2Providers() map[string]string { + // TODO get this from database or somewhere else? + // Maybe also seperate used and unused providers so we can force the registration of only 1 active provider for each type + return map[string]string{ + "github": "GitHub", + } +} \ No newline at end of file diff --git a/modules/auth/auth.go b/modules/auth/auth.go index 811f89074656..f7c10a4e3fe8 100644 --- a/modules/auth/auth.go +++ b/modules/auth/auth.go @@ -129,7 +129,7 @@ func SignedInUser(ctx *macaron.Context, sess session.Store) (*models.User, bool) if len(auths) == 2 && auths[0] == "Basic" { uname, passwd, _ := base.BasicAuthDecode(auths[1]) - u, err := models.UserSignIn(uname, passwd) + u, err := models.UserSignIn(uname, passwd, ctx, sess) if err != nil { if !models.IsErrUserNotExist(err) { log.Error(4, "UserSignIn: %v", err) diff --git a/modules/auth/auth_form.go b/modules/auth/auth_form.go index c403a9ce8021..5dae55c1ef9c 100644 --- a/modules/auth/auth_form.go +++ b/modules/auth/auth_form.go @@ -7,39 +7,38 @@ package auth import ( "github.com/go-macaron/binding" "gopkg.in/macaron.v1" - "golang.org/x/oauth2" ) // AuthenticationForm form for authentication type AuthenticationForm struct { - ID int64 - Type int `binding:"Range(2,5)"` - Name string `binding:"Required;MaxSize(30)"` - Host string - Port int - BindDN string - BindPassword string - UserBase string - UserDN string - AttributeUsername string - AttributeName string - AttributeSurname string - AttributeMail string - AttributesInBind bool - Filter string - AdminFilter string - IsActive bool - SMTPAuth string - SMTPHost string - SMTPPort int - AllowedDomains string - SecurityProtocol int `binding:"Range(0,2)"` - TLS bool - SkipVerify bool - PAMServiceName string - OAuth2Provider string - OAuth2ClientId string - OAuth2ClientSecret string + ID int64 + Type int `binding:"Range(2,6)"` + Name string `binding:"Required;MaxSize(30)"` + Host string + Port int + BindDN string + BindPassword string + UserBase string + UserDN string + AttributeUsername string + AttributeName string + AttributeSurname string + AttributeMail string + AttributesInBind bool + Filter string + AdminFilter string + IsActive bool + SMTPAuth string + SMTPHost string + SMTPPort int + AllowedDomains string + SecurityProtocol int `binding:"Range(0,2)"` + TLS bool + SkipVerify bool + PAMServiceName string + Oauth2Provider string + Oauth2Key string + Oauth2Secret string } // Validate validates fields diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index d9de6cbefa2f..04768cb36e77 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -1,15 +1,14 @@ package oauth2 import ( + "code.gitea.io/gitea/modules/setting" + "github.com/gorilla/sessions" "github.com/markbates/goth" "github.com/markbates/goth/gothic" - "github.com/gorilla/sessions" - "os" "github.com/markbates/goth/providers/github" - "code.gitea.io/gitea/modules/setting" + "gopkg.in/macaron.v1" "net/http" - "fmt" - "code.gitea.io/gitea/modules/context" + "os" "github.com/go-macaron/session" "github.com/satori/go.uuid" "encoding/base64" @@ -17,116 +16,117 @@ import ( ) var ( - oAuthUserSessionKey = "oauth2-user" - oAuthStateUniqueIdSessionKey = "oauth2-state-key" + sessionUsersStoreKey = "gitea-oauth-sessions" + oAuthUserSessionKey = "oauth2-user" + oAuthStateUniqueIDSessionKey = "oauth2-state-key" ) func init() { - gothic.Store = sessions.NewFilesystemStore(os.TempDir(), []byte("gitea-oauth-sessions")) + gothic.Store = sessions.NewFilesystemStore(os.TempDir(), []byte(sessionUsersStoreKey)) } // Auth OAuth2 auth service -func Auth(provider, clientId, clientSecret, userName, passwd string) (string, error) { +func Auth(provider, clientID, clientSecret string, ctx *macaron.Context, session session.Store) { + callbackURL := setting.AppURL + "user/oauth2/" + provider + "/callback" + goth.UseProviders( - github.New(clientId, clientSecret, setting.AppURL + "user/oauth2/github/callback"), + github.New(clientID, clientSecret, callbackURL, "user:email"), ) - ctx := context.Context{} - request := ctx.Req.Request - session := ctx.Session - - oauth2User := session.Get(oAuthUserSessionKey) - if oauth2User != nil { - // already authenticated, stop OAuth2 flow - session.Delete(oAuthUserSessionKey) - return oauth2User.(goth.User).Email, nil + gothic.GetProviderName = func(req *http.Request) (string, error) { + return provider, nil } gothic.SetState = func(req *http.Request) string { - state, uniqueId := encodeState(req.URL.String()) + state, uniqueID := encodeState(req.URL.String()) - session.Set(oAuthStateUniqueIdSessionKey, uniqueId) + session.Set(oAuthStateUniqueIDSessionKey, uniqueID) return state } - gothic.GetProviderName = func(r *http.Request) (string, error) { - provider := ctx.Params(":provider") - if provider == "" { - provider = "github" - } - return provider, nil - } - gothic.BeginAuthHandler(ctx.Resp, request) + request := ctx.Req.Request + response := ctx.Resp + /* + oauth2User := session.Get(oAuthUserSessionKey) + session.Delete(oAuthUserSessionKey) + if oauth2User != nil { + // already authenticated, stop OAuth2 authentication flow + http.Redirect(response, request, callbackUrl, http.StatusTemporaryRedirect) + return + } + */ - return nil, nil + gothic.BeginAuthHandler(response, request) } -// handle OAuth callback, resolve to a goth user and send back to original url +// ProviderCallback handles OAuth callback, resolve to a goth user and send back to original url // this will trigger a new authentication request, but because we save it in the session we can use that -func Callback(ctx *context.Context, session session.Store) { +func ProviderCallback(provider string, ctx *macaron.Context, session session.Store) (goth.User, string, error) { request := ctx.Req.Request res := ctx.Resp + gothic.GetProviderName = func(req *http.Request) (string, error) { + return provider, nil + } + user, err := gothic.CompleteUserAuth(res, request) if err != nil { - fmt.Fprintln(res, err) - return + return user, "", err } - redirectUrl := "" + redirectURL := "" state := gothic.GetState(request) if state == "" { - redirectUrl = "/TROUBLE" + redirectURL = "TROUBLE" } - decodedState, uniqueId, err := decodeState(state) - if err != nil { - redirectUrl = "/TROUBLE2" + if _, uniqueID, err := decodeState(state); err != nil { + redirectURL = "TROUBLE2" } else { - uniqueIdSession := session.Get(oAuthStateUniqueIdSessionKey) - session.Delete(oAuthStateUniqueIdSessionKey) + uniqueIDSession := session.Get(oAuthStateUniqueIDSessionKey) + session.Delete(oAuthStateUniqueIDSessionKey) - if uniqueIdSession != nil && uniqueIdSession == uniqueId { - redirectUrl = decodedState + if uniqueIDSession != nil && uniqueIDSession == uniqueID { + redirectURL = "" session.Set(oAuthUserSessionKey, user) } else { - redirectUrl = "/TROUBLE3" + redirectURL = "TROUBLE3" } } - ctx.Redirect(redirectUrl) + return user, redirectURL, nil } func encodeState(state string) (string, string) { - uniqueId := uuid.NewV4().String() - data := []byte(uniqueId + "|" + state) + uniqueID := uuid.NewV4().String() + data := []byte(uniqueID + "|" + state) - return base64.URLEncoding.EncodeToString(data), uniqueId + return base64.URLEncoding.EncodeToString(data), uniqueID } func decodeState(encodedState string) (string, string, error) { data, err := base64.URLEncoding.DecodeString(encodedState) if err != nil { - return nil, nil, err + return "", "", err } decodedState := string(data[:]) slices := strings.Split(decodedState, "|") - uniqueId := slices[0] + uniqueID := slices[0] // validate uuid - _, err = uuid.FromString(uniqueId) + _, err = uuid.FromString(uniqueID) if err != nil { - return nil, nil, err + return "", "", err } state := slices[1] - return state, uniqueId, nil + return state, uniqueID, nil } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 21bc1d5c77f7..91f30b18c3f1 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1056,6 +1056,9 @@ auths.allowed_domains_helper = Leave it empty to not restrict any domains. Multi auths.enable_tls = Enable TLS Encryption auths.skip_tls_verify = Skip TLS Verify auths.pam_service_name = PAM Service Name +auths.oauth2_provider = OAuth2 provider +auths.oauth2_clientID = Client ID (Key) +auths.oauth2_clientSecret = Client Secret auths.enable_auto_register = Enable Auto Registration auths.tips = Tips auths.edit = Edit Authentication Setting diff --git a/public/js/index.js b/public/js/index.js index 61b774b1fe2a..c99cf6363d78 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -973,6 +973,7 @@ function initAdmin() { $('.dldap').hide(); $('.smtp').hide(); $('.pam').hide(); + $('.oauth2').hide(); $('.has-tls').hide(); var authType = $(this).val(); @@ -990,6 +991,9 @@ function initAdmin() { case '5': // LDAP $('.dldap').show(); break; + case '6': // OAuth2 + $('.oauth2').show(); + break; } if (authType == '2' || authType == '5') { diff --git a/routers/admin/auths.go b/routers/admin/auths.go index e90dee6b613d..51a9200e4ed3 100644 --- a/routers/admin/auths.go +++ b/routers/admin/auths.go @@ -76,6 +76,7 @@ func NewAuthSource(ctx *context.Context) { ctx.Data["AuthSources"] = authSources ctx.Data["SecurityProtocols"] = securityProtocols ctx.Data["SMTPAuths"] = models.SMTPAuths + ctx.Data["OAuth2Providers"] = models.GetOAuth2Providers() ctx.HTML(200, tplAuthNew) } @@ -116,11 +117,9 @@ func parseSMTPConfig(form auth.AuthenticationForm) *models.SMTPConfig { func parseOAuth2Config(form auth.AuthenticationForm) *models.OAuth2Config { return &models.OAuth2Config{ - Provider: form.OAuth2Provider, - ClientId: form.OAuth2ClientId, - ClientSecret: form.OAuth2ClientSecret, - TLS: form.TLS, - SkipVerify: form.SkipVerify, + Provider: form.Oauth2Provider, + ClientID: form.Oauth2Key, + ClientSecret: form.Oauth2Secret, } } @@ -135,6 +134,7 @@ func NewAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) { ctx.Data["AuthSources"] = authSources ctx.Data["SecurityProtocols"] = securityProtocols ctx.Data["SMTPAuths"] = models.SMTPAuths + ctx.Data["OAuth2Providers"] = models.GetOAuth2Providers() hasTLS := false var config core.Conversion @@ -191,6 +191,7 @@ func EditAuthSource(ctx *context.Context) { ctx.Data["SecurityProtocols"] = securityProtocols ctx.Data["SMTPAuths"] = models.SMTPAuths + ctx.Data["OAuth2Providers"] = models.GetOAuth2Providers() source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid")) if err != nil { @@ -200,16 +201,20 @@ func EditAuthSource(ctx *context.Context) { ctx.Data["Source"] = source ctx.Data["HasTLS"] = source.HasTLS() + if source.IsOAuth2() { + ctx.Data["CurrentOAuth2Provider"] = models.GetOAuth2Providers()[source.OAuth2().Provider] + } ctx.HTML(200, tplAuthEdit) } -// EditAuthSourcePost resposne for editing auth source +// EditAuthSourcePost response for editing auth source func EditAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) { ctx.Data["Title"] = ctx.Tr("admin.auths.edit") ctx.Data["PageIsAdmin"] = true ctx.Data["PageIsAdminAuthentications"] = true ctx.Data["SMTPAuths"] = models.SMTPAuths + ctx.Data["OAuth2Providers"] = models.GetOAuth2Providers() source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid")) if err != nil { diff --git a/routers/org/setting.go b/routers/org/setting.go index 4faed77d0d5e..e89d0d4cec58 100644 --- a/routers/org/setting.go +++ b/routers/org/setting.go @@ -116,7 +116,7 @@ func SettingsDelete(ctx *context.Context) { org := ctx.Org.Organization if ctx.Req.Method == "POST" { - if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password")); err != nil { + if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password"), ctx.Context, ctx.Session); err != nil { if models.IsErrUserNotExist(err) { ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsDelete, nil) } else { diff --git a/routers/repo/http.go b/routers/repo/http.go index 695e758cdbc6..3210bc4a36da 100644 --- a/routers/repo/http.go +++ b/routers/repo/http.go @@ -117,7 +117,7 @@ func HTTP(ctx *context.Context) { return } - authUser, err = models.UserSignIn(authUsername, authPasswd) + authUser, err = models.UserSignIn(authUsername, authPasswd, ctx.Context, ctx.Session) if err != nil { if !models.IsErrUserNotExist(err) { ctx.Handle(http.StatusInternalServerError, "UserSignIn error: %v", err) diff --git a/routers/user/auth.go b/routers/user/auth.go index ef2a04005b64..4625fd4de89b 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -97,6 +97,8 @@ func SignIn(ctx *context.Context) { return } + ctx.Data["OAuth2Providers"] = models.GetOAuth2Providers() + ctx.HTML(200, tplSignIn) } @@ -109,7 +111,7 @@ func SignInPost(ctx *context.Context, form auth.SignInForm) { return } - u, err := models.UserSignIn(form.UserName, form.Password) + u, err := models.UserSignIn(form.UserName, form.Password, ctx.Context, ctx.Session) if err != nil { if models.IsErrUserNotExist(err) { ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form) @@ -148,6 +150,43 @@ func SignInPost(ctx *context.Context, form auth.SignInForm) { ctx.Redirect(setting.AppSubURL + "/") } +// SignInOAuth handles the OAuth2 login buttons +func SignInOAuth(ctx *context.Context) { + provider := ctx.Params(":provider") + models.OAuth2UserLogin(provider, ctx.Context, ctx.Session) +} + +// SignInOAuthCallback handles the callback from the given provider +func SignInOAuthCallback(ctx *context.Context) { + u, _, err := models.OAuth2UserLoginCallback(ctx.Context, ctx.Session) + + if err != nil { + ctx.Handle(500, "UserSignIn", err) + return + } + + ctx.Session.Set("uid", u.ID) + ctx.Session.Set("uname", u.Name) + + // Clear whatever CSRF has right now, force to generate a new one + ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL) + + // Register last login + u.SetLastLogin() + if err := models.UpdateUser(u); err != nil { + ctx.Handle(500, "UpdateUser", err) + return + } + + if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 { + ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL) + ctx.Redirect(redirectTo) + return + } + + ctx.Redirect(setting.AppSubURL + "/") +} + // SignOut sign out from login status func SignOut(ctx *context.Context) { ctx.Session.Delete("uid") diff --git a/routers/user/setting.go b/routers/user/setting.go index bba0f1e8766b..12476673aa27 100644 --- a/routers/user/setting.go +++ b/routers/user/setting.go @@ -443,7 +443,7 @@ func SettingsDelete(ctx *context.Context) { ctx.Data["PageIsSettingsDelete"] = true if ctx.Req.Method == "POST" { - if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password")); err != nil { + if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password"), ctx.Context, ctx.Session); err != nil { if models.IsErrUserNotExist(err) { ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsDelete, nil) } else { diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index 0879b274b163..e96efbfbe5a0 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -142,6 +142,32 @@ {{end}} + + {{if .Source.IsOAuth2}} + {{ $cfg:=.Source.OAuth2 }} +
+ + +
+
+ + +
+
+ + +
+ {{end}} +
diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl index 1edec0cb341c..ca1a0b0213dc 100644 --- a/templates/admin/auth/new.tmpl +++ b/templates/admin/auth/new.tmpl @@ -133,6 +133,31 @@
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
diff --git a/templates/user/auth/signin.tmpl b/templates/user/auth/signin.tmpl index 85b7e7027753..e1c2c9a211fe 100644 --- a/templates/user/auth/signin.tmpl +++ b/templates/user/auth/signin.tmpl @@ -36,6 +36,14 @@ {{.i18n.Tr "auth.sign_up_now" | Str2html}}
{{end}} + + {{if .OAuth2Providers}} + {{range $key, $value := .OAuth2Providers}} + + {{end}} + {{end}}
From f93aad830be2c57e0f38f33283454ee010ec0047 Mon Sep 17 00:00:00 2001 From: willemvd Date: Mon, 16 Jan 2017 09:36:05 +0100 Subject: [PATCH 03/40] fix indentation --- templates/admin/auth/edit.tmpl | 26 +++++++++++----------- templates/admin/auth/new.tmpl | 40 +++++++++++++++++----------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index e96efbfbe5a0..621d5b6d1310 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -145,19 +145,19 @@ {{if .Source.IsOAuth2}} {{ $cfg:=.Source.OAuth2 }} -
- - -
+
+ + +
diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl index ca1a0b0213dc..68a2548eec43 100644 --- a/templates/admin/auth/new.tmpl +++ b/templates/admin/auth/new.tmpl @@ -136,26 +136,26 @@
- - -
-
- - -
-
- - -
+ + +
+
+ + +
+
+ + +
From 280913b5c0a863f96f875ce368e9b1efe368f115 Mon Sep 17 00:00:00 2001 From: willemvd Date: Mon, 16 Jan 2017 11:55:04 +0100 Subject: [PATCH 04/40] prevent macaron.Context in models --- models/login_source.go | 30 ++++++++++++------------------ modules/auth/auth.go | 6 +++--- modules/auth/oauth2/oauth2.go | 16 ++++------------ routers/org/setting.go | 2 +- routers/repo/http.go | 14 +++++++------- routers/user/auth.go | 12 +++++++----- routers/user/setting.go | 2 +- 7 files changed, 35 insertions(+), 47 deletions(-) diff --git a/models/login_source.go b/models/login_source.go index 2a7f39ce328a..ada4c9529ebb 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -23,7 +23,7 @@ import ( "code.gitea.io/gitea/modules/auth/pam" "code.gitea.io/gitea/modules/auth/oauth2" "code.gitea.io/gitea/modules/log" - "gopkg.in/macaron.v1" + "net/http" "github.com/go-macaron/session" ) @@ -572,12 +572,12 @@ func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMCon // LoginViaOAuth2 queries if login/password is valid against the OAuth2.0 provider, // and create a local user if success when enabled. -func LoginViaOAuth2(cfg *OAuth2Config, ctx *macaron.Context, sess session.Store) { - oauth2.Auth(cfg.Provider, cfg.ClientID, cfg.ClientSecret, ctx, sess) +func LoginViaOAuth2(cfg *OAuth2Config, request *http.Request, response http.ResponseWriter, session session.Store) { + oauth2.Auth(cfg.Provider, cfg.ClientID, cfg.ClientSecret, request, response, session) } // ExternalUserLogin attempts a login using external source types. -func ExternalUserLogin(user *User, login, password string, source *LoginSource, autoRegister bool, ctx *macaron.Context, sess session.Store) (*User, error) { +func ExternalUserLogin(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) { if !source.IsActived { return nil, ErrLoginSourceNotActived } @@ -589,16 +589,13 @@ func ExternalUserLogin(user *User, login, password string, source *LoginSource, return LoginViaSMTP(user, login, password, source.ID, source.Cfg.(*SMTPConfig), autoRegister) case LoginPAM: return LoginViaPAM(user, login, password, source.ID, source.Cfg.(*PAMConfig), autoRegister) - case LoginOAuth2: - LoginViaOAuth2(source.Cfg.(*OAuth2Config), ctx, sess) - return nil, nil } return nil, ErrUnsupportedLoginType } // UserSignIn validates user name and password. -func UserSignIn(username, password string, ctx *macaron.Context, sess session.Store) (*User, error) { +func UserSignIn(username, password string) (*User, error) { var user *User if strings.Contains(username, "@") { user = &User{Email: strings.ToLower(strings.TrimSpace(username))} @@ -629,7 +626,7 @@ func UserSignIn(username, password string, ctx *macaron.Context, sess session.St return nil, ErrLoginSourceNotExist{user.LoginSource} } - return ExternalUserLogin(user, user.LoginName, password, &source, false, ctx, sess) + return ExternalUserLogin(user, user.LoginName, password, &source, false) } } @@ -639,7 +636,7 @@ func UserSignIn(username, password string, ctx *macaron.Context, sess session.St } for _, source := range sources { - authUser, err := ExternalUserLogin(nil, username, password, source, true, ctx, sess) + authUser, err := ExternalUserLogin(nil, username, password, source, true) if err == nil { return authUser, nil } @@ -650,9 +647,8 @@ func UserSignIn(username, password string, ctx *macaron.Context, sess session.St return nil, ErrUserNotExist{user.ID, user.Name, 0} } - // OAuth2UserLogin attempts a login using a OAuth2 source type -func OAuth2UserLogin(provider string, ctx *macaron.Context, sess session.Store) (*User, error) { +func OAuth2UserLogin(provider string, request *http.Request, response http.ResponseWriter, session session.Store) (*User, error) { sources := make([]*LoginSource, 0, 1) if err := x.UseBool().Find(&sources, &LoginSource{IsActived: true, Type: LoginOAuth2}); err != nil { return nil, err @@ -661,7 +657,7 @@ func OAuth2UserLogin(provider string, ctx *macaron.Context, sess session.Store) for _, source := range sources { // TODO how to put this in the xorm Find ? if source.Cfg.(*OAuth2Config).Provider == provider { - LoginViaOAuth2(source.Cfg.(*OAuth2Config), ctx, sess) + LoginViaOAuth2(source.Cfg.(*OAuth2Config), request, response, session) return nil, nil } } @@ -671,10 +667,8 @@ func OAuth2UserLogin(provider string, ctx *macaron.Context, sess session.Store) // OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful // login the user -func OAuth2UserLoginCallback(ctx *macaron.Context, sess session.Store) (*User, string, error) { - provider := ctx.Params(":provider") - - gothUser, redirectURL, error := oauth2.ProviderCallback(provider, ctx, sess) +func OAuth2UserLoginCallback(provider string, request *http.Request, response http.ResponseWriter, session session.Store) (*User, string, error) { + gothUser, redirectURL, error := oauth2.ProviderCallback(provider, request, response, session) if error != nil { return nil, redirectURL, error @@ -730,4 +724,4 @@ func GetOAuth2Providers() map[string]string { return map[string]string{ "github": "GitHub", } -} \ No newline at end of file +} diff --git a/modules/auth/auth.go b/modules/auth/auth.go index f7c10a4e3fe8..1de2d9e1d594 100644 --- a/modules/auth/auth.go +++ b/modules/auth/auth.go @@ -129,7 +129,7 @@ func SignedInUser(ctx *macaron.Context, sess session.Store) (*models.User, bool) if len(auths) == 2 && auths[0] == "Basic" { uname, passwd, _ := base.BasicAuthDecode(auths[1]) - u, err := models.UserSignIn(uname, passwd, ctx, sess) + u, err := models.UserSignIn(uname, passwd) if err != nil { if !models.IsErrUserNotExist(err) { log.Error(4, "UserSignIn: %v", err) @@ -188,7 +188,7 @@ func AssignForm(form interface{}, data map[string]interface{}) { func getRuleBody(field reflect.StructField, prefix string) string { for _, rule := range strings.Split(field.Tag.Get("binding"), ";") { if strings.HasPrefix(rule, prefix) { - return rule[len(prefix) : len(rule)-1] + return rule[len(prefix): len(rule) - 1] } } return "" @@ -246,7 +246,7 @@ func validate(errs binding.Errors, data map[string]interface{}, f Form, l macaro } if errs[0].FieldNames[0] == field.Name { - data["Err_"+field.Name] = true + data["Err_" + field.Name] = true trName := field.Tag.Get("locale") if len(trName) == 0 { diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index 04768cb36e77..7e0327fdd284 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -6,13 +6,12 @@ import ( "github.com/markbates/goth" "github.com/markbates/goth/gothic" "github.com/markbates/goth/providers/github" - "gopkg.in/macaron.v1" "net/http" "os" - "github.com/go-macaron/session" "github.com/satori/go.uuid" "encoding/base64" "strings" + "github.com/go-macaron/session" ) var ( @@ -26,7 +25,7 @@ func init() { } // Auth OAuth2 auth service -func Auth(provider, clientID, clientSecret string, ctx *macaron.Context, session session.Store) { +func Auth(provider, clientID, clientSecret string, request *http.Request, response http.ResponseWriter, session session.Store) { callbackURL := setting.AppURL + "user/oauth2/" + provider + "/callback" goth.UseProviders( @@ -45,9 +44,6 @@ func Auth(provider, clientID, clientSecret string, ctx *macaron.Context, session return state } - request := ctx.Req.Request - response := ctx.Resp - /* oauth2User := session.Get(oAuthUserSessionKey) session.Delete(oAuthUserSessionKey) @@ -63,16 +59,12 @@ func Auth(provider, clientID, clientSecret string, ctx *macaron.Context, session // ProviderCallback handles OAuth callback, resolve to a goth user and send back to original url // this will trigger a new authentication request, but because we save it in the session we can use that -func ProviderCallback(provider string, ctx *macaron.Context, session session.Store) (goth.User, string, error) { - request := ctx.Req.Request - - res := ctx.Resp - +func ProviderCallback(provider string, request *http.Request, response http.ResponseWriter, session session.Store) (goth.User, string, error) { gothic.GetProviderName = func(req *http.Request) (string, error) { return provider, nil } - user, err := gothic.CompleteUserAuth(res, request) + user, err := gothic.CompleteUserAuth(response, request) if err != nil { return user, "", err } diff --git a/routers/org/setting.go b/routers/org/setting.go index e89d0d4cec58..4faed77d0d5e 100644 --- a/routers/org/setting.go +++ b/routers/org/setting.go @@ -116,7 +116,7 @@ func SettingsDelete(ctx *context.Context) { org := ctx.Org.Organization if ctx.Req.Method == "POST" { - if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password"), ctx.Context, ctx.Session); err != nil { + if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password")); err != nil { if models.IsErrUserNotExist(err) { ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsDelete, nil) } else { diff --git a/routers/repo/http.go b/routers/repo/http.go index 3210bc4a36da..28d0ac99c01b 100644 --- a/routers/repo/http.go +++ b/routers/repo/http.go @@ -49,7 +49,7 @@ func HTTP(ctx *context.Context) { isWiki := false if strings.HasSuffix(reponame, ".wiki") { isWiki = true - reponame = reponame[:len(reponame)-5] + reponame = reponame[:len(reponame) - 5] } repoUser, err := models.GetUserByName(username) @@ -117,7 +117,7 @@ func HTTP(ctx *context.Context) { return } - authUser, err = models.UserSignIn(authUsername, authPasswd, ctx.Context, ctx.Session) + authUser, err = models.UserSignIn(authUsername, authPasswd) if err != nil { if !models.IsErrUserNotExist(err) { ctx.Handle(http.StatusInternalServerError, "UserSignIn error: %v", err) @@ -186,9 +186,9 @@ func HTTP(ctx *context.Context) { var lastLine int64 for { - head := input[lastLine : lastLine+2] + head := input[lastLine: lastLine + 2] if head[0] == '0' && head[1] == '0' { - size, err := strconv.ParseInt(string(input[lastLine+2:lastLine+4]), 16, 32) + size, err := strconv.ParseInt(string(input[lastLine + 2:lastLine + 4]), 16, 32) if err != nil { log.Error(4, "%v", err) return @@ -199,7 +199,7 @@ func HTTP(ctx *context.Context) { break } - line := input[lastLine : lastLine+size] + line := input[lastLine: lastLine + size] idx := bytes.IndexRune(line, '\000') if idx > -1 { line = line[:idx] @@ -317,7 +317,7 @@ func gitCommand(dir string, args ...string) []byte { func getGitConfig(option, dir string) string { out := string(gitCommand(dir, "config", option)) - return out[0 : len(out)-1] + return out[0: len(out) - 1] } func getConfigSetting(service, dir string) bool { @@ -426,7 +426,7 @@ func updateServerInfo(dir string) []byte { } func packetWrite(str string) []byte { - s := strconv.FormatInt(int64(len(str)+4), 16) + s := strconv.FormatInt(int64(len(str) + 4), 16) if len(s)%4 != 0 { s = strings.Repeat("0", 4-len(s)%4) + s } diff --git a/routers/user/auth.go b/routers/user/auth.go index 4625fd4de89b..01d3d7fc811d 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -58,7 +58,7 @@ func AutoSignIn(ctx *context.Context) (bool, error) { } if val, _ := ctx.GetSuperSecureCookie( - base.EncodeMD5(u.Rands+u.Passwd), setting.CookieRememberName); val != u.Name { + base.EncodeMD5(u.Rands + u.Passwd), setting.CookieRememberName); val != u.Name { return false, nil } @@ -111,7 +111,7 @@ func SignInPost(ctx *context.Context, form auth.SignInForm) { return } - u, err := models.UserSignIn(form.UserName, form.Password, ctx.Context, ctx.Session) + u, err := models.UserSignIn(form.UserName, form.Password) if err != nil { if models.IsErrUserNotExist(err) { ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form) @@ -124,7 +124,7 @@ func SignInPost(ctx *context.Context, form auth.SignInForm) { if form.Remember { days := 86400 * setting.LogInRememberDays ctx.SetCookie(setting.CookieUserName, u.Name, days, setting.AppSubURL) - ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd), + ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands + u.Passwd), setting.CookieRememberName, u.Name, days, setting.AppSubURL) } @@ -153,12 +153,14 @@ func SignInPost(ctx *context.Context, form auth.SignInForm) { // SignInOAuth handles the OAuth2 login buttons func SignInOAuth(ctx *context.Context) { provider := ctx.Params(":provider") - models.OAuth2UserLogin(provider, ctx.Context, ctx.Session) + models.OAuth2UserLogin(provider, ctx.Req.Request, ctx.Resp, ctx.Session) } // SignInOAuthCallback handles the callback from the given provider func SignInOAuthCallback(ctx *context.Context) { - u, _, err := models.OAuth2UserLoginCallback(ctx.Context, ctx.Session) + provider := ctx.Params(":provider") + + u, _, err := models.OAuth2UserLoginCallback(provider, ctx.Req.Request, ctx.Resp, ctx.Session) if err != nil { ctx.Handle(500, "UserSignIn", err) diff --git a/routers/user/setting.go b/routers/user/setting.go index 12476673aa27..bba0f1e8766b 100644 --- a/routers/user/setting.go +++ b/routers/user/setting.go @@ -443,7 +443,7 @@ func SettingsDelete(ctx *context.Context) { ctx.Data["PageIsSettingsDelete"] = true if ctx.Req.Method == "POST" { - if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password"), ctx.Context, ctx.Session); err != nil { + if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password")); err != nil { if models.IsErrUserNotExist(err) { ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsDelete, nil) } else { From b597a8671da747fe3ca8bed29a5876859982326a Mon Sep 17 00:00:00 2001 From: willemvd Date: Mon, 16 Jan 2017 14:03:05 +0100 Subject: [PATCH 05/40] show login button only when the OAuth2 consumer is configured (and activated) remove the use of sessions in our own code for validating request --- models/login_source.go | 40 ++++++++++++------ modules/auth/oauth2/oauth2.go | 80 +++-------------------------------- routers/admin/auths.go | 10 ++--- routers/user/auth.go | 11 +++-- 4 files changed, 45 insertions(+), 96 deletions(-) diff --git a/models/login_source.go b/models/login_source.go index ada4c9529ebb..de6ae8ff48e5 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -24,7 +24,6 @@ import ( "code.gitea.io/gitea/modules/auth/oauth2" "code.gitea.io/gitea/modules/log" "net/http" - "github.com/go-macaron/session" ) // LoginType represents an login type. @@ -570,10 +569,17 @@ func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMCon // \_______ /\____|__ /____/ |__| |___| /\_______ \ // \/ \/ \/ \/ +// OAuth2Providers contains the map of registered OAuth2 providers in Gitea (based on goth) +// key is used as technical name (for use in the callbackURL) +// value is used to display +var OAuth2Providers = map[string]string{ + "github": "GitHub", +} + // LoginViaOAuth2 queries if login/password is valid against the OAuth2.0 provider, // and create a local user if success when enabled. -func LoginViaOAuth2(cfg *OAuth2Config, request *http.Request, response http.ResponseWriter, session session.Store) { - oauth2.Auth(cfg.Provider, cfg.ClientID, cfg.ClientSecret, request, response, session) +func LoginViaOAuth2(cfg *OAuth2Config, request *http.Request, response http.ResponseWriter) { + oauth2.Auth(cfg.Provider, cfg.ClientID, cfg.ClientSecret, request, response) } // ExternalUserLogin attempts a login using external source types. @@ -648,7 +654,7 @@ func UserSignIn(username, password string) (*User, error) { } // OAuth2UserLogin attempts a login using a OAuth2 source type -func OAuth2UserLogin(provider string, request *http.Request, response http.ResponseWriter, session session.Store) (*User, error) { +func OAuth2UserLogin(provider string, request *http.Request, response http.ResponseWriter) (*User, error) { sources := make([]*LoginSource, 0, 1) if err := x.UseBool().Find(&sources, &LoginSource{IsActived: true, Type: LoginOAuth2}); err != nil { return nil, err @@ -657,7 +663,7 @@ func OAuth2UserLogin(provider string, request *http.Request, response http.Respo for _, source := range sources { // TODO how to put this in the xorm Find ? if source.Cfg.(*OAuth2Config).Provider == provider { - LoginViaOAuth2(source.Cfg.(*OAuth2Config), request, response, session) + LoginViaOAuth2(source.Cfg.(*OAuth2Config), request, response) return nil, nil } } @@ -667,8 +673,8 @@ func OAuth2UserLogin(provider string, request *http.Request, response http.Respo // OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful // login the user -func OAuth2UserLoginCallback(provider string, request *http.Request, response http.ResponseWriter, session session.Store) (*User, string, error) { - gothUser, redirectURL, error := oauth2.ProviderCallback(provider, request, response, session) +func OAuth2UserLoginCallback(provider string, request *http.Request, response http.ResponseWriter) (*User, string, error) { + gothUser, redirectURL, error := oauth2.ProviderCallback(provider, request, response) if error != nil { return nil, redirectURL, error @@ -681,7 +687,7 @@ func OAuth2UserLoginCallback(provider string, request *http.Request, response ht for _, source := range sources { // TODO how to put this in the xorm Find ? - if source.Cfg.(*OAuth2Config).Provider == provider { + if source.OAuth2().Provider == provider { user := &User{ LoginName: gothUser.UserID, LoginType: LoginOAuth2, @@ -715,13 +721,21 @@ func OAuth2UserLoginCallback(provider string, request *http.Request, response ht return nil, "", errors.New("No valid provider found") } -// GetOAuth2Providers returns the map of registered OAuth2 providers +// GetActiveOAuth2Providers returns the map of configured active OAuth2 providers // key is used as technical name (like in the callbackURL) // value is used to display -func GetOAuth2Providers() map[string]string { - // TODO get this from database or somewhere else? +func GetActiveOAuth2Providers() (map[string]string, error) { // Maybe also seperate used and unused providers so we can force the registration of only 1 active provider for each type - return map[string]string{ - "github": "GitHub", + + sources := make([]*LoginSource, 0, 1) + if err := x.UseBool().Find(&sources, &LoginSource{IsActived: true, Type: LoginOAuth2}); err != nil { + return nil, err + } + + providers := make(map[string]string) + for _, source := range sources { + providers[source.OAuth2().Provider] = OAuth2Providers[source.OAuth2().Provider] } + + return providers, nil } diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index 7e0327fdd284..8eee2eaf19bb 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -9,15 +9,10 @@ import ( "net/http" "os" "github.com/satori/go.uuid" - "encoding/base64" - "strings" - "github.com/go-macaron/session" ) var ( - sessionUsersStoreKey = "gitea-oauth-sessions" - oAuthUserSessionKey = "oauth2-user" - oAuthStateUniqueIDSessionKey = "oauth2-state-key" + sessionUsersStoreKey = "gitea-oauth-sessions" ) func init() { @@ -25,7 +20,7 @@ func init() { } // Auth OAuth2 auth service -func Auth(provider, clientID, clientSecret string, request *http.Request, response http.ResponseWriter, session session.Store) { +func Auth(provider, clientID, clientSecret string, request *http.Request, response http.ResponseWriter) { callbackURL := setting.AppURL + "user/oauth2/" + provider + "/callback" goth.UseProviders( @@ -37,29 +32,15 @@ func Auth(provider, clientID, clientSecret string, request *http.Request, respon } gothic.SetState = func(req *http.Request) string { - state, uniqueID := encodeState(req.URL.String()) - - session.Set(oAuthStateUniqueIDSessionKey, uniqueID) - - return state - } - - /* - oauth2User := session.Get(oAuthUserSessionKey) - session.Delete(oAuthUserSessionKey) - if oauth2User != nil { - // already authenticated, stop OAuth2 authentication flow - http.Redirect(response, request, callbackUrl, http.StatusTemporaryRedirect) - return + return uuid.NewV4().String() } - */ gothic.BeginAuthHandler(response, request) } // ProviderCallback handles OAuth callback, resolve to a goth user and send back to original url // this will trigger a new authentication request, but because we save it in the session we can use that -func ProviderCallback(provider string, request *http.Request, response http.ResponseWriter, session session.Store) (goth.User, string, error) { +func ProviderCallback(provider string, request *http.Request, response http.ResponseWriter) (goth.User, string, error) { gothic.GetProviderName = func(req *http.Request) (string, error) { return provider, nil } @@ -69,56 +50,5 @@ func ProviderCallback(provider string, request *http.Request, response http.Resp return user, "", err } - redirectURL := "" - - state := gothic.GetState(request) - if state == "" { - redirectURL = "TROUBLE" - } - - if _, uniqueID, err := decodeState(state); err != nil { - redirectURL = "TROUBLE2" - } else { - uniqueIDSession := session.Get(oAuthStateUniqueIDSessionKey) - session.Delete(oAuthStateUniqueIDSessionKey) - - if uniqueIDSession != nil && uniqueIDSession == uniqueID { - redirectURL = "" - session.Set(oAuthUserSessionKey, user) - } else { - redirectURL = "TROUBLE3" - } - } - - return user, redirectURL, nil -} - -func encodeState(state string) (string, string) { - uniqueID := uuid.NewV4().String() - data := []byte(uniqueID + "|" + state) - - return base64.URLEncoding.EncodeToString(data), uniqueID -} - -func decodeState(encodedState string) (string, string, error) { - data, err := base64.URLEncoding.DecodeString(encodedState) - - if err != nil { - return "", "", err - } - - decodedState := string(data[:]) - slices := strings.Split(decodedState, "|") - - uniqueID := slices[0] - - // validate uuid - _, err = uuid.FromString(uniqueID) - if err != nil { - return "", "", err - } - - state := slices[1] - - return state, uniqueID, nil + return user, "", nil } diff --git a/routers/admin/auths.go b/routers/admin/auths.go index 51a9200e4ed3..7eaf667cb15a 100644 --- a/routers/admin/auths.go +++ b/routers/admin/auths.go @@ -76,7 +76,7 @@ func NewAuthSource(ctx *context.Context) { ctx.Data["AuthSources"] = authSources ctx.Data["SecurityProtocols"] = securityProtocols ctx.Data["SMTPAuths"] = models.SMTPAuths - ctx.Data["OAuth2Providers"] = models.GetOAuth2Providers() + ctx.Data["OAuth2Providers"] = models.OAuth2Providers ctx.HTML(200, tplAuthNew) } @@ -134,7 +134,7 @@ func NewAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) { ctx.Data["AuthSources"] = authSources ctx.Data["SecurityProtocols"] = securityProtocols ctx.Data["SMTPAuths"] = models.SMTPAuths - ctx.Data["OAuth2Providers"] = models.GetOAuth2Providers() + ctx.Data["OAuth2Providers"] = models.OAuth2Providers hasTLS := false var config core.Conversion @@ -191,7 +191,7 @@ func EditAuthSource(ctx *context.Context) { ctx.Data["SecurityProtocols"] = securityProtocols ctx.Data["SMTPAuths"] = models.SMTPAuths - ctx.Data["OAuth2Providers"] = models.GetOAuth2Providers() + ctx.Data["OAuth2Providers"] = models.OAuth2Providers source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid")) if err != nil { @@ -202,7 +202,7 @@ func EditAuthSource(ctx *context.Context) { ctx.Data["HasTLS"] = source.HasTLS() if source.IsOAuth2() { - ctx.Data["CurrentOAuth2Provider"] = models.GetOAuth2Providers()[source.OAuth2().Provider] + ctx.Data["CurrentOAuth2Provider"] = models.OAuth2Providers[source.OAuth2().Provider] } ctx.HTML(200, tplAuthEdit) } @@ -214,7 +214,7 @@ func EditAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) { ctx.Data["PageIsAdminAuthentications"] = true ctx.Data["SMTPAuths"] = models.SMTPAuths - ctx.Data["OAuth2Providers"] = models.GetOAuth2Providers() + ctx.Data["OAuth2Providers"] = models.OAuth2Providers source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid")) if err != nil { diff --git a/routers/user/auth.go b/routers/user/auth.go index 01d3d7fc811d..3a37021fc8b8 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -97,7 +97,12 @@ func SignIn(ctx *context.Context) { return } - ctx.Data["OAuth2Providers"] = models.GetOAuth2Providers() + oauth2Providers, err := models.GetActiveOAuth2Providers() + if err != nil { + ctx.Handle(500, "UserSignIn", err) + return + } + ctx.Data["OAuth2Providers"] = oauth2Providers ctx.HTML(200, tplSignIn) } @@ -153,14 +158,14 @@ func SignInPost(ctx *context.Context, form auth.SignInForm) { // SignInOAuth handles the OAuth2 login buttons func SignInOAuth(ctx *context.Context) { provider := ctx.Params(":provider") - models.OAuth2UserLogin(provider, ctx.Req.Request, ctx.Resp, ctx.Session) + models.OAuth2UserLogin(provider, ctx.Req.Request, ctx.Resp) } // SignInOAuthCallback handles the callback from the given provider func SignInOAuthCallback(ctx *context.Context) { provider := ctx.Params(":provider") - u, _, err := models.OAuth2UserLoginCallback(provider, ctx.Req.Request, ctx.Resp, ctx.Session) + u, _, err := models.OAuth2UserLoginCallback(provider, ctx.Req.Request, ctx.Resp) if err != nil { ctx.Handle(500, "UserSignIn", err) From 8c2be7a160ed782c3d40d6b976401fc88dd20912 Mon Sep 17 00:00:00 2001 From: willemvd Date: Mon, 16 Jan 2017 14:18:47 +0100 Subject: [PATCH 06/40] move overrides of goth functions to init --- modules/auth/oauth2/oauth2.go | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index 8eee2eaf19bb..63f62f27fa42 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -13,26 +13,30 @@ import ( var ( sessionUsersStoreKey = "gitea-oauth-sessions" + providerHeaderKey = "gitea-oauth-provider" ) func init() { gothic.Store = sessions.NewFilesystemStore(os.TempDir(), []byte(sessionUsersStoreKey)) -} - -// Auth OAuth2 auth service -func Auth(provider, clientID, clientSecret string, request *http.Request, response http.ResponseWriter) { - callbackURL := setting.AppURL + "user/oauth2/" + provider + "/callback" - goth.UseProviders( - github.New(clientID, clientSecret, callbackURL, "user:email"), - ) + gothic.SetState = func(req *http.Request) string { + return uuid.NewV4().String() + } gothic.GetProviderName = func(req *http.Request) (string, error) { - return provider, nil + return req.Header.Get(providerHeaderKey), nil } +} - gothic.SetState = func(req *http.Request) string { - return uuid.NewV4().String() +// Auth OAuth2 auth service +func Auth(provider, clientID, clientSecret string, request *http.Request, response http.ResponseWriter) { + // not sure if goth is thread safe (?) when using multiple providers + request.Header.Set(providerHeaderKey, provider) + + if gothProvider, _ := goth.GetProvider(provider); gothProvider == nil { + goth.UseProviders( + github.New(clientID, clientSecret, setting.AppURL + "user/oauth2/" + provider + "/callback", "user:email"), + ) } gothic.BeginAuthHandler(response, request) @@ -41,9 +45,8 @@ func Auth(provider, clientID, clientSecret string, request *http.Request, respon // ProviderCallback handles OAuth callback, resolve to a goth user and send back to original url // this will trigger a new authentication request, but because we save it in the session we can use that func ProviderCallback(provider string, request *http.Request, response http.ResponseWriter) (goth.User, string, error) { - gothic.GetProviderName = func(req *http.Request) (string, error) { - return provider, nil - } + // not sure if goth is thread safe (?) when using multiple providers + request.Header.Set(providerHeaderKey, provider) user, err := gothic.CompleteUserAuth(response, request) if err != nil { From 2ba78334d0380a9f6be0945780e3900555b53d55 Mon Sep 17 00:00:00 2001 From: willemvd Date: Mon, 16 Jan 2017 14:29:40 +0100 Subject: [PATCH 07/40] create macaron group for oauth2 urls --- cmd/web.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/web.go b/cmd/web.go index 17ba57cb8a83..e0fc7fda170d 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -203,8 +203,10 @@ func runWeb(ctx *cli.Context) error { m.Post("/sign_up", bindIgnErr(auth.RegisterForm{}), user.SignUpPost) m.Get("/reset_password", user.ResetPasswd) m.Post("/reset_password", user.ResetPasswdPost) - m.Get("/oauth2/:provider", user.SignInOAuth) - m.Get("/oauth2/:provider/callback", user.SignInOAuthCallback) + m.Group("/oauth2", func() { + m.Get("/oauth2/:provider", user.SignInOAuth) + m.Get("/oauth2/:provider/callback", user.SignInOAuthCallback) + }) m.Group("/two_factor", func() { m.Get("", user.TwoFactor) m.Post("", bindIgnErr(auth.TwoFactorAuthForm{}), user.TwoFactorPost) From 8e1ea968a08072992e24df6dee43ce8be3cdd972 Mon Sep 17 00:00:00 2001 From: willemvd Date: Mon, 16 Jan 2017 14:42:22 +0100 Subject: [PATCH 08/40] fix the broken url (dubble oauth2) --- cmd/web.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/web.go b/cmd/web.go index e0fc7fda170d..4a87e8a5a925 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -204,8 +204,8 @@ func runWeb(ctx *cli.Context) error { m.Get("/reset_password", user.ResetPasswd) m.Post("/reset_password", user.ResetPasswdPost) m.Group("/oauth2", func() { - m.Get("/oauth2/:provider", user.SignInOAuth) - m.Get("/oauth2/:provider/callback", user.SignInOAuthCallback) + m.Get("/:provider", user.SignInOAuth) + m.Get("/:provider/callback", user.SignInOAuthCallback) }) m.Group("/two_factor", func() { m.Get("", user.TwoFactor) From 6c98fa7f93b0f8f22312da0031cbf88b2fb6fab1 Mon Sep 17 00:00:00 2001 From: willemvd Date: Tue, 17 Jan 2017 09:46:55 +0100 Subject: [PATCH 09/40] no support at this moment to use this and we should only consider implementing SkipVerify (waiting for https://github.com/markbates/goth/pull/87) --- models/login_source.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/models/login_source.go b/models/login_source.go index de6ae8ff48e5..373d96302d47 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -246,8 +246,6 @@ func (source *LoginSource) UseTLS() bool { return source.LDAP().SecurityProtocol != ldap.SecurityProtocolUnencrypted case LoginSMTP: return source.SMTP().TLS - case LoginOAuth2: - return true } return false From caeb911a072e5386b699f010cfd4f053a157deed Mon Sep 17 00:00:00 2001 From: willemvd Date: Mon, 23 Jan 2017 11:32:51 +0100 Subject: [PATCH 10/40] prevent net/http in modules (other then oauth2) --- models/login_source.go | 83 ++++-------------------------------------- models/user.go | 5 +++ routers/user/auth.go | 75 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 85 insertions(+), 78 deletions(-) diff --git a/models/login_source.go b/models/login_source.go index 373d96302d47..9bfe0a0d188d 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -21,9 +21,7 @@ import ( "code.gitea.io/gitea/modules/auth/ldap" "code.gitea.io/gitea/modules/auth/pam" - "code.gitea.io/gitea/modules/auth/oauth2" "code.gitea.io/gitea/modules/log" - "net/http" ) // LoginType represents an login type. @@ -574,12 +572,6 @@ var OAuth2Providers = map[string]string{ "github": "GitHub", } -// LoginViaOAuth2 queries if login/password is valid against the OAuth2.0 provider, -// and create a local user if success when enabled. -func LoginViaOAuth2(cfg *OAuth2Config, request *http.Request, response http.ResponseWriter) { - oauth2.Auth(cfg.Provider, cfg.ClientID, cfg.ClientSecret, request, response) -} - // ExternalUserLogin attempts a login using external source types. func ExternalUserLogin(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) { if !source.IsActived { @@ -651,87 +643,28 @@ func UserSignIn(username, password string) (*User, error) { return nil, ErrUserNotExist{user.ID, user.Name, 0} } -// OAuth2UserLogin attempts a login using a OAuth2 source type -func OAuth2UserLogin(provider string, request *http.Request, response http.ResponseWriter) (*User, error) { +// GetActiveOAuth2ProviderLoginSources returns all actived LoginOAuth2 sources +func GetActiveOAuth2ProviderLoginSources() ([]*LoginSource, error) { sources := make([]*LoginSource, 0, 1) if err := x.UseBool().Find(&sources, &LoginSource{IsActived: true, Type: LoginOAuth2}); err != nil { return nil, err } - - for _, source := range sources { - // TODO how to put this in the xorm Find ? - if source.Cfg.(*OAuth2Config).Provider == provider { - LoginViaOAuth2(source.Cfg.(*OAuth2Config), request, response) - return nil, nil - } - } - - return nil, errors.New("No valid provider found") -} - -// OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful -// login the user -func OAuth2UserLoginCallback(provider string, request *http.Request, response http.ResponseWriter) (*User, string, error) { - gothUser, redirectURL, error := oauth2.ProviderCallback(provider, request, response) - - if error != nil { - return nil, redirectURL, error - } - - sources := make([]*LoginSource, 0, 1) - if err := x.UseBool().Find(&sources, &LoginSource{IsActived: true, Type: LoginOAuth2}); err != nil { - return nil, "", err - } - - for _, source := range sources { - // TODO how to put this in the xorm Find ? - if source.OAuth2().Provider == provider { - user := &User{ - LoginName: gothUser.UserID, - LoginType: LoginOAuth2, - LoginSource: sources[0].ID, - } - - hasUser, err := x.Get(user) - if err != nil { - return nil, "", err - } - - if !hasUser { - user = &User{ - LowerName: strings.ToLower(gothUser.NickName), - Name: gothUser.NickName, - Email: gothUser.Email, - LoginType: LoginOAuth2, - LoginSource: sources[0].ID, - LoginName: gothUser.NickName, - IsActive: true, - // TODO should OAuth2 imported emails be private? - KeepEmailPrivate: true, - } - return user, redirectURL, CreateUser(user) - } - - return user, redirectURL, nil - } - } - - return nil, "", errors.New("No valid provider found") + return sources, nil } -// GetActiveOAuth2Providers returns the map of configured active OAuth2 providers +// GetActiveOAuth2ProviderNames returns the map of configured active OAuth2 providers // key is used as technical name (like in the callbackURL) // value is used to display -func GetActiveOAuth2Providers() (map[string]string, error) { +func GetActiveOAuth2ProviderNames() (map[string]string, error) { // Maybe also seperate used and unused providers so we can force the registration of only 1 active provider for each type - sources := make([]*LoginSource, 0, 1) - if err := x.UseBool().Find(&sources, &LoginSource{IsActived: true, Type: LoginOAuth2}); err != nil { + loginSources, err := GetActiveOAuth2ProviderLoginSources() + if err != nil { return nil, err } providers := make(map[string]string) - for _, source := range sources { + for _, source := range loginSources { providers[source.OAuth2().Provider] = OAuth2Providers[source.OAuth2().Provider] } diff --git a/models/user.go b/models/user.go index e6ba2fa6f398..665fd40b389f 100644 --- a/models/user.go +++ b/models/user.go @@ -1144,6 +1144,11 @@ func GetUserByEmail(email string) (*User, error) { return nil, ErrUserNotExist{0, email, 0} } +// GetUser checks if a user already exists +func GetUser(user *User) (bool, error) { + return x.Get(user) +} + // SearchUserOptions contains the options for searching type SearchUserOptions struct { Keyword string diff --git a/routers/user/auth.go b/routers/user/auth.go index 2664fb384c77..6ec270f7e037 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -17,6 +17,9 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "strings" + "net/http" + "code.gitea.io/gitea/modules/auth/oauth2" ) const ( @@ -109,7 +112,7 @@ func SignIn(ctx *context.Context) { return } - oauth2Providers, err := models.GetActiveOAuth2Providers() + oauth2Providers, err := models.GetActiveOAuth2ProviderNames() if err != nil { ctx.Handle(500, "UserSignIn", err) return @@ -319,14 +322,31 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR // SignInOAuth handles the OAuth2 login buttons func SignInOAuth(ctx *context.Context) { provider := ctx.Params(":provider") - models.OAuth2UserLogin(provider, ctx.Req.Request, ctx.Resp) + + loginSources, err := models.GetActiveOAuth2ProviderLoginSources() + if err != nil { + ctx.Handle(500, "SignIn", err) + return + } + + for _, source := range loginSources { + // TODO how to put this in the xorm Find ? + cfg := source.OAuth2() + if cfg.Provider == provider { + oauth2.Auth(cfg.Provider, cfg.ClientID, cfg.ClientSecret, ctx.Req.Request, ctx.Resp) + return + } + } + + ctx.Handle(500, "SignIn", errors.New("No valid provider found")) + return } // SignInOAuthCallback handles the callback from the given provider func SignInOAuthCallback(ctx *context.Context) { provider := ctx.Params(":provider") - u, _, err := models.OAuth2UserLoginCallback(provider, ctx.Req.Request, ctx.Resp) + u, _, err := oAuth2UserLoginCallback(provider, ctx.Req.Request, ctx.Resp) if err != nil { ctx.Handle(500, "UserSignIn", err) @@ -355,6 +375,55 @@ func SignInOAuthCallback(ctx *context.Context) { ctx.Redirect(setting.AppSubURL + "/") } +// OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful +// login the user +func oAuth2UserLoginCallback(provider string, request *http.Request, response http.ResponseWriter) (*models.User, string, error) { + gothUser, redirectURL, error := oauth2.ProviderCallback(provider, request, response) + + if error != nil { + return nil, redirectURL, error + } + + loginSources, err := models.GetActiveOAuth2ProviderLoginSources() + if err != nil { + return nil, "", err + } + + for _, source := range loginSources { + if source.OAuth2().Provider == provider { + user := &models.User{ + LoginName: gothUser.UserID, + LoginType: models.LoginOAuth2, + LoginSource: source.ID, + } + + hasUser, err := models.GetUser(user) + if err != nil { + return nil, "", err + } + + if !hasUser { + user = &models.User{ + LowerName: strings.ToLower(gothUser.NickName), + Name: gothUser.NickName, + Email: gothUser.Email, + LoginType: models.LoginOAuth2, + LoginSource: source.ID, + LoginName: gothUser.NickName, + IsActive: true, + // TODO should OAuth2 imported emails be private? + KeepEmailPrivate: true, + } + return user, redirectURL, models.CreateUser(user) + } + + return user, redirectURL, nil + } + } + + return nil, "", errors.New("No valid provider found") +} + // SignOut sign out from login status func SignOut(ctx *context.Context) { ctx.Session.Delete("uid") From f3f38667560d54a0f7ab3e71f24720f8a518f41f Mon Sep 17 00:00:00 2001 From: willemvd Date: Mon, 23 Jan 2017 12:30:24 +0100 Subject: [PATCH 11/40] use a new data sessions oauth2 folder for storing the oauth2 session data --- modules/auth/oauth2/oauth2.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index 63f62f27fa42..df8ae6eed380 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -2,6 +2,7 @@ package oauth2 import ( "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/log" "github.com/gorilla/sessions" "github.com/markbates/goth" "github.com/markbates/goth/gothic" @@ -9,15 +10,22 @@ import ( "net/http" "os" "github.com/satori/go.uuid" + "path/filepath" ) var ( - sessionUsersStoreKey = "gitea-oauth-sessions" - providerHeaderKey = "gitea-oauth-provider" + sessionUsersStoreKey = "gitea-oauth2-sessions" + providerHeaderKey = "gitea-oauth2-provider" ) func init() { - gothic.Store = sessions.NewFilesystemStore(os.TempDir(), []byte(sessionUsersStoreKey)) + dir, _ := setting.WorkDir() + tmpDir := filepath.Join(dir, "data", "sessions", "oauth2") + if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil { + log.Fatal(4, "Fail to create dir %s: %v", tmpDir, err) + } + + gothic.Store = sessions.NewFilesystemStore(tmpDir, []byte(sessionUsersStoreKey)) gothic.SetState = func(req *http.Request) string { return uuid.NewV4().String() @@ -35,7 +43,7 @@ func Auth(provider, clientID, clientSecret string, request *http.Request, respon if gothProvider, _ := goth.GetProvider(provider); gothProvider == nil { goth.UseProviders( - github.New(clientID, clientSecret, setting.AppURL + "user/oauth2/" + provider + "/callback", "user:email"), + github.New(clientID, clientSecret, setting.AppURL+"user/oauth2/"+provider+"/callback", "user:email"), ) } @@ -45,7 +53,7 @@ func Auth(provider, clientID, clientSecret string, request *http.Request, respon // ProviderCallback handles OAuth callback, resolve to a goth user and send back to original url // this will trigger a new authentication request, but because we save it in the session we can use that func ProviderCallback(provider string, request *http.Request, response http.ResponseWriter) (goth.User, string, error) { - // not sure if goth is thread safe (?) when using multiple providers + // not sure if goth is thread safe (?) when using multiple providers request.Header.Set(providerHeaderKey, provider) user, err := gothic.CompleteUserAuth(response, request) From 047d50bafe35229674347105686c4f570e3d1390 Mon Sep 17 00:00:00 2001 From: willemvd Date: Mon, 23 Jan 2017 13:30:06 +0100 Subject: [PATCH 12/40] update resolving, naming and security settings of sessions oauth2 directory --- modules/auth/oauth2/oauth2.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index df8ae6eed380..d13fd8e1d782 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -19,13 +19,12 @@ var ( ) func init() { - dir, _ := setting.WorkDir() - tmpDir := filepath.Join(dir, "data", "sessions", "oauth2") - if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil { - log.Fatal(4, "Fail to create dir %s: %v", tmpDir, err) + sessionDir := filepath.Join(setting.AppDataPath, "sessions", "oauth2") + if err := os.MkdirAll(sessionDir, 0700); err != nil { + log.Fatal(4, "Fail to create dir %s: %v", sessionDir, err) } - gothic.Store = sessions.NewFilesystemStore(tmpDir, []byte(sessionUsersStoreKey)) + gothic.Store = sessions.NewFilesystemStore(sessionDir, []byte(sessionUsersStoreKey)) gothic.SetState = func(req *http.Request) string { return uuid.NewV4().String() From ab31c24425720909bcaf72c09d85706b1f4b0dfd Mon Sep 17 00:00:00 2001 From: willemvd Date: Mon, 23 Jan 2017 15:15:19 +0100 Subject: [PATCH 13/40] add missing 2FA when this is enabled on the user --- routers/user/auth.go | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/routers/user/auth.go b/routers/user/auth.go index 6ec270f7e037..0790d16f8cae 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -353,26 +353,41 @@ func SignInOAuthCallback(ctx *context.Context) { return } - ctx.Session.Set("uid", u.ID) - ctx.Session.Set("uname", u.Name) + // If this user is enrolled in 2FA, we can't sign the user in just yet. + // Instead, redirect them to the 2FA authentication page. + _, err = models.GetTwoFactorByUID(u.ID) + if err != nil { + if models.IsErrTwoFactorNotEnrolled(err) { + ctx.Session.Set("uid", u.ID) + ctx.Session.Set("uname", u.Name) - // Clear whatever CSRF has right now, force to generate a new one - ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL) + // Clear whatever CSRF has right now, force to generate a new one + ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL) - // Register last login - u.SetLastLogin() - if err := models.UpdateUser(u); err != nil { - ctx.Handle(500, "UpdateUser", err) - return - } + // Register last login + u.SetLastLogin() + if err := models.UpdateUser(u); err != nil { + ctx.Handle(500, "UpdateUser", err) + return + } - if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 { - ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL) - ctx.Redirect(redirectTo) + if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 { + ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL) + ctx.Redirect(redirectTo) + return + } + + ctx.Redirect(setting.AppSubURL + "/") + } else { + ctx.Handle(500, "UserSignIn", err) + } return } - ctx.Redirect(setting.AppSubURL + "/") + // User needs to use 2FA, save data and redirect to 2FA page. + ctx.Session.Set("twofaUid", u.ID) + ctx.Session.Set("twofaRemember", false) + ctx.Redirect(setting.AppSubURL + "/user/two_factor") } // OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful From fe88e8727f2e7e793248087f38be3d075e7db792 Mon Sep 17 00:00:00 2001 From: willemvd Date: Tue, 24 Jan 2017 13:12:48 +0100 Subject: [PATCH 14/40] add password option for OAuth2 user , for use with git over http and login to the GUI --- models/login_source.go | 2 +- models/user.go | 10 ++++++++++ modules/auth/user_form.go | 2 +- routers/user/auth.go | 11 ++++++++--- routers/user/setting.go | 2 +- templates/admin/user/edit.tmpl | 2 +- templates/user/settings/password.tmpl | 4 +++- 7 files changed, 25 insertions(+), 8 deletions(-) diff --git a/models/login_source.go b/models/login_source.go index 9bfe0a0d188d..9b0a992f4d87 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -606,7 +606,7 @@ func UserSignIn(username, password string) (*User, error) { if hasUser { switch user.LoginType { - case LoginNoType, LoginPlain: + case LoginNoType, LoginPlain, LoginOAuth2: if user.ValidatePassword(password) { return user, nil } diff --git a/models/user.go b/models/user.go index 665fd40b389f..b8149b663ec2 100644 --- a/models/user.go +++ b/models/user.go @@ -196,6 +196,11 @@ func (u *User) IsLocal() bool { return u.LoginType <= LoginPlain } +// IsOAuth2 returns true if user login type is LoginOAuth2. +func (u *User) IsOAuth2() bool { + return u.LoginType == LoginOAuth2 +} + // HasForkedRepo checks if user has already forked a repository with given ID. func (u *User) HasForkedRepo(repoID int64) bool { _, has := HasForkedRepo(u.ID, repoID) @@ -389,6 +394,11 @@ func (u *User) ValidatePassword(passwd string) bool { return subtle.ConstantTimeCompare([]byte(u.Passwd), []byte(newUser.Passwd)) == 1 } +// IsPasswordSet checks if the password is set or left empty +func (u *User) IsPasswordSet() bool { + return !u.ValidatePassword("") +} + // UploadAvatar saves custom avatar for user. // FIXME: split uploads to different subdirs in case we have massive users. func (u *User) UploadAvatar(data []byte) error { diff --git a/modules/auth/user_form.go b/modules/auth/user_form.go index 0f083a2e005a..57e08f02c0fc 100644 --- a/modules/auth/user_form.go +++ b/modules/auth/user_form.go @@ -143,7 +143,7 @@ func (f *AddEmailForm) Validate(ctx *macaron.Context, errs binding.Errors) bindi // ChangePasswordForm form for changing password type ChangePasswordForm struct { - OldPassword string `form:"old_password" binding:"Required;MinSize(1);MaxSize(255)"` + OldPassword string `form:"old_password" binding:"MaxSize(255)"` Password string `form:"password" binding:"Required;MaxSize(255)"` Retype string `form:"retype"` } diff --git a/routers/user/auth.go b/routers/user/auth.go index 0790d16f8cae..406be58f2f04 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -126,6 +126,13 @@ func SignIn(ctx *context.Context) { func SignInPost(ctx *context.Context, form auth.SignInForm) { ctx.Data["Title"] = ctx.Tr("sign_in") + oauth2Providers, err := models.GetActiveOAuth2ProviderNames() + if err != nil { + ctx.Handle(500, "UserSignIn", err) + return + } + ctx.Data["OAuth2Providers"] = oauth2Providers + if ctx.HasError() { ctx.HTML(200, tplSignIn) return @@ -407,7 +414,7 @@ func oAuth2UserLoginCallback(provider string, request *http.Request, response ht for _, source := range loginSources { if source.OAuth2().Provider == provider { user := &models.User{ - LoginName: gothUser.UserID, + LoginName: gothUser.NickName, LoginType: models.LoginOAuth2, LoginSource: source.ID, } @@ -426,8 +433,6 @@ func oAuth2UserLoginCallback(provider string, request *http.Request, response ht LoginSource: source.ID, LoginName: gothUser.NickName, IsActive: true, - // TODO should OAuth2 imported emails be private? - KeepEmailPrivate: true, } return user, redirectURL, models.CreateUser(user) } diff --git a/routers/user/setting.go b/routers/user/setting.go index 8fc2be0ce43b..0b376ec7ef6c 100644 --- a/routers/user/setting.go +++ b/routers/user/setting.go @@ -200,7 +200,7 @@ func SettingsPasswordPost(ctx *context.Context, form auth.ChangePasswordForm) { return } - if !ctx.User.ValidatePassword(form.OldPassword) { + if ctx.User.IsPasswordSet() && !ctx.User.ValidatePassword(form.OldPassword) { ctx.Flash.Error(ctx.Tr("settings.password_incorrect")) } else if form.Password != form.Retype { ctx.Flash.Error(ctx.Tr("form.password_not_match")) diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl index 91fbf781f32f..cb7072763ba4 100644 --- a/templates/admin/user/edit.tmpl +++ b/templates/admin/user/edit.tmpl @@ -43,7 +43,7 @@
-
+

{{.i18n.Tr "admin.users.password_helper"}}

diff --git a/templates/user/settings/password.tmpl b/templates/user/settings/password.tmpl index dc8b19062f46..b59d1ff56fde 100644 --- a/templates/user/settings/password.tmpl +++ b/templates/user/settings/password.tmpl @@ -9,13 +9,15 @@ {{.i18n.Tr "settings.change_password"}}
- {{if .SignedUser.IsLocal}} + {{if or (.SignedUser.IsLocal) (.SignedUser.IsOAuth2)}}
{{.CsrfTokenHtml}} + {{if .SignedUser.IsPasswordSet}}
+ {{end}}
From 83c238bcb8fbc970787f98ae837356224335e047 Mon Sep 17 00:00:00 2001 From: willemvd Date: Wed, 25 Jan 2017 12:05:55 +0100 Subject: [PATCH 15/40] merge vendor.json incl goth library --- vendor/github.com/markbates/goth/README.md | 1 + vendor/github.com/markbates/goth/provider.go | 27 +++++++++++++++++-- .../markbates/goth/providers/github/github.go | 9 +++++-- .../goth/providers/github/session.go | 8 +++--- vendor/vendor.json | 18 +++++++++++++ 5 files changed, 55 insertions(+), 8 deletions(-) diff --git a/vendor/github.com/markbates/goth/README.md b/vendor/github.com/markbates/goth/README.md index a002ff8fddf0..d2b7b351fcab 100644 --- a/vendor/github.com/markbates/goth/README.md +++ b/vendor/github.com/markbates/goth/README.md @@ -36,6 +36,7 @@ $ go get github.com/markbates/goth * Lastfm * Linkedin * OneDrive +* OpenID Connect (auto discovery) * Paypal * SalesForce * Slack diff --git a/vendor/github.com/markbates/goth/provider.go b/vendor/github.com/markbates/goth/provider.go index ff83143135a5..a7689fad2a6e 100644 --- a/vendor/github.com/markbates/goth/provider.go +++ b/vendor/github.com/markbates/goth/provider.go @@ -1,7 +1,12 @@ package goth -import "fmt" -import "golang.org/x/oauth2" +import ( + "fmt" + "net/http" + + "golang.org/x/net/context" + "golang.org/x/oauth2" +) // Provider needs to be implemented for each 3rd party authentication provider // e.g. Facebook, Twitter, etc... @@ -15,6 +20,8 @@ type Provider interface { RefreshTokenAvailable() bool //Refresh token is provided by auth provider or not } +const NoAuthUrlErrorMessage = "an AuthURL has not been set" + // Providers is list of known/available providers. type Providers map[string]Provider @@ -49,3 +56,19 @@ func GetProvider(name string) (Provider, error) { func ClearProviders() { providers = Providers{} } + +// ContextForClient provides a context for use with oauth2. +func ContextForClient(h *http.Client) context.Context { + if h == nil { + return oauth2.NoContext + } + return context.WithValue(oauth2.NoContext, oauth2.HTTPClient, h) +} + +// HTTPClientWithFallBack to be used in all fetch operations. +func HTTPClientWithFallBack(h *http.Client) *http.Client { + if h != nil { + return h + } + return http.DefaultClient +} diff --git a/vendor/github.com/markbates/goth/providers/github/github.go b/vendor/github.com/markbates/goth/providers/github/github.go index 46d810d3c31b..f43cc58c8ef3 100644 --- a/vendor/github.com/markbates/goth/providers/github/github.go +++ b/vendor/github.com/markbates/goth/providers/github/github.go @@ -51,6 +51,7 @@ type Provider struct { ClientKey string Secret string CallbackURL string + HTTPClient *http.Client config *oauth2.Config } @@ -59,6 +60,10 @@ func (p *Provider) Name() string { return "github" } +func (p *Provider) Client() *http.Client { + return goth.HTTPClientWithFallBack(p.HTTPClient) +} + // Debug is a no-op for the github package. func (p *Provider) Debug(debug bool) {} @@ -79,7 +84,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { Provider: p.Name(), } - response, err := http.Get(ProfileURL + "?access_token=" + url.QueryEscape(sess.AccessToken)) + response, err := p.Client().Get(ProfileURL + "?access_token=" + url.QueryEscape(sess.AccessToken)) if err != nil { return user, err } @@ -146,7 +151,7 @@ func userFromReader(reader io.Reader, user *goth.User) error { } func getPrivateMail(p *Provider, sess *Session) (email string, err error) { - response, err := http.Get(EmailURL + "?access_token=" + url.QueryEscape(sess.AccessToken)) + response, err := p.Client().Get(EmailURL + "?access_token=" + url.QueryEscape(sess.AccessToken)) if err != nil { if response != nil { response.Body.Close() diff --git a/vendor/github.com/markbates/goth/providers/github/session.go b/vendor/github.com/markbates/goth/providers/github/session.go index fd7a8481bed1..f473a526e5fd 100644 --- a/vendor/github.com/markbates/goth/providers/github/session.go +++ b/vendor/github.com/markbates/goth/providers/github/session.go @@ -3,9 +3,9 @@ package github import ( "encoding/json" "errors" - "github.com/markbates/goth" - "golang.org/x/oauth2" "strings" + + "github.com/markbates/goth" ) // Session stores data during the auth process with Github. @@ -17,7 +17,7 @@ type Session struct { // GetAuthURL will return the URL set by calling the `BeginAuth` function on the Github provider. func (s Session) GetAuthURL() (string, error) { if s.AuthURL == "" { - return "", errors.New("an AuthURL has not be set") + return "", errors.New(goth.NoAuthUrlErrorMessage) } return s.AuthURL, nil } @@ -25,7 +25,7 @@ func (s Session) GetAuthURL() (string, error) { // Authorize the session with Github and return the access token to be stored for future use. func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { p := provider.(*Provider) - token, err := p.config.Exchange(oauth2.NoContext, params.Get("code")) + token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code")) if err != nil { return "", err } diff --git a/vendor/vendor.json b/vendor/vendor.json index 778edbd0b84f..9ee29e5a00f8 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -556,6 +556,24 @@ "revision": "d8eeeb8bae8896dd8e1b7e514ab0d396c4f12a1b", "revisionTime": "2016-11-03T02:43:54Z" }, + { + "checksumSHA1": "4qhmMAEMj7myggZi6D7o8ezDuxE=", + "path": "github.com/markbates/goth", + "revision": "e14124782db6fd5b43c32c254d3d8e5ef849c830", + "revisionTime": "2017-01-25T09:33:14Z" + }, + { + "checksumSHA1": "zcRIxzOSWwSM4fzvDXey20d1Gl4=", + "path": "github.com/markbates/goth/gothic", + "revision": "e14124782db6fd5b43c32c254d3d8e5ef849c830", + "revisionTime": "2017-01-25T09:33:14Z" + }, + { + "checksumSHA1": "/ECS41Dd3ucjCm/Tqxnaeyf/BWI=", + "path": "github.com/markbates/goth/providers/github", + "revision": "e14124782db6fd5b43c32c254d3d8e5ef849c830", + "revisionTime": "2017-01-25T09:33:14Z" + }, { "checksumSHA1": "9FJUwn3EIgASVki+p8IHgWVC5vQ=", "path": "github.com/mattn/go-sqlite3", From c65a21602148e36e40b5f5562a553466d9f2f2e4 Mon Sep 17 00:00:00 2001 From: willemvd Date: Wed, 25 Jan 2017 13:08:53 +0100 Subject: [PATCH 16/40] set a default provider instead of a empty option --- routers/admin/auths.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/routers/admin/auths.go b/routers/admin/auths.go index 7eaf667cb15a..345494b4f669 100644 --- a/routers/admin/auths.go +++ b/routers/admin/auths.go @@ -77,6 +77,13 @@ func NewAuthSource(ctx *context.Context) { ctx.Data["SecurityProtocols"] = securityProtocols ctx.Data["SMTPAuths"] = models.SMTPAuths ctx.Data["OAuth2Providers"] = models.OAuth2Providers + + // only the first as default + for key := range models.OAuth2Providers { + ctx.Data["oauth2_provider"] = key + break + } + ctx.HTML(200, tplAuthNew) } From 914f56a47f3d87b90af98783d25860691cfe41cd Mon Sep 17 00:00:00 2001 From: willemvd Date: Wed, 25 Jan 2017 13:51:14 +0100 Subject: [PATCH 17/40] add tip for registering a GitHub OAuth application --- options/locale/locale_en-US.ini | 1 + templates/admin/auth/new.tmpl | 2 ++ 2 files changed, 3 insertions(+) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index bddf02da491d..57839780b6db 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1089,6 +1089,7 @@ auths.oauth2_clientID = Client ID (Key) auths.oauth2_clientSecret = Client Secret auths.enable_auto_register = Enable Auto Registration auths.tips = Tips +auths.tip.github = Register a new OAuth application on https://github.com/settings/applications/new and use /user/oauth2/github/callback as "Authorization callback URL" auths.edit = Edit Authentication Setting auths.activated = This authentication is activated auths.new_success = New authentication '%s' has been added successfully. diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl index 68a2548eec43..a2e812580b4e 100644 --- a/templates/admin/auth/new.tmpl +++ b/templates/admin/auth/new.tmpl @@ -195,6 +195,8 @@
GMail Settings:

Host: smtp.gmail.com, Port: 587, Enable TLS Encryption: true

+
OAuth GitHub:
+

{{.i18n.Tr "admin.auths.tip.github"}}

From b4eb93ce7cf8b9fd03d0f2b257ccee18ccf890ec Mon Sep 17 00:00:00 2001 From: willemvd Date: Thu, 26 Jan 2017 10:18:29 +0100 Subject: [PATCH 18/40] remove unused redirectURL --- modules/auth/oauth2/oauth2.go | 6 +++--- routers/user/auth.go | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index d13fd8e1d782..add1d93aa1c2 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -51,14 +51,14 @@ func Auth(provider, clientID, clientSecret string, request *http.Request, respon // ProviderCallback handles OAuth callback, resolve to a goth user and send back to original url // this will trigger a new authentication request, but because we save it in the session we can use that -func ProviderCallback(provider string, request *http.Request, response http.ResponseWriter) (goth.User, string, error) { +func ProviderCallback(provider string, request *http.Request, response http.ResponseWriter) (goth.User, error) { // not sure if goth is thread safe (?) when using multiple providers request.Header.Set(providerHeaderKey, provider) user, err := gothic.CompleteUserAuth(response, request) if err != nil { - return user, "", err + return user, err } - return user, "", nil + return user, nil } diff --git a/routers/user/auth.go b/routers/user/auth.go index 406be58f2f04..d5dc40616dd6 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -353,7 +353,7 @@ func SignInOAuth(ctx *context.Context) { func SignInOAuthCallback(ctx *context.Context) { provider := ctx.Params(":provider") - u, _, err := oAuth2UserLoginCallback(provider, ctx.Req.Request, ctx.Resp) + u, err := oAuth2UserLoginCallback(provider, ctx.Req.Request, ctx.Resp) if err != nil { ctx.Handle(500, "UserSignIn", err) @@ -399,16 +399,16 @@ func SignInOAuthCallback(ctx *context.Context) { // OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful // login the user -func oAuth2UserLoginCallback(provider string, request *http.Request, response http.ResponseWriter) (*models.User, string, error) { - gothUser, redirectURL, error := oauth2.ProviderCallback(provider, request, response) +func oAuth2UserLoginCallback(provider string, request *http.Request, response http.ResponseWriter) (*models.User, error) { + gothUser, error := oauth2.ProviderCallback(provider, request, response) if error != nil { - return nil, redirectURL, error + return nil, error } loginSources, err := models.GetActiveOAuth2ProviderLoginSources() if err != nil { - return nil, "", err + return nil, err } for _, source := range loginSources { @@ -421,7 +421,7 @@ func oAuth2UserLoginCallback(provider string, request *http.Request, response ht hasUser, err := models.GetUser(user) if err != nil { - return nil, "", err + return nil, err } if !hasUser { @@ -434,14 +434,14 @@ func oAuth2UserLoginCallback(provider string, request *http.Request, response ht LoginName: gothUser.NickName, IsActive: true, } - return user, redirectURL, models.CreateUser(user) + return user, models.CreateUser(user) } - return user, redirectURL, nil + return user, nil } } - return nil, "", errors.New("No valid provider found") + return nil, errors.New("No valid provider found") } // SignOut sign out from login status From 7a6757f6a2402dfbafd597cc9b26ac24972e2e4d Mon Sep 17 00:00:00 2001 From: willemvd Date: Fri, 27 Jan 2017 08:40:38 +0100 Subject: [PATCH 19/40] at startup of Gitea register all configured providers and also on adding/deleting of new providers --- models/login_source.go | 24 +++++++++++ modules/auth/oauth2/oauth2.go | 43 +++++++++++++++---- routers/init.go | 1 + routers/user/auth.go | 2 +- vendor/github.com/markbates/goth/provider.go | 1 + .../markbates/goth/providers/github/github.go | 25 +++++++---- vendor/vendor.json | 16 +++---- 7 files changed, 85 insertions(+), 27 deletions(-) diff --git a/models/login_source.go b/models/login_source.go index 9b0a992f4d87..1c220b9f67a5 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/auth/ldap" "code.gitea.io/gitea/modules/auth/pam" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/auth/oauth2" ) // LoginType represents an login type. @@ -293,6 +294,10 @@ func CreateLoginSource(source *LoginSource) error { } _, err = x.Insert(source) + if err == nil && source.IsOAuth2() { + oAuth2Config := source.OAuth2() + oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret) + } return err } @@ -317,6 +322,11 @@ func GetLoginSourceByID(id int64) (*LoginSource, error) { // UpdateSource updates a LoginSource record in DB. func UpdateSource(source *LoginSource) error { _, err := x.Id(source.ID).AllCols().Update(source) + if err != nil && source.IsOAuth2() { + oAuth2Config := source.OAuth2() + oauth2.RemoveProvider(source.Name) + oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret) + } return err } @@ -329,6 +339,9 @@ func DeleteSource(source *LoginSource) error { return ErrLoginSourceInUse{source.ID} } _, err = x.Id(source.ID).Delete(new(LoginSource)) + if err != nil && source.IsOAuth2() { + oauth2.RemoveProvider(source.Name) + } return err } @@ -670,3 +683,14 @@ func GetActiveOAuth2ProviderNames() (map[string]string, error) { return providers, nil } + +// InitOAuth2 initialize the OAuth2 lib and register all active OAuth2 providers in the library +func InitOAuth2() { + oauth2.Init() + loginSources, _ := GetActiveOAuth2ProviderLoginSources() + + for _, source := range loginSources { + oAuth2Config := source.OAuth2() + oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret) + } +} diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index add1d93aa1c2..20415de9439d 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -6,11 +6,11 @@ import ( "github.com/gorilla/sessions" "github.com/markbates/goth" "github.com/markbates/goth/gothic" - "github.com/markbates/goth/providers/github" "net/http" "os" "github.com/satori/go.uuid" "path/filepath" + "github.com/markbates/goth/providers/github" ) var ( @@ -18,7 +18,8 @@ var ( providerHeaderKey = "gitea-oauth2-provider" ) -func init() { +// Init initialize the setup of the OAuth2 library +func Init() { sessionDir := filepath.Join(setting.AppDataPath, "sessions", "oauth2") if err := os.MkdirAll(sessionDir, 0700); err != nil { log.Fatal(4, "Fail to create dir %s: %v", sessionDir, err) @@ -33,19 +34,14 @@ func init() { gothic.GetProviderName = func(req *http.Request) (string, error) { return req.Header.Get(providerHeaderKey), nil } + } // Auth OAuth2 auth service -func Auth(provider, clientID, clientSecret string, request *http.Request, response http.ResponseWriter) { +func Auth(provider string, request *http.Request, response http.ResponseWriter) { // not sure if goth is thread safe (?) when using multiple providers request.Header.Set(providerHeaderKey, provider) - if gothProvider, _ := goth.GetProvider(provider); gothProvider == nil { - goth.UseProviders( - github.New(clientID, clientSecret, setting.AppURL+"user/oauth2/"+provider+"/callback", "user:email"), - ) - } - gothic.BeginAuthHandler(response, request) } @@ -62,3 +58,32 @@ func ProviderCallback(provider string, request *http.Request, response http.Resp return user, nil } + +// RegisterProvider register a OAuth2 provider in goth lib +func RegisterProvider(providerName, providerType, clientID, clientSecret string) { + provider := createProvider(providerName, providerType, clientID, clientSecret) + + goth.UseProviders(provider) +} + +// RemoveProvider removes the given OAuth2 provider from the goth lib +func RemoveProvider(providerName string) { + delete(goth.GetProviders(), providerName) +} + +// used to create different types of goth providers +func createProvider(providerName, providerType, clientID, clientSecret string) goth.Provider { + callbackURL := setting.AppURL + "user/oauth2/" + providerName + "/callback" + + var provider goth.Provider + + switch providerType { + case "github": + provider = github.New(clientID, clientSecret, callbackURL, "user:email") + } + + // always set the name so we can support multiple setups of 1 provider + provider.SetName(providerName) + + return provider +} diff --git a/routers/init.go b/routers/init.go index 23bf7ae62869..83e8feac45f4 100644 --- a/routers/init.go +++ b/routers/init.go @@ -54,6 +54,7 @@ func GlobalInit() { log.Fatal(4, "Fail to initialize ORM engine: %v", err) } models.HasEngine = true + models.InitOAuth2() models.LoadRepoConfig() models.NewRepoContext() diff --git a/routers/user/auth.go b/routers/user/auth.go index d5dc40616dd6..e3d71d2d2c26 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -340,7 +340,7 @@ func SignInOAuth(ctx *context.Context) { // TODO how to put this in the xorm Find ? cfg := source.OAuth2() if cfg.Provider == provider { - oauth2.Auth(cfg.Provider, cfg.ClientID, cfg.ClientSecret, ctx.Req.Request, ctx.Resp) + oauth2.Auth(cfg.Provider, ctx.Req.Request, ctx.Resp) return } } diff --git a/vendor/github.com/markbates/goth/provider.go b/vendor/github.com/markbates/goth/provider.go index a7689fad2a6e..58d0d60bbf7b 100644 --- a/vendor/github.com/markbates/goth/provider.go +++ b/vendor/github.com/markbates/goth/provider.go @@ -12,6 +12,7 @@ import ( // e.g. Facebook, Twitter, etc... type Provider interface { Name() string + SetName(name string) BeginAuth(state string) (Session, error) UnmarshalSession(string) (Session, error) FetchUser(Session) (User, error) diff --git a/vendor/github.com/markbates/goth/providers/github/github.go b/vendor/github.com/markbates/goth/providers/github/github.go index f43cc58c8ef3..4791900f4e50 100644 --- a/vendor/github.com/markbates/goth/providers/github/github.go +++ b/vendor/github.com/markbates/goth/providers/github/github.go @@ -38,9 +38,10 @@ var ( // one manually. func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { p := &Provider{ - ClientKey: clientKey, - Secret: secret, - CallbackURL: callbackURL, + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + providerName: "github", } p.config = newConfig(p, scopes) return p @@ -48,16 +49,22 @@ func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { // Provider is the implementation of `goth.Provider` for accessing Github. type Provider struct { - ClientKey string - Secret string - CallbackURL string - HTTPClient *http.Client - config *oauth2.Config + ClientKey string + Secret string + CallbackURL string + HTTPClient *http.Client + config *oauth2.Config + providerName string } // Name is the name used to retrieve this provider later. func (p *Provider) Name() string { - return "github" + return p.providerName +} + +// SetName is to update the name of the provider (needed in case of multiple providers of 1 type) +func (p *Provider) SetName(name string) { + p.providerName = name } func (p *Provider) Client() *http.Client { diff --git a/vendor/vendor.json b/vendor/vendor.json index 9ee29e5a00f8..256222264b24 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -557,22 +557,22 @@ "revisionTime": "2016-11-03T02:43:54Z" }, { - "checksumSHA1": "4qhmMAEMj7myggZi6D7o8ezDuxE=", + "checksumSHA1": "KJkDtEZHGmFuEtUHrX2/bm2K74o=", "path": "github.com/markbates/goth", - "revision": "e14124782db6fd5b43c32c254d3d8e5ef849c830", - "revisionTime": "2017-01-25T09:33:14Z" + "revision": "3b285de1061587f2a5ebb2b456da8d65a9cecee9", + "revisionTime": "2017-01-26T14:15:07Z" }, { "checksumSHA1": "zcRIxzOSWwSM4fzvDXey20d1Gl4=", "path": "github.com/markbates/goth/gothic", - "revision": "e14124782db6fd5b43c32c254d3d8e5ef849c830", - "revisionTime": "2017-01-25T09:33:14Z" + "revision": "3b285de1061587f2a5ebb2b456da8d65a9cecee9", + "revisionTime": "2017-01-26T14:15:07Z" }, { - "checksumSHA1": "/ECS41Dd3ucjCm/Tqxnaeyf/BWI=", + "checksumSHA1": "cR1jkLb/XPj+ffipSHlT4qnQ9g4=", "path": "github.com/markbates/goth/providers/github", - "revision": "e14124782db6fd5b43c32c254d3d8e5ef849c830", - "revisionTime": "2017-01-25T09:33:14Z" + "revision": "3b285de1061587f2a5ebb2b456da8d65a9cecee9", + "revisionTime": "2017-01-26T14:15:07Z" }, { "checksumSHA1": "9FJUwn3EIgASVki+p8IHgWVC5vQ=", From aae5f80c78616a378fabc3582fee0b0bd47954c6 Mon Sep 17 00:00:00 2001 From: willemvd Date: Fri, 27 Jan 2017 10:18:19 +0100 Subject: [PATCH 20/40] always use source.Name as provider key --- models/login_source.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/login_source.go b/models/login_source.go index 1c220b9f67a5..8e299ab2ef8a 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -678,7 +678,7 @@ func GetActiveOAuth2ProviderNames() (map[string]string, error) { providers := make(map[string]string) for _, source := range loginSources { - providers[source.OAuth2().Provider] = OAuth2Providers[source.OAuth2().Provider] + providers[source.Name] = OAuth2Providers[source.OAuth2().Provider] } return providers, nil From 9151dc8efccbc4a6ef5f2e51cf5b07a20eeb1179 Mon Sep 17 00:00:00 2001 From: willemvd Date: Fri, 27 Jan 2017 10:22:08 +0100 Subject: [PATCH 21/40] custom handling of errors in oauth2 request init + show better tip --- modules/auth/oauth2/oauth2.go | 12 ++++++++++-- options/locale/locale_en-US.ini | 2 +- routers/user/auth.go | 8 +++++--- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index 20415de9439d..2d5575798f01 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -38,11 +38,19 @@ func Init() { } // Auth OAuth2 auth service -func Auth(provider string, request *http.Request, response http.ResponseWriter) { +func Auth(provider string, request *http.Request, response http.ResponseWriter) error { // not sure if goth is thread safe (?) when using multiple providers request.Header.Set(providerHeaderKey, provider) - gothic.BeginAuthHandler(response, request) + // don't use the default gothic begin handler to prevent issues when some error occurs + // normally the gothic library will write some custom stuff to the response instead of our own nice error page + //gothic.BeginAuthHandler(response, request) + + url, err := gothic.GetAuthURL(response, request) + if err == nil { + http.Redirect(response, request, url, http.StatusTemporaryRedirect) + } + return err } // ProviderCallback handles OAuth callback, resolve to a goth user and send back to original url diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 57839780b6db..17173977a8e9 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1089,7 +1089,7 @@ auths.oauth2_clientID = Client ID (Key) auths.oauth2_clientSecret = Client Secret auths.enable_auto_register = Enable Auto Registration auths.tips = Tips -auths.tip.github = Register a new OAuth application on https://github.com/settings/applications/new and use /user/oauth2/github/callback as "Authorization callback URL" +auths.tip.github = Register a new OAuth application on https://github.com/settings/applications/new and use /user/oauth2//callback as "Authorization callback URL" auths.edit = Edit Authentication Setting auths.activated = This authentication is activated auths.new_success = New authentication '%s' has been added successfully. diff --git a/routers/user/auth.go b/routers/user/auth.go index e3d71d2d2c26..b072497193a2 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -338,9 +338,11 @@ func SignInOAuth(ctx *context.Context) { for _, source := range loginSources { // TODO how to put this in the xorm Find ? - cfg := source.OAuth2() - if cfg.Provider == provider { - oauth2.Auth(cfg.Provider, ctx.Req.Request, ctx.Resp) + if source.Name == provider { + err := oauth2.Auth(source.Name, ctx.Req.Request, ctx.Resp) + if err != nil { + ctx.Handle(500, "SignIn", err) + } return } } From 96d1af57fe9cfe052496dafc8cbd11174287c8be Mon Sep 17 00:00:00 2001 From: willemvd Date: Fri, 27 Jan 2017 11:04:17 +0100 Subject: [PATCH 22/40] more checks if provider exists and is active (less calls to db to check if provider exists) --- models/login_source.go | 16 ++++++++ routers/user/auth.go | 86 ++++++++++++++++++++---------------------- 2 files changed, 56 insertions(+), 46 deletions(-) diff --git a/models/login_source.go b/models/login_source.go index 8e299ab2ef8a..22c8f8587a73 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -665,6 +665,22 @@ func GetActiveOAuth2ProviderLoginSources() ([]*LoginSource, error) { return sources, nil } +// GetActiveOAuth2LoginSourceByName returns a OAuth2 LoginSource based on the given name +func GetActiveOAuth2LoginSourceByName(name string) (*LoginSource, error) { + loginSource := &LoginSource{ + Name: name, + Type: LoginOAuth2, + IsActived: true, + } + + has, err := x.UseBool().Get(loginSource) + if !has || err != nil { + return nil, err + } + + return loginSource, nil +} + // GetActiveOAuth2ProviderNames returns the map of configured active OAuth2 providers // key is used as technical name (like in the callbackURL) // value is used to display diff --git a/routers/user/auth.go b/routers/user/auth.go index b072497193a2..325858edf6ea 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -330,24 +330,16 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR func SignInOAuth(ctx *context.Context) { provider := ctx.Params(":provider") - loginSources, err := models.GetActiveOAuth2ProviderLoginSources() + loginSource, err := models.GetActiveOAuth2LoginSourceByName(provider) if err != nil { ctx.Handle(500, "SignIn", err) return } - for _, source := range loginSources { - // TODO how to put this in the xorm Find ? - if source.Name == provider { - err := oauth2.Auth(source.Name, ctx.Req.Request, ctx.Resp) - if err != nil { - ctx.Handle(500, "SignIn", err) - } - return - } + err = oauth2.Auth(loginSource.Name, ctx.Req.Request, ctx.Resp) + if err != nil { + ctx.Handle(500, "SignIn", err) } - - ctx.Handle(500, "SignIn", errors.New("No valid provider found")) return } @@ -355,7 +347,19 @@ func SignInOAuth(ctx *context.Context) { func SignInOAuthCallback(ctx *context.Context) { provider := ctx.Params(":provider") - u, err := oAuth2UserLoginCallback(provider, ctx.Req.Request, ctx.Resp) + // first look if the provider is still active + loginSource, err := models.GetActiveOAuth2LoginSourceByName(provider) + if err != nil { + ctx.Handle(500, "SignIn", err) + return + } + + if loginSource == nil { + ctx.Handle(500, "SignIn", errors.New("No valid provider found, check configured callback url in provider")) + return + } + + u, err := oAuth2UserLoginCallback(loginSource, ctx.Req.Request, ctx.Resp) if err != nil { ctx.Handle(500, "UserSignIn", err) @@ -401,49 +405,39 @@ func SignInOAuthCallback(ctx *context.Context) { // OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful // login the user -func oAuth2UserLoginCallback(provider string, request *http.Request, response http.ResponseWriter) (*models.User, error) { - gothUser, error := oauth2.ProviderCallback(provider, request, response) - - if error != nil { - return nil, error - } +func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) (*models.User, error) { + gothUser, err := oauth2.ProviderCallback(loginSource.Name, request, response) - loginSources, err := models.GetActiveOAuth2ProviderLoginSources() if err != nil { return nil, err } - for _, source := range loginSources { - if source.OAuth2().Provider == provider { - user := &models.User{ - LoginName: gothUser.NickName, - LoginType: models.LoginOAuth2, - LoginSource: source.ID, - } + user := &models.User{ + LoginName: gothUser.NickName, + LoginType: models.LoginOAuth2, + LoginSource: loginSource.ID, + } - hasUser, err := models.GetUser(user) - if err != nil { - return nil, err - } + hasUser, err := models.GetUser(user) + if err != nil { + return nil, err + } - if !hasUser { - user = &models.User{ - LowerName: strings.ToLower(gothUser.NickName), - Name: gothUser.NickName, - Email: gothUser.Email, - LoginType: models.LoginOAuth2, - LoginSource: source.ID, - LoginName: gothUser.NickName, - IsActive: true, - } - return user, models.CreateUser(user) - } + if hasUser { + return user, nil + } - return user, nil - } + user = &models.User{ + LowerName: strings.ToLower(gothUser.NickName), + Name: gothUser.NickName, + Email: gothUser.Email, + LoginType: models.LoginOAuth2, + LoginSource: loginSource.ID, + LoginName: gothUser.NickName, + IsActive: true, } + return user, models.CreateUser(user) - return nil, errors.New("No valid provider found") } // SignOut sign out from login status From c3f5d361b602410f9c328037afb4ca4089c48e74 Mon Sep 17 00:00:00 2001 From: willemvd Date: Fri, 27 Jan 2017 11:12:44 +0100 Subject: [PATCH 23/40] add ExternalLoginUser model and migration script to add it to database --- models/external_login_user.go | 13 +++++++++++++ models/migrations/migrations.go | 2 ++ models/migrations/v16.go | 25 +++++++++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 models/external_login_user.go create mode 100644 models/migrations/v16.go diff --git a/models/external_login_user.go b/models/external_login_user.go new file mode 100644 index 000000000000..f6f49c7da8ee --- /dev/null +++ b/models/external_login_user.go @@ -0,0 +1,13 @@ +package models + +// ExternalLoginUser makes the connecting between some existing user and additional external login sources +type ExternalLoginUser struct { + ExternalID string `xorm:"NOT NULL"` + UserID int64 `xorm:"NOT NULL"` + LoginSourceID int64 `xorm:"NOT NULL"` +} + +// GetExternalLogin checks if a externalID in loginSourceID scope already exists +func GetExternalLogin(externalLoginUser *ExternalLoginUser) (bool, error) { + return x.Get(externalLoginUser) +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 69408a071d2b..d8e00e529ea6 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -80,6 +80,8 @@ var migrations = []Migration{ NewMigration("create user column diff view style", createUserColumnDiffViewStyle), // v15 NewMigration("create user column allow create organization", createAllowCreateOrganizationColumn), + // v16 + NewMigration("add external login user", addExternalLoginUser), } // Migrate database to current version diff --git a/models/migrations/v16.go b/models/migrations/v16.go new file mode 100644 index 000000000000..be5161534194 --- /dev/null +++ b/models/migrations/v16.go @@ -0,0 +1,25 @@ +// Copyright 2016 Gitea. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "fmt" + + "github.com/go-xorm/xorm" +) + +// ExternalLoginUser makes the connecting between some existing user and additional external login sources +type ExternalLoginUser struct { + ExternalID string `xorm:"NOT NULL"` + UserID int64 `xorm:"NOT NULL"` + LoginSourceID int64 `xorm:"NOT NULL"` +} + +func addExternalLoginUser(x *xorm.Engine) error { + if err := x.Sync2(new(ExternalLoginUser)); err != nil { + return fmt.Errorf("Sync2: %v", err) + } + return nil +} From 2bf6b345b2ac23dea5bf9abf591cf31133dccfe2 Mon Sep 17 00:00:00 2001 From: willemvd Date: Fri, 27 Jan 2017 11:43:27 +0100 Subject: [PATCH 24/40] remove unused IsSocialLogin code --- templates/user/auth/signup.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/user/auth/signup.tmpl b/templates/user/auth/signup.tmpl index ca98302ed206..2f28d861c86a 100644 --- a/templates/user/auth/signup.tmpl +++ b/templates/user/auth/signup.tmpl @@ -5,7 +5,7 @@ {{.CsrfTokenHtml}}

- {{if .IsSocialLogin}}{{.i18n.Tr "social_sign_in" | Str2html}}{{else}}{{.i18n.Tr "sign_up"}}{{end}} + {{.i18n.Tr "sign_up"}}

{{template "base/alert" .}} @@ -45,7 +45,7 @@
{{end}}
From 7ccbc444f91986b33a13e43b5301ecafb488cc60 Mon Sep 17 00:00:00 2001 From: willemvd Date: Fri, 27 Jan 2017 13:53:50 +0100 Subject: [PATCH 25/40] create initial flow for linkAccount, todo: handle the POST of LinkAccount --- cmd/web.go | 1 + options/locale/locale_en-US.ini | 2 + public/css/index.css | 4 + routers/user/auth.go | 126 ++++++++++++++++++++++---- templates/user/auth/link_account.tmpl | 13 +++ templates/user/auth/signin.tmpl | 51 +---------- templates/user/auth/signin_inner.tmpl | 53 +++++++++++ templates/user/auth/signup.tmpl | 55 +---------- templates/user/auth/signup_inner.tmpl | 57 ++++++++++++ 9 files changed, 238 insertions(+), 124 deletions(-) create mode 100644 templates/user/auth/link_account.tmpl create mode 100644 templates/user/auth/signin_inner.tmpl create mode 100644 templates/user/auth/signup_inner.tmpl diff --git a/cmd/web.go b/cmd/web.go index 22145c4ad38c..968b2baf9140 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -208,6 +208,7 @@ func runWeb(ctx *cli.Context) error { m.Get("/:provider", user.SignInOAuth) m.Get("/:provider/callback", user.SignInOAuthCallback) }) + m.Combo("/linkaccount").Get(user.LinkAccount).Post(bindIgnErr(auth.SignInForm{}), user.LinkAccountPost) m.Group("/two_factor", func() { m.Get("", user.TwoFactor) m.Post("", bindIgnErr(auth.TwoFactorAuthForm{}), user.TwoFactorPost) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 17173977a8e9..2ea2de22b927 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -7,6 +7,8 @@ help = Help sign_in = Sign In sign_out = Sign Out sign_up = Sign Up +link_account = Link Account +link_account_signin_or_signup = Login with existing credentials to link your existing account to these new account, or sign up for a new account register = Register website = Website version = Version diff --git a/public/css/index.css b/public/css/index.css index d755e6127b97..0b01a1e8435a 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -2979,3 +2979,7 @@ footer .ui.language .menu { .ui.user.list .item .description a:hover { text-decoration: underline; } +.user.link-account:not(.icon) { + padding-top: 15px; + padding-bottom: 5px; +} \ No newline at end of file diff --git a/routers/user/auth.go b/routers/user/auth.go index 325858edf6ea..a0bf2d5f0b56 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -17,9 +17,9 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "strings" "net/http" "code.gitea.io/gitea/modules/auth/oauth2" + "github.com/markbates/goth" ) const ( @@ -33,6 +33,7 @@ const ( tplResetPassword base.TplName = "user/auth/reset_passwd" tplTwofa base.TplName = "user/auth/twofa" tplTwofaScratch base.TplName = "user/auth/twofa_scratch" + tplLinkAccount base.TplName = "user/auth/link_account" ) // AutoSignIn reads cookie and try to auto-login. @@ -359,13 +360,20 @@ func SignInOAuthCallback(ctx *context.Context) { return } - u, err := oAuth2UserLoginCallback(loginSource, ctx.Req.Request, ctx.Resp) + u, gothUser, err := oAuth2UserLoginCallback(loginSource, ctx.Req.Request, ctx.Resp) if err != nil { ctx.Handle(500, "UserSignIn", err) return } + if u == nil { + // no existing user is found, request attach or new account + ctx.Session.Set("linkAccountGothUser", gothUser) + ctx.Redirect(setting.AppSubURL + "/user/linkaccount") + return + } + // If this user is enrolled in 2FA, we can't sign the user in just yet. // Instead, redirect them to the 2FA authentication page. _, err = models.GetTwoFactorByUID(u.ID) @@ -405,11 +413,11 @@ func SignInOAuthCallback(ctx *context.Context) { // OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful // login the user -func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) (*models.User, error) { +func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) (*models.User, goth.User, error) { gothUser, err := oauth2.ProviderCallback(loginSource.Name, request, response) if err != nil { - return nil, err + return nil, goth.User{}, err } user := &models.User{ @@ -420,24 +428,106 @@ func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Requ hasUser, err := models.GetUser(user) if err != nil { - return nil, err + return nil, goth.User{}, err } if hasUser { - return user, nil + return user, goth.User{}, nil + } + + // search in external linked users + externalLoginUser := &models.ExternalLoginUser{ + ExternalID: gothUser.UserID, + LoginSourceID: loginSource.ID, + } + hasUser, err = models.GetExternalLogin(externalLoginUser) + if err != nil { + return nil, goth.User{}, err + } + if hasUser { + user, err = models.GetUserByID(externalLoginUser.UserID) + return user, goth.User{}, err + } + + // no user found to login + return nil, gothUser, nil + +} + +// LinkAccount shows the page where the user can decide to login or create a new account +func LinkAccount(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("link_account") + ctx.Data["LinkAccountMode"] = true + ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha + ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration + ctx.Data["ShowRegistrationButton"] = false + + gothUser := ctx.Session.Get("linkAccountGothUser") + if gothUser == nil { + ctx.Handle(500, "UserSignIn", errors.New("not in LinkAccount session")) + return + } + + ctx.Data["user_name"] = gothUser.(goth.User).NickName + ctx.Data["email"] = gothUser.(goth.User).Email + + oauth2Providers, err := models.GetActiveOAuth2ProviderNames() + if err != nil { + ctx.Handle(500, "UserSignIn", err) + return + } + // delete the OAuth2 provider the user used to get to this linkAccount (so he cannot try to register him self again with the same provider) + delete(oauth2Providers, gothUser.(goth.User).Provider) + ctx.Data["OAuth2Providers"] = oauth2Providers + + ctx.HTML(200, tplLinkAccount) +} + +// LinkAccountPost handle the coupling of external account with another account +func LinkAccountPost(ctx *context.Context, form auth.SignInForm) { + ctx.Data["Title"] = ctx.Tr("link_account") + ctx.Data["LinkAccountMode"] = true + + oauth2Providers, err := models.GetActiveOAuth2ProviderNames() + if err != nil { + ctx.Handle(500, "UserSignIn", err) + return + } + ctx.Data["OAuth2Providers"] = oauth2Providers + + if ctx.HasError() { + ctx.HTML(200, tplLinkAccount) + return } - user = &models.User{ - LowerName: strings.ToLower(gothUser.NickName), - Name: gothUser.NickName, - Email: gothUser.Email, - LoginType: models.LoginOAuth2, - LoginSource: loginSource.ID, - LoginName: gothUser.NickName, - IsActive: true, + u, err := models.UserSignIn(form.UserName, form.Password) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form) + } else { + ctx.Handle(500, "UserSignIn", err) + } + return + } + + // If this user is enrolled in 2FA, we can't sign the user in just yet. + // Instead, redirect them to the 2FA authentication page. + _, err = models.GetTwoFactorByUID(u.ID) + if err != nil { + if models.IsErrTwoFactorNotEnrolled(err) { + handleSignIn(ctx, u, form.Remember) + } else { + ctx.Handle(500, "UserSignIn", err) + } + return } - return user, models.CreateUser(user) + // User needs to use 2FA, save data and redirect to 2FA page. + ctx.Session.Set("twofaUid", u.ID) + ctx.Session.Set("twofaRemember", form.Remember) + ctx.Session.Set("linkAccount", true) + + ctx.Redirect(setting.AppSubURL + "/user/two_factor") } // SignOut sign out from login status @@ -459,11 +549,7 @@ func SignUp(ctx *context.Context) { ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha - if setting.Service.DisableRegistration { - ctx.Data["DisableRegistration"] = true - ctx.HTML(200, tplSignUp) - return - } + ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration ctx.HTML(200, tplSignUp) } diff --git a/templates/user/auth/link_account.tmpl b/templates/user/auth/link_account.tmpl new file mode 100644 index 000000000000..5dc54ca5a4d2 --- /dev/null +++ b/templates/user/auth/link_account.tmpl @@ -0,0 +1,13 @@ +{{template "base/head" .}} + +{{template "user/auth/signin_inner" .}} +{{template "user/auth/signup_inner" .}} +{{template "base/footer" .}} diff --git a/templates/user/auth/signin.tmpl b/templates/user/auth/signin.tmpl index e1c2c9a211fe..5ed8612e3f7d 100644 --- a/templates/user/auth/signin.tmpl +++ b/templates/user/auth/signin.tmpl @@ -1,52 +1,3 @@ {{template "base/head" .}} - +{{template "user/auth/signin_inner" .}} {{template "base/footer" .}} diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl new file mode 100644 index 000000000000..e5a21f7cb45b --- /dev/null +++ b/templates/user/auth/signin_inner.tmpl @@ -0,0 +1,53 @@ +
+
+
+
+ {{.CsrfTokenHtml}} +

+ {{.i18n.Tr "sign_in"}} +

+
+ {{template "base/alert" .}} +
+ + +
+
+ + +
+ {{if not .LinkAccountMode}} +
+ +
+ + +
+
+ {{end}} + +
+ + + {{.i18n.Tr "auth.forget_password"}} +
+ + {{if .ShowRegistrationButton}} + + {{end}} + + {{if .OAuth2Providers}} + {{range $key, $value := .OAuth2Providers}} + + {{end}} + {{end}} +
+
+
+
+
\ No newline at end of file diff --git a/templates/user/auth/signup.tmpl b/templates/user/auth/signup.tmpl index 2f28d861c86a..b814b466c8dc 100644 --- a/templates/user/auth/signup.tmpl +++ b/templates/user/auth/signup.tmpl @@ -1,56 +1,3 @@ {{template "base/head" .}} - +{{template "user/auth/signup_inner" .}} {{template "base/footer" .}} diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl new file mode 100644 index 000000000000..52196ff7977c --- /dev/null +++ b/templates/user/auth/signup_inner.tmpl @@ -0,0 +1,57 @@ +
+
+
+
+ {{.CsrfTokenHtml}} +

+ {{.i18n.Tr "sign_up"}} +

+
+ {{template "base/alert" .}} + {{if .DisableRegistration}} +

{{.i18n.Tr "auth.disable_register_prompt"}}

+ {{else}} +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ {{if .EnableCaptcha}} +
+ + {{.Captcha.CreateHtml}} +
+
+ + +
+ {{end}} + +
+ + +
+ + {{if not .LinkAccountMode}} + + {{end}} + {{end}} +
+
+
+
+
\ No newline at end of file From 084c45f6df50434e0f75ad11b9d7bd24026b72f4 Mon Sep 17 00:00:00 2001 From: willemvd Date: Fri, 27 Jan 2017 16:12:10 +0100 Subject: [PATCH 26/40] link a external account to an existing account (still need to handle wrong login and signup) and remove if user is removed --- models/error.go | 24 ++++++++++++++++++++++++ models/external_login_user.go | 31 +++++++++++++++++++++++++++++++ models/login_source.go | 17 +++++++++++++++-- models/user.go | 6 ++++++ routers/user/auth.go | 18 +++++++++++++++--- 5 files changed, 91 insertions(+), 5 deletions(-) diff --git a/models/error.go b/models/error.go index e0d33df6dcf5..b99df3a3c887 100644 --- a/models/error.go +++ b/models/error.go @@ -829,3 +829,27 @@ func IsErrUploadNotExist(err error) bool { func (err ErrUploadNotExist) Error() string { return fmt.Sprintf("attachment does not exist [id: %d, uuid: %s]", err.ID, err.UUID) } + +// ___________ __ .__ .____ .__ ____ ___ +// \_ _____/__ ____/ |_ ___________ ____ _____ | | | | ____ ____ |__| ____ | | \______ ___________ +// | __)_\ \/ /\ __\/ __ \_ __ \/ \\__ \ | | | | / _ \ / ___\| |/ \ | | / ___// __ \_ __ \ +// | \> < | | \ ___/| | \/ | \/ __ \| |__ | |__( <_> ) /_/ > | | \ | | /\___ \\ ___/| | \/ +// /_______ /__/\_ \ |__| \___ >__| |___| (____ /____/ |_______ \____/\___ /|__|___| / |______//____ >\___ >__| +// \/ \/ \/ \/ \/ \/ /_____/ \/ \/ \/ + +// ErrExternalLoginUserAlreadyExist represents a "ExternalLoginUserAlreadyExist" kind of error. +type ErrExternalLoginUserAlreadyExist struct { + ExternalID string + UserID int64 + LoginSourceID int64 +} + +// IsErrExternalLoginUserAlreadyExist checks if an error is a ExternalLoginUserAlreadyExist. +func IsErrExternalLoginUserAlreadyExist(err error) bool { + _, ok := err.(ErrExternalLoginUserAlreadyExist) + return ok +} + +func (err ErrExternalLoginUserAlreadyExist) Error() string { + return fmt.Sprintf("external login user already exists [externalID: %s, userID: %d, loginSourceID: %d]", err.ExternalID, err.UserID, err.LoginSourceID) +} diff --git a/models/external_login_user.go b/models/external_login_user.go index f6f49c7da8ee..329cf3ec14f2 100644 --- a/models/external_login_user.go +++ b/models/external_login_user.go @@ -1,5 +1,7 @@ package models +import "github.com/markbates/goth" + // ExternalLoginUser makes the connecting between some existing user and additional external login sources type ExternalLoginUser struct { ExternalID string `xorm:"NOT NULL"` @@ -11,3 +13,32 @@ type ExternalLoginUser struct { func GetExternalLogin(externalLoginUser *ExternalLoginUser) (bool, error) { return x.Get(externalLoginUser) } + +// LinkAccountToUser link the gothUser to the user +func LinkAccountToUser(user *User, gothUser goth.User) error { + loginSource, err := GetActiveOAuth2LoginSourceByName(gothUser.Provider) + if err != nil { + return err + } + + externalLoginUser := &ExternalLoginUser{ + ExternalID: gothUser.UserID, + UserID: user.ID, + LoginSourceID: loginSource.ID, + } + has, err := x.Get(externalLoginUser) + if err != nil { + return err + } else if has { + return ErrExternalLoginUserAlreadyExist{gothUser.UserID, user.ID, loginSource.ID} + } + + _, err = x.Insert(externalLoginUser) + return err +} + +// RemoveAllAccountLinks will remove all external login sources for the given user +func RemoveAllAccountLinks(user *User) error { + _, err := x.Delete(&ExternalLoginUser{UserID: user.ID}) + return err +} \ No newline at end of file diff --git a/models/login_source.go b/models/login_source.go index 22c8f8587a73..89d1176e25e9 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -338,10 +338,19 @@ func DeleteSource(source *LoginSource) error { } else if count > 0 { return ErrLoginSourceInUse{source.ID} } - _, err = x.Id(source.ID).Delete(new(LoginSource)) - if err != nil && source.IsOAuth2() { + + count, err = x.Count(&ExternalLoginUser{LoginSourceID: source.ID}) + if err != nil { + return err + } else if count > 0 { + return ErrLoginSourceInUse{source.ID} + } + + if source.IsOAuth2() { oauth2.RemoveProvider(source.Name) } + + _, err = x.Id(source.ID).Delete(new(LoginSource)) return err } @@ -645,6 +654,10 @@ func UserSignIn(username, password string) (*User, error) { } for _, source := range sources { + if source.IsOAuth2() { + // don't try to authenticate against OAuth2 sources + continue + } authUser, err := ExternalUserLogin(nil, username, password, source, true) if err == nil { return authUser, nil diff --git a/models/user.go b/models/user.go index ed415ec79ade..37ccb325dcab 100644 --- a/models/user.go +++ b/models/user.go @@ -918,6 +918,12 @@ func deleteUser(e *xorm.Session, u *User) error { return fmt.Errorf("clear assignee: %v", err) } + // ***** START: ExternalLoginUser ***** + if err = RemoveAllAccountLinks(u); err != nil { + return fmt.Errorf("ExternalLoginUser: %v", err) + } + // ***** END: ExternalLoginUser ***** + if _, err = e.Id(u.ID).Delete(new(User)); err != nil { return fmt.Errorf("Delete: %v", err) } diff --git a/routers/user/auth.go b/routers/user/auth.go index a0bf2d5f0b56..489b90cf44ef 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -487,12 +487,23 @@ func LinkAccount(ctx *context.Context) { func LinkAccountPost(ctx *context.Context, form auth.SignInForm) { ctx.Data["Title"] = ctx.Tr("link_account") ctx.Data["LinkAccountMode"] = true + ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha + ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration + ctx.Data["ShowRegistrationButton"] = false + + gothUser := ctx.Session.Get("linkAccountGothUser") + if gothUser == nil { + ctx.Handle(500, "UserSignIn", errors.New("not in LinkAccount session")) + return + } oauth2Providers, err := models.GetActiveOAuth2ProviderNames() if err != nil { ctx.Handle(500, "UserSignIn", err) return } + // delete the OAuth2 provider the user used to get to this linkAccount (so he cannot try to register him self again with the same provider) + delete(oauth2Providers, gothUser.(goth.User).Provider) ctx.Data["OAuth2Providers"] = oauth2Providers if ctx.HasError() { @@ -503,9 +514,9 @@ func LinkAccountPost(ctx *context.Context, form auth.SignInForm) { u, err := models.UserSignIn(form.UserName, form.Password) if err != nil { if models.IsErrUserNotExist(err) { - ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form) + ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplLinkAccount, &form) } else { - ctx.Handle(500, "UserSignIn", err) + ctx.Handle(500, "UserLinkAccount", err) } return } @@ -515,9 +526,10 @@ func LinkAccountPost(ctx *context.Context, form auth.SignInForm) { _, err = models.GetTwoFactorByUID(u.ID) if err != nil { if models.IsErrTwoFactorNotEnrolled(err) { + models.LinkAccountToUser(u, gothUser.(goth.User)) handleSignIn(ctx, u, form.Remember) } else { - ctx.Handle(500, "UserSignIn", err) + ctx.Handle(500, "UserLinkAccount", err) } return } From 6594d76b9e5e981c4d01b5f5a38c5ace8db33edc Mon Sep 17 00:00:00 2001 From: willemvd Date: Mon, 30 Jan 2017 13:28:08 +0100 Subject: [PATCH 27/40] remove the linked external account from the user his settings --- cmd/web.go | 1 + models/error.go | 16 ++++++++ models/external_login_user.go | 27 ++++++++++++- options/locale/locale_en-US.ini | 8 ++++ routers/user/setting.go | 38 +++++++++++++++++++ templates/user/settings/account_link.tmpl | 46 +++++++++++++++++++++++ templates/user/settings/navbar.tmpl | 3 ++ 7 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 templates/user/settings/account_link.tmpl diff --git a/cmd/web.go b/cmd/web.go index 968b2baf9140..00992f73a042 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -235,6 +235,7 @@ func runWeb(ctx *cli.Context) error { Post(bindIgnErr(auth.NewAccessTokenForm{}), user.SettingsApplicationsPost) m.Post("/applications/delete", user.SettingsDeleteApplication) m.Route("/delete", "GET,POST", user.SettingsDelete) + m.Combo("/account_link").Get(user.SettingsAccountLinks).Post(user.SettingsDeleteAccountLink) m.Group("/two_factor", func() { m.Get("", user.SettingsTwoFactor) m.Post("/regenerate_scratch", user.SettingsTwoFactorRegenerateScratch) diff --git a/models/error.go b/models/error.go index b99df3a3c887..d7f176eb4bb6 100644 --- a/models/error.go +++ b/models/error.go @@ -853,3 +853,19 @@ func IsErrExternalLoginUserAlreadyExist(err error) bool { func (err ErrExternalLoginUserAlreadyExist) Error() string { return fmt.Sprintf("external login user already exists [externalID: %s, userID: %d, loginSourceID: %d]", err.ExternalID, err.UserID, err.LoginSourceID) } + +// ErrExternalLoginUserNotExist represents a "ExternalLoginUserNotExist" kind of error. +type ErrExternalLoginUserNotExist struct { + UserID int64 + LoginSourceID int64 +} + +// IsErrExternalLoginUserNotExist checks if an error is a ExternalLoginUserNotExist. +func IsErrExternalLoginUserNotExist(err error) bool { + _, ok := err.(ErrExternalLoginUserNotExist) + return ok +} + +func (err ErrExternalLoginUserNotExist) Error() string { + return fmt.Sprintf("external login user link does not exists [userID: %d, loginSourceID: %d]", err.UserID, err.LoginSourceID) +} diff --git a/models/external_login_user.go b/models/external_login_user.go index 329cf3ec14f2..0cfee54ae603 100644 --- a/models/external_login_user.go +++ b/models/external_login_user.go @@ -14,6 +14,19 @@ func GetExternalLogin(externalLoginUser *ExternalLoginUser) (bool, error) { return x.Get(externalLoginUser) } +// ListAccountLinks returns a map with the ExternalLoginUser and its LoginSource +func ListAccountLinks(user *User) ([]*ExternalLoginUser, error) { + externalAccounts := make([]*ExternalLoginUser, 0, 5) + err := x.Where("user_id=?", user.ID). + Desc("external_id"). + Find(&externalAccounts) + + if err != nil { + return nil, err + } + return externalAccounts, nil +} + // LinkAccountToUser link the gothUser to the user func LinkAccountToUser(user *User, gothUser goth.User) error { loginSource, err := GetActiveOAuth2LoginSourceByName(gothUser.Provider) @@ -37,8 +50,20 @@ func LinkAccountToUser(user *User, gothUser goth.User) error { return err } +// RemoveAccountLink will remove all external login sources for the given user +func RemoveAccountLink(user *User, loginSourceID int64) (int64, error) { + deleted, err := x.Delete(&ExternalLoginUser{UserID: user.ID, LoginSourceID: loginSourceID}) + if err != nil { + return deleted, err + } + if deleted < 1 { + return deleted, ErrExternalLoginUserNotExist{user.ID, loginSourceID} + } + return deleted, err +} + // RemoveAllAccountLinks will remove all external login sources for the given user func RemoveAllAccountLinks(user *User) error { _, err := x.Delete(&ExternalLoginUser{UserID: user.ID}) return err -} \ No newline at end of file +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 2ea2de22b927..32c1dee4e1b8 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -278,6 +278,7 @@ applications = Applications orgs = Organizations delete = Delete Account twofa = Two-Factor Authentication +account_link = External Accounts uid = Uid public_profile = Public Profile @@ -380,6 +381,13 @@ then_enter_passcode = Then enter the passcode the application gives you: passcode_invalid = That passcode is invalid. Try again. twofa_enrolled = Your account has now been enrolled in two-factor authentication. Make sure to save your scratch token (%s), as it will only be shown once! +manage_account_links = Manage account links +manage_account_links_desc = External accounts linked to this account +account_links_not_available = There are no external accounts linked to this account +remove_account_link = Remove linked account +remove_account_link_desc = Delete this account link will remove all related access for your account. Do you want to continue? +remove_account_link_success = Account link has been removed successfully! + delete_account = Delete Your Account delete_prompt = The operation will delete your account permanently, and CANNOT be undone! confirm_delete_account = Confirm Deletion diff --git a/routers/user/setting.go b/routers/user/setting.go index 0b376ec7ef6c..b7e8cf8cd983 100644 --- a/routers/user/setting.go +++ b/routers/user/setting.go @@ -37,6 +37,7 @@ const ( tplSettingsApplications base.TplName = "user/settings/applications" tplSettingsTwofa base.TplName = "user/settings/twofa" tplSettingsTwofaEnroll base.TplName = "user/settings/twofa_enroll" + tplSettingsAccountLink base.TplName = "user/settings/account_link" tplSettingsDelete base.TplName = "user/settings/delete" tplSecurity base.TplName = "user/security" ) @@ -631,6 +632,43 @@ func SettingsTwoFactorEnrollPost(ctx *context.Context, form auth.TwoFactorAuthFo ctx.Redirect(setting.AppSubURL + "/user/settings/two_factor") } +// SettingsAccountLinks render the account links settings page +func SettingsAccountLinks(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsAccountLink"] = true + + accountLinks, err := models.ListAccountLinks(ctx.User) + if err != nil { + ctx.Handle(500, "ListAccountLinks", err) + return + } + + // map the provider display name with the LoginSource + sources := make(map[*models.LoginSource]string) + for _, externalAccount := range accountLinks { + if loginSource, err := models.GetLoginSourceByID(externalAccount.LoginSourceID); err == nil { + providerTechnicalName := loginSource.OAuth2().Provider + providerDisplayName := models.OAuth2Providers[providerTechnicalName] + sources[loginSource] = providerDisplayName + } + } + ctx.Data["AccountLinks"] = sources + + ctx.HTML(200, tplSettingsAccountLink) +} + +// SettingsDeleteAccountLink delete a single account link +func SettingsDeleteAccountLink(ctx *context.Context) { + if _, err := models.RemoveAccountLink(ctx.User, ctx.QueryInt64("loginSourceID")); err != nil { + ctx.Flash.Error("RemoveAccountLink: " + err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success")) + } + + ctx.Redirect(setting.AppSubURL + "/user/settings/account_link") + SettingsAccountLinks(ctx) +} + // SettingsDelete render user suicide page and response for delete user himself func SettingsDelete(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings") diff --git a/templates/user/settings/account_link.tmpl b/templates/user/settings/account_link.tmpl new file mode 100644 index 000000000000..d3e5520b5f45 --- /dev/null +++ b/templates/user/settings/account_link.tmpl @@ -0,0 +1,46 @@ +{{template "base/head" .}} + + + +{{template "base/footer" .}} diff --git a/templates/user/settings/navbar.tmpl b/templates/user/settings/navbar.tmpl index 7449fdbf7e01..d8b69cf5d06d 100644 --- a/templates/user/settings/navbar.tmpl +++ b/templates/user/settings/navbar.tmpl @@ -22,6 +22,9 @@ {{.i18n.Tr "settings.twofa"}} + + {{.i18n.Tr "settings.account_link"}} + {{.i18n.Tr "settings.delete"}} From 19ddb1547c10af95ea783e16e0daa250481d5d48 Mon Sep 17 00:00:00 2001 From: willemvd Date: Mon, 30 Jan 2017 13:29:31 +0100 Subject: [PATCH 28/40] make clear why we don't do anything here --- routers/user/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/user/auth.go b/routers/user/auth.go index 489b90cf44ef..aea84db6980a 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -341,7 +341,7 @@ func SignInOAuth(ctx *context.Context) { if err != nil { ctx.Handle(500, "SignIn", err) } - return + // redirect is done in oauth2.Auth } // SignInOAuthCallback handles the callback from the given provider From 57dbb746bd90713f39c7e84ece92ccda37d45549 Mon Sep 17 00:00:00 2001 From: willemvd Date: Mon, 30 Jan 2017 15:00:14 +0100 Subject: [PATCH 29/40] add license header --- models/external_login_user.go | 4 ++++ modules/auth/oauth2/oauth2.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/models/external_login_user.go b/models/external_login_user.go index 0cfee54ae603..0a73ea7d9b78 100644 --- a/models/external_login_user.go +++ b/models/external_login_user.go @@ -1,3 +1,7 @@ +// Copyright 2016 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + package models import "github.com/markbates/goth" diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index 2d5575798f01..db40e802db4f 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -1,3 +1,7 @@ +// Copyright 2016 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + package oauth2 import ( From 32a4c584df0961fc10c49a20b6ff112e9739ffbf Mon Sep 17 00:00:00 2001 From: willemvd Date: Tue, 31 Jan 2017 11:01:12 +0100 Subject: [PATCH 30/40] if user is unknown we allow him to register a new account or link it to some existing account --- cmd/web.go | 4 +- models/external_login_user.go | 3 +- routers/user/auth.go | 153 +++++++++++++++++---- routers/user/setting.go | 14 +- templates/user/auth/signin_inner.tmpl | 8 +- templates/user/auth/signup_inner.tmpl | 10 +- templates/user/settings/account_link.tmpl | 8 +- vendor/github.com/markbates/goth/README.md | 42 +++--- vendor/vendor.json | 14 +- 9 files changed, 189 insertions(+), 67 deletions(-) diff --git a/cmd/web.go b/cmd/web.go index 00992f73a042..368783ede0d3 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -208,7 +208,9 @@ func runWeb(ctx *cli.Context) error { m.Get("/:provider", user.SignInOAuth) m.Get("/:provider/callback", user.SignInOAuthCallback) }) - m.Combo("/linkaccount").Get(user.LinkAccount).Post(bindIgnErr(auth.SignInForm{}), user.LinkAccountPost) + m.Get("/link_account", user.LinkAccount) + m.Post("/link_account_signin", bindIgnErr(auth.SignInForm{}), user.LinkAccountPostSignIn) + m.Post("/link_account_signup", bindIgnErr(auth.RegisterForm{}), user.LinkAccountPostRegister) m.Group("/two_factor", func() { m.Get("", user.TwoFactor) m.Post("", bindIgnErr(auth.TwoFactorAuthForm{}), user.TwoFactorPost) diff --git a/models/external_login_user.go b/models/external_login_user.go index 0a73ea7d9b78..5c204b5f311f 100644 --- a/models/external_login_user.go +++ b/models/external_login_user.go @@ -22,12 +22,13 @@ func GetExternalLogin(externalLoginUser *ExternalLoginUser) (bool, error) { func ListAccountLinks(user *User) ([]*ExternalLoginUser, error) { externalAccounts := make([]*ExternalLoginUser, 0, 5) err := x.Where("user_id=?", user.ID). - Desc("external_id"). + Desc("login_source_id"). Find(&externalAccounts) if err != nil { return nil, err } + return externalAccounts, nil } diff --git a/routers/user/auth.go b/routers/user/auth.go index aea84db6980a..3b06f018fd08 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -20,6 +20,7 @@ import ( "net/http" "code.gitea.io/gitea/modules/auth/oauth2" "github.com/markbates/goth" + "strings" ) const ( @@ -370,7 +371,7 @@ func SignInOAuthCallback(ctx *context.Context) { if u == nil { // no existing user is found, request attach or new account ctx.Session.Set("linkAccountGothUser", gothUser) - ctx.Redirect(setting.AppSubURL + "/user/linkaccount") + ctx.Redirect(setting.AppSubURL + "/user/link_account") return } @@ -421,7 +422,7 @@ func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Requ } user := &models.User{ - LoginName: gothUser.NickName, + LoginName: gothUser.UserID, LoginType: models.LoginOAuth2, LoginSource: loginSource.ID, } @@ -462,6 +463,10 @@ func LinkAccount(ctx *context.Context) { ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration ctx.Data["ShowRegistrationButton"] = false + // use this to set the right link into the signIn and signUp templates in the link_account template + ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin" + ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup" + gothUser := ctx.Session.Get("linkAccountGothUser") if gothUser == nil { ctx.Handle(500, "UserSignIn", errors.New("not in LinkAccount session")) @@ -471,50 +476,37 @@ func LinkAccount(ctx *context.Context) { ctx.Data["user_name"] = gothUser.(goth.User).NickName ctx.Data["email"] = gothUser.(goth.User).Email - oauth2Providers, err := models.GetActiveOAuth2ProviderNames() - if err != nil { - ctx.Handle(500, "UserSignIn", err) - return - } - // delete the OAuth2 provider the user used to get to this linkAccount (so he cannot try to register him self again with the same provider) - delete(oauth2Providers, gothUser.(goth.User).Provider) - ctx.Data["OAuth2Providers"] = oauth2Providers - ctx.HTML(200, tplLinkAccount) } -// LinkAccountPost handle the coupling of external account with another account -func LinkAccountPost(ctx *context.Context, form auth.SignInForm) { +// LinkAccountPostSignIn handle the coupling of external account with another account using signIn +func LinkAccountPostSignIn(ctx *context.Context, signInForm auth.SignInForm) { ctx.Data["Title"] = ctx.Tr("link_account") ctx.Data["LinkAccountMode"] = true + ctx.Data["LinkAccountModeSignIn"] = true ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration ctx.Data["ShowRegistrationButton"] = false + // use this to set the right link into the signIn and signUp templates in the link_account template + ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin" + ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup" + gothUser := ctx.Session.Get("linkAccountGothUser") if gothUser == nil { ctx.Handle(500, "UserSignIn", errors.New("not in LinkAccount session")) return } - oauth2Providers, err := models.GetActiveOAuth2ProviderNames() - if err != nil { - ctx.Handle(500, "UserSignIn", err) - return - } - // delete the OAuth2 provider the user used to get to this linkAccount (so he cannot try to register him self again with the same provider) - delete(oauth2Providers, gothUser.(goth.User).Provider) - ctx.Data["OAuth2Providers"] = oauth2Providers - if ctx.HasError() { ctx.HTML(200, tplLinkAccount) return } - u, err := models.UserSignIn(form.UserName, form.Password) + u, err := models.UserSignIn(signInForm.UserName, signInForm.Password) if err != nil { if models.IsErrUserNotExist(err) { - ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplLinkAccount, &form) + ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplLinkAccount, &signInForm) } else { ctx.Handle(500, "UserLinkAccount", err) } @@ -527,7 +519,7 @@ func LinkAccountPost(ctx *context.Context, form auth.SignInForm) { if err != nil { if models.IsErrTwoFactorNotEnrolled(err) { models.LinkAccountToUser(u, gothUser.(goth.User)) - handleSignIn(ctx, u, form.Remember) + handleSignIn(ctx, u, signInForm.Remember) } else { ctx.Handle(500, "UserLinkAccount", err) } @@ -536,12 +528,121 @@ func LinkAccountPost(ctx *context.Context, form auth.SignInForm) { // User needs to use 2FA, save data and redirect to 2FA page. ctx.Session.Set("twofaUid", u.ID) - ctx.Session.Set("twofaRemember", form.Remember) + ctx.Session.Set("twofaRemember", signInForm.Remember) ctx.Session.Set("linkAccount", true) ctx.Redirect(setting.AppSubURL + "/user/two_factor") } +// LinkAccountPostRegister handle the creation of a new account for an external account using signUp +func LinkAccountPostRegister(ctx *context.Context, cpt *captcha.Captcha, form auth.RegisterForm) { + ctx.Data["Title"] = ctx.Tr("link_account") + ctx.Data["LinkAccountMode"] = true + ctx.Data["LinkAccountModeRegister"] = true + ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha + ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration + ctx.Data["ShowRegistrationButton"] = false + + // use this to set the right link into the signIn and signUp templates in the link_account template + ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin" + ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup" + + gothUser := ctx.Session.Get("linkAccountGothUser") + if gothUser == nil { + ctx.Handle(500, "UserSignUp", errors.New("not in LinkAccount session")) + return + } + + if ctx.HasError() { + ctx.HTML(200, tplLinkAccount) + return + } + + if setting.Service.DisableRegistration { + ctx.Error(403) + return + } + + if setting.Service.EnableCaptcha && !cpt.VerifyReq(ctx.Req) { + ctx.Data["Err_Captcha"] = true + ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplLinkAccount, &form) + return + } + + if (len(strings.TrimSpace(form.Password)) > 0 || len(strings.TrimSpace(form.Retype)) > 0) && form.Password != form.Retype { + ctx.Data["Err_Password"] = true + ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplLinkAccount, &form) + return + } + if len(strings.TrimSpace(form.Password)) > 0 && len(form.Password) < setting.MinPasswordLength { + ctx.Data["Err_Password"] = true + ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplLinkAccount, &form) + return + } + + loginSource, err := models.GetActiveOAuth2LoginSourceByName(gothUser.(goth.User).Provider) + if err != nil { + ctx.Handle(500, "CreateUser", err) + } + + u := &models.User{ + Name: form.UserName, + Email: form.Email, + Passwd: form.Password, + IsActive: !setting.Service.RegisterEmailConfirm, + LoginType: models.LoginOAuth2, + LoginSource: loginSource.ID, + LoginName: gothUser.(goth.User).UserID, + } + + if err := models.CreateUser(u); err != nil { + switch { + case models.IsErrUserAlreadyExist(err): + ctx.Data["Err_UserName"] = true + ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplLinkAccount, &form) + case models.IsErrEmailAlreadyUsed(err): + ctx.Data["Err_Email"] = true + ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplLinkAccount, &form) + case models.IsErrNameReserved(err): + ctx.Data["Err_UserName"] = true + ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(models.ErrNameReserved).Name), tplLinkAccount, &form) + case models.IsErrNamePatternNotAllowed(err): + ctx.Data["Err_UserName"] = true + ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplLinkAccount, &form) + default: + ctx.Handle(500, "CreateUser", err) + } + return + } + log.Trace("Account created: %s", u.Name) + + // Auto-set admin for the only user. + if models.CountUsers() == 1 { + u.IsAdmin = true + u.IsActive = true + if err := models.UpdateUser(u); err != nil { + ctx.Handle(500, "UpdateUser", err) + return + } + } + + // Send confirmation email + if setting.Service.RegisterEmailConfirm && u.ID > 1 { + models.SendActivateAccountMail(ctx.Context, u) + ctx.Data["IsSendRegisterMail"] = true + ctx.Data["Email"] = u.Email + ctx.Data["Hours"] = setting.Service.ActiveCodeLives / 60 + ctx.HTML(200, TplActivate) + + if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil { + log.Error(4, "Set cache(MailResendLimit) fail: %v", err) + } + return + } + + ctx.Redirect(setting.AppSubURL + "/user/login") +} + // SignOut sign out from login status func SignOut(ctx *context.Context) { ctx.Session.Delete("uid") diff --git a/routers/user/setting.go b/routers/user/setting.go index b7e8cf8cd983..3de38041d5f9 100644 --- a/routers/user/setting.go +++ b/routers/user/setting.go @@ -647,8 +647,13 @@ func SettingsAccountLinks(ctx *context.Context) { sources := make(map[*models.LoginSource]string) for _, externalAccount := range accountLinks { if loginSource, err := models.GetLoginSourceByID(externalAccount.LoginSourceID); err == nil { - providerTechnicalName := loginSource.OAuth2().Provider - providerDisplayName := models.OAuth2Providers[providerTechnicalName] + var providerDisplayName string + if loginSource.IsOAuth2() { + providerTechnicalName := loginSource.OAuth2().Provider + providerDisplayName = models.OAuth2Providers[providerTechnicalName] + } else { + providerDisplayName = loginSource.Name + } sources[loginSource] = providerDisplayName } } @@ -665,8 +670,9 @@ func SettingsDeleteAccountLink(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success")) } - ctx.Redirect(setting.AppSubURL + "/user/settings/account_link") - SettingsAccountLinks(ctx) + ctx.JSON(200, map[string]interface{}{ + "redirect": setting.AppSubURL + "/user/settings/account_link", + }) } // SettingsDelete render user suicide page and response for delete user himself diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index e5a21f7cb45b..276c8e826a36 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -1,18 +1,20 @@
-
+ {{.CsrfTokenHtml}}

{{.i18n.Tr "sign_in"}}

+ {{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn)}} {{template "base/alert" .}} -
+ {{end}} +
-
+
diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl index 52196ff7977c..96dbede46cf4 100644 --- a/templates/user/auth/signup_inner.tmpl +++ b/templates/user/auth/signup_inner.tmpl @@ -1,17 +1,19 @@
- + {{.CsrfTokenHtml}}

{{.i18n.Tr "sign_up"}}

+ {{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister)}} {{template "base/alert" .}} + {{end}} {{if .DisableRegistration}}

{{.i18n.Tr "auth.disable_register_prompt"}}

{{else}} -
+
@@ -19,11 +21,11 @@
-
+
-
+
diff --git a/templates/user/settings/account_link.tmpl b/templates/user/settings/account_link.tmpl index d3e5520b5f45..fa14cb525be3 100644 --- a/templates/user/settings/account_link.tmpl +++ b/templates/user/settings/account_link.tmpl @@ -9,10 +9,11 @@ {{.i18n.Tr "settings.manage_account_links"}}
-
@@ -36,10 +38,10 @@ diff --git a/vendor/github.com/markbates/goth/README.md b/vendor/github.com/markbates/goth/README.md index d2b7b351fcab..11bff7bb676b 100644 --- a/vendor/github.com/markbates/goth/README.md +++ b/vendor/github.com/markbates/goth/README.md @@ -77,38 +77,44 @@ Would I love to see more providers? Certainly! Would you love to contribute one? * Mark Bates * Tyler Bunnell * Corey McGrillis +* willemvd * Rakesh Goyal * Andy Grunwald -* Kevin Fitzpatrick -* Sharad Ganapathy * Glenn Walker +* Kevin Fitzpatrick * Ben Tranter +* Sharad Ganapathy +* Andrew Chilton * sharadgana -* Geoff Franks +* Aurorae * Craig P Jolicoeur * Zac Bergquist -* Aurorae -* Rafael Quintela +* Geoff Franks +* Raphael Geronimi +* Noah Shibley +* lumost * oov +* Felix Lamouroux +* Rafael Quintela * Tyler -* Noah Shibley * DenSm -* Jacob Walker * Samy KACIMI -* Roy * dante gray -* Raphael Geronimi * Noah -* Jerome Touffe-Blin -* Johnny Boursiquot +* Jacob Walker +* Marin Martinic +* Roy * Omni Adams -* Masanobu YOSHIOKA -* Jonathan Hall -* HaiMing.Yin -* Albin Gilles +* Sasa Brankovic +* dkhamsing * Dante Swift -* Felix Lamouroux +* Attila Domokos +* Albin Gilles * Syed Zubairuddin -* dkhamsing -* Sasa Brankovic +* Johnny Boursiquot +* Jerome Touffe-Blin * bryanl +* Masanobu YOSHIOKA +* Jonathan Hall +* HaiMing.Yin +* Sairam Kunala diff --git a/vendor/vendor.json b/vendor/vendor.json index 256222264b24..f801a53bf832 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -557,22 +557,22 @@ "revisionTime": "2016-11-03T02:43:54Z" }, { - "checksumSHA1": "KJkDtEZHGmFuEtUHrX2/bm2K74o=", + "checksumSHA1": "76X/yW8bCWehh+9v/IgYqCc+x3E=", "path": "github.com/markbates/goth", - "revision": "3b285de1061587f2a5ebb2b456da8d65a9cecee9", - "revisionTime": "2017-01-26T14:15:07Z" + "revision": "db75f461a1f003bf2eee09916c657a1d641e0db3", + "revisionTime": "2017-01-27T16:37:43Z" }, { "checksumSHA1": "zcRIxzOSWwSM4fzvDXey20d1Gl4=", "path": "github.com/markbates/goth/gothic", - "revision": "3b285de1061587f2a5ebb2b456da8d65a9cecee9", - "revisionTime": "2017-01-26T14:15:07Z" + "revision": "db75f461a1f003bf2eee09916c657a1d641e0db3", + "revisionTime": "2017-01-27T16:37:43Z" }, { "checksumSHA1": "cR1jkLb/XPj+ffipSHlT4qnQ9g4=", "path": "github.com/markbates/goth/providers/github", - "revision": "3b285de1061587f2a5ebb2b456da8d65a9cecee9", - "revisionTime": "2017-01-26T14:15:07Z" + "revision": "db75f461a1f003bf2eee09916c657a1d641e0db3", + "revisionTime": "2017-01-27T16:37:43Z" }, { "checksumSHA1": "9FJUwn3EIgASVki+p8IHgWVC5vQ=", From 770ba31c5dd5dd90690617613114c63a2f0c45af Mon Sep 17 00:00:00 2001 From: willemvd Date: Tue, 31 Jan 2017 12:49:56 +0100 Subject: [PATCH 31/40] we are in 2017... --- models/external_login_user.go | 2 +- modules/auth/oauth2/oauth2.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/models/external_login_user.go b/models/external_login_user.go index 5c204b5f311f..ade1b8a13bab 100644 --- a/models/external_login_user.go +++ b/models/external_login_user.go @@ -1,4 +1,4 @@ -// Copyright 2016 The Gitea Authors. All rights reserved. +// Copyright 2017 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index db40e802db4f..8fb581a47fd5 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -1,4 +1,4 @@ -// Copyright 2016 The Gitea Authors. All rights reserved. +// Copyright 2017 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. From 527d6e11358ff3c5a62d34a454172fe498bae80e Mon Sep 17 00:00:00 2001 From: willemvd Date: Thu, 2 Feb 2017 14:36:20 +0100 Subject: [PATCH 32/40] sign up with button on signin page (als change OAuth2Provider structure so we can store basic stuff about providers) --- models/login_source.go | 23 +++++++++++++++-------- options/locale/locale_en-US.ini | 1 + public/css/index.css | 17 +++++++++++++++++ public/img/github.png | Bin 0 -> 1151 bytes routers/user/auth.go | 4 ++-- routers/user/setting.go | 2 +- templates/admin/auth/edit.tmpl | 4 ++-- templates/admin/auth/new.tmpl | 2 +- templates/user/auth/signin_inner.tmpl | 12 +++++++----- 9 files changed, 46 insertions(+), 19 deletions(-) create mode 100644 public/img/github.png diff --git a/models/login_source.go b/models/login_source.go index 89d1176e25e9..66f9fe5c6924 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -587,11 +587,18 @@ func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMCon // \_______ /\____|__ /____/ |__| |___| /\_______ \ // \/ \/ \/ \/ +// OAuth2Provider describes the display values of a single OAuth2 provider +type OAuth2Provider struct { + Name string + DisplayName string + Image string +} + // OAuth2Providers contains the map of registered OAuth2 providers in Gitea (based on goth) -// key is used as technical name (for use in the callbackURL) -// value is used to display -var OAuth2Providers = map[string]string{ - "github": "GitHub", +// key is used to map the OAuth2Provider with the goth provider type (also in LoginSource.OAuth2Config.Provider) +// value is used to store display data +var OAuth2Providers = map[string]OAuth2Provider{ + "github": {Name: "github", DisplayName:"GitHub", Image: "/img/github.png"}, } // ExternalUserLogin attempts a login using external source types. @@ -694,10 +701,10 @@ func GetActiveOAuth2LoginSourceByName(name string) (*LoginSource, error) { return loginSource, nil } -// GetActiveOAuth2ProviderNames returns the map of configured active OAuth2 providers +// GetActiveOAuth2Providers returns the map of configured active OAuth2 providers // key is used as technical name (like in the callbackURL) -// value is used to display -func GetActiveOAuth2ProviderNames() (map[string]string, error) { +// values to display +func GetActiveOAuth2Providers() (map[string]OAuth2Provider, error) { // Maybe also seperate used and unused providers so we can force the registration of only 1 active provider for each type loginSources, err := GetActiveOAuth2ProviderLoginSources() @@ -705,7 +712,7 @@ func GetActiveOAuth2ProviderNames() (map[string]string, error) { return nil, err } - providers := make(map[string]string) + providers := make(map[string]OAuth2Provider) for _, source := range loginSources { providers[source.Name] = OAuth2Providers[source.OAuth2().Provider] } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 32c1dee4e1b8..7385b92e8d91 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -5,6 +5,7 @@ dashboard = Dashboard explore = Explore help = Help sign_in = Sign In +sign_in_with = Sign in with sign_out = Sign Out sign_up = Sign Up link_account = Link Account diff --git a/public/css/index.css b/public/css/index.css index 0b01a1e8435a..c9015bcc3698 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -2982,4 +2982,21 @@ footer .ui.language .menu { .user.link-account:not(.icon) { padding-top: 15px; padding-bottom: 5px; +} +.signin .oauth2 div { + display: inline-block; +} +.signin .oauth2 div p { + margin: 10px 5px 0 0; + float: left; +} +.signin .oauth2 a { + margin-right: 5px; +} +.signin .oauth2 a:last-child { + margin-right: 0px; +} +.signin .oauth2 img { + width: 32px; + height: 32px; } \ No newline at end of file diff --git a/public/img/github.png b/public/img/github.png new file mode 100644 index 0000000000000000000000000000000000000000..1fa19c55d2f71505edf0f4d70840e817e7861c06 GIT binary patch literal 1151 zcmV-_1c3XAP)aRaZbWef~;-qbQN~T3m5@KGy*SRcU1Y@_%gT?qoIV1 z#+_K&r^Z*p_=X zkV~rhX7~W|+y`hDck^w~4$6pjXcK4iU7?KWf;Lf`Z;tj*Mzu%tC~tV5KpFQ5a$y3v z3i_Y8%G&()sD>)o4V&Xsyo>oz!sg>$oQln{8&p9xws&p52?j$IbSrY-#c&;_K{1<- z>#$gnal1hk3`P@|0B(UCJXEH}Vt4|z5QNG24X@!|T!~8nPIDPv!*7@j0o0%t7ArH} z!;phpDmA|b7C;V8bNtGD5>^fI9<(Y>!j=xVJq>cO0Be*h^gQHfKaXal`$3MLFI8j} zEP@;z>`?%RK#mq+m10F6feg;@D1b8{gGY+Z?I%J8&v+ETGmybVG|De<5Mhug4BpHyFaa{S*~3NIn;?S;|J-qX$Y3m*dKf@cjDZZ+4>&jn zGPuFxrTH5mgL4879)%3n^f-XEAcIE(4!(gLjSf8Dj4y+40uBa2j(*HQ06#*G1_c~U zh8(?>fdJly98C$FXMezJ83^Dt$kBqpy1tfy06v9W83M@6d%#cD0OmoChGZasA&{ea zfpHdCjb`NuhCv2* zgk20d&cgtX3$-Z1!7`B1P&D_b`Q{h~87&iKN$Dkz0(dFXlAis20KiQiG=B>e!X5cV z)`LQN0Zr@aGyerBr1c65c7Q?}goBYw>5p26V+a&dhhl|Kfr|JUJL6EiP+|7i3zuTG zgjRd5LO&>?Q;H4H5Tl?1qp?BW?bdPJiQ#w+SK$yeF*Mr>N8={EgE3G{MxkM;fzO4C zn2VG1ZnrcBLylfY&iFKc1xmoVkv=*)|djts1F)e8hB5r zgarjJhg+RiUQL_*j}HcWx(@t2RKhm}Pt|*%G7gLo!2VE7p0^C#vckvACIz?O7H?uO zW?(2j!YODHA%HcY7z{y6>);z<4phYZDSg>l42ozDHVV4~N0fP^J3Ipc)Z&P!J8=$F z#5^47c>w3cwIi26C47kOkn=2nOXA*{lPfGZPR6hJ3}2uFW?-?D1E|KyNj)WZ#YD&; z%>X81*QDQ=R>ZF!2JkCZOlvNl!-bgRK>%}bF>>{>8nXgkK^;`WauMn;4V6%bSFwT* zYjPXlbu5AkEFGclQc!_vyp9cg{9$4RT!#S=-ie{+I}iroI{ZE=#`q{01pu+n%tOqP Rd2;{&002ovPDHLkV1i^k^r8R& literal 0 HcmV?d00001 diff --git a/routers/user/auth.go b/routers/user/auth.go index 3b06f018fd08..12f125c9f537 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -114,7 +114,7 @@ func SignIn(ctx *context.Context) { return } - oauth2Providers, err := models.GetActiveOAuth2ProviderNames() + oauth2Providers, err := models.GetActiveOAuth2Providers() if err != nil { ctx.Handle(500, "UserSignIn", err) return @@ -128,7 +128,7 @@ func SignIn(ctx *context.Context) { func SignInPost(ctx *context.Context, form auth.SignInForm) { ctx.Data["Title"] = ctx.Tr("sign_in") - oauth2Providers, err := models.GetActiveOAuth2ProviderNames() + oauth2Providers, err := models.GetActiveOAuth2Providers() if err != nil { ctx.Handle(500, "UserSignIn", err) return diff --git a/routers/user/setting.go b/routers/user/setting.go index 3de38041d5f9..de5a1baca421 100644 --- a/routers/user/setting.go +++ b/routers/user/setting.go @@ -650,7 +650,7 @@ func SettingsAccountLinks(ctx *context.Context) { var providerDisplayName string if loginSource.IsOAuth2() { providerTechnicalName := loginSource.OAuth2().Provider - providerDisplayName = models.OAuth2Providers[providerTechnicalName] + providerDisplayName = models.OAuth2Providers[providerTechnicalName].DisplayName } else { providerDisplayName = loginSource.Name } diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index 621d5b6d1310..84b62f6e87b4 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -149,11 +149,11 @@ diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl index a2e812580b4e..24257a1b65ad 100644 --- a/templates/admin/auth/new.tmpl +++ b/templates/admin/auth/new.tmpl @@ -143,7 +143,7 @@
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index 276c8e826a36..34fcea08fcbe 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -42,11 +42,13 @@ {{end}} {{if .OAuth2Providers}} - {{range $key, $value := .OAuth2Providers}} - - {{end}} +
+
+
+

{{.i18n.Tr "sign_in_with"}}

{{range $key, $value := .OAuth2Providers}}{{$value.DisplayName}}{{end}} +
+
+
{{end}}
From a7381b5ce10e2b478cac11cc4be8842c96c96abb Mon Sep 17 00:00:00 2001 From: willemvd Date: Thu, 2 Feb 2017 15:48:38 +0100 Subject: [PATCH 33/40] prevent panic when non-existing provider is in database or cannot be created --- modules/auth/oauth2/oauth2.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index 8fb581a47fd5..bf486d5a0bb8 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -75,7 +75,9 @@ func ProviderCallback(provider string, request *http.Request, response http.Resp func RegisterProvider(providerName, providerType, clientID, clientSecret string) { provider := createProvider(providerName, providerType, clientID, clientSecret) - goth.UseProviders(provider) + if provider != nil { + goth.UseProviders(provider) + } } // RemoveProvider removes the given OAuth2 provider from the goth lib @@ -94,8 +96,10 @@ func createProvider(providerName, providerType, clientID, clientSecret string) g provider = github.New(clientID, clientSecret, callbackURL, "user:email") } - // always set the name so we can support multiple setups of 1 provider - provider.SetName(providerName) + // always set the name if provider is created so we can support multiple setups of 1 provider + if provider != nil { + provider.SetName(providerName) + } return provider } From b64ee7d2cdcc9a9e5aeaf12c7e4884869404d82d Mon Sep 17 00:00:00 2001 From: willemvd Date: Thu, 2 Feb 2017 15:49:01 +0100 Subject: [PATCH 34/40] fix err check so update of source is working --- models/login_source.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/login_source.go b/models/login_source.go index 66f9fe5c6924..8d5d08dea6aa 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -322,7 +322,7 @@ func GetLoginSourceByID(id int64) (*LoginSource, error) { // UpdateSource updates a LoginSource record in DB. func UpdateSource(source *LoginSource) error { _, err := x.Id(source.ID).AllCols().Update(source) - if err != nil && source.IsOAuth2() { + if err == nil && source.IsOAuth2() { oAuth2Config := source.OAuth2() oauth2.RemoveProvider(source.Name) oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret) From 769c7475493612b7e93af934ff055254b92c27b0 Mon Sep 17 00:00:00 2001 From: willemvd Date: Sun, 5 Feb 2017 17:57:19 +0100 Subject: [PATCH 35/40] merge master forces update of database to newer version --- models/migrations/migrations.go | 2 +- models/migrations/v17.go | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 models/migrations/v17.go diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 95a5cc065564..4c80b0a3cffe 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -82,7 +82,7 @@ var migrations = []Migration{ NewMigration("create user column allow create organization", createAllowCreateOrganizationColumn), // V16 -> v17 NewMigration("create repo unit table and add units for all repos", addUnitsToTables), - // v16 + // v17 -> v18 NewMigration("add external login user", addExternalLoginUser), } diff --git a/models/migrations/v17.go b/models/migrations/v17.go new file mode 100644 index 000000000000..be5161534194 --- /dev/null +++ b/models/migrations/v17.go @@ -0,0 +1,25 @@ +// Copyright 2016 Gitea. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "fmt" + + "github.com/go-xorm/xorm" +) + +// ExternalLoginUser makes the connecting between some existing user and additional external login sources +type ExternalLoginUser struct { + ExternalID string `xorm:"NOT NULL"` + UserID int64 `xorm:"NOT NULL"` + LoginSourceID int64 `xorm:"NOT NULL"` +} + +func addExternalLoginUser(x *xorm.Engine) error { + if err := x.Sync2(new(ExternalLoginUser)); err != nil { + return fmt.Errorf("Sync2: %v", err) + } + return nil +} From 6b16f42e83817f7e89ccc876fb4180d6ed0bbbfa Mon Sep 17 00:00:00 2001 From: willemvd Date: Mon, 6 Feb 2017 08:55:01 +0100 Subject: [PATCH 36/40] from gorilla/sessions docs: "Important Note: If you aren't using gorilla/mux, you need to wrap your handlers with context.ClearHandler as or else you will leak memory!" (we're using gorilla/sessions for storing oauth2 sessions) --- cmd/web.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd/web.go b/cmd/web.go index 1d1914f4d070..51ea29b048d4 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -41,6 +41,7 @@ import ( "github.com/go-macaron/toolbox" "github.com/urfave/cli" macaron "gopkg.in/macaron.v1" + context2 "github.com/gorilla/context" ) // CmdWeb represents the available web sub-command. @@ -663,11 +664,11 @@ func runWeb(ctx *cli.Context) error { var err error switch setting.Protocol { case setting.HTTP: - err = runHTTP(listenAddr, m) + err = runHTTP(listenAddr, context2.ClearHandler(m)) case setting.HTTPS: - err = runHTTPS(listenAddr, setting.CertFile, setting.KeyFile, m) + err = runHTTPS(listenAddr, setting.CertFile, setting.KeyFile, context2.ClearHandler(m)) case setting.FCGI: - err = fcgi.Serve(nil, m) + err = fcgi.Serve(nil, context2.ClearHandler(m)) case setting.UnixSocket: if err := os.Remove(listenAddr); err != nil && !os.IsNotExist(err) { log.Fatal(4, "Failed to remove unix socket directory %s: %v", listenAddr, err) @@ -683,7 +684,7 @@ func runWeb(ctx *cli.Context) error { if err = os.Chmod(listenAddr, os.FileMode(setting.UnixSocketPermission)); err != nil { log.Fatal(4, "Failed to set permission of unix socket: %v", err) } - err = http.Serve(listener, m) + err = http.Serve(listener, context2.ClearHandler(m)) default: log.Fatal(4, "Invalid protocol: %s", setting.Protocol) } From 0fa2e40736397cabfde05a84c4e5d1f84cee506c Mon Sep 17 00:00:00 2001 From: willemvd Date: Wed, 8 Feb 2017 08:47:15 +0100 Subject: [PATCH 37/40] fix missed password reset --- routers/user/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/user/auth.go b/routers/user/auth.go index 12f125c9f537..f156891cf493 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -870,7 +870,7 @@ func ForgotPasswdPost(ctx *context.Context) { return } - if !u.IsLocal() { + if !u.IsLocal() && !u.IsOAuth2() { ctx.Data["Err_Email"] = true ctx.RenderWithErr(ctx.Tr("auth.non_local_account"), tplForgotPassword, nil) return From f54f07a2ee187e1f6a1ab7a34cb8bd3137f8a751 Mon Sep 17 00:00:00 2001 From: willemvd Date: Wed, 8 Feb 2017 09:02:09 +0100 Subject: [PATCH 38/40] use updated goth lib that now supports getting the OAuth2 user if the AccessToken is still valid instead of re-authenticating (prevent flooding the OAuth2 provider) --- routers/user/auth.go | 12 +++++ vendor/github.com/markbates/goth/README.md | 23 ++++++++++ .../markbates/goth/gothic/gothic.go | 45 +++++++++++++++---- .../markbates/goth/providers/github/github.go | 5 +++ vendor/vendor.json | 18 ++++---- 5 files changed, 86 insertions(+), 17 deletions(-) diff --git a/routers/user/auth.go b/routers/user/auth.go index f156891cf493..5b9297d3498a 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -338,6 +338,14 @@ func SignInOAuth(ctx *context.Context) { return } + // try to do a direct callback flow, so we don't authenticate the user again but use the valid accesstoken to get the user + user, gothUser, err := oAuth2UserLoginCallback(loginSource, ctx.Req.Request, ctx.Resp) + if err == nil && user != nil { + // we got the user without going through the whole OAuth2 authentication flow again + handleOAuth2SignIn(user, gothUser, ctx, err) + return + } + err = oauth2.Auth(loginSource.Name, ctx.Req.Request, ctx.Resp) if err != nil { ctx.Handle(500, "SignIn", err) @@ -363,6 +371,10 @@ func SignInOAuthCallback(ctx *context.Context) { u, gothUser, err := oAuth2UserLoginCallback(loginSource, ctx.Req.Request, ctx.Resp) + handleOAuth2SignIn(u, gothUser, ctx, err) +} + +func handleOAuth2SignIn(u *models.User, gothUser goth.User, ctx *context.Context, err error) { if err != nil { ctx.Handle(500, "UserSignIn", err) return diff --git a/vendor/github.com/markbates/goth/README.md b/vendor/github.com/markbates/goth/README.md index 11bff7bb676b..b481683bcc2b 100644 --- a/vendor/github.com/markbates/goth/README.md +++ b/vendor/github.com/markbates/goth/README.md @@ -17,12 +17,14 @@ $ go get github.com/markbates/goth ## Supported Providers * Amazon +* Auth0 * Bitbucket * Box * Cloud Foundry * Dailymotion * Deezer * Digital Ocean +* Discord * Dropbox * Facebook * Fitbit @@ -35,6 +37,7 @@ $ go get github.com/markbates/goth * Intercom * Lastfm * Linkedin +* Meetup * OneDrive * OpenID Connect (auto discovery) * Paypal @@ -56,6 +59,26 @@ $ go get github.com/markbates/goth See the [examples](examples) folder for a working application that lets users authenticate through Twitter, Facebook, Google Plus etc. +To run the example either clone the source from GitHub + +```text +$ git clone git@github.com:markbates/goth.git +``` +or use +```text +$ go get github.com/markbates/goth +``` +```text +$ cd goth/examples +$ go get -v +$ go build +$ ./examples +``` + +Now open up your browser and go to [http://localhost:3000](http://localhost:3000) to see the example. + +To actually use the different providers, please make sure you configure them given the system environments as defined in the examples/main.go file + ## Issues Issues always stand a significantly better chance of getting fixed if the are accompanied by a diff --git a/vendor/github.com/markbates/goth/gothic/gothic.go b/vendor/github.com/markbates/goth/gothic/gothic.go index 1ac31f3af02d..f6aaf2d1170d 100644 --- a/vendor/github.com/markbates/goth/gothic/gothic.go +++ b/vendor/github.com/markbates/goth/gothic/gothic.go @@ -111,9 +111,8 @@ func GetAuthURL(res http.ResponseWriter, req *http.Request) (string, error) { return "", err } - session, _ := Store.Get(req, SessionName) - session.Values[SessionName] = sess.Marshal() - err = session.Save(req, res) + err = storeInSession(providerName, sess.Marshal(), req, res) + if err != nil { return "", err } @@ -146,18 +145,29 @@ var CompleteUserAuth = func(res http.ResponseWriter, req *http.Request) (goth.Us return goth.User{}, err } - session, _ := Store.Get(req, SessionName) - - if session.Values[SessionName] == nil { - return goth.User{}, errors.New("could not find a matching session for this request") + value, err := getFromSession(providerName, req) + if err != nil { + return goth.User{}, err } - sess, err := provider.UnmarshalSession(session.Values[SessionName].(string)) + sess, err := provider.UnmarshalSession(value) if err != nil { return goth.User{}, err } + user, err := provider.FetchUser(sess) + if err == nil { + // user can be found with existing session data + return user, err + } + + // get new token and retry fetch _, err = sess.Authorize(provider, req.URL.Query()) + if err != nil { + return goth.User{}, err + } + + err = storeInSession(providerName, sess.Marshal(), req, res) if err != nil { return goth.User{}, err @@ -188,3 +198,22 @@ func getProviderName(req *http.Request) (string, error) { } return provider, nil } + +func storeInSession(key string, value string, req *http.Request, res http.ResponseWriter) error { + session, _ := Store.Get(req, key + SessionName) + + session.Values[key] = value + + return session.Save(req, res) +} + +func getFromSession(key string, req *http.Request) (string, error) { + session, _ := Store.Get(req, key + SessionName) + + value := session.Values[key] + if value == nil { + return "", errors.New("could not find a matching session for this request") + } + + return value.(string), nil +} \ No newline at end of file diff --git a/vendor/github.com/markbates/goth/providers/github/github.go b/vendor/github.com/markbates/goth/providers/github/github.go index 4791900f4e50..866150e63a88 100644 --- a/vendor/github.com/markbates/goth/providers/github/github.go +++ b/vendor/github.com/markbates/goth/providers/github/github.go @@ -91,6 +91,11 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { Provider: p.Name(), } + if user.AccessToken == "" { + // data is not yet retrieved since accessToken is still empty + return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) + } + response, err := p.Client().Get(ProfileURL + "?access_token=" + url.QueryEscape(sess.AccessToken)) if err != nil { return user, err diff --git a/vendor/vendor.json b/vendor/vendor.json index a6278998b9cb..a4c1f2b04657 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -551,22 +551,22 @@ "revisionTime": "2016-11-03T02:43:54Z" }, { - "checksumSHA1": "76X/yW8bCWehh+9v/IgYqCc+x3E=", + "checksumSHA1": "O3KUfEXQPfdQ+tCMpP2RAIRJJqY=", "path": "github.com/markbates/goth", - "revision": "db75f461a1f003bf2eee09916c657a1d641e0db3", - "revisionTime": "2017-01-27T16:37:43Z" + "revision": "450379d2950a65070b23cc93c53436553add4484", + "revisionTime": "2017-02-06T19:46:32Z" }, { - "checksumSHA1": "zcRIxzOSWwSM4fzvDXey20d1Gl4=", + "checksumSHA1": "MkFKwLV3icyUo4oP0BgEs+7+R1Y=", "path": "github.com/markbates/goth/gothic", - "revision": "db75f461a1f003bf2eee09916c657a1d641e0db3", - "revisionTime": "2017-01-27T16:37:43Z" + "revision": "450379d2950a65070b23cc93c53436553add4484", + "revisionTime": "2017-02-06T19:46:32Z" }, { - "checksumSHA1": "cR1jkLb/XPj+ffipSHlT4qnQ9g4=", + "checksumSHA1": "ZFqznX3/ZW65I4QeepiHQdE69nA=", "path": "github.com/markbates/goth/providers/github", - "revision": "db75f461a1f003bf2eee09916c657a1d641e0db3", - "revisionTime": "2017-01-27T16:37:43Z" + "revision": "450379d2950a65070b23cc93c53436553add4484", + "revisionTime": "2017-02-06T19:46:32Z" }, { "checksumSHA1": "9FJUwn3EIgASVki+p8IHgWVC5vQ=", From 779e84b14d4012bd40fbae9a6a41c280a3e9f495 Mon Sep 17 00:00:00 2001 From: willemvd Date: Wed, 8 Feb 2017 09:09:15 +0100 Subject: [PATCH 39/40] manual merge the changes in signup.tmpl to signup_inner.tmpl --- templates/user/auth/signup_inner.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl index 96dbede46cf4..869f3d5342cc 100644 --- a/templates/user/auth/signup_inner.tmpl +++ b/templates/user/auth/signup_inner.tmpl @@ -48,7 +48,7 @@ {{if not .LinkAccountMode}} {{end}} {{end}} From 61fe261c97abf8b7b2bc766f9e46b0bdbda33a7c Mon Sep 17 00:00:00 2001 From: willemvd Date: Tue, 21 Feb 2017 23:28:00 +0100 Subject: [PATCH 40/40] prepare merge --- models/migrations/{v17.go => v18.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename models/migrations/{v17.go => v18.go} (100%) diff --git a/models/migrations/v17.go b/models/migrations/v18.go similarity index 100% rename from models/migrations/v17.go rename to models/migrations/v18.go