Browse Source

Oauth2 consumer (#679)

* initial stuff for oauth2 login, fails on:
* login button on the signIn page to start the OAuth2 flow and a callback for each provider
Only GitHub is implemented for now
* show login button only when the OAuth2 consumer is configured (and activated)
* create macaron group for oauth2 urls
* prevent net/http in modules (other then oauth2)
* use a new data sessions oauth2 folder for storing the oauth2 session data
* add missing 2FA when this is enabled on the user
* add password option for OAuth2 user , for use with git over http and login to the GUI
* add tip for registering a GitHub OAuth application
* at startup of Gitea register all configured providers and also on adding/deleting of new providers
* custom handling of errors in oauth2 request init + show better tip
* add ExternalLoginUser model and migration script to add it to database
* link a external account to an existing account (still need to handle wrong login and signup) and remove if user is removed
* remove the linked external account from the user his settings
* if user is unknown we allow him to register a new account or link it to some existing account
* sign up with button on signin page (als change OAuth2Provider structure so we can store basic stuff about providers)

* 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)

* 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)
Willem van Dreumel 2 years ago
parent
commit
01d957677f
76 changed files with 7275 additions and 137 deletions
  1. 13 4
      cmd/web.go
  2. 40 0
      models/error.go
  3. 74 0
      models/external_login_user.go
  4. 147 13
      models/login_source.go
  5. 2 0
      models/migrations/migrations.go
  6. 25 0
      models/migrations/v18.go
  7. 21 0
      models/user.go
  8. 2 2
      modules/auth/auth.go
  9. 4 1
      modules/auth/auth_form.go
  10. 105 0
      modules/auth/oauth2/oauth2.go
  11. 1 1
      modules/auth/user_form.go
  12. 15 0
      options/locale/locale_en-US.ini
  13. 21 0
      public/css/index.css
  14. BIN
      public/img/github.png
  15. 6 2
      public/js/index.js
  16. 28 1
      routers/admin/auths.go
  17. 1 0
      routers/init.go
  18. 6 6
      routers/repo/http.go
  19. 350 8
      routers/user/auth.go
  20. 45 1
      routers/user/setting.go
  21. 26 0
      templates/admin/auth/edit.tmpl
  22. 27 0
      templates/admin/auth/new.tmpl
  23. 1 1
      templates/admin/user/edit.tmpl
  24. 13 0
      templates/user/auth/link_account.tmpl
  25. 1 42
      templates/user/auth/signin.tmpl
  26. 57 0
      templates/user/auth/signin_inner.tmpl
  27. 1 54
      templates/user/auth/signup.tmpl
  28. 59 0
      templates/user/auth/signup_inner.tmpl
  29. 48 0
      templates/user/settings/account_link.tmpl
  30. 3 0
      templates/user/settings/navbar.tmpl
  31. 3 1
      templates/user/settings/password.tmpl
  32. 27 0
      vendor/github.com/gorilla/context/LICENSE
  33. 10 0
      vendor/github.com/gorilla/context/README.md
  34. 143 0
      vendor/github.com/gorilla/context/context.go
  35. 88 0
      vendor/github.com/gorilla/context/doc.go
  36. 27 0
      vendor/github.com/gorilla/mux/LICENSE
  37. 299 0
      vendor/github.com/gorilla/mux/README.md
  38. 26 0
      vendor/github.com/gorilla/mux/context_gorilla.go
  39. 24 0
      vendor/github.com/gorilla/mux/context_native.go
  40. 235 0
      vendor/github.com/gorilla/mux/doc.go
  41. 542 0
      vendor/github.com/gorilla/mux/mux.go
  42. 316 0
      vendor/github.com/gorilla/mux/regexp.go
  43. 636 0
      vendor/github.com/gorilla/mux/route.go
  44. 27 0
      vendor/github.com/gorilla/securecookie/LICENSE
  45. 78 0
      vendor/github.com/gorilla/securecookie/README.md
  46. 61 0
      vendor/github.com/gorilla/securecookie/doc.go
  47. 25 0
      vendor/github.com/gorilla/securecookie/fuzz.go
  48. 646 0
      vendor/github.com/gorilla/securecookie/securecookie.go
  49. 27 0
      vendor/github.com/gorilla/sessions/LICENSE
  50. 81 0
      vendor/github.com/gorilla/sessions/README.md
  51. 199 0
      vendor/github.com/gorilla/sessions/doc.go
  52. 102 0
      vendor/github.com/gorilla/sessions/lex.go
  53. 241 0
      vendor/github.com/gorilla/sessions/sessions.go
  54. 295 0
      vendor/github.com/gorilla/sessions/store.go
  55. 22 0
      vendor/github.com/markbates/goth/LICENSE.txt
  56. 143 0
      vendor/github.com/markbates/goth/README.md
  57. 10 0
      vendor/github.com/markbates/goth/doc.go
  58. 219 0
      vendor/github.com/markbates/goth/gothic/gothic.go
  59. 75 0
      vendor/github.com/markbates/goth/provider.go
  60. 224 0
      vendor/github.com/markbates/goth/providers/github/github.go
  61. 56 0
      vendor/github.com/markbates/goth/providers/github/session.go
  62. 21 0
      vendor/github.com/markbates/goth/session.go
  63. 30 0
      vendor/github.com/markbates/goth/user.go
  64. 3 0
      vendor/golang.org/x/oauth2/AUTHORS
  65. 31 0
      vendor/golang.org/x/oauth2/CONTRIBUTING.md
  66. 3 0
      vendor/golang.org/x/oauth2/CONTRIBUTORS
  67. 27 0
      vendor/golang.org/x/oauth2/LICENSE
  68. 65 0
      vendor/golang.org/x/oauth2/README.md
  69. 25 0
      vendor/golang.org/x/oauth2/client_appengine.go
  70. 76 0
      vendor/golang.org/x/oauth2/internal/oauth2.go
  71. 227 0
      vendor/golang.org/x/oauth2/internal/token.go
  72. 69 0
      vendor/golang.org/x/oauth2/internal/transport.go
  73. 341 0
      vendor/golang.org/x/oauth2/oauth2.go
  74. 158 0
      vendor/golang.org/x/oauth2/token.go
  75. 132 0
      vendor/golang.org/x/oauth2/transport.go
  76. 18 0
      vendor/vendor.json

+ 13 - 4
cmd/web.go

@@ -41,6 +41,7 @@ import (
41 41
 	"github.com/go-macaron/toolbox"
42 42
 	"github.com/urfave/cli"
43 43
 	macaron "gopkg.in/macaron.v1"
44
+	context2 "github.com/gorilla/context"
44 45
 )
45 46
 
46 47
 // CmdWeb represents the available web sub-command.
@@ -210,6 +211,13 @@ func runWeb(ctx *cli.Context) error {
210 211
 		m.Post("/sign_up", bindIgnErr(auth.RegisterForm{}), user.SignUpPost)
211 212
 		m.Get("/reset_password", user.ResetPasswd)
212 213
 		m.Post("/reset_password", user.ResetPasswdPost)
214
+		m.Group("/oauth2", func() {
215
+			m.Get("/:provider", user.SignInOAuth)
216
+			m.Get("/:provider/callback", user.SignInOAuthCallback)
217
+		})
218
+		m.Get("/link_account", user.LinkAccount)
219
+		m.Post("/link_account_signin", bindIgnErr(auth.SignInForm{}), user.LinkAccountPostSignIn)
220
+		m.Post("/link_account_signup", bindIgnErr(auth.RegisterForm{}), user.LinkAccountPostRegister)
213 221
 		m.Group("/two_factor", func() {
214 222
 			m.Get("", user.TwoFactor)
215 223
 			m.Post("", bindIgnErr(auth.TwoFactorAuthForm{}), user.TwoFactorPost)
@@ -236,6 +244,7 @@ func runWeb(ctx *cli.Context) error {
236 244
 			Post(bindIgnErr(auth.NewAccessTokenForm{}), user.SettingsApplicationsPost)
237 245
 		m.Post("/applications/delete", user.SettingsDeleteApplication)
238 246
 		m.Route("/delete", "GET,POST", user.SettingsDelete)
247
+		m.Combo("/account_link").Get(user.SettingsAccountLinks).Post(user.SettingsDeleteAccountLink)
239 248
 		m.Group("/two_factor", func() {
240 249
 			m.Get("", user.SettingsTwoFactor)
241 250
 			m.Post("/regenerate_scratch", user.SettingsTwoFactorRegenerateScratch)
@@ -671,11 +680,11 @@ func runWeb(ctx *cli.Context) error {
671 680
 	var err error
672 681
 	switch setting.Protocol {
673 682
 	case setting.HTTP:
674
-		err = runHTTP(listenAddr, m)
683
+		err = runHTTP(listenAddr, context2.ClearHandler(m))
675 684
 	case setting.HTTPS:
676
-		err = runHTTPS(listenAddr, setting.CertFile, setting.KeyFile, m)
685
+		err = runHTTPS(listenAddr, setting.CertFile, setting.KeyFile, context2.ClearHandler(m))
677 686
 	case setting.FCGI:
678
-		err = fcgi.Serve(nil, m)
687
+		err = fcgi.Serve(nil, context2.ClearHandler(m))
679 688
 	case setting.UnixSocket:
680 689
 		if err := os.Remove(listenAddr); err != nil && !os.IsNotExist(err) {
681 690
 			log.Fatal(4, "Failed to remove unix socket directory %s: %v", listenAddr, err)
@@ -691,7 +700,7 @@ func runWeb(ctx *cli.Context) error {
691 700
 		if err = os.Chmod(listenAddr, os.FileMode(setting.UnixSocketPermission)); err != nil {
692 701
 			log.Fatal(4, "Failed to set permission of unix socket: %v", err)
693 702
 		}
694
-		err = http.Serve(listener, m)
703
+		err = http.Serve(listener, context2.ClearHandler(m))
695 704
 	default:
696 705
 		log.Fatal(4, "Invalid protocol: %s", setting.Protocol)
697 706
 	}

+ 40 - 0
models/error.go

@@ -847,3 +847,43 @@ func IsErrUploadNotExist(err error) bool {
847 847
 func (err ErrUploadNotExist) Error() string {
848 848
 	return fmt.Sprintf("attachment does not exist [id: %d, uuid: %s]", err.ID, err.UUID)
849 849
 }
850
+
851
+//  ___________         __                             .__    .____                 .__          ____ ___
852
+//  \_   _____/__  ____/  |_  ___________  ____ _____  |  |   |    |    ____   ____ |__| ____   |    |   \______ ___________
853
+//   |    __)_\  \/  /\   __\/ __ \_  __ \/    \\__  \ |  |   |    |   /  _ \ / ___\|  |/    \  |    |   /  ___// __ \_  __ \
854
+//   |        \>    <  |  | \  ___/|  | \/   |  \/ __ \|  |__ |    |__(  <_> ) /_/  >  |   |  \ |    |  /\___ \\  ___/|  | \/
855
+//  /_______  /__/\_ \ |__|  \___  >__|  |___|  (____  /____/ |_______ \____/\___  /|__|___|  / |______//____  >\___  >__|
856
+//          \/      \/           \/           \/     \/               \/    /_____/         \/               \/     \/
857
+
858
+// ErrExternalLoginUserAlreadyExist represents a "ExternalLoginUserAlreadyExist" kind of error.
859
+type ErrExternalLoginUserAlreadyExist struct {
860
+	ExternalID    string
861
+	UserID        int64
862
+	LoginSourceID int64
863
+}
864
+
865
+// IsErrExternalLoginUserAlreadyExist checks if an error is a ExternalLoginUserAlreadyExist.
866
+func IsErrExternalLoginUserAlreadyExist(err error) bool {
867
+	_, ok := err.(ErrExternalLoginUserAlreadyExist)
868
+	return ok
869
+}
870
+
871
+func (err ErrExternalLoginUserAlreadyExist) Error() string {
872
+	return fmt.Sprintf("external login user already exists [externalID: %s, userID: %d, loginSourceID: %d]", err.ExternalID, err.UserID, err.LoginSourceID)
873
+}
874
+
875
+// ErrExternalLoginUserNotExist represents a "ExternalLoginUserNotExist" kind of error.
876
+type ErrExternalLoginUserNotExist struct {
877
+	UserID        int64
878
+	LoginSourceID int64
879
+}
880
+
881
+// IsErrExternalLoginUserNotExist checks if an error is a ExternalLoginUserNotExist.
882
+func IsErrExternalLoginUserNotExist(err error) bool {
883
+	_, ok := err.(ErrExternalLoginUserNotExist)
884
+	return ok
885
+}
886
+
887
+func (err ErrExternalLoginUserNotExist) Error() string {
888
+	return fmt.Sprintf("external login user link does not exists [userID: %d, loginSourceID: %d]", err.UserID, err.LoginSourceID)
889
+}

+ 74 - 0
models/external_login_user.go

@@ -0,0 +1,74 @@
1
+// Copyright 2017 The Gitea Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package models
6
+
7
+import "github.com/markbates/goth"
8
+
9
+// ExternalLoginUser makes the connecting between some existing user and additional external login sources
10
+type ExternalLoginUser struct {
11
+	ExternalID    string `xorm:"NOT NULL"`
12
+	UserID        int64 `xorm:"NOT NULL"`
13
+	LoginSourceID int64 `xorm:"NOT NULL"`
14
+}
15
+
16
+// GetExternalLogin checks if a externalID in loginSourceID scope already exists
17
+func GetExternalLogin(externalLoginUser *ExternalLoginUser) (bool, error) {
18
+	return x.Get(externalLoginUser)
19
+}
20
+
21
+// ListAccountLinks returns a map with the ExternalLoginUser and its LoginSource
22
+func ListAccountLinks(user *User) ([]*ExternalLoginUser, error) {
23
+	externalAccounts := make([]*ExternalLoginUser, 0, 5)
24
+	err := x.Where("user_id=?", user.ID).
25
+		Desc("login_source_id").
26
+		Find(&externalAccounts)
27
+
28
+	if err != nil {
29
+		return nil, err
30
+	}
31
+
32
+	return externalAccounts, nil
33
+}
34
+
35
+// LinkAccountToUser link the gothUser to the user
36
+func LinkAccountToUser(user *User, gothUser goth.User) error {
37
+	loginSource, err := GetActiveOAuth2LoginSourceByName(gothUser.Provider)
38
+	if err != nil {
39
+		return err
40
+	}
41
+
42
+	externalLoginUser := &ExternalLoginUser{
43
+		ExternalID:    gothUser.UserID,
44
+		UserID:        user.ID,
45
+		LoginSourceID: loginSource.ID,
46
+	}
47
+	has, err := x.Get(externalLoginUser)
48
+	if err != nil {
49
+		return err
50
+	} else if has {
51
+		return ErrExternalLoginUserAlreadyExist{gothUser.UserID, user.ID, loginSource.ID}
52
+	}
53
+
54
+	_, err = x.Insert(externalLoginUser)
55
+	return err
56
+}
57
+
58
+// RemoveAccountLink will remove all external login sources for the given user
59
+func RemoveAccountLink(user *User, loginSourceID int64) (int64, error) {
60
+	deleted, err := x.Delete(&ExternalLoginUser{UserID: user.ID, LoginSourceID: loginSourceID})
61
+	if err != nil {
62
+		return deleted, err
63
+	}
64
+	if deleted < 1 {
65
+		return deleted, ErrExternalLoginUserNotExist{user.ID, loginSourceID}
66
+	}
67
+	return deleted, err
68
+}
69
+
70
+// RemoveAllAccountLinks will remove all external login sources for the given user
71
+func RemoveAllAccountLinks(user *User) error {
72
+	_, err := x.Delete(&ExternalLoginUser{UserID: user.ID})
73
+	return err
74
+}

+ 147 - 13
models/login_source.go

@@ -22,6 +22,7 @@ import (
22 22
 	"code.gitea.io/gitea/modules/auth/ldap"
23 23
 	"code.gitea.io/gitea/modules/auth/pam"
24 24
 	"code.gitea.io/gitea/modules/log"
25
+	"code.gitea.io/gitea/modules/auth/oauth2"
25 26
 )
26 27
 
27 28
 // LoginType represents an login type.
@@ -30,19 +31,21 @@ type LoginType int
30 31
 // Note: new type must append to the end of list to maintain compatibility.
31 32
 const (
32 33
 	LoginNoType LoginType = iota
33
-	LoginPlain            // 1
34
-	LoginLDAP             // 2
35
-	LoginSMTP             // 3
36
-	LoginPAM              // 4
37
-	LoginDLDAP            // 5
34
+	LoginPlain   // 1
35
+	LoginLDAP    // 2
36
+	LoginSMTP    // 3
37
+	LoginPAM     // 4
38
+	LoginDLDAP   // 5
39
+	LoginOAuth2  // 6
38 40
 )
39 41
 
40 42
 // LoginNames contains the name of LoginType values.
41 43
 var LoginNames = map[LoginType]string{
42
-	LoginLDAP:  "LDAP (via BindDN)",
43
-	LoginDLDAP: "LDAP (simple auth)", // Via direct bind
44
-	LoginSMTP:  "SMTP",
45
-	LoginPAM:   "PAM",
44
+	LoginLDAP:   "LDAP (via BindDN)",
45
+	LoginDLDAP:  "LDAP (simple auth)", // Via direct bind
46
+	LoginSMTP:   "SMTP",
47
+	LoginPAM:    "PAM",
48
+	LoginOAuth2: "OAuth2",
46 49
 }
47 50
 
48 51
 // SecurityProtocolNames contains the name of SecurityProtocol values.
@@ -57,6 +60,7 @@ var (
57 60
 	_ core.Conversion = &LDAPConfig{}
58 61
 	_ core.Conversion = &SMTPConfig{}
59 62
 	_ core.Conversion = &PAMConfig{}
63
+	_ core.Conversion = &OAuth2Config{}
60 64
 )
61 65
 
62 66
 // LDAPConfig holds configuration for LDAP login source.
@@ -115,6 +119,23 @@ func (cfg *PAMConfig) ToDB() ([]byte, error) {
115 119
 	return json.Marshal(cfg)
116 120
 }
117 121
 
122
+// OAuth2Config holds configuration for the OAuth2 login source.
123
+type OAuth2Config struct {
124
+	Provider     string
125
+	ClientID     string
126
+	ClientSecret string
127
+}
128
+
129
+// FromDB fills up an OAuth2Config from serialized format.
130
+func (cfg *OAuth2Config) FromDB(bs []byte) error {
131
+	return json.Unmarshal(bs, cfg)
132
+}
133
+
134
+// ToDB exports an SMTPConfig to a serialized format.
135
+func (cfg *OAuth2Config) ToDB() ([]byte, error) {
136
+	return json.Marshal(cfg)
137
+}
138
+
118 139
 // LoginSource represents an external way for authorizing users.
119 140
 type LoginSource struct {
120 141
 	ID        int64 `xorm:"pk autoincr"`
@@ -162,6 +183,8 @@ func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) {
162 183
 			source.Cfg = new(SMTPConfig)
163 184
 		case LoginPAM:
164 185
 			source.Cfg = new(PAMConfig)
186
+		case LoginOAuth2:
187
+			source.Cfg = new(OAuth2Config)
165 188
 		default:
166 189
 			panic("unrecognized login source type: " + com.ToStr(*val))
167 190
 		}
@@ -203,6 +226,11 @@ func (source *LoginSource) IsPAM() bool {
203 226
 	return source.Type == LoginPAM
204 227
 }
205 228
 
229
+// IsOAuth2 returns true of this source is of the OAuth2 type.
230
+func (source *LoginSource) IsOAuth2() bool {
231
+	return source.Type == LoginOAuth2
232
+}
233
+
206 234
 // HasTLS returns true of this source supports TLS.
207 235
 func (source *LoginSource) HasTLS() bool {
208 236
 	return ((source.IsLDAP() || source.IsDLDAP()) &&
@@ -250,6 +278,11 @@ func (source *LoginSource) PAM() *PAMConfig {
250 278
 	return source.Cfg.(*PAMConfig)
251 279
 }
252 280
 
281
+// OAuth2 returns OAuth2Config for this source, if of OAuth2 type.
282
+func (source *LoginSource) OAuth2() *OAuth2Config {
283
+	return source.Cfg.(*OAuth2Config)
284
+}
285
+
253 286
 // CreateLoginSource inserts a LoginSource in the DB if not already
254 287
 // existing with the given name.
255 288
 func CreateLoginSource(source *LoginSource) error {
@@ -261,12 +294,16 @@ func CreateLoginSource(source *LoginSource) error {
261 294
 	}
262 295
 
263 296
 	_, err = x.Insert(source)
297
+	if err == nil && source.IsOAuth2() {
298
+		oAuth2Config := source.OAuth2()
299
+		oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret)
300
+	}
264 301
 	return err
265 302
 }
266 303
 
267 304
 // LoginSources returns a slice of all login sources found in DB.
268 305
 func LoginSources() ([]*LoginSource, error) {
269
-	auths := make([]*LoginSource, 0, 5)
306
+	auths := make([]*LoginSource, 0, 6)
270 307
 	return auths, x.Find(&auths)
271 308
 }
272 309
 
@@ -285,6 +322,11 @@ func GetLoginSourceByID(id int64) (*LoginSource, error) {
285 322
 // UpdateSource updates a LoginSource record in DB.
286 323
 func UpdateSource(source *LoginSource) error {
287 324
 	_, err := x.Id(source.ID).AllCols().Update(source)
325
+	if err == nil && source.IsOAuth2() {
326
+		oAuth2Config := source.OAuth2()
327
+		oauth2.RemoveProvider(source.Name)
328
+		oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret)
329
+	}
288 330
 	return err
289 331
 }
290 332
 
@@ -296,6 +338,18 @@ func DeleteSource(source *LoginSource) error {
296 338
 	} else if count > 0 {
297 339
 		return ErrLoginSourceInUse{source.ID}
298 340
 	}
341
+
342
+	count, err = x.Count(&ExternalLoginUser{LoginSourceID: source.ID})
343
+	if err != nil {
344
+		return err
345
+	} else if count > 0 {
346
+		return ErrLoginSourceInUse{source.ID}
347
+	}
348
+
349
+	if source.IsOAuth2() {
350
+		oauth2.RemoveProvider(source.Name)
351
+	}
352
+
299 353
 	_, err = x.Id(source.ID).Delete(new(LoginSource))
300 354
 	return err
301 355
 }
@@ -444,7 +498,7 @@ func LoginViaSMTP(user *User, login, password string, sourceID int64, cfg *SMTPC
444 498
 		idx := strings.Index(login, "@")
445 499
 		if idx == -1 {
446 500
 			return nil, ErrUserNotExist{0, login, 0}
447
-		} else if !com.IsSliceContainsStr(strings.Split(cfg.AllowedDomains, ","), login[idx+1:]) {
501
+		} else if !com.IsSliceContainsStr(strings.Split(cfg.AllowedDomains, ","), login[idx + 1:]) {
448 502
 			return nil, ErrUserNotExist{0, login, 0}
449 503
 		}
450 504
 	}
@@ -526,6 +580,27 @@ func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMCon
526 580
 	return user, CreateUser(user)
527 581
 }
528 582
 
583
+//  ________      _____          __  .__     ________
584
+//  \_____  \    /  _  \  __ ___/  |_|  |__  \_____  \
585
+//   /   |   \  /  /_\  \|  |  \   __\  |  \  /  ____/
586
+//  /    |    \/    |    \  |  /|  | |   Y  \/       \
587
+//  \_______  /\____|__  /____/ |__| |___|  /\_______ \
588
+//          \/         \/                 \/         \/
589
+
590
+// OAuth2Provider describes the display values of a single OAuth2 provider
591
+type OAuth2Provider struct {
592
+	Name string
593
+	DisplayName string
594
+	Image string
595
+}
596
+
597
+// OAuth2Providers contains the map of registered OAuth2 providers in Gitea (based on goth)
598
+// key is used to map the OAuth2Provider with the goth provider type (also in LoginSource.OAuth2Config.Provider)
599
+// value is used to store display data
600
+var OAuth2Providers = map[string]OAuth2Provider{
601
+	"github":   {Name: "github", DisplayName:"GitHub", Image: "/img/github.png"},
602
+}
603
+
529 604
 // ExternalUserLogin attempts a login using external source types.
530 605
 func ExternalUserLogin(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) {
531 606
 	if !source.IsActived {
@@ -560,7 +635,7 @@ func UserSignIn(username, password string) (*User, error) {
560 635
 
561 636
 	if hasUser {
562 637
 		switch user.LoginType {
563
-		case LoginNoType, LoginPlain:
638
+		case LoginNoType, LoginPlain, LoginOAuth2:
564 639
 			if user.ValidatePassword(password) {
565 640
 				return user, nil
566 641
 			}
@@ -580,12 +655,16 @@ func UserSignIn(username, password string) (*User, error) {
580 655
 		}
581 656
 	}
582 657
 
583
-	sources := make([]*LoginSource, 0, 3)
658
+	sources := make([]*LoginSource, 0, 5)
584 659
 	if err = x.UseBool().Find(&sources, &LoginSource{IsActived: true}); err != nil {
585 660
 		return nil, err
586 661
 	}
587 662
 
588 663
 	for _, source := range sources {
664
+		if source.IsOAuth2() {
665
+			// don't try to authenticate against OAuth2 sources
666
+			continue
667
+		}
589 668
 		authUser, err := ExternalUserLogin(nil, username, password, source, true)
590 669
 		if err == nil {
591 670
 			return authUser, nil
@@ -596,3 +675,58 @@ func UserSignIn(username, password string) (*User, error) {
596 675
 
597 676
 	return nil, ErrUserNotExist{user.ID, user.Name, 0}
598 677
 }
678
+
679
+// GetActiveOAuth2ProviderLoginSources returns all actived LoginOAuth2 sources
680
+func GetActiveOAuth2ProviderLoginSources() ([]*LoginSource, error) {
681
+	sources := make([]*LoginSource, 0, 1)
682
+	if err := x.UseBool().Find(&sources, &LoginSource{IsActived: true, Type: LoginOAuth2}); err != nil {
683
+		return nil, err
684
+	}
685
+	return sources, nil
686
+}
687
+
688
+// GetActiveOAuth2LoginSourceByName returns a OAuth2 LoginSource based on the given name
689
+func GetActiveOAuth2LoginSourceByName(name string) (*LoginSource, error) {
690
+	loginSource := &LoginSource{
691
+		Name:      name,
692
+		Type:      LoginOAuth2,
693
+		IsActived: true,
694
+	}
695
+
696
+	has, err := x.UseBool().Get(loginSource)
697
+	if !has || err != nil {
698
+		return nil, err
699
+	}
700
+
701
+	return loginSource, nil
702
+}
703
+
704
+// GetActiveOAuth2Providers returns the map of configured active OAuth2 providers
705
+// key is used as technical name (like in the callbackURL)
706
+// values to display
707
+func GetActiveOAuth2Providers() (map[string]OAuth2Provider, error) {
708
+	// Maybe also seperate used and unused providers so we can force the registration of only 1 active provider for each type
709
+
710
+	loginSources, err := GetActiveOAuth2ProviderLoginSources()
711
+	if err != nil {
712
+		return nil, err
713
+	}
714
+
715
+	providers := make(map[string]OAuth2Provider)
716
+	for _, source := range loginSources {
717
+		providers[source.Name] = OAuth2Providers[source.OAuth2().Provider]
718
+	}
719
+
720
+	return providers, nil
721
+}
722
+
723
+// InitOAuth2 initialize the OAuth2 lib and register all active OAuth2 providers in the library
724
+func InitOAuth2() {
725
+	oauth2.Init()
726
+	loginSources, _ := GetActiveOAuth2ProviderLoginSources()
727
+
728
+	for _, source := range loginSources {
729
+		oAuth2Config := source.OAuth2()
730
+		oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret)
731
+	}
732
+}

+ 2 - 0
models/migrations/migrations.go

@@ -84,6 +84,8 @@ var migrations = []Migration{
84 84
 	NewMigration("create repo unit table and add units for all repos", addUnitsToTables),
85 85
 	// v17 -> v18
86 86
 	NewMigration("set protect branches updated with created", setProtectedBranchUpdatedWithCreated),
87
+	// v18 -> v19
88
+	NewMigration("add external login user", addExternalLoginUser),
87 89
 }
88 90
 
89 91
 // Migrate database to current version

+ 25 - 0
models/migrations/v18.go

@@ -0,0 +1,25 @@
1
+// Copyright 2016 Gitea. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package migrations
6
+
7
+import (
8
+	"fmt"
9
+
10
+	"github.com/go-xorm/xorm"
11
+)
12
+
13
+// ExternalLoginUser makes the connecting between some existing user and additional external login sources
14
+type ExternalLoginUser struct {
15
+	ExternalID    string `xorm:"NOT NULL"`
16
+	UserID        int64 `xorm:"NOT NULL"`
17
+	LoginSourceID int64 `xorm:"NOT NULL"`
18
+}
19
+
20
+func addExternalLoginUser(x *xorm.Engine) error {
21
+	if err := x.Sync2(new(ExternalLoginUser)); err != nil {
22
+		return fmt.Errorf("Sync2: %v", err)
23
+	}
24
+	return nil
25
+}

+ 21 - 0
models/user.go

@@ -196,6 +196,11 @@ func (u *User) IsLocal() bool {
196 196
 	return u.LoginType <= LoginPlain
197 197
 }
198 198
 
199
+// IsOAuth2 returns true if user login type is LoginOAuth2.
200
+func (u *User) IsOAuth2() bool {
201
+	return u.LoginType == LoginOAuth2
202
+}
203
+
199 204
 // HasForkedRepo checks if user has already forked a repository with given ID.
200 205
 func (u *User) HasForkedRepo(repoID int64) bool {
201 206
 	_, has := HasForkedRepo(u.ID, repoID)
@@ -397,6 +402,11 @@ func (u *User) ValidatePassword(passwd string) bool {
397 402
 	return subtle.ConstantTimeCompare([]byte(u.Passwd), []byte(newUser.Passwd)) == 1
398 403
 }
399 404
 
405
+// IsPasswordSet checks if the password is set or left empty
406
+func (u *User) IsPasswordSet() bool {
407
+	return !u.ValidatePassword("")
408
+}
409
+
400 410
 // UploadAvatar saves custom avatar for user.
401 411
 // FIXME: split uploads to different subdirs in case we have massive users.
402 412
 func (u *User) UploadAvatar(data []byte) error {
@@ -947,6 +957,12 @@ func deleteUser(e *xorm.Session, u *User) error {
947 957
 		return fmt.Errorf("clear assignee: %v", err)
948 958
 	}
949 959
 
960
+	// ***** START: ExternalLoginUser *****
961
+	if err = RemoveAllAccountLinks(u); err != nil {
962
+		return fmt.Errorf("ExternalLoginUser: %v", err)
963
+	}
964
+	// ***** END: ExternalLoginUser *****
965
+
950 966
 	if _, err = e.Id(u.ID).Delete(new(User)); err != nil {
951 967
 		return fmt.Errorf("Delete: %v", err)
952 968
 	}
@@ -1190,6 +1206,11 @@ func GetUserByEmail(email string) (*User, error) {
1190 1206
 	return nil, ErrUserNotExist{0, email, 0}
1191 1207
 }
1192 1208
 
1209
+// GetUser checks if a user already exists
1210
+func GetUser(user *User) (bool, error) {
1211
+	return x.Get(user)
1212
+}
1213
+
1193 1214
 // SearchUserOptions contains the options for searching
1194 1215
 type SearchUserOptions struct {
1195 1216
 	Keyword  string

+ 2 - 2
modules/auth/auth.go

@@ -179,7 +179,7 @@ func AssignForm(form interface{}, data map[string]interface{}) {
179 179
 func getRuleBody(field reflect.StructField, prefix string) string {
180 180
 	for _, rule := range strings.Split(field.Tag.Get("binding"), ";") {
181 181
 		if strings.HasPrefix(rule, prefix) {
182
-			return rule[len(prefix) : len(rule)-1]
182
+			return rule[len(prefix): len(rule) - 1]
183 183
 		}
184 184
 	}
185 185
 	return ""
@@ -237,7 +237,7 @@ func validate(errs binding.Errors, data map[string]interface{}, f Form, l macaro
237 237
 		}
238 238
 
239 239
 		if errs[0].FieldNames[0] == field.Name {
240
-			data["Err_"+field.Name] = true
240
+			data["Err_" + field.Name] = true
241 241
 
242 242
 			trName := field.Tag.Get("locale")
243 243
 			if len(trName) == 0 {

+ 4 - 1
modules/auth/auth_form.go

@@ -12,7 +12,7 @@ import (
12 12
 // AuthenticationForm form for authentication
13 13
 type AuthenticationForm struct {
14 14
 	ID                int64
15
-	Type              int    `binding:"Range(2,5)"`
15
+	Type              int    `binding:"Range(2,6)"`
16 16
 	Name              string `binding:"Required;MaxSize(30)"`
17 17
 	Host              string
18 18
 	Port              int
@@ -36,6 +36,9 @@ type AuthenticationForm struct {
36 36
 	TLS               bool
37 37
 	SkipVerify        bool
38 38
 	PAMServiceName    string
39
+	Oauth2Provider    string
40
+	Oauth2Key	  string
41
+	Oauth2Secret      string
39 42
 }
40 43
 
41 44
 // Validate validates fields

+ 105 - 0
modules/auth/oauth2/oauth2.go

@@ -0,0 +1,105 @@
1
+// Copyright 2017 The Gitea Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package oauth2
6
+
7
+import (
8
+	"code.gitea.io/gitea/modules/setting"
9
+	"code.gitea.io/gitea/modules/log"
10
+	"github.com/gorilla/sessions"
11
+	"github.com/markbates/goth"
12
+	"github.com/markbates/goth/gothic"
13
+	"net/http"
14
+	"os"
15
+	"github.com/satori/go.uuid"
16
+	"path/filepath"
17
+	"github.com/markbates/goth/providers/github"
18
+)
19
+
20
+var (
21
+	sessionUsersStoreKey = "gitea-oauth2-sessions"
22
+	providerHeaderKey    = "gitea-oauth2-provider"
23
+)
24
+
25
+// Init initialize the setup of the OAuth2 library
26
+func Init() {
27
+	sessionDir := filepath.Join(setting.AppDataPath, "sessions", "oauth2")
28
+	if err := os.MkdirAll(sessionDir, 0700); err != nil {
29
+		log.Fatal(4, "Fail to create dir %s: %v", sessionDir, err)
30
+	}
31
+
32
+	gothic.Store = sessions.NewFilesystemStore(sessionDir, []byte(sessionUsersStoreKey))
33
+
34
+	gothic.SetState = func(req *http.Request) string {
35
+		return uuid.NewV4().String()
36
+	}
37
+
38
+	gothic.GetProviderName = func(req *http.Request) (string, error) {
39
+		return req.Header.Get(providerHeaderKey), nil
40
+	}
41
+
42
+}
43
+
44
+// Auth OAuth2 auth service
45
+func Auth(provider string, request *http.Request, response http.ResponseWriter) error {
46
+	// not sure if goth is thread safe (?) when using multiple providers
47
+	request.Header.Set(providerHeaderKey, provider)
48
+
49
+	// don't use the default gothic begin handler to prevent issues when some error occurs
50
+	// normally the gothic library will write some custom stuff to the response instead of our own nice error page
51
+	//gothic.BeginAuthHandler(response, request)
52
+
53
+	url, err := gothic.GetAuthURL(response, request)
54
+	if err == nil {
55
+		http.Redirect(response, request, url, http.StatusTemporaryRedirect)
56
+	}
57
+	return err
58
+}
59
+
60
+// ProviderCallback handles OAuth callback, resolve to a goth user and send back to original url
61
+// this will trigger a new authentication request, but because we save it in the session we can use that
62
+func ProviderCallback(provider string, request *http.Request, response http.ResponseWriter) (goth.User, error) {
63
+	// not sure if goth is thread safe (?) when using multiple providers
64
+	request.Header.Set(providerHeaderKey, provider)
65
+
66
+	user, err := gothic.CompleteUserAuth(response, request)
67
+	if err != nil {
68
+		return user, err
69
+	}
70
+
71
+	return user, nil
72
+}
73
+
74
+// RegisterProvider register a OAuth2 provider in goth lib
75
+func RegisterProvider(providerName, providerType, clientID, clientSecret string) {
76
+	provider := createProvider(providerName, providerType, clientID, clientSecret)
77
+
78
+	if provider != nil {
79
+		goth.UseProviders(provider)
80
+	}
81
+}
82
+
83
+// RemoveProvider removes the given OAuth2 provider from the goth lib
84
+func RemoveProvider(providerName string) {
85
+	delete(goth.GetProviders(), providerName)
86
+}
87
+
88
+// used to create different types of goth providers
89
+func createProvider(providerName, providerType, clientID, clientSecret string) goth.Provider {
90
+	callbackURL := setting.AppURL + "user/oauth2/" + providerName + "/callback"
91
+
92
+	var provider goth.Provider
93
+
94
+	switch providerType {
95
+	case "github":
96
+		provider = github.New(clientID, clientSecret, callbackURL, "user:email")
97
+	}
98
+
99
+	// always set the name if provider is created so we can support multiple setups of 1 provider
100
+	if provider != nil {
101
+		provider.SetName(providerName)
102
+	}
103
+
104
+	return provider
105
+}

+ 1 - 1
modules/auth/user_form.go

@@ -143,7 +143,7 @@ func (f *AddEmailForm) Validate(ctx *macaron.Context, errs binding.Errors) bindi
143 143
 
144 144
 // ChangePasswordForm form for changing password
145 145
 type ChangePasswordForm struct {
146
-	OldPassword string `form:"old_password" binding:"Required;MinSize(1);MaxSize(255)"`
146
+	OldPassword string `form:"old_password" binding:"MaxSize(255)"`
147 147
 	Password    string `form:"password" binding:"Required;MaxSize(255)"`
148 148
 	Retype      string `form:"retype"`
149 149
 }

+ 15 - 0
options/locale/locale_en-US.ini

@@ -5,8 +5,11 @@ dashboard = Dashboard
5 5
 explore = Explore
6 6
 help = Help
7 7
 sign_in = Sign In
8
+sign_in_with = Sign in with
8 9
 sign_out = Sign Out
9 10
 sign_up = Sign Up
11
+link_account = Link Account
12
+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
10 13
 register = Register
11 14
 website = Website
12 15
 version = Version
@@ -277,6 +280,7 @@ applications = Applications
277 280
 orgs = Organizations
278 281
 delete = Delete Account
279 282
 twofa = Two-Factor Authentication
283
+account_link = External Accounts
280 284
 uid = Uid
281 285
 
282 286
 public_profile = Public Profile
@@ -379,6 +383,13 @@ then_enter_passcode = Then enter the passcode the application gives you:
379 383
 passcode_invalid = That passcode is invalid. Try again.
380 384
 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!
381 385
 
386
+manage_account_links = Manage account links
387
+manage_account_links_desc = External accounts linked to this account
388
+account_links_not_available = There are no external accounts linked to this account
389
+remove_account_link = Remove linked account
390
+remove_account_link_desc = Delete this account link will remove all related access for your account. Do you want to continue?
391
+remove_account_link_success = Account link has been removed successfully!
392
+
382 393
 delete_account = Delete Your Account
383 394
 delete_prompt = The operation will delete your account permanently, and <strong>CANNOT</strong> be undone!
384 395
 confirm_delete_account = Confirm Deletion
@@ -1106,8 +1117,12 @@ auths.allowed_domains_helper = Leave it empty to not restrict any domains. Multi
1106 1117
 auths.enable_tls = Enable TLS Encryption
1107 1118
 auths.skip_tls_verify = Skip TLS Verify
1108 1119
 auths.pam_service_name = PAM Service Name
1120
+auths.oauth2_provider = OAuth2 provider
1121
+auths.oauth2_clientID = Client ID (Key)
1122
+auths.oauth2_clientSecret = Client Secret
1109 1123
 auths.enable_auto_register = Enable Auto Registration
1110 1124
 auths.tips = Tips
1125
+auths.tip.github = Register a new OAuth application on https://github.com/settings/applications/new and use <host>/user/oauth2/<Authentication Name>/callback as "Authorization callback URL"
1111 1126
 auths.edit = Edit Authentication Setting
1112 1127
 auths.activated = This authentication is activated
1113 1128
 auths.new_success = New authentication '%s' has been added successfully.

+ 21 - 0
public/css/index.css

@@ -2983,3 +2983,24 @@ footer .ui.language .menu {
2983 2983
 .ui.user.list .item .description a:hover {
2984 2984
   text-decoration: underline;
2985 2985
 }
2986
+.user.link-account:not(.icon) {
2987
+  padding-top: 15px;
2988
+  padding-bottom: 5px;
2989
+}
2990
+.signin .oauth2 div {
2991
+  display: inline-block;
2992
+}
2993
+.signin .oauth2 div p {
2994
+  margin: 10px 5px 0 0;
2995
+  float: left;
2996
+}
2997
+.signin .oauth2 a {
2998
+  margin-right: 5px;
2999
+}
3000
+.signin .oauth2 a:last-child {
3001
+  margin-right: 0px;
3002
+}
3003
+.signin .oauth2 img {
3004
+  width: 32px;
3005
+  height: 32px;
3006
+}

BIN
public/img/github.png


+ 6 - 2
public/js/index.js

@@ -1019,9 +1019,9 @@ function initAdmin() {
1019 1019
     // New authentication
1020 1020
     if ($('.admin.new.authentication').length > 0) {
1021 1021
         $('#auth_type').change(function () {
1022
-            $('.ldap, .dldap, .smtp, .pam, .has-tls').hide();
1022
+            $('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls').hide();
1023 1023
 
1024
-            $('.ldap input[required], .dldap input[required], .smtp input[required], .pam input[required], .has-tls input[required]').removeAttr('required');
1024
+            $('.ldap input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required] .has-tls input[required]').removeAttr('required');
1025 1025
 
1026 1026
             var authType = $(this).val();
1027 1027
             switch (authType) {
@@ -1042,6 +1042,10 @@ function initAdmin() {
1042 1042
                     $('.dldap').show();
1043 1043
                     $('.dldap div.required:not(.ldap) input').attr('required', 'required');
1044 1044
                     break;
1045
+                case '6':     // OAuth2
1046
+                    $('.oauth2').show();
1047
+                    $('.oauth2 input').attr('required', 'required');
1048
+                    break;
1045 1049
             }
1046 1050
 
1047 1051
             if (authType == '2' || authType == '5') {

+ 28 - 1
routers/admin/auths.go

@@ -53,6 +53,7 @@ var (
53 53
 		{models.LoginNames[models.LoginDLDAP], models.LoginDLDAP},
54 54
 		{models.LoginNames[models.LoginSMTP], models.LoginSMTP},
55 55
 		{models.LoginNames[models.LoginPAM], models.LoginPAM},
56
+		{models.LoginNames[models.LoginOAuth2], models.LoginOAuth2},
56 57
 	}
57 58
 	securityProtocols = []dropdownItem{
58 59
 		{models.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted},
@@ -75,6 +76,14 @@ func NewAuthSource(ctx *context.Context) {
75 76
 	ctx.Data["AuthSources"] = authSources
76 77
 	ctx.Data["SecurityProtocols"] = securityProtocols
77 78
 	ctx.Data["SMTPAuths"] = models.SMTPAuths
79
+	ctx.Data["OAuth2Providers"] = models.OAuth2Providers
80
+
81
+	// only the first as default
82
+	for key := range models.OAuth2Providers {
83
+		ctx.Data["oauth2_provider"] = key
84
+		break
85
+	}
86
+
78 87
 	ctx.HTML(200, tplAuthNew)
79 88
 }
80 89
 
@@ -113,6 +122,14 @@ func parseSMTPConfig(form auth.AuthenticationForm) *models.SMTPConfig {
113 122
 	}
114 123
 }
115 124
 
125
+func parseOAuth2Config(form auth.AuthenticationForm) *models.OAuth2Config {
126
+	return &models.OAuth2Config{
127
+		Provider:     form.Oauth2Provider,
128
+		ClientID:     form.Oauth2Key,
129
+		ClientSecret: form.Oauth2Secret,
130
+	}
131
+}
132
+
116 133
 // NewAuthSourcePost response for adding an auth source
117 134
 func NewAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) {
118 135
 	ctx.Data["Title"] = ctx.Tr("admin.auths.new")
@@ -124,6 +141,7 @@ func NewAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) {
124 141
 	ctx.Data["AuthSources"] = authSources
125 142
 	ctx.Data["SecurityProtocols"] = securityProtocols
126 143
 	ctx.Data["SMTPAuths"] = models.SMTPAuths
144
+	ctx.Data["OAuth2Providers"] = models.OAuth2Providers
127 145
 
128 146
 	hasTLS := false
129 147
 	var config core.Conversion
@@ -138,6 +156,8 @@ func NewAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) {
138 156
 		config = &models.PAMConfig{
139 157
 			ServiceName: form.PAMServiceName,
140 158
 		}
159
+	case models.LoginOAuth2:
160
+		config = parseOAuth2Config(form)
141 161
 	default:
142 162
 		ctx.Error(400)
143 163
 		return
@@ -178,6 +198,7 @@ func EditAuthSource(ctx *context.Context) {
178 198
 
179 199
 	ctx.Data["SecurityProtocols"] = securityProtocols
180 200
 	ctx.Data["SMTPAuths"] = models.SMTPAuths
201
+	ctx.Data["OAuth2Providers"] = models.OAuth2Providers
181 202
 
182 203
 	source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
183 204
 	if err != nil {
@@ -187,16 +208,20 @@ func EditAuthSource(ctx *context.Context) {
187 208
 	ctx.Data["Source"] = source
188 209
 	ctx.Data["HasTLS"] = source.HasTLS()
189 210
 
211
+	if source.IsOAuth2() {
212
+		ctx.Data["CurrentOAuth2Provider"] = models.OAuth2Providers[source.OAuth2().Provider]
213
+	}
190 214
 	ctx.HTML(200, tplAuthEdit)
191 215
 }
192 216
 
193
-// EditAuthSourcePost resposne for editing auth source
217
+// EditAuthSourcePost response for editing auth source
194 218
 func EditAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) {
195 219
 	ctx.Data["Title"] = ctx.Tr("admin.auths.edit")
196 220
 	ctx.Data["PageIsAdmin"] = true
197 221
 	ctx.Data["PageIsAdminAuthentications"] = true
198 222
 
199 223
 	ctx.Data["SMTPAuths"] = models.SMTPAuths
224
+	ctx.Data["OAuth2Providers"] = models.OAuth2Providers
200 225
 
201 226
 	source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
202 227
 	if err != nil {
@@ -221,6 +246,8 @@ func EditAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) {
221 246
 		config = &models.PAMConfig{
222 247
 			ServiceName: form.PAMServiceName,
223 248
 		}
249
+	case models.LoginOAuth2:
250
+		config = parseOAuth2Config(form)
224 251
 	default:
225 252
 		ctx.Error(400)
226 253
 		return

+ 1 - 0
routers/init.go

@@ -54,6 +54,7 @@ func GlobalInit() {
54 54
 			log.Fatal(4, "Failed to initialize ORM engine: %v", err)
55 55
 		}
56 56
 		models.HasEngine = true
57
+		models.InitOAuth2()
57 58
 
58 59
 		models.LoadRepoConfig()
59 60
 		models.NewRepoContext()

+ 6 - 6
routers/repo/http.go

@@ -59,7 +59,7 @@ func HTTP(ctx *context.Context) {
59 59
 	isWiki := false
60 60
 	if strings.HasSuffix(reponame, ".wiki") {
61 61
 		isWiki = true
62
-		reponame = reponame[:len(reponame)-5]
62
+		reponame = reponame[:len(reponame) - 5]
63 63
 	}
64 64
 
65 65
 	repoUser, err := models.GetUserByName(username)
@@ -191,9 +191,9 @@ func HTTP(ctx *context.Context) {
191 191
 
192 192
 		var lastLine int64
193 193
 		for {
194
-			head := input[lastLine : lastLine+2]
194
+			head := input[lastLine: lastLine + 2]
195 195
 			if head[0] == '0' && head[1] == '0' {
196
-				size, err := strconv.ParseInt(string(input[lastLine+2:lastLine+4]), 16, 32)
196
+				size, err := strconv.ParseInt(string(input[lastLine + 2:lastLine + 4]), 16, 32)
197 197
 				if err != nil {
198 198
 					log.Error(4, "%v", err)
199 199
 					return
@@ -204,7 +204,7 @@ func HTTP(ctx *context.Context) {
204 204
 					break
205 205
 				}
206 206
 
207
-				line := input[lastLine : lastLine+size]
207
+				line := input[lastLine: lastLine + size]
208 208
 				idx := bytes.IndexRune(line, '\000')
209 209
 				if idx > -1 {
210 210
 					line = line[:idx]
@@ -370,7 +370,7 @@ func gitCommand(dir string, args ...string) []byte {
370 370
 
371 371
 func getGitConfig(option, dir string) string {
372 372
 	out := string(gitCommand(dir, "config", option))
373
-	return out[0 : len(out)-1]
373
+	return out[0: len(out) - 1]
374 374
 }
375 375
 
376 376
 func getConfigSetting(service, dir string) bool {
@@ -501,7 +501,7 @@ func updateServerInfo(dir string) []byte {
501 501
 }
502 502
 
503 503
 func packetWrite(str string) []byte {
504
-	s := strconv.FormatInt(int64(len(str)+4), 16)
504
+	s := strconv.FormatInt(int64(len(str) + 4), 16)
505 505
 	if len(s)%4 != 0 {
506 506
 		s = strings.Repeat("0", 4-len(s)%4) + s
507 507
 	}

+ 350 - 8
routers/user/auth.go

@@ -17,6 +17,10 @@ import (
17 17
 	"code.gitea.io/gitea/modules/context"
18 18
 	"code.gitea.io/gitea/modules/log"
19 19
 	"code.gitea.io/gitea/modules/setting"
20
+	"net/http"
21
+	"code.gitea.io/gitea/modules/auth/oauth2"
22
+	"github.com/markbates/goth"
23
+	"strings"
20 24
 )
21 25
 
22 26
 const (
@@ -30,6 +34,7 @@ const (
30 34
 	tplResetPassword  base.TplName = "user/auth/reset_passwd"
31 35
 	tplTwofa          base.TplName = "user/auth/twofa"
32 36
 	tplTwofaScratch   base.TplName = "user/auth/twofa_scratch"
37
+	tplLinkAccount    base.TplName = "user/auth/link_account"
33 38
 )
34 39
 
35 40
 // AutoSignIn reads cookie and try to auto-login.
@@ -61,7 +66,7 @@ func AutoSignIn(ctx *context.Context) (bool, error) {
61 66
 	}
62 67
 
63 68
 	if val, _ := ctx.GetSuperSecureCookie(
64
-		base.EncodeMD5(u.Rands+u.Passwd), setting.CookieRememberName); val != u.Name {
69
+		base.EncodeMD5(u.Rands + u.Passwd), setting.CookieRememberName); val != u.Name {
65 70
 		return false, nil
66 71
 	}
67 72
 
@@ -109,6 +114,13 @@ func SignIn(ctx *context.Context) {
109 114
 		return
110 115
 	}
111 116
 
117
+	oauth2Providers, err := models.GetActiveOAuth2Providers()
118
+	if err != nil {
119
+		ctx.Handle(500, "UserSignIn", err)
120
+		return
121
+	}
122
+	ctx.Data["OAuth2Providers"] = oauth2Providers
123
+
112 124
 	ctx.HTML(200, tplSignIn)
113 125
 }
114 126
 
@@ -116,6 +128,13 @@ func SignIn(ctx *context.Context) {
116 128
 func SignInPost(ctx *context.Context, form auth.SignInForm) {
117 129
 	ctx.Data["Title"] = ctx.Tr("sign_in")
118 130
 
131
+	oauth2Providers, err := models.GetActiveOAuth2Providers()
132
+	if err != nil {
133
+		ctx.Handle(500, "UserSignIn", err)
134
+		return
135
+	}
136
+	ctx.Data["OAuth2Providers"] = oauth2Providers
137
+
119 138
 	if ctx.HasError() {
120 139
 		ctx.HTML(200, tplSignIn)
121 140
 		return
@@ -277,7 +296,7 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR
277 296
 	if remember {
278 297
 		days := 86400 * setting.LogInRememberDays
279 298
 		ctx.SetCookie(setting.CookieUserName, u.Name, days, setting.AppSubURL)
280
-		ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
299
+		ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands + u.Passwd),
281 300
 			setting.CookieRememberName, u.Name, days, setting.AppSubURL)
282 301
 	}
283 302
 
@@ -309,6 +328,333 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR
309 328
 	}
310 329
 }
311 330
 
331
+// SignInOAuth handles the OAuth2 login buttons
332
+func SignInOAuth(ctx *context.Context) {
333
+	provider := ctx.Params(":provider")
334
+
335
+	loginSource, err := models.GetActiveOAuth2LoginSourceByName(provider)
336
+	if err != nil {
337
+		ctx.Handle(500, "SignIn", err)
338
+		return
339
+	}
340
+
341
+	// try to do a direct callback flow, so we don't authenticate the user again but use the valid accesstoken to get the user
342
+	user, gothUser, err := oAuth2UserLoginCallback(loginSource, ctx.Req.Request, ctx.Resp)
343
+	if err == nil && user != nil {
344
+		// we got the user without going through the whole OAuth2 authentication flow again
345
+		handleOAuth2SignIn(user, gothUser, ctx, err)
346
+		return
347
+	}
348
+
349
+	err = oauth2.Auth(loginSource.Name, ctx.Req.Request, ctx.Resp)
350
+	if err != nil {
351
+		ctx.Handle(500, "SignIn", err)
352
+	}
353
+	// redirect is done in oauth2.Auth
354
+}
355
+
356
+// SignInOAuthCallback handles the callback from the given provider
357
+func SignInOAuthCallback(ctx *context.Context) {
358
+	provider := ctx.Params(":provider")
359
+
360
+	// first look if the provider is still active
361
+	loginSource, err := models.GetActiveOAuth2LoginSourceByName(provider)
362
+	if err != nil {
363
+		ctx.Handle(500, "SignIn", err)
364
+		return
365
+	}
366
+
367
+	if loginSource == nil {
368
+		ctx.Handle(500, "SignIn", errors.New("No valid provider found, check configured callback url in provider"))
369
+		return
370
+	}
371
+
372
+	u, gothUser, err := oAuth2UserLoginCallback(loginSource, ctx.Req.Request, ctx.Resp)
373
+
374
+	handleOAuth2SignIn(u, gothUser, ctx, err)
375
+}
376
+
377
+func handleOAuth2SignIn(u *models.User, gothUser goth.User, ctx *context.Context, err error) {
378
+	if err != nil {
379
+		ctx.Handle(500, "UserSignIn", err)
380
+		return
381
+	}
382
+
383
+	if u == nil {
384
+		// no existing user is found, request attach or new account
385
+		ctx.Session.Set("linkAccountGothUser", gothUser)
386
+		ctx.Redirect(setting.AppSubURL + "/user/link_account")
387
+		return
388
+	}
389
+
390
+	// If this user is enrolled in 2FA, we can't sign the user in just yet.
391
+	// Instead, redirect them to the 2FA authentication page.
392
+	_, err = models.GetTwoFactorByUID(u.ID)
393
+	if err != nil {
394
+		if models.IsErrTwoFactorNotEnrolled(err) {
395
+			ctx.Session.Set("uid", u.ID)
396
+			ctx.Session.Set("uname", u.Name)
397
+
398
+			// Clear whatever CSRF has right now, force to generate a new one
399
+			ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL)
400
+
401
+			// Register last login
402
+			u.SetLastLogin()
403
+			if err := models.UpdateUser(u); err != nil {
404
+				ctx.Handle(500, "UpdateUser", err)
405
+				return
406
+			}
407
+
408
+			if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 {
409
+				ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL)
410
+				ctx.Redirect(redirectTo)
411
+				return
412
+			}
413
+
414
+			ctx.Redirect(setting.AppSubURL + "/")
415
+		} else {
416
+			ctx.Handle(500, "UserSignIn", err)
417
+		}
418
+		return
419
+	}
420
+
421
+	// User needs to use 2FA, save data and redirect to 2FA page.
422
+	ctx.Session.Set("twofaUid", u.ID)
423
+	ctx.Session.Set("twofaRemember", false)
424
+	ctx.Redirect(setting.AppSubURL + "/user/two_factor")
425
+}
426
+
427
+// OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful
428
+// login the user
429
+func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) (*models.User, goth.User, error) {
430
+	gothUser, err := oauth2.ProviderCallback(loginSource.Name, request, response)
431
+
432
+	if err != nil {
433
+		return nil, goth.User{}, err
434
+	}
435
+
436
+	user := &models.User{
437
+		LoginName:   gothUser.UserID,
438
+		LoginType:   models.LoginOAuth2,
439
+		LoginSource: loginSource.ID,
440
+	}
441
+
442
+	hasUser, err := models.GetUser(user)
443
+	if err != nil {
444
+		return nil, goth.User{}, err
445
+	}
446
+
447
+	if hasUser {
448
+		return user, goth.User{}, nil
449
+	}
450
+
451
+	// search in external linked users
452
+	externalLoginUser := &models.ExternalLoginUser{
453
+		ExternalID:    gothUser.UserID,
454
+		LoginSourceID: loginSource.ID,
455
+	}
456
+	hasUser, err = models.GetExternalLogin(externalLoginUser)
457
+	if err != nil {
458
+		return nil, goth.User{}, err
459
+	}
460
+	if hasUser {
461
+		user, err = models.GetUserByID(externalLoginUser.UserID)
462
+		return user, goth.User{}, err
463
+	}
464
+
465
+	// no user found to login
466
+	return nil, gothUser, nil
467
+
468
+}
469
+
470
+// LinkAccount shows the page where the user can decide to login or create a new account
471
+func LinkAccount(ctx *context.Context) {
472
+	ctx.Data["Title"] = ctx.Tr("link_account")
473
+	ctx.Data["LinkAccountMode"] = true
474
+	ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
475
+	ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
476
+	ctx.Data["ShowRegistrationButton"] = false
477
+
478
+	// use this to set the right link into the signIn and signUp templates in the link_account template
479
+	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
480
+	ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
481
+
482
+	gothUser := ctx.Session.Get("linkAccountGothUser")
483
+	if gothUser == nil {
484
+		ctx.Handle(500, "UserSignIn", errors.New("not in LinkAccount session"))
485
+		return
486
+	}
487
+
488
+	ctx.Data["user_name"] = gothUser.(goth.User).NickName
489
+	ctx.Data["email"] = gothUser.(goth.User).Email
490
+
491
+	ctx.HTML(200, tplLinkAccount)
492
+}
493
+
494
+// LinkAccountPostSignIn handle the coupling of external account with another account using signIn
495
+func LinkAccountPostSignIn(ctx *context.Context, signInForm auth.SignInForm) {
496
+	ctx.Data["Title"] = ctx.Tr("link_account")
497
+	ctx.Data["LinkAccountMode"] = true
498
+	ctx.Data["LinkAccountModeSignIn"] = true
499
+	ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
500
+	ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
501
+	ctx.Data["ShowRegistrationButton"] = false
502
+
503
+	// use this to set the right link into the signIn and signUp templates in the link_account template
504
+	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
505
+	ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
506
+
507
+	gothUser := ctx.Session.Get("linkAccountGothUser")
508
+	if gothUser == nil {
509
+		ctx.Handle(500, "UserSignIn", errors.New("not in LinkAccount session"))
510
+		return
511
+	}
512
+
513
+	if ctx.HasError() {
514
+		ctx.HTML(200, tplLinkAccount)
515
+		return
516
+	}
517
+
518
+	u, err := models.UserSignIn(signInForm.UserName, signInForm.Password)
519
+	if err != nil {
520
+		if models.IsErrUserNotExist(err) {
521
+			ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplLinkAccount, &signInForm)
522
+		} else {
523
+			ctx.Handle(500, "UserLinkAccount", err)
524
+		}
525
+		return
526
+	}
527
+
528
+	// If this user is enrolled in 2FA, we can't sign the user in just yet.
529
+	// Instead, redirect them to the 2FA authentication page.
530
+	_, err = models.GetTwoFactorByUID(u.ID)
531
+	if err != nil {
532
+		if models.IsErrTwoFactorNotEnrolled(err) {
533
+			models.LinkAccountToUser(u, gothUser.(goth.User))
534
+			handleSignIn(ctx, u, signInForm.Remember)
535
+		} else {
536
+			ctx.Handle(500, "UserLinkAccount", err)
537
+		}
538
+		return
539
+	}
540
+
541
+	// User needs to use 2FA, save data and redirect to 2FA page.
542
+	ctx.Session.Set("twofaUid", u.ID)
543
+	ctx.Session.Set("twofaRemember", signInForm.Remember)
544
+	ctx.Session.Set("linkAccount", true)
545
+
546
+	ctx.Redirect(setting.AppSubURL + "/user/two_factor")
547
+}
548
+
549
+// LinkAccountPostRegister handle the creation of a new account for an external account using signUp
550
+func LinkAccountPostRegister(ctx *context.Context, cpt *captcha.Captcha, form auth.RegisterForm) {
551
+	ctx.Data["Title"] = ctx.Tr("link_account")
552
+	ctx.Data["LinkAccountMode"] = true
553
+	ctx.Data["LinkAccountModeRegister"] = true
554
+	ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
555
+	ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
556
+	ctx.Data["ShowRegistrationButton"] = false
557
+
558
+	// use this to set the right link into the signIn and signUp templates in the link_account template
559
+	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
560
+	ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
561
+
562
+	gothUser := ctx.Session.Get("linkAccountGothUser")
563
+	if gothUser == nil {
564
+		ctx.Handle(500, "UserSignUp", errors.New("not in LinkAccount session"))
565
+		return
566
+	}
567
+
568
+	if ctx.HasError() {
569
+		ctx.HTML(200, tplLinkAccount)
570
+		return
571
+	}
572
+
573
+	if setting.Service.DisableRegistration {
574
+		ctx.Error(403)
575
+		return
576
+	}
577
+
578
+	if setting.Service.EnableCaptcha && !cpt.VerifyReq(ctx.Req) {
579
+		ctx.Data["Err_Captcha"] = true
580
+		ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplLinkAccount, &form)
581
+		return
582
+	}
583
+
584
+	if (len(strings.TrimSpace(form.Password)) > 0 || len(strings.TrimSpace(form.Retype)) > 0) && form.Password != form.Retype {
585
+		ctx.Data["Err_Password"] = true
586
+		ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplLinkAccount, &form)
587
+		return
588
+	}
589
+	if len(strings.TrimSpace(form.Password)) > 0 && len(form.Password) < setting.MinPasswordLength {
590
+		ctx.Data["Err_Password"] = true
591
+		ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplLinkAccount, &form)
592
+		return
593
+	}
594
+
595
+	loginSource, err := models.GetActiveOAuth2LoginSourceByName(gothUser.(goth.User).Provider)
596
+	if err != nil {
597
+		ctx.Handle(500, "CreateUser", err)
598
+	}
599
+
600
+	u := &models.User{
601
+		Name:        form.UserName,
602
+		Email:       form.Email,
603
+		Passwd:      form.Password,
604
+		IsActive:    !setting.Service.RegisterEmailConfirm,
605
+		LoginType:   models.LoginOAuth2,
606
+		LoginSource: loginSource.ID,
607
+		LoginName:   gothUser.(goth.User).UserID,
608
+	}
609
+
610
+	if err := models.CreateUser(u); err != nil {
611
+		switch {
612
+		case models.IsErrUserAlreadyExist(err):
613
+			ctx.Data["Err_UserName"] = true
614
+			ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplLinkAccount, &form)
615
+		case models.IsErrEmailAlreadyUsed(err):
616
+			ctx.Data["Err_Email"] = true
617
+			ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplLinkAccount, &form)
618
+		case models.IsErrNameReserved(err):
619
+			ctx.Data["Err_UserName"] = true
620
+			ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(models.ErrNameReserved).Name), tplLinkAccount, &form)
621
+		case models.IsErrNamePatternNotAllowed(err):
622
+			ctx.Data["Err_UserName"] = true
623
+			ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplLinkAccount, &form)
624
+		default:
625
+			ctx.Handle(500, "CreateUser", err)
626
+		}
627
+		return
628
+	}
629
+	log.Trace("Account created: %s", u.Name)
630
+
631
+	// Auto-set admin for the only user.
632
+	if models.CountUsers() == 1 {
633
+		u.IsAdmin = true
634
+		u.IsActive = true
635
+		if err := models.UpdateUser(u); err != nil {
636
+			ctx.Handle(500, "UpdateUser", err)
637
+			return
638
+		}
639
+	}
640
+
641
+	// Send confirmation email
642
+	if setting.Service.RegisterEmailConfirm && u.ID > 1 {
643
+		models.SendActivateAccountMail(ctx.Context, u)
644
+		ctx.Data["IsSendRegisterMail"] = true
645
+		ctx.Data["Email"] = u.Email
646
+		ctx.Data["Hours"] = setting.Service.ActiveCodeLives / 60
647
+		ctx.HTML(200, TplActivate)
648
+
649
+		if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
650
+			log.Error(4, "Set cache(MailResendLimit) fail: %v", err)
651
+		}
652
+		return
653
+	}
654
+
655
+	ctx.Redirect(setting.AppSubURL + "/user/login")
656
+}
657
+
312 658
 // SignOut sign out from login status
313 659
 func SignOut(ctx *context.Context) {
314 660
 	ctx.Session.Delete("uid")
@@ -328,11 +674,7 @@ func SignUp(ctx *context.Context) {
328 674
 
329 675
 	ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
330 676
 
331
-	if setting.Service.DisableRegistration {
332
-		ctx.Data["DisableRegistration"] = true
333
-		ctx.HTML(200, tplSignUp)
334
-		return
335
-	}
677
+	ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
336 678
 
337 679
 	ctx.HTML(200, tplSignUp)
338 680
 }
@@ -540,7 +882,7 @@ func ForgotPasswdPost(ctx *context.Context) {
540 882
 		return
541 883
 	}
542 884
 
543
-	if !u.IsLocal() {
885
+	if !u.IsLocal() && !u.IsOAuth2() {
544 886
 		ctx.Data["Err_Email"] = true
545 887
 		ctx.RenderWithErr(ctx.Tr("auth.non_local_account"), tplForgotPassword, nil)
546 888
 		return

+ 45 - 1
routers/user/setting.go

@@ -37,6 +37,7 @@ const (
37 37
 	tplSettingsApplications base.TplName = "user/settings/applications"
38 38
 	tplSettingsTwofa        base.TplName = "user/settings/twofa"
39 39
 	tplSettingsTwofaEnroll  base.TplName = "user/settings/twofa_enroll"
40
+	tplSettingsAccountLink  base.TplName = "user/settings/account_link"
40 41
 	tplSettingsDelete       base.TplName = "user/settings/delete"
41 42
 	tplSecurity             base.TplName = "user/security"
42 43
 )
@@ -200,7 +201,7 @@ func SettingsPasswordPost(ctx *context.Context, form auth.ChangePasswordForm) {
200 201
 		return
201 202
 	}
202 203
 
203
-	if !ctx.User.ValidatePassword(form.OldPassword) {
204
+	if ctx.User.IsPasswordSet() && !ctx.User.ValidatePassword(form.OldPassword) {
204 205
 		ctx.Flash.Error(ctx.Tr("settings.password_incorrect"))
205 206
 	} else if form.Password != form.Retype {
206 207
 		ctx.Flash.Error(ctx.Tr("form.password_not_match"))
@@ -631,6 +632,49 @@ func SettingsTwoFactorEnrollPost(ctx *context.Context, form auth.TwoFactorAuthFo
631 632
 	ctx.Redirect(setting.AppSubURL + "/user/settings/two_factor")
632 633
 }
633 634
 
635
+// SettingsAccountLinks render the account links settings page
636
+func SettingsAccountLinks(ctx *context.Context) {
637
+	ctx.Data["Title"] = ctx.Tr("settings")
638
+	ctx.Data["PageIsSettingsAccountLink"] = true
639
+
640
+	accountLinks, err := models.ListAccountLinks(ctx.User)
641
+	if err != nil {
642
+		ctx.Handle(500, "ListAccountLinks", err)
643
+		return
644
+	}
645
+
646
+	// map the provider display name with the LoginSource
647
+	sources := make(map[*models.LoginSource]string)
648
+	for _, externalAccount := range accountLinks {
649
+		if loginSource, err := models.GetLoginSourceByID(externalAccount.LoginSourceID); err == nil {
650
+			var providerDisplayName string
651
+			if loginSource.IsOAuth2() {
652
+				providerTechnicalName := loginSource.OAuth2().Provider
653
+				providerDisplayName = models.OAuth2Providers[providerTechnicalName].DisplayName
654
+			} else {
655
+				providerDisplayName = loginSource.Name
656
+			}
657
+			sources[loginSource] = providerDisplayName
658
+		}
659
+	}
660
+	ctx.Data["AccountLinks"] = sources
661
+
662
+	ctx.HTML(200, tplSettingsAccountLink)
663
+}
664
+
665
+// SettingsDeleteAccountLink delete a single account link
666
+func SettingsDeleteAccountLink(ctx *context.Context) {
667
+	if _, err := models.RemoveAccountLink(ctx.User, ctx.QueryInt64("loginSourceID")); err != nil {
668
+		ctx.Flash.Error("RemoveAccountLink: " + err.Error())
669
+	} else {
670
+		ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success"))
671
+	}
672
+
673
+	ctx.JSON(200, map[string]interface{}{
674
+		"redirect": setting.AppSubURL + "/user/settings/account_link",
675
+	})
676
+}
677
+
634 678
 // SettingsDelete render user suicide page and response for delete user himself
635 679
 func SettingsDelete(ctx *context.Context) {
636 680
 	ctx.Data["Title"] = ctx.Tr("settings")

+ 26 - 0
templates/admin/auth/edit.tmpl

@@ -142,6 +142,32 @@
142 142
 							</div>
143 143
 						{{end}}
144 144
 
145
+						<!-- OAuth2 -->
146
+						{{if .Source.IsOAuth2}}
147
+							{{ $cfg:=.Source.OAuth2 }}
148
+							<div class="inline required field">
149
+								<label>{{.i18n.Tr "admin.auths.oauth2_provider"}}</label>
150
+								<div class="ui selection type dropdown">
151
+									<input type="hidden" id="oauth2_provider" name="oauth2_provider" value="{{$cfg.Provider}}" required>
152
+									<div class="text">{{.CurrentOAuth2Provider.DisplayName}}</div>
153
+									<i class="dropdown icon"></i>
154
+									<div class="menu">
155
+										{{range $key, $value := .OAuth2Providers}}
156
+											<div class="item" data-value="{{$key}}">{{$value.DisplayName}}</div>
157
+										{{end}}
158
+									</div>
159
+								</div>
160
+							</div>
161
+							<div class="required field">
162
+								<label for="oauth2_key">{{.i18n.Tr "admin.auths.oauth2_clientID"}}</label>
163
+								<input id="oauth2_key" name="oauth2_key" value="{{$cfg.ClientID}}" required>
164
+							</div>
165
+							<div class="required field">
166
+								<label for="oauth2_secret">{{.i18n.Tr "admin.auths.oauth2_clientSecret"}}</label>
167
+								<input id="oauth2_secret" name="oauth2_secret" value="{{$cfg.ClientSecret}}" required>
168
+							</div>
169
+						{{end}}
170
+
145 171
 						<div class="inline field {{if not .Source.IsSMTP}}hide{{end}}">
146 172
 							<div class="ui checkbox">
147 173
 								<label><strong>{{.i18n.Tr "admin.auths.enable_tls"}}</strong></label>

+ 27 - 0
templates/admin/auth/new.tmpl

@@ -133,6 +133,31 @@
133 133
 							<input id="pam_service_name" name="pam_service_name" value="{{.pam_service_name}}" />
134 134
 						</div>
135 135
 
136
+						<!-- OAuth2 -->
137
+						<div class="oauth2 field {{if not (eq .type 6)}}hide{{end}}">
138
+							<div class="inline required field">
139
+								<label>{{.i18n.Tr "admin.auths.oauth2_provider"}}</label>
140
+								<div class="ui selection type dropdown">
141
+									<input type="hidden" id="oauth2_provider" name="oauth2_provider" value="{{.oauth2_provider}}">
142
+									<div class="text">{{.oauth2_provider}}</div>
143
+									<i class="dropdown icon"></i>
144
+									<div class="menu">
145
+										{{range $key, $value := .OAuth2Providers}}
146
+											<div class="item" data-value="{{$key}}">{{$value.DisplayName}}</div>
147
+										{{end}}
148
+									</div>
149
+								</div>
150
+							</div>
151
+							<div class="required field">
152
+								<label for="oauth2_key">{{.i18n.Tr "admin.auths.oauth2_clientID"}}</label>
153
+								<input id="oauth2_key" name="oauth2_key" value="{{.oauth2_key}}">
154
+							</div>
155
+							<div class="required field">
156
+								<label for="oauth2_secret">{{.i18n.Tr "admin.auths.oauth2_clientSecret"}}</label>
157
+								<input id="oauth2_secret" name="oauth2_secret" value="{{.oauth2_secret}}">
158
+							</div>
159
+						</div>
160
+
136 161
 						<div class="ldap field">
137 162
 							<div class="ui checkbox">
138 163
 								<label><strong>{{.i18n.Tr "admin.auths.attributes_in_bind"}}</strong></label>
@@ -170,6 +195,8 @@
170 195
 				<div class="ui attached segment">
171 196
 					<h5>GMail Settings:</h5>
172 197
 					<p>Host: smtp.gmail.com, Port: 587, Enable TLS Encryption: true</p>
198
+					<h5>OAuth GitHub:</h5>
199
+					<p>{{.i18n.Tr "admin.auths.tip.github"}}</p>
173 200
 				</div>
174 201
 			</div>
175 202
 		</div>

+ 1 - 1
templates/admin/user/edit.tmpl

@@ -43,7 +43,7 @@
43 43
 							<input id="email" name="email" type="email" value="{{.User.Email}}" autofocus required>
44 44
 						</div>
45 45
 						<input class="fake" type="password">
46
-						<div class="local field {{if .Err_Password}}error{{end}} {{if not (eq .User.LoginSource 0)}}hide{{end}}">
46
+						<div class="local field {{if .Err_Password}}error{{end}} {{if not (or (.User.IsLocal) (.User.IsOAuth2))}}hide{{end}}">
47 47
 							<label for="password">{{.i18n.Tr "password"}}</label>
48 48
 							<input id="password" name="password" type="password">
49 49
 							<p class="help">{{.i18n.Tr "admin.users.password_helper"}}</p>

+ 13 - 0
templates/user/auth/link_account.tmpl

@@ -0,0 +1,13 @@
1
+{{template "base/head" .}}
2
+<div class="user link-account">
3
+	<div class="ui middle very relaxed page grid">
4
+		<div class="column">
5
+			<p class="large center">
6
+				{{.i18n.Tr "link_account_signin_or_signup"}}
7
+			</p>
8
+		</div>
9
+	</div>
10
+</div>
11
+{{template "user/auth/signin_inner" .}}
12
+{{template "user/auth/signup_inner" .}}
13
+{{template "base/footer" .}}

+ 1 - 42
templates/user/auth/signin.tmpl

@@ -1,44 +1,3 @@
1 1
 {{template "base/head" .}}
2
-<div class="user signin">
3
-	<div class="ui middle very relaxed page grid">
4
-		<div class="column">
5
-			<form class="ui form" action="{{.Link}}" method="post">
6
-				{{.CsrfTokenHtml}}
7
-				<h3 class="ui top attached header">
8
-					{{.i18n.Tr "sign_in"}}
9
-				</h3>
10
-				<div class="ui attached segment">
11
-					{{template "base/alert" .}}
12
-					<div class="required inline field {{if .Err_UserName}}error{{end}}">
13
-						<label for="user_name">{{.i18n.Tr "home.uname_holder"}}</label>
14
-						<input id="user_name" name="user_name" value="{{.user_name}}" autofocus required>
15
-					</div>
16
-					<div class="required inline field {{if .Err_Password}}error{{end}}">
17
-						<label for="password">{{.i18n.Tr "password"}}</label>
18
-						<input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" required>
19
-					</div>
20
-					<div class="inline field">
21
-						<label></label>
22
-						<div class="ui checkbox">
23
-							<label>{{.i18n.Tr "auth.remember_me"}}</label>
24
-							<input name="remember" type="checkbox">
25
-						</div>
26
-					</div>
27
-
28
-					<div class="inline field">
29
-						<label></label>
30
-						<button class="ui green button">{{.i18n.Tr "sign_in"}}</button>
31
-						<a href="{{AppSubUrl}}/user/forget_password">{{.i18n.Tr "auth.forget_password"}}</a>
32
-					</div>
33
-					{{if .ShowRegistrationButton}}
34
-						<div class="inline field">
35
-							<label></label>
36
-							<a href="{{AppSubUrl}}/user/sign_up">{{.i18n.Tr "auth.sign_up_now" | Str2html}}</a>
37
-						</div>
38
-					{{end}}
39
-				</div>
40
-			</form>
41
-		</div>
42
-	</div>
43
-</div>
2
+{{template "user/auth/signin_inner" .}}
44 3
 {{template "base/footer" .}}

+ 57 - 0
templates/user/auth/signin_inner.tmpl

@@ -0,0 +1,57 @@
1
+<div class="user signin{{if .LinkAccountMode}} icon{{end}}">
2
+	<div class="ui middle very relaxed page grid">
3
+		<div class="column">
4
+			<form class="ui form" action="{{if not .LinkAccountMode}}{{.Link}}{{else}}{{.SignInLink}}{{end}}" method="post">
5
+				{{.CsrfTokenHtml}}
6
+				<h3 class="ui top attached header">
7
+					{{.i18n.Tr "sign_in"}}
8
+				</h3>
9
+				<div class="ui attached segment">
10
+					{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn)}}
11
+					{{template "base/alert" .}}
12
+					{{end}}
13
+					<div class="required inline field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
14
+						<label for="user_name">{{.i18n.Tr "home.uname_holder"}}</label>
15
+						<input id="user_name" name="user_name" value="{{.user_name}}" autofocus required>
16
+					</div>
17
+					<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
18
+						<label for="password">{{.i18n.Tr "password"}}</label>
19
+						<input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" required>
20
+					</div>
21
+					{{if not .LinkAccountMode}}
22
+					<div class="inline field">
23
+						<label></label>
24
+						<div class="ui checkbox">
25
+							<label>{{.i18n.Tr "auth.remember_me"}}</label>
26
+							<input name="remember" type="checkbox">
27
+						</div>
28
+					</div>
29
+					{{end}}
30
+
31
+					<div class="inline field">
32
+						<label></label>
33
+						<button class="ui green button">{{.i18n.Tr "sign_in"}}</button>
34
+						<a href="{{AppSubUrl}}/user/forget_password">{{.i18n.Tr "auth.forget_password"}}</a>
35
+					</div>
36
+
37
+					{{if .ShowRegistrationButton}}
38
+						<div class="inline field">
39
+							<label></label>
40
+							<a href="{{AppSubUrl}}/user/sign_up">{{.i18n.Tr "auth.sign_up_now" | Str2html}}</a>
41
+						</div>
42
+					{{end}}
43
+
44
+					{{if .OAuth2Providers}}
45
+					<div class="ui attached segment">
46
+						<div class="oauth2 center">
47
+							<div>
48
+								<p>{{.i18n.Tr "sign_in_with"}}</p>{{range $key, $value := .OAuth2Providers}}<a href="{{AppSubUrl}}/user/oauth2/{{$key}}"><img alt="{{$value.DisplayName}}" title="{{$value.DisplayName}}" src="{{AppSubUrl}}{{$value.Image}}"></a>{{end}}
49
+							</div>
50
+						</div>
51
+					</div>
52
+					{{end}}
53
+				</div>
54
+			</form>
55
+		</div>
56
+	</div>
57
+</div>

+ 1 - 54
templates/user/auth/signup.tmpl

@@ -1,56 +1,3 @@
1 1
 {{template "base/head" .}}
2
-<div class="user signup">
3
-	<div class="ui middle very relaxed page grid">
4
-		<div class="column">
5
-			<form class="ui form" action="{{.Link}}" method="post">
6
-				{{.CsrfTokenHtml}}
7
-				<h3 class="ui top attached header">
8
-					{{if .IsSocialLogin}}{{.i18n.Tr "social_sign_in" | Str2html}}{{else}}{{.i18n.Tr "sign_up"}}{{end}}
9
-				</h3>
10
-				<div class="ui attached segment">
11
-					{{template "base/alert" .}}
12
-					{{if .DisableRegistration}}
13
-						<p>{{.i18n.Tr "auth.disable_register_prompt"}}</p>
14
-					{{else}}
15
-						<div class="required inline field {{if .Err_UserName}}error{{end}}">
16
-							<label for="user_name">{{.i18n.Tr "username"}}</label>
17
-							<input id="user_name" name="user_name" value="{{.user_name}}" autofocus required>
18
-						</div>
19
-						<div class="required inline field {{if .Err_Email}}error{{end}}">
20
-							<label for="email">{{.i18n.Tr "email"}}</label>
21
-							<input id="email" name="email" type="email" value="{{.email}}" required>
22
-						</div>
23
-						<div class="required inline field {{if .Err_Password}}error{{end}}">
24
-							<label for="password">{{.i18n.Tr "password"}}</label>
25
-							<input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" required>
26
-						</div>
27
-						<div class="required inline field {{if .Err_Password}}error{{end}}">
28
-							<label for="retype">{{.i18n.Tr "re_type"}}</label>
29
-							<input id="retype" name="retype" type="password" value="{{.retype}}" autocomplete="off" required>
30
-						</div>
31
-						{{if .EnableCaptcha}}
32
-							<div class="inline field">
33
-								<label></label>
34
-								{{.Captcha.CreateHtml}}
35
-							</div>
36
-							<div class="required inline field {{if .Err_Captcha}}error{{end}}">
37
-								<label for="captcha">{{.i18n.Tr "captcha"}}</label>
38
-								<input id="captcha" name="captcha" value="{{.captcha}}" autocomplete="off">
39
-							</div>
40
-						{{end}}
41
-
42
-						<div class="inline field">
43
-							<label></label>
44
-							<button class="ui green button">{{.i18n.Tr "auth.create_new_account"}}</button>
45
-						</div>
46
-						<div class="inline field">
47
-							<label></label>
48
-							<a href="{{AppSubUrl}}/user/login">{{if .IsSocialLogin}}{{.i18n.Tr "auth.social_register_helper_msg"}}{{else}}{{.i18n.Tr "auth.register_helper_msg"}}{{end}}</a>
49
-						</div>
50
-					{{end}}
51
-				</div>
52
-			</form>
53
-		</div>
54
-	</div>
55
-</div>
2
+{{template "user/auth/signup_inner" .}}
56 3
 {{template "base/footer" .}}

+ 59 - 0
templates/user/auth/signup_inner.tmpl

@@ -0,0 +1,59 @@
1
+<div class="user signup{{if .LinkAccountMode}} icon{{end}}">
2
+	<div class="ui middle very relaxed page grid">
3
+		<div class="column">
4
+			<form class="ui form" action="{{if not .LinkAccountMode}}{{.Link}}{{else}}{{.SignUpLink}}{{end}}" method="post">
5
+				{{.CsrfTokenHtml}}
6
+				<h3 class="ui top attached header">
7
+					{{.i18n.Tr "sign_up"}}
8
+				</h3>
9
+				<div class="ui attached segment">
10
+					{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister)}}
11
+					{{template "base/alert" .}}
12
+					{{end}}
13
+					{{if .DisableRegistration}}
14
+						<p>{{.i18n.Tr "auth.disable_register_prompt"}}</p>
15
+					{{else}}
16
+						<div class="required inline field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
17
+							<label for="user_name">{{.i18n.Tr "username"}}</label>
18
+							<input id="user_name" name="user_name" value="{{.user_name}}" autofocus required>
19
+						</div>
20
+						<div class="required inline field {{if .Err_Email}}error{{end}}">
21
+							<label for="email">{{.i18n.Tr "email"}}</label>
22
+							<input id="email" name="email" type="email" value="{{.email}}" required>
23
+						</div>
24
+						<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
25
+							<label for="password">{{.i18n.Tr "password"}}</label>
26
+							<input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" required>
27
+						</div>
28
+						<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
29
+							<label for="retype">{{.i18n.Tr "re_type"}}</label>
30
+							<input id="retype" name="retype" type="password" value="{{.retype}}" autocomplete="off" required>
31
+						</div>
32
+						{{if .EnableCaptcha}}
33
+							<div class="inline field">
34
+								<label></label>
35
+								{{.Captcha.CreateHtml}}
36
+							</div>
37
+							<div class="required inline field {{if .Err_Captcha}}error{{end}}">
38
+								<label for="captcha">{{.i18n.Tr "captcha"}}</label>
39
+								<input id="captcha" name="captcha" value="{{.captcha}}" autocomplete="off">
40
+							</div>
41
+						{{end}}
42
+
43
+						<div class="inline field">
44
+							<label></label>
45
+							<button class="ui green button">{{.i18n.Tr "auth.create_new_account"}}</button>
46
+						</div>
47
+
48
+						{{if not .LinkAccountMode}}
49
+						<div class="inline field">
50
+							<label></label>
51
+							<a href="{{AppSubUrl}}/user/login">{{.i18n.Tr "auth.register_helper_msg"}}</a>
52
+						</div>
53
+						{{end}}
54
+					{{end}}
55
+				</div>
56
+			</form>
57
+		</div>
58
+	</div>
59
+</div>

+ 48 - 0
templates/user/settings/account_link.tmpl

@@ -0,0 +1,48 @@
1
+{{template "base/head" .}}
2
+<div class="user settings account_link">
3
+	<div class="ui container">
4
+		<div class="ui grid">
5
+			{{template "user/settings/navbar" .}}
6
+			<div class="twelve wide column content">
7
+				{{template "base/alert" .}}
8
+				<h4 class="ui top attached header">
9
+					{{.i18n.Tr "settings.manage_account_links"}}
10
+				</h4>
11
+				<div class="ui attached segment">
12
+					<div class="ui key list">
13
+						<div class="item">
14
+							{{.i18n.Tr "settings.manage_account_links_desc"}}
15
+						</div>
16
+						{{if .AccountLinks}}
17
+						{{range $loginSource, $provider := .AccountLinks}}
18
+							<div class="item ui grid">
19
+								<div class="column">
20
+									<strong>{{$provider}}</strong>
21
+									{{if $loginSource.IsActived}}<span class="text red">{{$.i18n.Tr "settings.active"}}</span>{{end}}
22
+									<div class="ui right">
23
+										<button class="ui red tiny button delete-button" data-url="{{$.Link}}" data-id="{{$loginSource.ID}}">
24
+											{{$.i18n.Tr "settings.delete_key"}}
25
+										</button>
26
+									</div>
27
+								</div>
28
+							</div>
29
+						{{end}}
30
+						{{end}}
31
+					</div>
32
+				</div>
33
+			</div>
34
+		</div>
35
+	</div>
36
+</div>
37
+
38
+<div class="ui small basic delete modal">
39
+	<div class="ui icon header">
40
+		<i class="trash icon"></i>
41
+		{{.i18n.Tr "settings.remove_account_link"}}
42
+	</div>
43
+	<div class="content">
44
+		<p>{{.i18n.Tr "settings.remove_account_link_desc"}}</p>
45
+	</div>
46
+	{{template "base/delete_modal_actions" .}}
47
+</div>
48
+{{template "base/footer" .}}

+ 3 - 0
templates/user/settings/navbar.tmpl

@@ -22,6 +22,9 @@
22 22
 		<a class="{{if .PageIsSettingsTwofa}}active{{end}} item" href="{{AppSubUrl}}/user/settings/two_factor">
23 23
 			{{.i18n.Tr "settings.twofa"}}
24 24
 		</a>
25
+		<a class="{{if .PageIsSettingsAccountLink}}active{{end}} item" href="{{AppSubUrl}}/user/settings/account_link">
26
+			{{.i18n.Tr "settings.account_link"}}
27
+		</a>
25 28
 		<a class="{{if .PageIsSettingsDelete}}active{{end}} item" href="{{AppSubUrl}}/user/settings/delete">
26 29
 			{{.i18n.Tr "settings.delete"}}
27 30
 		</a>

+ 3 - 1
templates/user/settings/password.tmpl

@@ -9,13 +9,15 @@
9 9
 					{{.i18n.Tr "settings.change_password"}}
10 10
 				</h4>
11 11
 				<div class="ui attached segment">
12
-					{{if .SignedUser.IsLocal}}
12
+					{{if or (.SignedUser.IsLocal) (.SignedUser.IsOAuth2)}}
13 13
 					<form class="ui form" action="{{.Link}}" method="post">
14 14
 						{{.CsrfTokenHtml}}
15
+						{{if .SignedUser.IsPasswordSet}}
15 16
 						<div class="required field {{if .Err_OldPassword}}error{{end}}">
16 17
 							<label for="old_password">{{.i18n.Tr "settings.old_password"}}</label>
17 18
 							<input id="old_password" name="old_password" type="password" autocomplete="off" autofocus required>
18 19
 						</div>
20
+						{{end}}
19 21
 						<div class="required field {{if .Err_Password}}error{{end}}">
20 22
 							<label for="password">{{.i18n.Tr "settings.new_password"}}</label>
21 23
 							<input id="password" name="password" type="password" autocomplete="off" required>

+ 27 - 0
vendor/github.com/gorilla/context/LICENSE

@@ -0,0 +1,27 @@
1
+Copyright (c) 2012 Rodrigo Moraes. All rights reserved.
2
+
3
+Redistribution and use in source and binary forms, with or without
4
+modification, are permitted provided that the following conditions are
5
+met:
6
+
7
+	 * Redistributions of source code must retain the above copyright
8
+notice, this list of conditions and the following disclaimer.
9
+	 * Redistributions in binary form must reproduce the above
10
+copyright notice, this list of conditions and the following disclaimer
11
+in the documentation and/or other materials provided with the
12
+distribution.
13
+	 * Neither the name of Google Inc. nor the names of its
14
+contributors may be used to endorse or promote products derived from
15
+this software without specific prior written permission.
16
+
17
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 10 - 0
vendor/github.com/gorilla/context/README.md

@@ -0,0 +1,10 @@
1
+context
2
+=======
3
+[![Build Status](https://travis-ci.org/gorilla/context.png?branch=master)](https://travis-ci.org/gorilla/context)
4
+
5
+gorilla/context is a general purpose registry for global request variables.
6
+
7
+> Note: gorilla/context, having been born well before `context.Context` existed, does not play well
8
+> 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()`.
9
+
10
+Read the full documentation here: http://www.gorillatoolkit.org/pkg/context

+ 143 - 0
vendor/github.com/gorilla/context/context.go

@@ -0,0 +1,143 @@
1
+// Copyright 2012 The Gorilla Authors. All rights reserved.
2
+// Use of this source code is governed by a BSD-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package context
6
+
7
+import (
8
+	"net/http"
9
+	"sync"
10
+	"time"
11
+)
12
+
13
+var (
14
+	mutex sync.RWMutex
15
+	data  = make(map[*http.Request]map[interface{}]interface{})
16
+	datat = make(map[*http.Request]int64)
17
+)
18
+
19
+// Set stores a value for a given key in a given request.
20
+func Set(r *http.Request, key, val interface{}) {
21
+	mutex.Lock()
22
+	if data[r] == nil {
23
+		data[r] = make(map[interface{}]interface{})
24
+		datat[r] = time.Now().Unix()
25
+	}
26
+	data[r][key] = val
27
+	mutex.Unlock()
28
+}
29
+
30
+// Get returns a value stored for a given key in a given request.
31
+func Get(r *http.Request, key interface{}) interface{} {
32
+	mutex.RLock()
33
+	if ctx := data[r]; ctx != nil {
34
+		value := ctx[key]
35
+		mutex.RUnlock()
36
+		return value
37
+	}
38
+	mutex.RUnlock()
39
+	return nil
40
+}
41
+
42
+// GetOk returns stored value and presence state like multi-value return of map access.
43
+func GetOk(r *http.Request, key interface{}) (interface{}, bool) {
44
+	mutex.RLock()
45
+	if _, ok := data[r]; ok {
46
+		value, ok := data[r][key]
47
+		mutex.RUnlock()
48
+		return value, ok
49
+	}
50
+	mutex.RUnlock()
51
+	return nil, false
52
+}
53
+
54
+// GetAll returns all stored values for the request as a map. Nil is returned for invalid requests.
55
+func GetAll(r *http.Request) map[interface{}]interface{} {
56
+	mutex.RLock()
57
+	if context, ok := data[r]; ok {
58
+		result := make(map[interface{}]interface{}, len(context))
59
+		for k, v := range context {
60
+			result[k] = v
61
+		}
62
+		mutex.RUnlock()
63
+		return result
64
+	}
65
+	mutex.RUnlock()
66
+	return nil
67
+}
68
+
69
+// GetAllOk returns all stored values for the request as a map and a boolean value that indicates if
70
+// the request was registered.
71
+func GetAllOk(r *http.Request) (map[interface{}]interface{}, bool) {
72
+	mutex.RLock()
73
+	context, ok := data[r]
74
+	result := make(map[interface{}]interface{}, len(context))
75
+	for k, v := range context {
76
+		result[k] = v
77
+	}
78
+	mutex.RUnlock()
79
+	return result, ok
80
+}
81
+
82
+// Delete removes a value stored for a given key in a given request.
83
+func Delete(r *http.Request, key interface{}) {
84
+	mutex.Lock()
85
+	if data[r] != nil {
86
+		delete(data[r], key)
87
+	}
88
+	mutex.Unlock()
89
+}
90
+
91
+// Clear removes all values stored for a given request.
92
+//
93
+// This is usually called by a handler wrapper to clean up request
94
+// variables at the end of a request lifetime. See ClearHandler().
95
+func Clear(r *http.Request) {
96
+	mutex.Lock()
97
+	clear(r)
98
+	mutex.Unlock()
99
+}
100
+
101
+// clear is Clear without the lock.
102
+func clear(r *http.Request) {
103
+	delete(data, r)
104
+	delete(datat, r)
105
+}
106
+
107
+// Purge removes request data stored for longer than maxAge, in seconds.
108
+// It returns the amount of requests removed.
109
+//
110
+// If maxAge <= 0, all request data is removed.
111
+//
112
+// This is only used for sanity check: in case context cleaning was not
113
+// properly set some request data can be kept forever, consuming an increasing
114
+// amount of memory. In case this is detected, Purge() must be called
115
+// periodically until the problem is fixed.
116
+func Purge(maxAge int) int {
117
+	mutex.Lock()
118
+	count := 0
119
+	if maxAge <= 0 {
120
+		count = len(data)
121
+		data = make(map[*http.Request]map[interface{}]interface{})
122
+		datat = make(map[*http.Request]int64)
123
+	} else {
124
+		min := time.Now().Unix() - int64(maxAge)
125
+		for r := range data {
126
+			if datat[r] < min {
127
+				clear(r)
128
+				count++
129
+			}
130
+		}
131
+	}
132
+	mutex.Unlock()
133
+	return count
134
+}
135
+
136
+// ClearHandler wraps an http.Handler and clears request values at the end
137
+// of a request lifetime.
138
+func ClearHandler(h http.Handler) http.Handler {
139
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
140
+		defer Clear(r)
141
+		h.ServeHTTP(w, r)
142
+	})
143
+}

+ 88 - 0
vendor/github.com/gorilla/context/doc.go

@@ -0,0 +1,88 @@
1
+// Copyright 2012 The Gorilla Authors. All rights reserved.
2
+// Use of this source code is governed by a BSD-style
3
+// license that can be found in the LICENSE file.
4
+
5
+/*
6
+Package context stores values shared during a request lifetime.
7
+
8
+Note: gorilla/context, having been born well before `context.Context` existed,
9
+does not play well > with the shallow copying of the request that
10
+[`http.Request.WithContext`](https://golang.org/pkg/net/http/#Request.WithContext)
11
+(added to net/http Go 1.7 onwards) performs. You should either use *just*
12
+gorilla/context, or moving forward, the new `http.Request.Context()`.
13
+
14
+For example, a router can set variables extracted from the URL and later
15
+application handlers can access those values, or it can be used to store
16
+sessions values to be saved at the end of a request. There are several
17
+others common uses.
18
+
19
+The idea was posted by Brad Fitzpatrick to the go-nuts mailing list:
20
+
21
+	http://groups.google.com/group/golang-nuts/msg/e2d679d303aa5d53
22
+
23
+Here's the basic usage: first define the keys that you will need. The key
24
+type is interface{} so a key can be of any type that supports equality.
25
+Here we define a key using a custom int type to avoid name collisions:
26
+
27
+	package foo
28
+
29
+	import (
30
+		"github.com/gorilla/context"
31
+	)
32
+
33
+	type key int
34
+
35
+	const MyKey key = 0
36
+
37
+Then set a variable. Variables are bound to an http.Request object, so you
38
+need a request instance to set a value:
39
+
40
+	context.Set(r, MyKey, "bar")
41
+
42
+The application can later access the variable using the same key you provided:
43
+
44
+	func MyHandler(w http.ResponseWriter, r *http.Request) {
45
+		// val is "bar".
46
+		val := context.Get(r, foo.MyKey)
47
+
48
+		// returns ("bar", true)
49
+		val, ok := context.GetOk(r, foo.MyKey)
50
+		// ...
51
+	}
52
+
53
+And that's all about the basic usage. We discuss some other ideas below.
54
+
55
+Any type can be stored in the context. To enforce a given type, make the key
56
+private and wrap Get() and Set() to accept and return values of a specific
57
+type:
58
+
59
+	type key int
60
+
61
+	const mykey key = 0
62
+
63
+	// GetMyKey returns a value for this package from the request values.
64
+	func GetMyKey(r *http.Request) SomeType {
65
+		if rv := context.Get(r, mykey); rv != nil {
66
+			return rv.(SomeType)
67
+		}
68
+		return nil
69
+	}
70
+
71
+	// SetMyKey sets a value for this package in the request values.
72
+	func SetMyKey(r *http.Request, val SomeType) {
73
+		context.Set(r, mykey, val)
74
+	}
75
+
76
+Variables must be cleared at the end of a request, to remove all values
77
+that were stored. This can be done in an http.Handler, after a request was
78
+served. Just call Clear() passing the request:
79
+
80
+	context.Clear(r)
81
+
82
+...or use ClearHandler(), which conveniently wraps an http.Handler to clear
83
+variables at the end of a request lifetime.
84
+
85
+The Routers from the packages gorilla/mux and gorilla/pat call Clear()
86
+so if you are using either of them you don't need to clear the context manually.
87
+*/
88
+package context

+ 27 - 0
vendor/github.com/gorilla/mux/LICENSE

@@ -0,0 +1,27 @@
1
+Copyright (c) 2012 Rodrigo Moraes. All rights reserved.
2
+
3
+Redistribution and use in source and binary forms, with or without
4
+modification, are permitted provided that the following conditions are
5
+met:
6
+
7
+	 * Redistributions of source code must retain the above copyright
8
+notice, this list of conditions and the following disclaimer.
9
+	 * Redistributions in binary form must reproduce the above
10
+copyright notice, this list of conditions and the following disclaimer
11
+in the documentation and/or other materials provided with the
12
+distribution.
13
+	 * Neither the name of Google Inc. nor the names of its
14
+contributors may be used to endorse or promote products derived from
15
+this software without specific prior written permission.
16
+
17
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 299 - 0
vendor/github.com/gorilla/mux/README.md

@@ -0,0 +1,299 @@
1
+gorilla/mux
2
+===
3
+[![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux)
4
+[![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux)
5
+
6
+![Gorilla Logo](http://www.gorillatoolkit.org/static/images/gorilla-icon-64.png)
7
+
8
+http://www.gorillatoolkit.org/pkg/mux
9
+
10
+Package `gorilla/mux` implements a request router and dispatcher for matching incoming requests to
11
+their respective handler.
12
+
13
+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:
14
+
15
+* It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`.
16
+* Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers.
17
+* URL hosts and paths can have variables with an optional regular expression.
18
+* Registered URLs can be built, or "reversed", which helps maintaining references to resources.
19
+* 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.
20
+
21
+---
22
+
23
+* [Install](#install)
24
+* [Examples](#examples)
25
+* [Matching Routes](#matching-routes)
26
+* [Static Files](#static-files)
27
+* [Registered URLs](#registered-urls)
28
+* [Full Example](#full-example)
29
+
30
+---
31
+
32
+## Install
33
+
34
+With a [correctly configured](https://golang.org/doc/install#testing) Go toolchain:
35
+
36
+```sh
37
+go get -u github.com/gorilla/mux
38
+```
39
+
40
+## Examples
41
+
42
+Let's start registering a couple of URL paths and handlers:
43
+
44
+```go
45
+func main() {
46
+	r := mux.NewRouter()
47
+	r.HandleFunc("/", HomeHandler)
48
+	r.HandleFunc("/products", ProductsHandler)
49
+	r.HandleFunc("/articles", ArticlesHandler)
50
+	http.Handle("/", r)
51
+}
52
+```
53
+
54
+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.
55
+
56
+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:
57
+
58
+```go
59
+r := mux.NewRouter()
60
+r.HandleFunc("/products/{key}", ProductHandler)
61
+r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
62
+r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)
63
+```
64
+
65
+The names are used to create a map of route variables which can be retrieved calling `mux.Vars()`:
66
+
67
+```go
68
+vars := mux.Vars(request)
69
+category := vars["category"]
70
+```
71
+
72
+And this is all you need to know about the basic usage. More advanced options are explained below.
73
+
74
+### Matching Routes
75
+
76
+Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables:
77
+
78
+```go
79
+r := mux.NewRouter()
80
+// Only matches if domain is "www.example.com".
81
+r.Host("www.example.com")
82
+// Matches a dynamic subdomain.
83
+r.Host("{subdomain:[a-z]+}.domain.com")
84
+```
85
+
86
+There are several other matchers that can be added. To match path prefixes:
87
+
88
+```go
89
+r.PathPrefix("/products/")
90
+```
91
+
92
+...or HTTP methods:
93
+
94
+```go
95
+r.Methods("GET", "POST")
96
+```
97
+
98
+...or URL schemes:
99
+
100
+```go
101
+r.Schemes("https")
102
+```
103
+
104
+...or header values:
105
+
106
+```go
107
+r.Headers("X-Requested-With", "XMLHttpRequest")
108
+```
109
+
110
+...or query values:
111
+
112
+```go
113
+r.Queries("key", "value")
114
+```
115
+
116
+...or to use a custom matcher function:
117
+
118
+```go
119
+r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool {
120
+	return r.ProtoMajor == 0
121
+})
122
+```
123
+
124
+...and finally, it is possible to combine several matchers in a single route:
125
+
126
+```go
127
+r.HandleFunc("/products", ProductsHandler).
128
+  Host("www.example.com").
129
+  Methods("GET").
130
+  Schemes("http")
131
+```
132
+
133
+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".
134
+
135
+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:
136
+
137
+```go
138
+r := mux.NewRouter()
139
+s := r.Host("www.example.com").Subrouter()
140
+```
141
+
142
+Then register routes in the subrouter:
143
+
144
+```go
145
+s.HandleFunc("/products/", ProductsHandler)
146
+s.HandleFunc("/products/{key}", ProductHandler)
147
+s.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)
148
+```
149
+
150
+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.
151
+
152
+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.
153
+
154
+There's one more thing about subroutes. When a subrouter has a path prefix, the inner routes use it as base for their paths:
155
+
156
+```go
157
+r := mux.NewRouter()
158
+s := r.PathPrefix("/products").Subrouter()
159
+// "/products/"
160
+s.HandleFunc("/", ProductsHandler)
161
+// "/products/{key}/"
162
+s.HandleFunc("/{key}/", ProductHandler)
163
+// "/products/{key}/details"
164
+s.HandleFunc("/{key}/details", ProductDetailsHandler)
165
+```
166
+
167
+### Static Files
168
+
169
+Note that the path provided to `PathPrefix()` represents a "wildcard": calling
170
+`PathPrefix("/static/").Handler(...)` means that the handler will be passed any
171
+request that matches "/static/*". This makes it easy to serve static files with mux:
172
+
173
+```go
174
+func main() {
175
+	var dir string
176
+
177
+	flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir")
178
+	flag.Parse()
179
+	r := mux.NewRouter()
180
+
181
+	// This will serve files under http://localhost:8000/static/<filename>
182
+	r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir))))
183
+
184
+	srv := &http.Server{
185
+		Handler:      r,
186
+		Addr:         "127.0.0.1:8000",
187
+		// Good practice: enforce timeouts for servers you create!
188
+		WriteTimeout: 15 * time.Second,
189
+		ReadTimeout:  15 * time.Second,
190
+	}
191
+
192
+	log.Fatal(srv.ListenAndServe())
193
+}
194
+```
195
+
196
+### Registered URLs
197
+
198
+Now let's see how to build registered URLs.
199
+
200
+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:
201
+
202
+```go
203
+r := mux.NewRouter()
204
+r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
205
+  Name("article")
206
+```
207
+
208
+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:
209
+
210
+```go
211
+url, err := r.Get("article").URL("category", "technology", "id", "42")
212
+```
213
+
214
+...and the result will be a `url.URL` with the following path:
215
+
216
+```
217
+"/articles/technology/42"
218
+```
219
+
220
+This also works for host variables:
221
+
222
+```go
223
+r := mux.NewRouter()
224
+r.Host("{subdomain}.domain.com").
225
+  Path("/articles/{category}/{id:[0-9]+}").
226
+  HandlerFunc(ArticleHandler).
227
+  Name("article")
228
+
229
+// url.String() will be "http://news.domain.com/articles/technology/42"
230
+url, err := r.Get("article").URL("subdomain", "news",
231
+                                 "category", "technology",
232
+                                 "id", "42")
233
+```
234
+
235
+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.
236
+
237
+Regex support also exists for matching Headers within a route. For example, we could do:
238
+
239
+```go
240
+r.HeadersRegexp("Content-Type", "application/(text|json)")
241
+```
242
+
243
+...and the route will match both requests with a Content-Type of `application/json` as well as `application/text`
244
+
245
+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:
246
+
247
+```go
248
+// "http://news.domain.com/"
249
+host, err := r.Get("article").URLHost("subdomain", "news")
250
+
251
+// "/articles/technology/42"
252
+path, err := r.Get("article").URLPath("category", "technology", "id", "42")
253
+```
254
+
255
+And if you use subrouters, host and path defined separately can be built as well:
256
+
257
+```go
258
+r := mux.NewRouter()
259
+s := r.Host("{subdomain}.domain.com").Subrouter()
260
+s.Path("/articles/{category}/{id:[0-9]+}").
261
+  HandlerFunc(ArticleHandler).
262
+  Name("article")
263
+
264
+// "http://news.domain.com/articles/technology/42"
265
+url, err := r.Get("article").URL("subdomain", "news",
266
+                                 "category", "technology",
267
+                                 "id", "42")
268
+```
269
+
270
+## Full Example
271
+
272
+Here's a complete, runnable example of a small `mux` based server:
273
+
274
+```go
275
+package main
276
+
277
+import (
278
+	"net/http"
279
+	"log"
280
+	"github.com/gorilla/mux"
281
+)
282
+
283
+func YourHandler(w http.ResponseWriter, r *http.Request) {
284
+	w.Write([]byte("Gorilla!\n"))
285
+}
286
+
287
+func main() {
288
+	r := mux.NewRouter()
289
+	// Routes consist of a path and a handler function.
290
+	r.HandleFunc("/", YourHandler)
291
+
292
+	// Bind to a port and pass our router in
293
+	log.Fatal(http.ListenAndServe(":8000", r))
294
+}
295
+```
296
+
297
+## License
298
+
299
+BSD licensed. See the LICENSE file for details.

+ 26 - 0
vendor/github.com/gorilla/mux/context_gorilla.go

@@ -0,0 +1,26 @@
1
+// +build !go1.7
2
+
3
+package mux
4
+
5
+import (
6
+	"net/http"
7
+
8
+	"github.com/gorilla/context"
9
+)
10
+
11
+func contextGet(r *http.Request, key interface{}) interface{} {
12
+	return context.Get(r, key)
13
+}
14
+
15
+func contextSet(r *http.Request, key, val interface{}) *http.Request {
16
+	if val == nil {
17
+		return r
18
+	}
19
+
20
+	context.Set(r, key, val)
21
+	return r
22
+}
23
+
24
+func contextClear(r *http.Request) {
25
+	context.Clear(r)
26
+}

+ 24 - 0
vendor/github.com/gorilla/mux/context_native.go

@@ -0,0 +1,24 @@
1
+// +build go1.7
2
+
3
+package mux
4
+
5
+import (
6
+	"context"
7
+	"net/http"
8
+)
9
+
10
+func contextGet(r *http.Request, key interface{}) interface{} {
11
+	return r.Context().Value(key)
12
+}
13
+
14
+func contextSet(r *http.Request, key, val interface{}) *http.Request {
15
+	if val == nil {
16
+		return r
17
+	}
18
+
19
+	return r.WithContext(context.WithValue(r.Context(), key, val))
20
+}
21
+
22
+func contextClear(r *http.Request) {
23
+	return
24
+}

+ 235 - 0
vendor/github.com/gorilla/mux/doc.go

@@ -0,0 +1,235 @@
1
+// Copyright 2012 The Gorilla Authors. All rights reserved.
2
+// Use of this source code is governed by a BSD-style
3
+// license that can be found in the LICENSE file.
4
+
5
+/*
6
+Package mux implements a request router and dispatcher.
7
+
8
+The name mux stands for "HTTP request multiplexer". Like the standard
9
+http.ServeMux, mux.Router matches incoming requests against a list of
10
+registered routes and calls a handler for the route that matches the URL
11
+or other conditions. The main features are:
12
+
13
+	* Requests can be matched based on URL host, path, path prefix, schemes,
14
+	  header and query values, HTTP methods or using custom matchers.
15
+	* URL hosts and paths can have variables with an optional regular
16
+	  expression.
17
+	* Registered URLs can be built, or "reversed", which helps maintaining
18
+	  references to resources.
19
+	* Routes can be used as subrouters: nested routes are only tested if the
20
+	  parent route matches. This is useful to define groups of routes that
21
+	  share common conditions like a host, a path prefix or other repeated
22
+	  attributes. As a bonus, this optimizes request matching.
23
+	* It implements the http.Handler interface so it is compatible with the
24
+	  standard http.ServeMux.
25
+
26
+Let's start registering a couple of URL paths and handlers:
27
+
28
+	func main() {
29
+		r := mux.NewRouter()
30
+		r.HandleFunc("/", HomeHandler)
31
+		r.HandleFunc("/products", ProductsHandler)
32
+		r.HandleFunc("/articles", ArticlesHandler)
33
+		http.Handle("/", r)
34
+	}
35
+
36
+Here we register three routes mapping URL paths to handlers. This is
37
+equivalent to how http.HandleFunc() works: if an incoming request URL matches
38
+one of the paths, the corresponding handler is called passing
39
+(http.ResponseWriter, *http.Request) as parameters.
40
+
41
+Paths can have variables. They are defined using the format {name} or
42
+{name:pattern}. If a regular expression pattern is not defined, the matched
43
+variable will be anything until the next slash. For example:
44
+
45
+	r := mux.NewRouter()
46
+	r.HandleFunc("/products/{key}", ProductHandler)
47
+	r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
48
+	r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)
49
+
50
+Groups can be used inside patterns, as long as they are non-capturing (?:re). For example:
51
+