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)tokarchuk/v1.17
parent
fd941db246
commit
01d957677f
@ -0,0 +1,74 @@ |
|||||||
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package models |
||||||
|
|
||||||
|
import "github.com/markbates/goth" |
||||||
|
|
||||||
|
// ExternalLoginUser makes the connecting between some existing user and additional external login sources
|
||||||
|
type ExternalLoginUser struct { |
||||||
|
ExternalID string `xorm:"NOT NULL"` |
||||||
|
UserID int64 `xorm:"NOT NULL"` |
||||||
|
LoginSourceID int64 `xorm:"NOT NULL"` |
||||||
|
} |
||||||
|
|
||||||
|
// GetExternalLogin checks if a externalID in loginSourceID scope already exists
|
||||||
|
func GetExternalLogin(externalLoginUser *ExternalLoginUser) (bool, error) { |
||||||
|
return x.Get(externalLoginUser) |
||||||
|
} |
||||||
|
|
||||||
|
// ListAccountLinks returns a map with the ExternalLoginUser and its LoginSource
|
||||||
|
func ListAccountLinks(user *User) ([]*ExternalLoginUser, error) { |
||||||
|
externalAccounts := make([]*ExternalLoginUser, 0, 5) |
||||||
|
err := x.Where("user_id=?", user.ID). |
||||||
|
Desc("login_source_id"). |
||||||
|
Find(&externalAccounts) |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return externalAccounts, nil |
||||||
|
} |
||||||
|
|
||||||
|
// LinkAccountToUser link the gothUser to the user
|
||||||
|
func LinkAccountToUser(user *User, gothUser goth.User) error { |
||||||
|
loginSource, err := GetActiveOAuth2LoginSourceByName(gothUser.Provider) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
externalLoginUser := &ExternalLoginUser{ |
||||||
|
ExternalID: gothUser.UserID, |
||||||
|
UserID: user.ID, |
||||||
|
LoginSourceID: loginSource.ID, |
||||||
|
} |
||||||
|
has, err := x.Get(externalLoginUser) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} else if has { |
||||||
|
return ErrExternalLoginUserAlreadyExist{gothUser.UserID, user.ID, loginSource.ID} |
||||||
|
} |
||||||
|
|
||||||
|
_, err = x.Insert(externalLoginUser) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// RemoveAccountLink will remove all external login sources for the given user
|
||||||
|
func RemoveAccountLink(user *User, loginSourceID int64) (int64, error) { |
||||||
|
deleted, err := x.Delete(&ExternalLoginUser{UserID: user.ID, LoginSourceID: loginSourceID}) |
||||||
|
if err != nil { |
||||||
|
return deleted, err |
||||||
|
} |
||||||
|
if deleted < 1 { |
||||||
|
return deleted, ErrExternalLoginUserNotExist{user.ID, loginSourceID} |
||||||
|
} |
||||||
|
return deleted, err |
||||||
|
} |
||||||
|
|
||||||
|
// RemoveAllAccountLinks will remove all external login sources for the given user
|
||||||
|
func RemoveAllAccountLinks(user *User) error { |
||||||
|
_, err := x.Delete(&ExternalLoginUser{UserID: user.ID}) |
||||||
|
return err |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
// Copyright 2016 Gitea. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package migrations |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
|
||||||
|
"github.com/go-xorm/xorm" |
||||||
|
) |
||||||
|
|
||||||
|
// ExternalLoginUser makes the connecting between some existing user and additional external login sources
|
||||||
|
type ExternalLoginUser struct { |
||||||
|
ExternalID string `xorm:"NOT NULL"` |
||||||
|
UserID int64 `xorm:"NOT NULL"` |
||||||
|
LoginSourceID int64 `xorm:"NOT NULL"` |
||||||
|
} |
||||||
|
|
||||||
|
func addExternalLoginUser(x *xorm.Engine) error { |
||||||
|
if err := x.Sync2(new(ExternalLoginUser)); err != nil { |
||||||
|
return fmt.Errorf("Sync2: %v", err) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,105 @@ |
|||||||
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package oauth2 |
||||||
|
|
||||||
|
import ( |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
"code.gitea.io/gitea/modules/log" |
||||||
|
"github.com/gorilla/sessions" |
||||||
|
"github.com/markbates/goth" |
||||||
|
"github.com/markbates/goth/gothic" |
||||||
|
"net/http" |
||||||
|
"os" |
||||||
|
"github.com/satori/go.uuid" |
||||||
|
"path/filepath" |
||||||
|
"github.com/markbates/goth/providers/github" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
sessionUsersStoreKey = "gitea-oauth2-sessions" |
||||||
|
providerHeaderKey = "gitea-oauth2-provider" |
||||||
|
) |
||||||
|
|
||||||
|
// Init initialize the setup of the OAuth2 library
|
||||||
|
func Init() { |
||||||
|
sessionDir := filepath.Join(setting.AppDataPath, "sessions", "oauth2") |
||||||
|
if err := os.MkdirAll(sessionDir, 0700); err != nil { |
||||||
|
log.Fatal(4, "Fail to create dir %s: %v", sessionDir, err) |
||||||
|
} |
||||||
|
|
||||||
|
gothic.Store = sessions.NewFilesystemStore(sessionDir, []byte(sessionUsersStoreKey)) |
||||||
|
|
||||||
|
gothic.SetState = func(req *http.Request) string { |
||||||
|
return uuid.NewV4().String() |
||||||
|
} |
||||||
|
|
||||||
|
gothic.GetProviderName = func(req *http.Request) (string, error) { |
||||||
|
return req.Header.Get(providerHeaderKey), nil |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// Auth OAuth2 auth service
|
||||||
|
func Auth(provider string, request *http.Request, response http.ResponseWriter) error { |
||||||
|
// not sure if goth is thread safe (?) when using multiple providers
|
||||||
|
request.Header.Set(providerHeaderKey, provider) |
||||||
|
|
||||||
|
// don't use the default gothic begin handler to prevent issues when some error occurs
|
||||||
|
// normally the gothic library will write some custom stuff to the response instead of our own nice error page
|
||||||
|
//gothic.BeginAuthHandler(response, request)
|
||||||
|
|
||||||
|
url, err := gothic.GetAuthURL(response, request) |
||||||
|
if err == nil { |
||||||
|
http.Redirect(response, request, url, http.StatusTemporaryRedirect) |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// ProviderCallback handles OAuth callback, resolve to a goth user and send back to original url
|
||||||
|
// this will trigger a new authentication request, but because we save it in the session we can use that
|
||||||
|
func ProviderCallback(provider string, request *http.Request, response http.ResponseWriter) (goth.User, error) { |
||||||
|
// not sure if goth is thread safe (?) when using multiple providers
|
||||||
|
request.Header.Set(providerHeaderKey, provider) |
||||||
|
|
||||||
|
user, err := gothic.CompleteUserAuth(response, request) |
||||||
|
if err != nil { |
||||||
|
return user, err |
||||||
|
} |
||||||
|
|
||||||
|
return user, nil |
||||||
|
} |
||||||
|
|
||||||
|
// RegisterProvider register a OAuth2 provider in goth lib
|
||||||
|
func RegisterProvider(providerName, providerType, clientID, clientSecret string) { |
||||||
|
provider := createProvider(providerName, providerType, clientID, clientSecret) |
||||||
|
|
||||||
|
if provider != nil { |
||||||
|
goth.UseProviders(provider) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// RemoveProvider removes the given OAuth2 provider from the goth lib
|
||||||
|
func RemoveProvider(providerName string) { |
||||||
|
delete(goth.GetProviders(), providerName) |
||||||
|
} |
||||||
|
|
||||||
|
// used to create different types of goth providers
|
||||||
|
func createProvider(providerName, providerType, clientID, clientSecret string) goth.Provider { |
||||||
|
callbackURL := setting.AppURL + "user/oauth2/" + providerName + "/callback" |
||||||
|
|
||||||
|
var provider goth.Provider |
||||||
|
|
||||||
|
switch providerType { |
||||||
|
case "github": |
||||||
|
provider = github.New(clientID, clientSecret, callbackURL, "user:email") |
||||||
|
} |
||||||
|
|
||||||
|
// always set the name if provider is created so we can support multiple setups of 1 provider
|
||||||
|
if provider != nil { |
||||||
|
provider.SetName(providerName) |
||||||
|
} |
||||||
|
|
||||||
|
return provider |
||||||
|
} |
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,13 @@ |
|||||||
|
{{template "base/head" .}} |
||||||
|
<div class="user link-account"> |
||||||
|
<div class="ui middle very relaxed page grid"> |
||||||
|
<div class="column"> |
||||||
|
<p class="large center"> |
||||||
|
{{.i18n.Tr "link_account_signin_or_signup"}} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{template "user/auth/signin_inner" .}} |
||||||
|
{{template "user/auth/signup_inner" .}} |
||||||
|
{{template "base/footer" .}} |
@ -1,44 +1,3 @@ |
|||||||
{{template "base/head" .}} |
{{template "base/head" .}} |
||||||
<div class="user signin"> |
{{template "user/auth/signin_inner" .}} |
||||||
<div class="ui middle very relaxed page grid"> |
|
||||||
<div class="column"> |
|
||||||
<form class="ui form" action="{{.Link}}" method="post"> |
|
||||||
{{.CsrfTokenHtml}} |
|
||||||
<h3 class="ui top attached header"> |
|
||||||
{{.i18n.Tr "sign_in"}} |
|
||||||
</h3> |
|
||||||
<div class="ui attached segment"> |
|
||||||
{{template "base/alert" .}} |
|
||||||
<div class="required inline field {{if .Err_UserName}}error{{end}}"> |
|
||||||
<label for="user_name">{{.i18n.Tr "home.uname_holder"}}</label> |
|
||||||
<input id="user_name" name="user_name" value="{{.user_name}}" autofocus required> |
|
||||||
</div> |
|
||||||
<div class="required inline field {{if .Err_Password}}error{{end}}"> |
|
||||||
<label for="password">{{.i18n.Tr "password"}}</label> |
|
||||||
<input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" required> |
|
||||||
</div> |
|
||||||
<div class="inline field"> |
|
||||||
<label></label> |
|
||||||
<div class="ui checkbox"> |
|
||||||
<label>{{.i18n.Tr "auth.remember_me"}}</label> |
|
||||||
<input name="remember" type="checkbox"> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="inline field"> |
|
||||||
<label></label> |
|
||||||
<button class="ui green button">{{.i18n.Tr "sign_in"}}</button> |
|
||||||
<a href="{{AppSubUrl}}/user/forget_password">{{.i18n.Tr "auth.forget_password"}}</a> |
|
||||||
</div> |
|
||||||
{{if .ShowRegistrationButton}} |
|
||||||
<div class="inline field"> |
|
||||||
<label></label> |
|
||||||
<a href="{{AppSubUrl}}/user/sign_up">{{.i18n.Tr "auth.sign_up_now" | Str2html}}</a> |
|
||||||
</div> |
|
||||||
{{end}} |
|
||||||
</div> |
|
||||||
</form> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
{{template "base/footer" .}} |
{{template "base/footer" .}} |
||||||
|
@ -0,0 +1,57 @@ |
|||||||
|
<div class="user signin{{if .LinkAccountMode}} icon{{end}}"> |
||||||
|
<div class="ui middle very relaxed page grid"> |
||||||
|
<div class="column"> |
||||||
|
<form class="ui form" action="{{if not .LinkAccountMode}}{{.Link}}{{else}}{{.SignInLink}}{{end}}" method="post"> |
||||||
|
{{.CsrfTokenHtml}} |
||||||
|
<h3 class="ui top attached header"> |
||||||
|
{{.i18n.Tr "sign_in"}} |
||||||
|
</h3> |
||||||
|
<div class="ui attached segment"> |
||||||
|
{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn)}} |
||||||
|
{{template "base/alert" .}} |
||||||
|
{{end}} |
||||||
|
<div class="required inline field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}"> |
||||||
|
<label for="user_name">{{.i18n.Tr "home.uname_holder"}}</label> |
||||||
|
<input id="user_name" name="user_name" value="{{.user_name}}" autofocus required> |
||||||
|
</div> |
||||||
|
<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}"> |
||||||
|
<label for="password">{{.i18n.Tr "password"}}</label> |
||||||
|
<input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" required> |
||||||
|
</div> |
||||||
|
{{if not .LinkAccountMode}} |
||||||
|
<div class="inline field"> |
||||||
|
<label></label> |
||||||
|
<div class="ui checkbox"> |
||||||
|
<label>{{.i18n.Tr "auth.remember_me"}}</label> |
||||||
|
<input name="remember" type="checkbox"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{end}} |
||||||
|
|
||||||
|
<div class="inline field"> |
||||||
|
<label></label> |
||||||
|
<button class="ui green button">{{.i18n.Tr "sign_in"}}</button> |
||||||
|
<a href="{{AppSubUrl}}/user/forget_password">{{.i18n.Tr "auth.forget_password"}}</a> |
||||||
|
</div> |
||||||
|
|
||||||
|
{{if .ShowRegistrationButton}} |
||||||
|
<div class="inline field"> |
||||||
|
<label></label> |
||||||
|
<a href="{{AppSubUrl}}/user/sign_up">{{.i18n.Tr "auth.sign_up_now" | Str2html}}</a> |
||||||
|
</div> |
||||||
|
{{end}} |
||||||
|
|
||||||
|
{{if .OAuth2Providers}} |
||||||
|
<div class="ui attached segment"> |
||||||
|
<div class="oauth2 center"> |
||||||
|
<div> |
||||||
|
<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}} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
@ -1,56 +1,3 @@ |
|||||||
{{template "base/head" .}} |
{{template "base/head" .}} |
||||||
<div class="user signup"> |
{{template "user/auth/signup_inner" .}} |
||||||
<div class="ui middle very relaxed page grid"> |
|
||||||
<div class="column"> |
|
||||||
<form class="ui form" action="{{.Link}}" method="post"> |
|
||||||
{{.CsrfTokenHtml}} |
|
||||||
<h3 class="ui top attached header"> |
|
||||||
{{if .IsSocialLogin}}{{.i18n.Tr "social_sign_in" | Str2html}}{{else}}{{.i18n.Tr "sign_up"}}{{end}} |
|
||||||
</h3> |
|
||||||
<div class="ui attached segment"> |
|
||||||
{{template "base/alert" .}} |
|
||||||
{{if .DisableRegistration}} |
|
||||||
<p>{{.i18n.Tr "auth.disable_register_prompt"}}</p> |
|
||||||
{{else}} |
|
||||||
<div class="required inline field {{if .Err_UserName}}error{{end}}"> |
|
||||||
<label for="user_name">{{.i18n.Tr "username"}}</label> |
|
||||||
<input id="user_name" name="user_name" value="{{.user_name}}" autofocus required> |
|
||||||
</div> |
|
||||||
<div class="required inline field {{if .Err_Email}}error{{end}}"> |
|
||||||
<label for="email">{{.i18n.Tr "email"}}</label> |
|
||||||
<input id="email" name="email" type="email" value="{{.email}}" required> |
|
||||||
</div> |
|
||||||
<div class="required inline field {{if .Err_Password}}error{{end}}"> |
|
||||||
<label for="password">{{.i18n.Tr "password"}}</label> |
|
||||||
<input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" required> |
|
||||||
</div> |
|
||||||
<div class="required inline field {{if .Err_Password}}error{{end}}"> |
|
||||||
<label for="retype">{{.i18n.Tr "re_type"}}</label> |
|
||||||
<input id="retype" name="retype" type="password" value="{{.retype}}" autocomplete="off" required> |
|
||||||
</div> |
|
||||||
{{if .EnableCaptcha}} |
|
||||||
<div class="inline field"> |
|
||||||
<label></label> |
|
||||||
{{.Captcha.CreateHtml}} |
|
||||||
</div> |
|
||||||
<div class="required inline field {{if .Err_Captcha}}error{{end}}"> |
|
||||||
<label for="captcha">{{.i18n.Tr "captcha"}}</label> |
|
||||||
<input id="captcha" name="captcha" value="{{.captcha}}" autocomplete="off"> |
|
||||||
</div> |
|
||||||
{{end}} |
|
||||||
|
|
||||||
<div class="inline field"> |
|
||||||
<label></label> |
|
||||||
<button class="ui green button">{{.i18n.Tr "auth.create_new_account"}}</button> |
|
||||||
</div> |
|
||||||
<div class="inline field"> |
|
||||||
<label></label> |
|
||||||
<a href="{{AppSubUrl}}/user/login">{{if .IsSocialLogin}}{{.i18n.Tr "auth.social_register_helper_msg"}}{{else}}{{.i18n.Tr "auth.register_helper_msg"}}{{end}}</a> |
|
||||||
</div> |
|
||||||
{{end}} |
|
||||||
</div> |
|
||||||
</form> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
{{template "base/footer" .}} |
{{template "base/footer" .}} |
||||||
|
@ -0,0 +1,59 @@ |
|||||||
|
<div class="user signup{{if .LinkAccountMode}} icon{{end}}"> |
||||||
|
<div class="ui middle very relaxed page grid"> |
||||||
|
<div class="column"> |
||||||
|
<form class="ui form" action="{{if not .LinkAccountMode}}{{.Link}}{{else}}{{.SignUpLink}}{{end}}" method="post"> |
||||||
|
{{.CsrfTokenHtml}} |
||||||
|
<h3 class="ui top attached header"> |
||||||
|
{{.i18n.Tr "sign_up"}} |
||||||
|
</h3> |
||||||
|
<div class="ui attached segment"> |
||||||
|
{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister)}} |
||||||
|
{{template "base/alert" .}} |
||||||
|
{{end}} |
||||||
|
{{if .DisableRegistration}} |
||||||
|
<p>{{.i18n.Tr "auth.disable_register_prompt"}}</p> |
||||||
|
{{else}} |
||||||
|
<div class="required inline field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}"> |
||||||
|
<label for="user_name">{{.i18n.Tr "username"}}</label> |
||||||
|
<input id="user_name" name="user_name" value="{{.user_name}}" autofocus required> |
||||||
|
</div> |
||||||
|
<div class="required inline field {{if .Err_Email}}error{{end}}"> |
||||||
|
<label for="email">{{.i18n.Tr "email"}}</label> |
||||||
|
<input id="email" name="email" type="email" value="{{.email}}" required> |
||||||
|
</div> |
||||||
|
<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}"> |
||||||
|
<label for="password">{{.i18n.Tr "password"}}</label> |
||||||
|
<input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" required> |
||||||
|
</div> |
||||||
|
<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}"> |
||||||
|
<label for="retype">{{.i18n.Tr "re_type"}}</label> |
||||||
|
<input id="retype" name="retype" type="password" value="{{.retype}}" autocomplete="off" required> |
||||||
|
</div> |
||||||
|
{{if .EnableCaptcha}} |
||||||
|
<div class="inline field"> |
||||||
|
<label></label> |
||||||
|
{{.Captcha.CreateHtml}} |
||||||
|
</div> |
||||||
|
<div class="required inline field {{if .Err_Captcha}}error{{end}}"> |
||||||
|
<label for="captcha">{{.i18n.Tr "captcha"}}</label> |
||||||
|
<input id="captcha" name="captcha" value="{{.captcha}}" autocomplete="off"> |
||||||
|
</div> |
||||||
|
{{end}} |
||||||
|
|
||||||
|
<div class="inline field"> |
||||||
|
<label></label> |
||||||
|
<button class="ui green button">{{.i18n.Tr "auth.create_new_account"}}</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
{{if not .LinkAccountMode}} |
||||||
|
<div class="inline field"> |
||||||
|
<label></label> |
||||||
|
<a href="{{AppSubUrl}}/user/login">{{.i18n.Tr "auth.register_helper_msg"}}</a> |
||||||
|
</div> |
||||||
|
{{end}} |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
@ -0,0 +1,48 @@ |
|||||||
|
{{template "base/head" .}} |
||||||
|
<div class="user settings account_link"> |
||||||
|
<div class="ui container"> |
||||||
|
<div class="ui grid"> |
||||||
|
{{template "user/settings/navbar" .}} |
||||||
|
<div class="twelve wide column content"> |
||||||
|
{{template "base/alert" .}} |
||||||
|
<h4 class="ui top attached header"> |
||||||
|
{{.i18n.Tr "settings.manage_account_links"}} |
||||||
|
</h4> |
||||||
|
<div class="ui attached segment"> |
||||||
|
<div class="ui key list"> |
||||||
|
<div class="item"> |
||||||
|
{{.i18n.Tr "settings.manage_account_links_desc"}} |
||||||
|
</div> |
||||||
|
{{if .AccountLinks}} |
||||||
|
{{range $loginSource, $provider := .AccountLinks}} |
||||||
|
<div class="item ui grid"> |
||||||
|
<div class="column"> |
||||||
|
<strong>{{$provider}}</strong> |
||||||
|
{{if $loginSource.IsActived}}<span class="text red">{{$.i18n.Tr "settings.active"}}</span>{{end}} |
||||||
|
<div class="ui right"> |
||||||
|
<button class="ui red tiny button delete-button" data-url="{{$.Link}}" data-id="{{$loginSource.ID}}"> |
||||||
|
{{$.i18n.Tr "settings.delete_key"}} |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{end}} |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="ui small basic delete modal"> |
||||||
|
<div class="ui icon header"> |
||||||
|
<i class="trash icon"></i> |
||||||
|
{{.i18n.Tr "settings.remove_account_link"}} |
||||||
|
</div> |
||||||
|
<div class="content"> |
||||||
|
<p>{{.i18n.Tr "settings.remove_account_link_desc"}}</p> |
||||||
|
</div> |
||||||
|
{{template "base/delete_modal_actions" .}} |
||||||
|
</div> |
||||||
|
{{template "base/footer" .}} |
@ -0,0 +1,27 @@ |
|||||||
|
Copyright (c) 2012 Rodrigo Moraes. All rights reserved. |
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without |
||||||
|
modification, are permitted provided that the following conditions are |
||||||
|
met: |
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright |
||||||
|
notice, this list of conditions and the following disclaimer. |
||||||
|
* Redistributions in binary form must reproduce the above |
||||||
|
copyright notice, this list of conditions and the following disclaimer |
||||||
|
in the documentation and/or other materials provided with the |
||||||
|
distribution. |
||||||
|
* Neither the name of Google Inc. nor the names of its |
||||||
|
contributors may be used to endorse or promote products derived from |
||||||
|
this software without specific prior written permission. |
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@ -0,0 +1,10 @@ |
|||||||
|
context |
||||||
|
======= |
||||||
|
[![Build Status](https://travis-ci.org/gorilla/context.png?branch=master)](https://travis-ci.org/gorilla/context) |
||||||
|
|
||||||
|
gorilla/context is a general purpose registry for global request variables. |
||||||
|
|
||||||
|
> Note: gorilla/context, having been born well before `context.Context` existed, does not play well |
||||||
|
> with the shallow copying of the request that [`http.Request.WithContext`](https://golang.org/pkg/net/http/#Request.WithContext) (added to net/http Go 1.7 onwards) performs. You should either use *just* gorilla/context, or moving forward, the new `http.Request.Context()`. |
||||||
|
|
||||||
|
Read the full documentation here: http://www.gorillatoolkit.org/pkg/context |
@ -0,0 +1,143 @@ |
|||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package context |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
mutex sync.RWMutex |
||||||
|
data = make(map[*http.Request]map[interface{}]interface{}) |
||||||
|
datat = make(map[*http.Request]int64) |
||||||
|
) |
||||||
|
|
||||||
|
// Set stores a value for a given key in a given request.
|
||||||
|
func Set(r *http.Request, key, val interface{}) { |
||||||
|
mutex.Lock() |
||||||
|
if data[r] == nil { |
||||||
|
data[r] = make(map[interface{}]interface{}) |
||||||
|
datat[r] = time.Now().Unix() |
||||||
|
} |
||||||
|
data[r][key] = val |
||||||
|
mutex.Unlock() |
||||||
|
} |
||||||
|
|
||||||
|
// Get returns a value stored for a given key in a given request.
|
||||||
|
func Get(r *http.Request, key interface{}) interface{} { |
||||||
|
mutex.RLock() |
||||||
|
if ctx := data[r]; ctx != nil { |
||||||
|
value := ctx[key] |
||||||
|
mutex.RUnlock() |
||||||
|
return value |
||||||
|
} |
||||||
|
mutex.RUnlock() |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetOk returns stored value and presence state like multi-value return of map access.
|
||||||
|
func GetOk(r *http.Request, key interface{}) (interface{}, bool) { |
||||||
|
mutex.RLock() |
||||||
|
if _, ok := data[r]; ok { |
||||||
|
value, ok := data[r][key] |
||||||
|
mutex.RUnlock() |
||||||
|
return value, ok |
||||||
|
} |
||||||
|
mutex.RUnlock() |
||||||
|
return nil, false |
||||||
|
} |
||||||
|
|
||||||
|
// GetAll returns all stored values for the request as a map. Nil is returned for invalid requests.
|
||||||
|
func GetAll(r *http.Request) map[interface{}]interface{} { |
||||||
|
mutex.RLock() |
||||||
|
if context, ok := data[r]; ok { |
||||||
|
result := make(map[interface{}]interface{}, len(context)) |
||||||
|
for k, v := range context { |
||||||
|
result[k] = v |
||||||
|
} |
||||||
|
mutex.RUnlock() |
||||||
|
return result |
||||||
|
} |
||||||
|
mutex.RUnlock() |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetAllOk returns all stored values for the request as a map and a boolean value that indicates if
|
||||||
|
// the request was registered.
|
||||||
|
func GetAllOk(r *http.Request) (map[interface{}]interface{}, bool) { |
||||||
|
mutex.RLock() |
||||||
|
context, ok := data[r] |
||||||
|
result := make(map[interface{}]interface{}, len(context)) |
||||||
|
for k, v := range context { |
||||||
|
result[k] = v |
||||||
|
} |
||||||
|
mutex.RUnlock() |
||||||
|
return result, ok |
||||||
|
} |
||||||
|
|
||||||
|
// Delete removes a value stored for a given key in a given request.
|
||||||
|
func Delete(r *http.Request, key interface{}) { |
||||||
|
mutex.Lock() |
||||||
|
if data[r] != nil { |
||||||
|
delete(data[r], key) |
||||||
|
} |
||||||
|
mutex.Unlock() |
||||||
|
} |
||||||
|
|
||||||
|
// Clear removes all values stored for a given request.
|
||||||
|
//
|
||||||
|
// This is usually called by a handler wrapper to clean up request
|
||||||
|
// variables at the end of a request lifetime. See ClearHandler().
|
||||||
|
func Clear(r *http.Request) { |
||||||
|
mutex.Lock() |
||||||
|
clear(r) |
||||||
|
mutex.Unlock() |
||||||
|
} |
||||||
|
|
||||||
|
// clear is Clear without the lock.
|
||||||
|
func clear(r *http.Request) { |
||||||
|
delete(data, r) |
||||||
|
delete(datat, r) |
||||||
|
} |
||||||
|
|
||||||
|
// Purge removes request data stored for longer than maxAge, in seconds.
|
||||||
|
// It returns the amount of requests removed.
|
||||||
|
//
|
||||||
|
// If maxAge <= 0, all request data is removed.
|
||||||
|
//
|
||||||
|
// This is only used for sanity check: in case context cleaning was not
|
||||||
|
// properly set some request data can be kept forever, consuming an increasing
|
||||||
|
// amount of memory. In case this is detected, Purge() must be called
|
||||||
|
// periodically until the problem is fixed.
|
||||||
|
func Purge(maxAge int) int { |
||||||
|
mutex.Lock() |
||||||
|
count := 0 |
||||||
|
if maxAge <= 0 { |
||||||
|
count = len(data) |
||||||
|
data = make(map[*http.Request]map[interface{}]interface{}) |
||||||
|
datat = make(map[*http.Request]int64) |
||||||
|
} else { |
||||||
|
min := time.Now().Unix() - int64(maxAge) |
||||||
|
for r := range data { |
||||||
|
if datat[r] < min { |
||||||
|
clear(r) |
||||||
|
count++ |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
mutex.Unlock() |
||||||
|
return count |
||||||
|
} |
||||||
|
|
||||||
|
// ClearHandler wraps an http.Handler and clears request values at the end
|
||||||
|
// of a request lifetime.
|
||||||
|
func ClearHandler(h http.Handler) http.Handler { |
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
||||||
|
defer Clear(r) |
||||||
|
h.ServeHTTP(w, r) |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,88 @@ |
|||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
/* |
||||||
|
Package context stores values shared during a request lifetime. |
||||||
|
|
||||||
|
Note: gorilla/context, having been born well before `context.Context` existed, |
||||||
|
does not play well > with the shallow copying of the request that |
||||||
|
[`http.Request.WithContext`](https://golang.org/pkg/net/http/#Request.WithContext)
|
||||||
|
(added to net/http Go 1.7 onwards) performs. You should either use *just* |
||||||
|
gorilla/context, or moving forward, the new `http.Request.Context()`. |
||||||
|
|
||||||
|
For example, a router can set variables extracted from the URL and later |
||||||
|
application handlers can access those values, or it can be used to store |
||||||
|
sessions values to be saved at the end of a request. There are several |
||||||
|
others common uses. |
||||||
|
|
||||||
|
The idea was posted by Brad Fitzpatrick to the go-nuts mailing list: |
||||||
|
|
||||||
|
http://groups.google.com/group/golang-nuts/msg/e2d679d303aa5d53
|
||||||
|
|
||||||
|
Here's the basic usage: first define the keys that you will need. The key |
||||||
|
type is interface{} so a key can be of any type that supports equality. |
||||||
|
Here we define a key using a custom int type to avoid name collisions: |
||||||
|
|
||||||
|
package foo |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/gorilla/context" |
||||||
|
) |
||||||
|
|
||||||
|
type key int |
||||||
|
|
||||||
|
const MyKey key = 0 |
||||||
|
|
||||||
|
Then set a variable. Variables are bound to an http.Request object, so you |
||||||
|
need a request instance to set a value: |
||||||
|
|
||||||
|
context.Set(r, MyKey, "bar") |
||||||
|
|
||||||
|
The application can later access the variable using the same key you provided: |
||||||
|
|
||||||
|
func MyHandler(w http.ResponseWriter, r *http.Request) { |
||||||
|
// val is "bar".
|
||||||
|
val := context.Get(r, foo.MyKey) |
||||||
|
|
||||||
|
// returns ("bar", true)
|
||||||
|
val, ok := context.GetOk(r, foo.MyKey) |
||||||
|
// ...
|
||||||
|
} |
||||||
|
|
||||||
|
And that's all about the basic usage. We discuss some other ideas below. |
||||||
|
|
||||||
|
Any type can be stored in the context. To enforce a given type, make the key |
||||||
|
private and wrap Get() and Set() to accept and return values of a specific |
||||||
|
type: |
||||||
|
|
||||||
|
type key int |
||||||
|
|
||||||
|
const mykey key = 0 |
||||||
|
|
||||||
|
// GetMyKey returns a value for this package from the request values.
|
||||||
|
func GetMyKey(r *http.Request) SomeType { |
||||||
|
if rv := context.Get(r, mykey); rv != nil { |
||||||
|
return rv.(SomeType) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// SetMyKey sets a value for this package in the request values.
|
||||||
|
func SetMyKey(r *http.Request, val SomeType) { |
||||||
|
context.Set(r, mykey, val) |
||||||
|
} |
||||||
|
|
||||||
|
Variables must be cleared at the end of a request, to remove all values |
||||||
|
that were stored. This can be done in an http.Handler, after a request was |
||||||
|
served. Just call Clear() passing the request: |
||||||
|
|
||||||
|
context.Clear(r) |
||||||
|
|
||||||
|
...or use ClearHandler(), which conveniently wraps an http.Handler to clear |
||||||
|
variables at the end of a request lifetime. |
||||||
|
|
||||||
|
The Routers from the packages gorilla/mux and gorilla/pat call Clear() |
||||||
|
so if you are using either of them you don't need to clear the context manually. |
||||||
|
*/ |
||||||
|
package context |
@ -0,0 +1,27 @@ |
|||||||
|
Copyright (c) 2012 Rodrigo Moraes. All rights reserved. |
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without |
||||||
|
modification, are permitted provided that the following conditions are |
||||||
|
met: |
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright |
||||||
|
notice, this list of conditions and the following disclaimer. |
||||||
|
* Redistributions in binary form must reproduce the above |
||||||
|
copyright notice, this list of conditions and the following disclaimer |
||||||
|
in the documentation and/or other materials provided with the |
||||||
|
distribution. |
||||||
|
* Neither the name of Google Inc. nor the names of its |
||||||
|
contributors may be used to endorse or promote products derived from |
||||||
|
this software without specific prior written permission. |
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@ -0,0 +1,299 @@ |
|||||||
|
gorilla/mux |
||||||
|
=== |
||||||
|
[![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) |
||||||
|
[![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux) |
||||||
|
|
||||||
|
![Gorilla Logo](http://www.gorillatoolkit.org/static/images/gorilla-icon-64.png) |
||||||
|
|
||||||
|
http://www.gorillatoolkit.org/pkg/mux |
||||||
|
|
||||||
|
Package `gorilla/mux` implements a request router and dispatcher for matching incoming requests to |
||||||
|
their respective handler. |
||||||
|
|
||||||
|
The name mux stands for "HTTP request multiplexer". Like the standard `http.ServeMux`, `mux.Router` matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are: |
||||||
|
|
||||||
|
* It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`. |
||||||
|
* Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers. |
||||||
|
* URL hosts and paths can have variables with an optional regular expression. |
||||||
|
* Registered URLs can be built, or "reversed", which helps maintaining references to resources. |
||||||
|
* Routes can be used as subrouters: nested routes are only tested if the parent route matches. This is useful to define groups of routes that share common conditions like a host, a path prefix or other repeated attributes. As a bonus, this optimizes request matching. |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
* [Install](#install) |
||||||
|
* [Examples](#examples) |
||||||
|
* [Matching Routes](#matching-routes) |
||||||
|
* [Static Files](#static-files) |
||||||
|
* [Registered URLs](#registered-urls) |
||||||
|
* [Full Example](#full-example) |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## Install |
||||||
|
|
||||||
|
With a [correctly configured](https://golang.org/doc/install#testing) Go toolchain: |
||||||
|
|
||||||
|
```sh |
||||||
|
go get -u github.com/gorilla/mux |
||||||
|
``` |
||||||
|
|
||||||
|
## Examples |
||||||
|
|
||||||
|
Let's start registering a couple of URL paths and handlers: |
||||||
|
|
||||||
|
```go |
||||||
|
func main() { |
||||||
|
r := mux.NewRouter() |
||||||
|
r.HandleFunc("/", HomeHandler) |
||||||
|
r.HandleFunc("/products", ProductsHandler) |
||||||
|
r.HandleFunc("/articles", ArticlesHandler) |
||||||
|
http.Handle("/", r) |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Here we register three routes mapping URL paths to handlers. This is equivalent to how `http.HandleFunc()` works: if an incoming request URL matches one of the paths, the corresponding handler is called passing (`http.ResponseWriter`, `*http.Request`) as parameters. |
||||||
|
|
||||||
|
Paths can have variables. They are defined using the format `{name}` or `{name:pattern}`. If a regular expression pattern is not defined, the matched variable will be anything until the next slash. For example: |
||||||
|
|
||||||
|
```go |
||||||
|
r := mux.NewRouter() |
||||||
|
r.HandleFunc("/products/{key}", ProductHandler) |
||||||
|
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) |
||||||
|
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) |
||||||
|
``` |
||||||
|
|
||||||
|
The names are used to create a map of route variables which can be retrieved calling `mux.Vars()`: |
||||||
|
|
||||||
|
```go |
||||||
|
vars := mux.Vars(request) |
||||||
|
category := vars["category"] |
||||||
|
``` |
||||||
|
|
||||||
|
And this is all you need to know about the basic usage. More advanced options are explained below. |
||||||
|
|
||||||
|
### Matching Routes |
||||||
|
|
||||||
|
Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables: |
||||||
|
|
||||||
|
```go |
||||||
|
r := mux.NewRouter() |
||||||
|
// Only matches if domain is "www.example.com". |
||||||
|
r.Host("www.example.com") |
||||||
|
// Matches a dynamic subdomain. |
||||||
|
r.Host("{subdomain:[a-z]+}.domain.com") |
||||||
|
``` |
||||||
|
|
||||||
|
There are several other matchers that can be added. To match path prefixes: |
||||||
|
|
||||||
|
```go |
||||||
|
r.PathPrefix("/products/") |
||||||
|
``` |
||||||
|
|
||||||
|
...or HTTP methods: |
||||||
|
|
||||||
|
```go |
||||||
|
r.Methods("GET", "POST") |
||||||
|
``` |
||||||
|
|
||||||
|
...or URL schemes: |
||||||
|
|
||||||
|
```go |
||||||
|
r.Schemes("https") |
||||||
|
``` |
||||||
|
|
||||||
|
...or header values: |
||||||
|
|
||||||
|
```go |
||||||
|
r.Headers("X-Requested-With", "XMLHttpRequest") |
||||||
|
``` |
||||||
|
|
||||||
|
...or query values: |
||||||
|
|
||||||
|
```go |
||||||
|
r.Queries("key", "value") |
||||||
|
``` |
||||||
|
|
||||||
|
...or to use a custom matcher function: |
||||||
|
|
||||||
|
```go |
||||||
|
r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { |
||||||
|
return r.ProtoMajor == 0 |
||||||
|
}) |
||||||
|
``` |
||||||
|
|
||||||
|
...and finally, it is possible to combine several matchers in a single route: |
||||||
|
|
||||||
|
```go |
||||||
|
r.HandleFunc("/products", ProductsHandler). |
||||||
|
Host("www.example.com"). |
||||||
|
Methods("GET"). |
||||||
|
Schemes("http") |
||||||
|
``` |
||||||
|
|
||||||
|
Setting the same matching conditions again and again can be boring, so we have a way to group several routes that share the same requirements. We call it "subrouting". |
||||||
|
|
||||||
|
For example, let's say we have several URLs that should only match when the host is `www.example.com`. Create a route for that host and get a "subrouter" from it: |
||||||
|
|
||||||
|
```go |
||||||
|
r := mux.NewRouter() |
||||||
|
s := r.Host("www.example.com").Subrouter() |
||||||
|
``` |
||||||
|
|
||||||
|
Then register routes in the subrouter: |
||||||
|
|
||||||
|
```go |
||||||
|
s.HandleFunc("/products/", ProductsHandler) |
||||||
|
s.HandleFunc("/products/{key}", ProductHandler) |
||||||
|
s.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) |
||||||
|
``` |
||||||
|
|
||||||
|
The three URL paths we registered above will only be tested if the domain is `www.example.com`, because the subrouter is tested first. This is not only convenient, but also optimizes request matching. You can create subrouters combining any attribute matchers accepted by a route. |
||||||
|
|
||||||
|
Subrouters can be used to create domain or path "namespaces": you define subrouters in a central place and then parts of the app can register its paths relatively to a given subrouter. |
||||||
|
|
||||||
|
There's one more thing about subroutes. When a subrouter has a path prefix, the inner routes use it as base for their paths: |
||||||
|
|
||||||
|
```go |
||||||
|
r := mux.NewRouter() |
||||||
|
s := r.PathPrefix("/products").Subrouter() |
||||||
|
// "/products/" |
||||||
|
s.HandleFunc("/", ProductsHandler) |
||||||
|
// "/products/{key}/" |
||||||
|
s.HandleFunc("/{key}/", ProductHandler) |
||||||
|
// "/products/{key}/details" |
||||||
|
s.HandleFunc("/{key}/details", ProductDetailsHandler) |
||||||
|
``` |
||||||
|
|
||||||
|
### Static Files |
||||||
|
|
||||||
|
Note that the path provided to `PathPrefix()` represents a "wildcard": calling |
||||||
|
`PathPrefix("/static/").Handler(...)` means that the handler will be passed any |
||||||
|
request that matches "/static/*". This makes it easy to serve static files with mux: |
||||||
|
|
||||||
|
```go |
||||||
|
func main() { |
||||||
|
var dir string |
||||||
|
|
||||||
|
flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir") |
||||||
|
flag.Parse() |
||||||
|
r := mux.NewRouter() |
||||||
|
|
||||||
|
// This will serve files under http://localhost:8000/static/<filename> |
||||||
|
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) |
||||||
|
|
||||||
|
srv := &http.Server{ |
||||||
|
Handler: r, |
||||||
|
Addr: "127.0.0.1:8000", |
||||||
|
// Good practice: enforce timeouts for servers you create! |
||||||
|
WriteTimeout: 15 * time.Second, |
||||||
|
ReadTimeout: 15 * time.Second, |
||||||
|
} |
||||||
|
|
||||||
|
log.Fatal(srv.ListenAndServe()) |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### Registered URLs |
||||||
|
|
||||||
|
Now let's see how to build registered URLs. |
||||||
|
|
||||||
|
Routes can be named. All routes that define a name can have their URLs built, or "reversed". We define a name calling `Name()` on a route. For example: |
||||||
|
|
||||||
|
```go |
||||||
|
r := mux.NewRouter() |
||||||
|
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). |
||||||
|
Name("article") |
||||||
|
``` |
||||||
|
|
||||||
|
To build a URL, get the route and call the `URL()` method, passing a sequence of key/value pairs for the route variables. For the previous route, we would do: |
||||||
|
|
||||||
|
```go |
||||||
|
url, err := r.Get("article").URL("category", "technology", "id", "42") |
||||||
|
``` |
||||||
|
|
||||||
|
...and the result will be a `url.URL` with the following path: |
||||||
|
|
||||||
|
``` |
||||||
|
"/articles/technology/42" |
||||||
|
``` |
||||||
|
|
||||||
|
This also works for host variables: |
||||||
|
|
||||||
|
```go |
||||||
|
r := mux.NewRouter() |
||||||
|
r.Host("{subdomain}.domain.com"). |
||||||
|
Path("/articles/{category}/{id:[0-9]+}"). |
||||||
|
HandlerFunc(ArticleHandler). |
||||||
|
Name("article") |
||||||
|
|
||||||
|
// url.String() will be "http://news.domain.com/articles/technology/42" |
||||||
|
url, err := r.Get("article").URL("subdomain", "news", |
||||||
|
"category", "technology", |
||||||
|
"id", "42") |
||||||
|
``` |
||||||
|
|
||||||
|
All variables defined in the route are required, and their values must conform to the corresponding patterns. These requirements guarantee that a generated URL will always match a registered route -- the only exception is for explicitly defined "build-only" routes which never match. |
||||||
|
|
||||||
|
Regex support also exists for matching Headers within a route. For example, we could do: |
||||||
|
|
||||||
|
```go |
||||||
|
r.HeadersRegexp("Content-Type", "application/(text|json)") |
||||||
|
``` |
||||||
|
|
||||||
|
...and the route will match both requests with a Content-Type of `application/json` as well as `application/text` |
||||||
|
|
||||||
|
There's also a way to build only the URL host or path for a route: use the methods `URLHost()` or `URLPath()` instead. For the previous route, we would do: |
||||||
|
|
||||||
|
```go |
||||||
|
// "http://news.domain.com/" |
||||||
|
host, err := r.Get("article").URLHost("subdomain", "news") |
||||||
|
|
||||||
|
// "/articles/technology/42" |
||||||
|
path, err := r.Get("article").URLPath("category", "technology", "id", "42") |
||||||
|
``` |
||||||
|
|
||||||
|
And if you use subrouters, host and path defined separately can be built as well: |
||||||
|
|
||||||
|
```go |
||||||
|
r := mux.NewRouter() |
||||||
|
s := r.Host("{subdomain}.domain.com").Subrouter() |
||||||
|
s.Path("/articles/{category}/{id:[0-9]+}"). |
||||||
|
HandlerFunc(ArticleHandler). |
||||||
|
Name("article") |
||||||
|
|
||||||
|
// "http://news.domain.com/articles/technology/42" |
||||||
|
url, err := r.Get("article").URL("subdomain", "news", |
||||||
|
"category", "technology", |
||||||
|
"id", "42") |
||||||
|
``` |
||||||
|
|
||||||
|
## Full Example |
||||||
|
|
||||||
|
Here's a complete, runnable example of a small `mux` based server: |
||||||
|
|
||||||
|
```go |
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
"log" |
||||||
|
"github.com/gorilla/mux" |
||||||
|
) |
||||||
|
|
||||||
|
func YourHandler(w http.ResponseWriter, r *http.Request) { |
||||||
|
w.Write([]byte("Gorilla!\n")) |
||||||
|
} |
||||||
|
|
||||||
|
func main() { |
||||||
|
r := mux.NewRouter() |
||||||
|
// Routes consist of a path and a handler function. |
||||||
|
r.HandleFunc("/", YourHandler) |
||||||
|
|
||||||
|
// Bind to a port and pass our router in |
||||||
|
log.Fatal(http.ListenAndServe(":8000", r)) |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## License |
||||||
|
|
||||||
|
BSD licensed. See the LICENSE file for details. |
@ -0,0 +1,26 @@ |
|||||||
|
// +build !go1.7
|
||||||
|
|
||||||
|
package mux |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"github.com/gorilla/context" |
||||||
|
) |
||||||
|
|
||||||
|
func contextGet(r *http.Request, key interface{}) interface{} { |
||||||
|
return context.Get(r, key) |
||||||
|
} |
||||||
|
|
||||||
|
func contextSet(r *http.Request, key, val interface{}) *http.Request { |
||||||
|
if val == nil { |
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
context.Set(r, key, val) |
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
func contextClear(r *http.Request) { |
||||||
|
context.Clear(r) |
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
// +build go1.7
|
||||||
|
|
||||||
|
package mux |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"net/http" |
||||||
|
) |
||||||
|
|
||||||
|
func contextGet(r *http.Request, key interface{}) interface{} { |
||||||
|
return r.Context().Value(key) |
||||||
|
} |
||||||
|
|
||||||
|
func contextSet(r *http.Request, key, val interface{}) *http.Request { |
||||||
|
if val == nil { |
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
return r.WithContext(context.WithValue(r.Context(), key, val)) |
||||||
|
} |
||||||
|
|
||||||
|
func contextClear(r *http.Request) { |
||||||
|
return |
||||||
|
} |
@ -0,0 +1,235 @@ |
|||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
/* |
||||||
|
Package mux implements a request router and dispatcher. |
||||||
|
|
||||||
|
The name mux stands for "HTTP request multiplexer". Like the standard |
||||||
|
http.ServeMux, mux.Router matches incoming requests against a list of |
||||||
|
registered routes and calls a handler for the route that matches the URL |
||||||
|
or other conditions. The main features are: |
||||||
|
|
||||||
|
* Requests can be matched based on URL host, path, path prefix, schemes, |
||||||
|
header and query values, HTTP methods or using custom matchers. |
||||||
|
* URL hosts and paths can have variables with an optional regular |
||||||
|
expression. |
||||||
|
* Registered URLs can be built, or "reversed", which helps maintaining |
||||||
|
references to resources. |
||||||
|
* Routes can be used as subrouters: nested routes are only tested if the |
||||||
|
parent route matches. This is useful to define groups of routes that |
||||||
|
share common conditions like a host, a path prefix or other repeated |
||||||
|
attributes. As a bonus, this optimizes request matching. |
||||||
|
* It implements the http.Handler interface so it is compatible with the |
||||||
|
standard http.ServeMux. |
||||||
|
|
||||||
|
Let's start registering a couple of URL paths and handlers: |
||||||
|
|
||||||
|
func main() { |
||||||
|
r := mux.NewRouter() |
||||||
|
r.HandleFunc("/", HomeHandler) |
||||||
|
r.HandleFunc("/products", ProductsHandler) |
||||||
|
r.HandleFunc("/articles", ArticlesHandler) |
||||||
|
http.Handle("/", r) |
||||||
|
} |
||||||
|
|
||||||
|
Here we register three routes mapping URL paths to handlers. This is |
||||||
|
equivalent to how http.HandleFunc() works: if an incoming request URL matches |
||||||
|
one of the paths, the corresponding handler is called passing |
||||||
|
(http.ResponseWriter, *http.Request) as parameters. |
||||||
|
|
||||||
|
Paths can have variables. They are defined using the format {name} or |
||||||
|
{name:pattern}. If a regular expression pattern is not defined, the matched |
||||||
|
variable will be anything until the next slash. For example: |
||||||
|
|
||||||
|
r := mux.NewRouter() |
||||||
|
r.HandleFunc("/products/{key}", ProductHandler) |
||||||
|
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) |
||||||
|
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) |
||||||
|
|
||||||
|
Groups can be used inside patterns, as long as they are non-capturing (?:re). For example: |
||||||
|
|
||||||
|
r.HandleFunc("/articles/{category}/{sort:(?:asc|desc|new)}", ArticlesCategoryHandler) |
||||||
|
|
||||||
|
The names are used to create a map of route variables which can be retrieved |
||||||
|
calling mux.Vars(): |
||||||
|
|
||||||
|
vars := mux.Vars(request) |
||||||
|
category := vars["category"] |
||||||
|
|
||||||
|
And this is all you need to know about the basic usage. More advanced options |
||||||
|
are explained below. |
||||||
|
|
||||||
|
Routes can also be restricted to a domain or subdomain. Just define a host |
||||||
|
pattern to be matched. They can also have variables: |
||||||
|
|
||||||
|
r := mux.NewRouter() |
||||||
|
// Only matches if domain is "www.example.com".
|
||||||
|
r.Host("www.example.com") |
||||||
|
// Matches a dynamic subdomain.
|
||||||
|
r.Host("{subdomain:[a-z]+}.domain.com") |
||||||
|
|
||||||
|
There are several other matchers that can be added. To match path prefixes: |
||||||
|
|
||||||
|
r.PathPrefix("/products/") |
||||||
|
|
||||||
|
...or HTTP methods: |
||||||
|
|
||||||
|
r.Methods("GET", "POST") |
||||||
|
|
||||||
|
...or URL schemes: |
||||||
|
|
||||||
|
r.Schemes("https") |
||||||
|
|
||||||
|
...or header values: |
||||||
|
|
||||||
|
r.Headers("X-Requested-With", "XMLHttpRequest") |
||||||
|
|
||||||
|
...or query values: |
||||||
|
|
||||||
|
r.Queries("key", "value") |
||||||
|
|
||||||
|
...or to use a custom matcher function: |
||||||
|
|
||||||
|
r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { |
||||||
|
return r.ProtoMajor == 0 |
||||||
|
}) |
||||||
|
|
||||||
|
...and finally, it is possible to combine several matchers in a single route: |
||||||
|
|
||||||
|
r.HandleFunc("/products", ProductsHandler). |
||||||
|
Host("www.example.com"). |
||||||
|
Methods("GET"). |
||||||
|
Schemes("http") |
||||||
|
|
||||||
|
Setting the same matching conditions again and again can be boring, so we have |
||||||
|
a way to group several routes that share the same requirements. |
||||||
|
We call it "subrouting". |
||||||
|
|
||||||
|
For example, let's say we have several URLs that should only match when the |
||||||
|
host is "www.example.com". Create a route for that host and get a "subrouter" |
||||||
|
from it: |
||||||
|
|
||||||
|
r := mux.NewRouter() |
||||||
|
s := r.Host("www.example.com").Subrouter() |
||||||
|
|
||||||
|
Then register routes in the subrouter: |
||||||
|
|
||||||
|
s.HandleFunc("/products/", ProductsHandler) |
||||||
|
s.HandleFunc("/products/{key}", ProductHandler) |
||||||
|
s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) |
||||||
|
|
||||||
|
The three URL paths we registered above will only be tested if the domain is |
||||||
|
"www.example.com", because the subrouter is tested first. This is not |
||||||
|
only convenient, but also optimizes request matching. You can create |
||||||
|
subrouters combining any attribute matchers accepted by a route. |
||||||
|
|
||||||
|
Subrouters can be used to create domain or path "namespaces": you define |
||||||
|
subrouters in a central place and then parts of the app can register its |
||||||
|
paths relatively to a given subrouter. |
||||||
|
|
||||||
|
There's one more thing about subroutes. When a subrouter has a path prefix, |
||||||
|
the inner routes use it as base for their paths: |
||||||
|
|
||||||
|
r := mux.NewRouter() |
||||||
|
s := r.PathPrefix("/products").Subrouter() |
||||||
|
// "/products/"
|
||||||
|
s.HandleFunc("/", ProductsHandler) |
||||||
|
// "/products/{key}/"
|
||||||
|
s.HandleFunc("/{key}/", ProductHandler) |
||||||
|
// "/products/{key}/details"
|
||||||
|
s.HandleFunc("/{key}/details", ProductDetailsHandler) |
||||||
|
|
||||||
|
Note that the path provided to PathPrefix() represents a "wildcard": calling |
||||||
|
PathPrefix("/static/").Handler(...) means that the handler will be passed any |
||||||
|
request that matches "/static/*". This makes it easy to serve static files with mux: |
||||||
|
|
||||||
|
func main() { |
||||||
|
var dir string |
||||||
|
|
||||||
|
flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir") |
||||||
|
flag.Parse() |
||||||
|
r := mux.NewRouter() |
||||||
|
|
||||||
|
// This will serve files under http://localhost:8000/static/<filename>
|
||||||
|
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) |
||||||
|
|
||||||
|
srv := &http.Server{ |
||||||
|
Handler: r, |
||||||
|
Addr: "127.0.0.1:8000", |
||||||
|
// Good practice: enforce timeouts for servers you create!
|
||||||
|
WriteTimeout: 15 * time.Second, |
||||||
|
ReadTimeout: 15 * time.Second, |
||||||
|
} |
||||||
|
|
||||||
|
log.Fatal(srv.ListenAndServe()) |
||||||
|
} |
||||||
|
|
||||||
|
Now let's see how to build registered URLs. |
||||||
|
|
||||||
|
Routes can be named. All routes that define a name can have their URLs built, |
||||||
|
or "reversed". We define a name calling Name() on a route. For example: |
||||||
|
|
||||||
|
r := mux.NewRouter() |
||||||
|
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). |
||||||
|
Name("article") |
||||||
|
|
||||||
|
To build a URL, get the route and call the URL() method, passing a sequence of |
||||||
|
key/value pairs for the route variables. For the previous route, we would do: |
||||||
|
|
||||||
|
url, err := r.Get("article").URL("category", "technology", "id", "42") |
||||||
|
|
||||||
|
...and the result will be a url.URL with the following path: |
||||||
|
|
||||||
|
"/articles/technology/42" |
||||||
|
|
||||||
|
This also works for host variables: |
||||||
|
|
||||||
|
r := mux.NewRouter() |
||||||
|
r.Host("{subdomain}.domain.com"). |
||||||
|
Path("/articles/{category}/{id:[0-9]+}"). |
||||||
|
HandlerFunc(ArticleHandler). |
||||||
|
Name("article") |
||||||
|
|
||||||
|
// url.String() will be "http://news.domain.com/articles/technology/42"
|
||||||
|
url, err := r.Get("article").URL("subdomain", "news", |
||||||
|
"category", "technology", |
||||||
|
"id", "42") |
||||||
|
|
||||||
|
All variables defined in the route are required, and their values must |
||||||
|
conform to the corresponding patterns. These requirements guarantee that a |
||||||
|
generated URL will always match a registered route -- the only exception is |
||||||
|
for explicitly defined "build-only" routes which never match. |
||||||
|
|
||||||
|
Regex support also exists for matching Headers within a route. For example, we could do: |
||||||
|
|
||||||
|
r.HeadersRegexp("Content-Type", "application/(text|json)") |
||||||
|
|
||||||
|
...and the route will match both requests with a Content-Type of `application/json` as well as |
||||||
|
`application/text` |
||||||
|
|
||||||
|
There's also a way to build only the URL host or path for a route: |
||||||
|
use the methods URLHost() or URLPath() instead. For the previous route, |
||||||
|
we would do: |
||||||
|
|
||||||
|
// "http://news.domain.com/"
|
||||||
|
host, err := r.Get("article").URLHost("subdomain", "news") |
||||||
|
|
||||||
|
// "/articles/technology/42"
|
||||||
|
path, err := r.Get("article").URLPath("category", "technology", "id", "42") |
||||||
|
|
||||||
|
And if you use subrouters, host and path defined separately can be built |
||||||
|
as well: |
||||||
|
|
||||||
|
r := mux.NewRouter() |
||||||
|
s := r.Host("{subdomain}.domain.com").Subrouter() |
||||||
|
s.Path("/articles/{category}/{id:[0-9]+}"). |
||||||
|
HandlerFunc(ArticleHandler). |
||||||
|
Name("article") |
||||||
|
|
||||||
|
// "http://news.domain.com/articles/technology/42"
|
||||||
|
url, err := r.Get("article").URL("subdomain", "news", |
||||||
|
"category", "technology", |
||||||
|
"id", "42") |
||||||
|
*/ |
||||||
|
package mux |
@ -0,0 +1,542 @@ |
|||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package mux |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"path" |
||||||
|
"regexp" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
// NewRouter returns a new router instance.
|
||||||
|
func NewRouter() *Router { |
||||||
|
return &Router{namedRoutes: make(map[string]*Route), KeepContext: false} |
||||||
|
} |
||||||
|
|
||||||
|
// Router registers routes to be matched and dispatches a handler.
|
||||||
|
//
|
||||||
|
// It implements the http.Handler interface, so it can be registered to serve
|
||||||
|
// requests:
|
||||||
|
//
|
||||||
|
// var router = mux.NewRouter()
|
||||||
|
//
|
||||||
|
// func main() {
|
||||||
|
// http.Handle("/", router)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Or, for Google App Engine, register it in a init() function:
|
||||||
|
//
|
||||||
|
// func init() {
|
||||||
|
// http.Handle("/", router)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// This will send all incoming requests to the router.
|
||||||
|
type Router struct { |
||||||
|
// Configurable Handler to be used when no route matches.
|
||||||
|
NotFoundHandler http.Handler |
||||||
|
// Parent route, if this is a subrouter.
|
||||||
|
parent parentRoute |
||||||
|
// Routes to be matched, in order.
|
||||||
|
routes []*Route |
||||||
|
// Routes by name for URL building.
|
||||||
|
namedRoutes map[string]*Route |
||||||
|
// See Router.StrictSlash(). This defines the flag for new routes.
|
||||||
|
strictSlash bool |
||||||
|
// See Router.SkipClean(). This defines the flag for new routes.
|
||||||
|
skipClean bool |
||||||
|
// If true, do not clear the request context after handling the request.
|
||||||
|
// This has no effect when go1.7+ is used, since the context is stored
|
||||||
|
// on the request itself.
|
||||||
|
KeepContext bool |
||||||
|
// see Router.UseEncodedPath(). This defines a flag for all routes.
|
||||||
|
useEncodedPath bool |
||||||
|
} |
||||||
|
|
||||||
|
// Match matches registered routes against the request.
|
||||||
|
func (r *Router) Match(req *http.Request, match *RouteMatch) bool { |
||||||
|
for _, route := range r.routes { |
||||||
|
if route.Match(req, match) { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Closest match for a router (includes sub-routers)
|
||||||
|
if r.NotFoundHandler != nil { |
||||||
|
match.Handler = r.NotFoundHandler |
||||||
|
return true |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// ServeHTTP dispatches the handler registered in the matched route.
|
||||||
|
//
|
||||||
|
// When there is a match, the route variables can be retrieved calling
|
||||||
|
// mux.Vars(request).
|
||||||
|
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { |
||||||
|
if !r.skipClean { |
||||||
|
path := req.URL.Path |
||||||
|
if r.useEncodedPath { |
||||||
|
path = getPath(req) |
||||||
|
} |
||||||
|
// Clean path to canonical form and redirect.
|
||||||
|
if p := cleanPath(path); p != path { |
||||||
|
|
||||||
|
// Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query.
|
||||||
|
// This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue:
|
||||||
|
// http://code.google.com/p/go/issues/detail?id=5252
|
||||||
|
url := *req.URL |
||||||
|
url.Path = p |
||||||
|
p = url.String() |
||||||
|
|
||||||
|
w.Header().Set("Location", p) |
||||||
|
w.WriteHeader(http.StatusMovedPermanently) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
var match RouteMatch |
||||||
|
var handler http.Handler |
||||||
|
if r.Match(req, &match) { |
||||||
|
handler = match.Handler |
||||||
|
req = setVars(req, match.Vars) |
||||||
|
req = setCurrentRoute(req, match.Route) |
||||||
|
} |
||||||
|
if handler == nil { |
||||||
|
handler = http.NotFoundHandler() |
||||||
|
} |
||||||
|
if !r.KeepContext { |
||||||
|
defer contextClear(req) |
||||||
|
} |
||||||
|
handler.ServeHTTP(w, req) |
||||||
|
} |
||||||
|
|
||||||
|
// Get returns a route registered with the given name.
|
||||||
|
func (r *Router) Get(name string) *Route { |
||||||
|
return r.getNamedRoutes()[name] |
||||||
|
} |
||||||
|
|
||||||
|
// GetRoute returns a route registered with the given name. This method
|
||||||
|
// was renamed to Get() and remains here for backwards compatibility.
|
||||||
|
func (r *Router) GetRoute(name string) *Route { |
||||||
|
return r.getNamedRoutes()[name] |
||||||
|
} |
||||||
|
|
||||||
|
// StrictSlash defines the trailing slash behavior for new routes. The initial
|
||||||
|
// value is false.
|
||||||
|
//
|
||||||
|
// When true, if the route path is "/path/", accessing "/path" will redirect
|
||||||
|
// to the former and vice versa. In other words, your application will always
|
||||||
|
// see the path as specified in the route.
|
||||||
|
//
|
||||||
|
// When false, if the route path is "/path", accessing "/path/" will not match
|
||||||
|
// this route and vice versa.
|
||||||
|
//
|
||||||
|
// Special case: when a route sets a path prefix using the PathPrefix() method,
|
||||||
|
// strict slash is ignored for that route because the redirect behavior can't
|
||||||
|
// be determined from a prefix alone. However, any subrouters created from that
|
||||||
|
// route inherit the original StrictSlash setting.
|
||||||
|
func (r *Router) StrictSlash(value bool) *Router { |
||||||
|
r.strictSlash = value |
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
// SkipClean defines the path cleaning behaviour for new routes. The initial
|
||||||
|
// value is false. Users should be careful about which routes are not cleaned
|
||||||
|
//
|
||||||
|
// When true, if the route path is "/path//to", it will remain with the double
|
||||||
|
// slash. This is helpful if you have a route like: /fetch/http://xkcd.com/534/
|
||||||
|
//
|
||||||
|
// When false, the path will be cleaned, so /fetch/http://xkcd.com/534/ will
|
||||||
|
// become /fetch/http/xkcd.com/534
|
||||||
|
func (r *Router) SkipClean(value bool) *Router { |
||||||
|
r.skipClean = value |
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
// UseEncodedPath tells the router to match the encoded original path
|
||||||
|
// to the routes.
|
||||||
|
// For eg. "/path/foo%2Fbar/to" will match the path "/path/{var}/to".
|
||||||
|
// This behavior has the drawback of needing to match routes against
|
||||||
|
// r.RequestURI instead of r.URL.Path. Any modifications (such as http.StripPrefix)
|
||||||
|
// to r.URL.Path will not affect routing when this flag is on and thus may
|
||||||
|
// induce unintended behavior.
|
||||||
|
//
|
||||||
|
// If not called, the router will match the unencoded path to the routes.
|
||||||
|
// For eg. "/path/foo%2Fbar/to" will match the path "/path/foo/bar/to"
|
||||||
|
func (r *Router) UseEncodedPath() *Router { |
||||||
|
r.useEncodedPath = true |
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// parentRoute
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// getNamedRoutes returns the map where named routes are registered.
|
||||||
|
func (r *Router) getNamedRoutes() map[string]*Route { |
||||||
|
if r.namedRoutes == nil { |
||||||
|
if r.parent != nil { |
||||||
|
r.namedRoutes = r.parent.getNamedRoutes() |
||||||
|
} else { |
||||||
|
r.namedRoutes = make(map[string]*Route) |
||||||
|
} |
||||||
|
} |
||||||
|
return r.namedRoutes |
||||||
|
} |
||||||
|
|
||||||
|
// getRegexpGroup returns regexp definitions from the parent route, if any.
|
||||||
|
func (r *Router) getRegexpGroup() *routeRegexpGroup { |
||||||
|
if r.parent != nil { |
||||||
|
return r.parent.getRegexpGroup() |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (r *Router) buildVars(m map[string]string) map[string]string { |
||||||
|
if r.parent != nil { |
||||||
|
m = r.parent.buildVars(m) |
||||||
|
} |
||||||
|
return m |
||||||
|
} |
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Route factories
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// NewRoute registers an empty route.
|
||||||
|
func (r *Router) NewRoute() *Route { |
||||||
|
route := &Route{parent: r, strictSlash: r.strictSlash, skipClean: r.skipClean, useEncodedPath: r.useEncodedPath} |
||||||
|
r.routes = append(r.routes, route) |
||||||
|
return route |
||||||
|
} |
||||||
|
|
||||||
|
// Handle registers a new route with a matcher for the URL path.
|
||||||
|
// See Route.Path() and Route.Handler().
|
||||||
|
func (r *Router) Handle(path string, handler http.Handler) *Route { |
||||||
|
return r.NewRoute().Path(path).Handler(handler) |
||||||
|
} |
||||||
|
|
||||||
|
// HandleFunc registers a new route with a matcher for the URL path.
|
||||||
|
// See Route.Path() and Route.HandlerFunc().
|
||||||
|
func (r *Router) HandleFunc(path string, f func(http.ResponseWriter, |
||||||
|
*http.Request)) *Route { |
||||||
|
return r.NewRoute().Path(path).HandlerFunc(f) |
||||||
|
} |
||||||
|
|
||||||
|
// Headers registers a new route with a matcher for request header values.
|
||||||
|
// See Route.Headers().
|
||||||
|
func (r *Router) Headers(pairs ...string) *Route { |
||||||
|
return r.NewRoute().Headers(pairs...) |
||||||
|
} |
||||||
|
|
||||||
|
// Host registers a new route with a matcher for the URL host.
|
||||||
|
// See Route.Host().
|
||||||
|
func (r *Router) Host(tpl string) *Route { |
||||||
|
return r.NewRoute().Host(tpl) |
||||||
|
} |
||||||
|
|
||||||
|
// MatcherFunc registers a new route with a custom matcher function.
|
||||||
|
// See Route.MatcherFunc().
|
||||||
|
func (r *Router) MatcherFunc(f MatcherFunc) *Route { |
||||||
|
return r.NewRoute().MatcherFunc(f) |
||||||
|
} |
||||||
|
|
||||||
|
// Methods registers a new route with a matcher for HTTP methods.
|
||||||
|
// See Route.Methods().
|
||||||
|
func (r *Router) Methods(methods ...string) *Route { |
||||||
|
return r.NewRoute().Methods(methods...) |
||||||
|
} |
||||||
|
|
||||||
|
// Path registers a new route with a matcher for the URL path.
|
||||||
|
// See Route.Path().
|
||||||
|
func (r *Router) Path(tpl string) *Route { |
||||||
|
return r.NewRoute().Path(tpl) |
||||||
|
} |
||||||
|
|
||||||
|
// PathPrefix registers a new route with a matcher for the URL path prefix.
|
||||||
|
// See Route.PathPrefix().
|
||||||
|
func (r *Router) PathPrefix(tpl string) *Route { |
||||||
|
return r.NewRoute().PathPrefix(tpl) |
||||||
|
} |
||||||
|
|
||||||
|
// Queries registers a new route with a matcher for URL query values.
|
||||||
|
// See Route.Queries().
|
||||||
|
func (r *Router) Queries(pairs ...string) *Route { |
||||||
|
return r.NewRoute().Queries(pairs...) |
||||||
|
} |
||||||
|
|
||||||
|
// Schemes registers a new route with a matcher for URL schemes.
|
||||||
|
// See Route.Schemes().
|
||||||
|
func (r *Router) Schemes(schemes ...string) *Route { |
||||||
|
return r.NewRoute().Schemes(schemes...) |
||||||
|
} |
||||||
|
|
||||||
|
// BuildVarsFunc registers a new route with a custom function for modifying
|
||||||
|
// route variables before building a URL.
|
||||||
|
func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route { |
||||||
|
return r.NewRoute().BuildVarsFunc(f) |
||||||
|
} |
||||||
|
|
||||||
|
// Walk walks the router and all its sub-routers, calling walkFn for each route
|
||||||
|
// in the tree. The routes are walked in the order they were added. Sub-routers
|
||||||
|
// are explored depth-first.
|
||||||
|
func (r *Router) Walk(walkFn WalkFunc) error { |
||||||
|
return r.walk(walkFn, []*Route{}) |
||||||
|
} |
||||||
|
|
||||||
|
// SkipRouter is used as a return value from WalkFuncs to indicate that the
|
||||||
|
// router that walk is about to descend down to should be skipped.
|
||||||
|
var SkipRouter = errors.New("skip this router") |
||||||
|
|
||||||
|
// WalkFunc is the type of the function called for each route visited by Walk.
|
||||||
|
// At every invocation, it is given the current route, and the current router,
|
||||||
|
// and a list of ancestor routes that lead to the current route.
|
||||||
|
type WalkFunc func(route *Route, router *Router, ancestors []*Route) error |
||||||
|
|
||||||
|
func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error { |
||||||
|
for _, t := range r.routes { |
||||||
|
if t.regexp == nil || t.regexp.path == nil || t.regexp.path.template == "" { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
err := walkFn(t, r, ancestors) |
||||||
|
if err == SkipRouter { |
||||||
|
continue |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
for _, sr := range t.matchers { |
||||||
|
if h, ok := sr.(*Router); ok { |
||||||
|
err := h.walk(walkFn, ancestors) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if h, ok := t.handler.(*Router); ok { |
||||||
|
ancestors = append(ancestors, t) |
||||||
|
err := h.walk(walkFn, ancestors) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
ancestors = ancestors[:len(ancestors)-1] |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Context
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// RouteMatch stores information about a matched route.
|
||||||
|
type RouteMatch struct { |
||||||
|
Route *Route |
||||||
|
Handler http.Handler |
||||||
|
Vars map[string]string |
||||||
|
} |
||||||
|
|
||||||
|
type contextKey int |
||||||
|
|
||||||
|
const ( |
||||||
|
varsKey contextKey = iota |
||||||
|
routeKey |
||||||
|
) |
||||||
|
|
||||||
|
// Vars returns the route variables for the current request, if any.
|
||||||
|
func Vars(r *http.Request) map[string]string { |
||||||
|
if rv := contextGet(r, varsKey); rv != nil { |
||||||
|
return rv.(map[string]string) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// CurrentRoute returns the matched route for the current request, if any.
|
||||||
|
// This only works when called inside the handler of the matched route
|
||||||
|
// because the matched route is stored in the request context which is cleared
|
||||||
|
// after the handler returns, unless the KeepContext option is set on the
|
||||||
|
// Router.
|
||||||
|
func CurrentRoute(r *http.Request) *Route { |
||||||
|
if rv := contextGet(r, routeKey); rv != nil { |
||||||
|
return rv.(*Route) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func setVars(r *http.Request, val interface{}) *http.Request { |
||||||
|
return contextSet(r, varsKey, val) |
||||||
|
} |
||||||
|
|
||||||
|
func setCurrentRoute(r *http.Request, val interface{}) *http.Request { |
||||||
|
return contextSet(r, routeKey, val) |
||||||
|
} |
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// getPath returns the escaped path if possible; doing what URL.EscapedPath()
|
||||||
|
// which was added in go1.5 does
|
||||||
|
func getPath(req *http.Request) string { |
||||||
|
if req.RequestURI != "" { |
||||||
|
// Extract the path from RequestURI (which is escaped unlike URL.Path)
|
||||||
|
// as detailed here as detailed in https://golang.org/pkg/net/url/#URL
|
||||||
|
// for < 1.5 server side workaround
|
||||||
|
// http://localhost/path/here?v=1 -> /path/here
|
||||||
|
path := req.RequestURI |
||||||
|
path = strings.TrimPrefix(path, req.URL.Scheme+`://`) |
||||||
|
path = strings.TrimPrefix(path, req.URL.Host) |
||||||
|
if i := strings.LastIndex(path, "?"); i > -1 { |
||||||
|
path = path[:i] |
||||||
|
} |
||||||
|
if i := strings.LastIndex(path, "#"); i > -1 { |
||||||
|
path = path[:i] |
||||||
|
} |
||||||
|
return path |
||||||
|
} |
||||||
|
return req.URL.Path |
||||||
|
} |
||||||
|
|
||||||
|
// cleanPath returns the canonical path for p, eliminating . and .. elements.
|
||||||
|
// Borrowed from the net/http package.
|
||||||
|
func cleanPath(p string) string { |
||||||
|
if p == "" { |
||||||
|
return "/" |
||||||
|
} |
||||||
|
if p[0] != '/' { |
||||||
|
p = "/" + p |
||||||
|
} |
||||||
|
np := path.Clean(p) |
||||||
|
// path.Clean removes trailing slash except for root;
|
||||||
|
// put the trailing slash back if necessary.
|
||||||
|
if p[len(p)-1] == '/' && np != "/" { |
||||||
|
np += "/" |
||||||
|
} |
||||||
|
|
||||||
|
return np |
||||||
|
} |
||||||
|
|
||||||
|
// uniqueVars returns an error if two slices contain duplicated strings.
|
||||||
|
func uniqueVars(s1, s2 []string) error { |
||||||
|
for _, v1 := range s1 { |
||||||
|
for _, v2 := range s2 { |
||||||
|
if v1 == v2 { |
||||||
|
return fmt.Errorf("mux: duplicated route variable %q", v2) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// checkPairs returns the count of strings passed in, and an error if
|
||||||
|
// the count is not an even number.
|
||||||
|
func checkPairs(pairs ...string) (int, error) { |
||||||
|
length := len(pairs) |
||||||
|
if length%2 != 0 { |
||||||
|
return length, fmt.Errorf( |
||||||
|
"mux: number of parameters must be multiple of 2, got %v", pairs) |
||||||
|
} |
||||||
|
return length, nil |
||||||
|
} |
||||||
|
|
||||||
|
// mapFromPairsToString converts variadic string parameters to a
|
||||||
|
// string to string map.
|
||||||
|
func mapFromPairsToString(pairs ...string) (map[string]string, error) { |
||||||
|
length, err := checkPairs(pairs...) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
m := make(map[string]string, length/2) |
||||||
|
for i := 0; i < length; i += 2 { |
||||||
|
m[pairs[i]] = pairs[i+1] |
||||||
|
} |
||||||
|
return m, nil |
||||||
|
} |
||||||
|
|
||||||
|
// mapFromPairsToRegex converts variadic string paramers to a
|
||||||
|
// string to regex map.
|
||||||
|
func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) { |
||||||
|
length, err := checkPairs(pairs...) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
m := make(map[string]*regexp.Regexp, length/2) |
||||||
|
for i := 0; i < length; i += 2 { |
||||||
|
regex, err := regexp.Compile(pairs[i+1]) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
m[pairs[i]] = regex |
||||||
|
} |
||||||
|
return m, nil |
||||||
|
} |
||||||
|
|
||||||
|
// matchInArray returns true if the given string value is in the array.
|
||||||
|
func matchInArray(arr []string, value string) bool { |
||||||
|
for _, v := range arr { |
||||||
|
if v == value { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// matchMapWithString returns true if the given key/value pairs exist in a given map.
|
||||||
|
func matchMapWithString(toCheck map[string]string, toMatch map[string][]string, canonicalKey bool) bool { |
||||||
|
for k, v := range toCheck { |
||||||
|
// Check if key exists.
|
||||||
|
if canonicalKey { |
||||||
|
k = http.CanonicalHeaderKey(k) |
||||||
|
} |
||||||
|
if values := toMatch[k]; values == nil { |
||||||
|
return false |
||||||
|
} else if v != "" { |
||||||
|
// If value was defined as an empty string we only check that the
|
||||||
|
// key exists. Otherwise we also check for equality.
|
||||||
|
valueExists := false |
||||||
|
for _, value := range values { |
||||||
|
if v == value { |
||||||
|
valueExists = true |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
if !valueExists { |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
// matchMapWithRegex returns true if the given key/value pairs exist in a given map compiled against
|
||||||
|
// the given regex
|
||||||
|
func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]string, canonicalKey bool) bool { |
||||||
|
for k, v := range toCheck { |
||||||
|
// Check if key exists.
|
||||||
|
if canonicalKey { |
||||||
|
k = http.CanonicalHeaderKey(k) |
||||||
|
} |
||||||
|
if values := toMatch[k]; values == nil { |
||||||
|
return false |
||||||
|
} else if v != nil { |
||||||
|
// If value was defined as an empty string we only check that the
|
||||||
|
// key exists. Otherwise we also check for equality.
|
||||||
|
valueExists := false |
||||||
|
for _, value := range values { |
||||||
|
if v.MatchString(value) { |
||||||
|
valueExists = true |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
if !valueExists { |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
@ -0,0 +1,316 @@ |
|||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package mux |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"regexp" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
// newRouteRegexp parses a route template and returns a routeRegexp,
|
||||||
|
// used to match a host, a path or a query string.
|
||||||
|
//
|
||||||
|
// It will extract named variables, assemble a regexp to be matched, create
|
||||||
|
// a "reverse" template to build URLs and compile regexps to validate variable
|
||||||
|
// values used in URL building.
|
||||||
|
//
|
||||||
|
// Previously we accepted only Python-like identifiers for variable
|
||||||
|
// names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that
|
||||||
|
// name and pattern can't be empty, and names can't contain a colon.
|
||||||
|
func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash, useEncodedPath bool) (*routeRegexp, error) { |
||||||
|
// Check if it is well-formed.
|
||||||
|
idxs, errBraces := braceIndices(tpl) |
||||||
|
if errBraces != nil { |
||||||
|
return nil, errBraces |
||||||
|
} |
||||||
|
// Backup the original.
|
||||||
|
template := tpl |
||||||
|
// Now let's parse it.
|
||||||
|
defaultPattern := "[^/]+" |
||||||
|
if matchQuery { |
||||||
|
defaultPattern = "[^?&]*" |
||||||
|
} else if matchHost { |
||||||
|
defaultPattern = "[^.]+" |
||||||
|
matchPrefix = false |
||||||
|
} |
||||||
|
// Only match strict slash if not matching
|
||||||
|
if matchPrefix || matchHost || matchQuery { |
||||||
|
strictSlash = false |
||||||
|
} |
||||||
|
// Set a flag for strictSlash.
|
||||||
|
endSlash := false |
||||||
|
if strictSlash && strings.HasSuffix(tpl, "/") { |
||||||
|
tpl = tpl[:len(tpl)-1] |
||||||
|
endSlash = true |
||||||
|
} |
||||||
|
varsN := make([]string, len(idxs)/2) |
||||||
|
varsR := make([]*regexp.Regexp, len(idxs)/2) |
||||||
|
pattern := bytes.NewBufferString("") |
||||||
|
pattern.WriteByte('^') |
||||||
|
reverse := bytes.NewBufferString("") |
||||||
|
var end int |
||||||
|
var err error |
||||||
|
for i := 0; i < len(idxs); i += 2 { |
||||||
|
// Set all values we are interested in.
|
||||||
|
raw := tpl[end:idxs[i]] |
||||||
|
end = idxs[i+1] |
||||||
|
parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2) |
||||||
|
name := parts[0] |
||||||
|
patt := defaultPattern |
||||||
|
if len(parts) == 2 { |
||||||
|
patt = parts[1] |
||||||
|
} |
||||||
|
// Name or pattern can't be empty.
|
||||||
|
if name == "" || patt == "" { |
||||||
|
return nil, fmt.Errorf("mux: missing name or pattern in %q", |
||||||
|
tpl[idxs[i]:end]) |
||||||
|
} |
||||||
|
// Build the regexp pattern.
|
||||||
|
fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt) |
||||||
|
|
||||||
|
// Build the reverse template.
|
||||||
|
fmt.Fprintf(reverse, "%s%%s", raw) |
||||||
|
|
||||||
|
// Append variable name and compiled pattern.
|
||||||
|
varsN[i/2] = name |
||||||
|
varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt)) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
// Add the remaining.
|
||||||
|
raw := tpl[end:] |
||||||
|
pattern.WriteString(regexp.QuoteMeta(raw)) |
||||||
|
if strictSlash { |
||||||
|
pattern.WriteString("[/]?") |
||||||
|
} |
||||||
|
if matchQuery { |
||||||
|
// Add the default pattern if the query value is empty
|
||||||
|
if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" { |
||||||
|
pattern.WriteString(defaultPattern) |
||||||
|
} |
||||||
|
} |
||||||
|
if !matchPrefix { |
||||||
|
pattern.WriteByte('$') |
||||||
|
} |
||||||
|
reverse.WriteString(raw) |
||||||
|
if endSlash { |
||||||
|
reverse.WriteByte('/') |
||||||
|
} |
||||||
|
// Compile full regexp.
|
||||||
|
reg, errCompile := regexp.Compile(pattern.String()) |
||||||
|
if errCompile != nil { |
||||||
|
return nil, errCompile |
||||||
|
} |
||||||
|
// Done!
|
||||||
|
return &routeRegexp{ |
||||||
|
template: template, |
||||||
|
matchHost: matchHost, |
||||||
|
matchQuery: matchQuery, |
||||||
|
strictSlash: strictSlash, |
||||||
|
useEncodedPath: useEncodedPath, |
||||||
|
regexp: reg, |
||||||
|
reverse: reverse.String(), |
||||||
|
varsN: varsN, |
||||||
|
varsR: varsR, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
// routeRegexp stores a regexp to match a host or path and information to
|
||||||
|
// collect and validate route variables.
|
||||||
|
type routeRegexp struct { |
||||||
|
// The unmodified template.
|
||||||
|
template string |
||||||
|
// True for host match, false for path or query string match.
|
||||||
|
matchHost bool |
||||||
|
// True for query string match, false for path and host match.
|
||||||
|
matchQuery bool |
||||||
|
// The strictSlash value defined on the route, but disabled if PathPrefix was used.
|
||||||
|
strictSlash bool |
||||||
|
// Determines whether to use encoded path from getPath function or unencoded
|
||||||
|
// req.URL.Path for path matching
|
||||||
|
useEncodedPath bool |
||||||
|
// Expanded regexp.
|
||||||
|
regexp *regexp.Regexp |
||||||
|
// Reverse template.
|
||||||
|
reverse string |
||||||
|
// Variable names.
|
||||||
|
varsN []string |
||||||
|
// Variable regexps (validators).
|
||||||
|
varsR []*regexp.Regexp |
||||||
|
} |
||||||
|
|
||||||
|
// Match matches the regexp against the URL host or path.
|
||||||
|
func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { |
||||||
|
if !r.matchHost { |
||||||
|
if r.matchQuery { |
||||||
|
return r.matchQueryString(req) |
||||||
|
} |
||||||
|
path := req.URL.Path |
||||||
|
if r.useEncodedPath { |
||||||
|
path = getPath(req) |
||||||
|
} |
||||||
|
return r.regexp.MatchString(path) |
||||||
|
} |
||||||
|
|
||||||
|
return r.regexp.MatchString(getHost(req)) |
||||||
|
} |
||||||
|
|
||||||
|
// url builds a URL part using the given values.
|
||||||
|
func (r *routeRegexp) url(values map[string]string) (string, error) { |
||||||
|
urlValues := make([]interface{}, len(r.varsN)) |
||||||
|
for k, v := range r.varsN { |
||||||
|
value, ok := values[v] |
||||||
|
if !ok { |
||||||
|
return "", fmt.Errorf("mux: missing route variable %q", v) |
||||||
|
} |
||||||
|
urlValues[k] = value |
||||||
|
} |
||||||
|
rv := fmt.Sprintf(r.reverse, urlValues...) |
||||||
|
if !r.regexp.MatchString(rv) { |
||||||
|
// The URL is checked against the full regexp, instead of checking
|
||||||
|
// individual variables. This is faster but to provide a good error
|
||||||
|
// message, we check individual regexps if the URL doesn't match.
|
||||||
|
for k, v := range r.varsN { |
||||||
|
if !r.varsR[k].MatchString(values[v]) { |
||||||
|
return "", fmt.Errorf( |
||||||
|
"mux: variable %q doesn't match, expected %q", values[v], |
||||||
|
r.varsR[k].String()) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return rv, nil |
||||||
|
} |
||||||
|
|
||||||
|
// getURLQuery returns a single query parameter from a request URL.
|
||||||
|
// For a URL with foo=bar&baz=ding, we return only the relevant key
|
||||||
|
// value pair for the routeRegexp.
|
||||||
|
func (r *routeRegexp) getURLQuery(req *http.Request) string { |
||||||
|
if !r.matchQuery { |
||||||
|
return "" |
||||||
|
} |
||||||
|
templateKey := strings.SplitN(r.template, "=", 2)[0] |
||||||
|
for key, vals := range req.URL.Query() { |
||||||
|
if key == templateKey && len(vals) > 0 { |
||||||
|
return key + "=" + vals[0] |
||||||
|
} |
||||||
|
} |
||||||
|
return "" |
||||||
|
} |
||||||
|
|
||||||
|
func (r *routeRegexp) matchQueryString(req *http.Request) bool { |
||||||
|
return r.regexp.MatchString(r.getURLQuery(req)) |
||||||
|
} |
||||||
|
|
||||||
|
// braceIndices returns the first level curly brace indices from a string.
|
||||||
|
// It returns an error in case of unbalanced braces.
|
||||||
|
func braceIndices(s string) ([]int, error) { |
||||||
|
var level, idx int |
||||||
|
var idxs []int |
||||||
|
for i := 0; i < len(s); i++ { |
||||||
|
switch s[i] { |
||||||
|
case '{': |
||||||
|
if level++; level == 1 { |
||||||
|
idx = i |
||||||
|
} |
||||||
|
case '}': |
||||||
|
if level--; level == 0 { |
||||||
|
idxs = append(idxs, idx, i+1) |
||||||
|
} else if level < 0 { |
||||||
|
return nil, fmt.Errorf("mux: unbalanced braces in %q", s) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if level != 0 { |
||||||
|
return nil, fmt.Errorf("mux: unbalanced braces in %q", s) |
||||||
|
} |
||||||
|
return idxs, nil |
||||||
|
} |
||||||
|
|
||||||
|
// varGroupName builds a capturing group name for the indexed variable.
|
||||||
|
func varGroupName(idx int) string { |
||||||
|
return "v" + strconv.Itoa(idx) |
||||||
|
} |
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// routeRegexpGroup
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// routeRegexpGroup groups the route matchers that carry variables.
|
||||||
|
type routeRegexpGroup struct { |
||||||
|
host *routeRegexp |
||||||
|
path *routeRegexp |
||||||
|
queries []*routeRegexp |
||||||
|
} |
||||||
|
|
||||||
|
// setMatch extracts the variables from the URL once a route matches.
|
||||||
|
func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { |
||||||
|
// Store host variables.
|
||||||
|
if v.host != nil { |
||||||
|
host := getHost(req) |
||||||
|
matches := v.host.regexp.FindStringSubmatchIndex(host) |
||||||
|
if len(matches) > 0 { |
||||||
|
extractVars(host, matches, v.host.varsN, m.Vars) |
||||||
|
} |
||||||
|
} |
||||||
|
path := req.URL.Path |
||||||
|
if r.useEncodedPath { |
||||||
|
path = getPath(req) |
||||||
|
} |
||||||
|
// Store path variables.
|
||||||
|
if v.path != nil { |
||||||
|
matches := v.path.regexp.FindStringSubmatchIndex(path) |
||||||
|
if len(matches) > 0 { |
||||||
|
extractVars(path, matches, v.path.varsN, m.Vars) |
||||||
|
// Check if we should redirect.
|
||||||
|
if v.path.strictSlash { |
||||||
|
p1 := strings.HasSuffix(path, "/") |
||||||
|
p2 := strings.HasSuffix(v.path.template, "/") |
||||||
|
if p1 != p2 { |
||||||
|
u, _ := url.Parse(req.URL.String()) |
||||||
|
if p1 { |
||||||
|
u.Path = u.Path[:len(u.Path)-1] |
||||||
|
} else { |
||||||
|
u.Path += "/" |
||||||
|
} |
||||||
|
m.Handler = http.RedirectHandler(u.String(), 301) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
// Store query string variables.
|
||||||
|
for _, q := range v.queries { |
||||||
|
queryURL := q.getURLQuery(req) |
||||||
|
matches := q.regexp.FindStringSubmatchIndex(queryURL) |
||||||
|
if len(matches) > 0 { |
||||||
|
extractVars(queryURL, matches, q.varsN, m.Vars) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// getHost tries its best to return the request host.
|
||||||
|
func getHost(r *http.Request) string { |
||||||
|
if r.URL.IsAbs() { |
||||||
|
return r.URL.Host |
||||||
|
} |
||||||
|
host := r.Host |
||||||
|
// Slice off any port information.
|
||||||
|
if i := strings.Index(host, ":"); i != -1 { |
||||||
|
host = host[:i] |
||||||
|
} |
||||||
|
return host |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
func extractVars(input string, matches []int, names []string, output map[string]string) { |
||||||
|
for i, name := range names { |
||||||
|
output[name] = input[matches[2*i+2]:matches[2*i+3]] |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,636 @@ |
|||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package mux |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"regexp" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
// Route stores information to match a request and build URLs.
|
||||||
|
type Route struct { |
||||||
|
// Parent where the route was registered (a Router).
|
||||||
|
parent parentRoute |
||||||
|
// Request handler for the route.
|
||||||
|
handler http.Handler |
||||||
|
// List of matchers.
|
||||||
|
matchers []matcher |
||||||
|
// Manager for the variables from host and path.
|
||||||
|
regexp *routeRegexpGroup |
||||||
|
// If true, when the path pattern is "/path/", accessing "/path" will
|
||||||
|
// redirect to the former and vice versa.
|
||||||
|
strictSlash bool |
||||||
|
// If true, when the path pattern is "/path//to", accessing "/path//to"
|
||||||
|
// will not redirect
|
||||||
|
skipClean bool |
||||||
|
// If true, "/path/foo%2Fbar/to" will match the path "/path/{var}/to"
|
||||||
|
useEncodedPath bool |
||||||
|
// If true, this route never matches: it is only used to build URLs.
|
||||||
|
buildOnly bool |
||||||
|
// The name used to build URLs.
|
||||||
|
name string |
||||||
|
// Error resulted from building a route.
|
||||||
|
err error |
||||||
|
|
||||||
|
buildVarsFunc BuildVarsFunc |
||||||
|
} |
||||||
|
|
||||||
|
func (r *Route) SkipClean() bool { |
||||||
|
return r.skipClean |
||||||
|
} |
||||||
|
|
||||||
|
// Match matches the route against the request.
|
||||||
|
func (r *Route) Match(req *http.Request, match *RouteMatch) bool { |
||||||
|
if r.buildOnly || r.err != nil { |
||||||
|
return false |
||||||
|
} |
||||||
|
// Match everything.
|
||||||
|
for _, m := range r.matchers { |
||||||
|
if matched := m.Match(req, match); !matched { |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
// Yay, we have a match. Let's collect some info about it.
|
||||||
|
if match.Route == nil { |
||||||
|
match.Route = r |
||||||
|
} |
||||||
|
if match.Handler == nil { |
||||||
|
match.Handler = r.handler |
||||||
|
} |
||||||
|
if match.Vars == nil { |
||||||
|
match.Vars = make(map[string]string) |
||||||
|
} |
||||||
|
// Set variables.
|
||||||
|
if r.regexp != nil { |
||||||
|
r.regexp.setMatch(req, match, r) |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Route attributes
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// GetError returns an error resulted from building the route, if any.
|
||||||
|
func (r *Route) GetError() error { |
||||||
|
return r.err |
||||||
|
} |
||||||
|
|
||||||
|
// BuildOnly sets the route to never match: it is only used to build URLs.
|
||||||
|
func (r *Route) BuildOnly() *Route { |
||||||
|
r.buildOnly = true |
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
// Handler --------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Handler sets a handler for the route.
|
||||||
|
func (r *Route) Handler(handler http.Handler) *Route { |
||||||
|
if r.err == nil { |
||||||
|
r.handler = handler |
||||||
|
} |
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
// HandlerFunc sets a handler function for the route.
|
||||||
|
func (r *Route) HandlerFunc(f func(http.ResponseWriter, *http.Request)) *Route { |
||||||
|
return r.Handler(http.HandlerFunc(f)) |
||||||
|
} |
||||||
|
|
||||||
|
// GetHandler returns the handler for the route, if any.
|
||||||
|
func (r *Route) GetHandler() http.Handler { |
||||||
|
return r.handler |
||||||
|
} |
||||||
|
|
||||||
|
// Name -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Name sets the name for the route, used to build URLs.
|
||||||
|
// If the name was registered already it will be overwritten.
|
||||||
|
func (r *Route) Name(name string) *Route { |
||||||
|
if r.name != "" { |
||||||
|
r.err = fmt.Errorf("mux: route already has name %q, can't set %q", |
||||||
|
r.name, name) |
||||||
|
} |
||||||
|
if r.err == nil { |
||||||
|
r.name = name |
||||||
|
r.getNamedRoutes()[name] = r |
||||||
|
} |
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
// GetName returns the name for the route, if any.
|
||||||
|
func (r *Route) GetName() string { |
||||||
|
return r.name |
||||||
|
} |
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Matchers
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// matcher types try to match a request.
|
||||||
|
type matcher interface { |
||||||
|
Match(*http.Request, *RouteMatch) bool |
||||||
|
} |
||||||
|
|
||||||
|
// addMatcher adds a matcher to the route.
|
||||||
|
func (r *Route) addMatcher(m matcher) *Route { |
||||||
|
if r.err == nil { |
||||||
|
r.matchers = append(r.matchers, m) |
||||||
|
} |
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
// addRegexpMatcher adds a host or path matcher and builder to a route.
|
||||||
|
func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery bool) error { |
||||||
|
if r.err != nil { |
||||||
|
return r.err |
||||||
|
} |
||||||
|
r.regexp = r.getRegexpGroup() |
||||||
|
if !matchHost && !matchQuery { |
||||||
|
if len(tpl) == 0 || tpl[0] != '/' { |
||||||
|
return fmt.Errorf("mux: path must start with a slash, got %q", tpl) |
||||||
|
} |
||||||
|
if r.regexp.path != nil { |
||||||
|
tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl |
||||||
|
} |
||||||
|
} |
||||||
|
rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, matchQuery, r.strictSlash, r.useEncodedPath) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
for _, q := range r.regexp.queries { |
||||||
|
if err = uniqueVars(rr.varsN, q.varsN); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
if matchHost { |
||||||
|
if r.regexp.path != nil { |
||||||
|
if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
r.regexp.host = rr |
||||||
|
} else { |
||||||
|
if r.regexp.host != nil { |
||||||
|
if err = uniqueVars(rr.varsN, r.regexp.host.varsN); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
if matchQuery { |
||||||
|
r.regexp.queries = append(r.regexp.queries, rr) |
||||||
|
} else { |
||||||
|
r.regexp.path = rr |
||||||
|
} |
||||||
|
} |
||||||
|
r.addMatcher(rr) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Headers --------------------------------------------------------------------
|
||||||
|
|
||||||
|
// headerMatcher matches the request against header values.
|
||||||
|
type headerMatcher map[string]string |
||||||
|
|
||||||
|
func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool { |
||||||
|
return matchMapWithString(m, r.Header, true) |
||||||
|
} |
||||||
|
|
||||||
|
// Headers adds a matcher for request header values.
|
||||||
|
// It accepts a sequence of key/value pairs to be matched. For example:
|
||||||
|
//
|
||||||
|
// r := mux.NewRouter()
|
||||||
|
// r.Headers("Content-Type", "application/json",
|
||||||
|
// "X-Requested-With", "XMLHttpRequest")
|
||||||
|
//
|
||||||
|
// The above route will only match if both request header values match.
|
||||||
|
// If the value is an empty string, it will match any value if the key is set.
|
||||||
|
func (r *Route) Headers(pairs ...string) *Route { |
||||||
|
if r.err == nil { |
||||||
|
var headers map[string]string |
||||||
|
headers, r.err = mapFromPairsToString(pairs...) |
||||||
|
return r.addMatcher(headerMatcher(headers)) |
||||||
|
} |
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
// headerRegexMatcher matches the request against the route given a regex for the header
|
||||||
|
type headerRegexMatcher map[string]*regexp.Regexp |
||||||
|
|
||||||
|
func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool { |
||||||
|
return matchMapWithRegex(m, r.Header, true) |
||||||
|
} |
||||||
|
|
||||||
|
// HeadersRegexp accepts a sequence of key/value pairs, where the value has regex
|
||||||
|
// support. For example:
|
||||||
|
//
|
||||||
|
// r := mux.NewRouter()
|
||||||
|
// r.HeadersRegexp("Content-Type", "application/(text|json)",
|
||||||
|
// "X-Requested-With", "XMLHttpRequest")
|
||||||
|
//
|
||||||
|
// The above route will only match if both the request header matches both regular expressions.
|
||||||
|
// It the value is an empty string, it will match any value if the key is set.
|
||||||
|
func (r *Route) HeadersRegexp(pairs ...string) *Route { |
||||||
|
if r.err == nil { |
||||||
|
var headers map[string]*regexp.Regexp |
||||||
|
headers, r.err = mapFromPairsToRegex(pairs...) |
||||||
|
return r.addMatcher(headerRegexMatcher(headers)) |
||||||
|
} |
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
// Host -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Host adds a matcher for the URL host.
|
||||||
|
// It accepts a template with zero or more URL variables enclosed by {}.
|
||||||
|
// Variables can define an optional regexp pattern to be matched:
|
||||||
|
//
|
||||||
|
// - {name} matches anything until the next dot.
|
||||||
|
//
|
||||||
|
// - {name:pattern} matches the given regexp pattern.
|
||||||
|
//
|
||||||
|
// For example:
|
||||||
|
//
|
||||||
|
// r := mux.NewRouter()
|
||||||
|
// r.Host("www.example.com")
|
||||||
|
// r.Host("{subdomain}.domain.com")
|
||||||
|
// r.Host("{subdomain:[a-z]+}.domain.com")
|
||||||
|
//
|
||||||
|
// Variable names must be unique in a given route. They can be retrieved
|
||||||
|
// calling mux.Vars(request).
|
||||||
|
func (r *Route) Host(tpl string) *Route { |
||||||
|
r.err = r.addRegexpMatcher(tpl, true, false, false) |
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
// MatcherFunc ----------------------------------------------------------------
|
||||||
|
|
||||||
|
// MatcherFunc is the function signature used by custom matchers.
|
||||||
|
type MatcherFunc func(*http.Request, *RouteMatch) bool |
||||||
|
|
||||||
|
// Match returns the match for a given request.
|
||||||
|
func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool { |
||||||
|
return m(r, match) |
||||||
|
} |
||||||
|
|
||||||
|
// MatcherFunc adds a custom function to be used as request matcher.
|
||||||
|
func (r *Route) MatcherFunc(f MatcherFunc) *Route { |
||||||
|
return r.addMatcher(f) |
||||||
|
} |
||||||
|
|
||||||
|
// Methods --------------------------------------------------------------------
|
||||||
|
|
||||||
|
// methodMatcher matches the request against HTTP methods.
|
||||||
|
type methodMatcher []string |
||||||
|
|
||||||
|
func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool { |
||||||
|
return matchInArray(m, r.Method) |
||||||
|
} |
||||||
|
|
||||||
|
// Methods adds a matcher for HTTP methods.
|
||||||
|
// It accepts a sequence of one or more methods to be matched, e.g.:
|
||||||
|
// "GET", "POST", "PUT".
|
||||||
|
func (r *Route) Methods(methods ...string) *Route { |
||||||
|
for k, v := range methods { |
||||||
|
methods[k] = strings.ToUpper(v) |
||||||
|
} |
||||||
|
return r.addMatcher(methodMatcher(methods)) |
||||||
|
} |
||||||
|
|
||||||
|
// Path -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Path adds a matcher for the URL path.
|
||||||
|
// It accepts a template with zero or more URL variables enclosed by {}. The
|
||||||
|
// template must start with a "/".
|
||||||
|
// Variables can define an optional regexp pattern to be matched:
|
||||||
|
//
|
||||||
|
// - {name} matches anything until the next slash.
|
||||||
|
//
|
||||||
|
// - {name:pattern} matches the given regexp pattern.
|
||||||
|
//
|
||||||
|
// For example:
|
||||||
|
//
|
||||||
|
// r := mux.NewRouter()
|
||||||
|
// r.Path("/products/").Handler(ProductsHandler)
|
||||||
|
// r.Path("/products/{key}").Handler(ProductsHandler)
|
||||||
|
// r.Path("/articles/{category}/{id:[0-9]+}").
|
||||||
|
// Handler(ArticleHandler)
|
||||||
|
//
|
||||||
|
// Variable names must be unique in a given route. They can be retrieved
|
||||||
|
// calling mux.Vars(request).
|
||||||
|
func (r *Route) Path(tpl string) *Route { |
||||||
|
r.err = r.addRegexpMatcher(tpl, false, false, false) |
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
// PathPrefix -----------------------------------------------------------------
|
||||||
|
|
||||||
|
// PathPrefix adds a matcher for the URL path prefix. This matches if the given
|
||||||
|
// template is a prefix of the full URL path. See Route.Path() for details on
|
||||||
|
// the tpl argument.
|
||||||
|
//
|
||||||
|
// Note that it does not treat slashes specially ("/foobar/" will be matched by
|
||||||
|
// the prefix "/foo") so you may want to use a trailing slash here.
|
||||||
|
//
|
||||||
|
// Also note that the setting of Router.StrictSlash() has no effect on routes
|
||||||
|
// with a PathPrefix matcher.
|
||||||
|
func (r *Route) PathPrefix(tpl string) *Route { |
||||||
|
r.err = r.addRegexpMatcher(tpl, false, true, false) |
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
// Query ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Queries adds a matcher for URL query values.
|
||||||
|
// It accepts a sequence of key/value pairs. Values may define variables.
|
||||||
|
// For example:
|
||||||
|
//
|
||||||
|
// r := mux.NewRouter()
|
||||||
|
// r.Queries("foo", "bar", "id", "{id:[0-9]+}")
|
||||||
|
//
|
||||||
|
// The above route will only match if the URL contains the defined queries
|
||||||
|
// values, e.g.: ?foo=bar&id=42.
|
||||||
|
//
|
||||||
|
// It the value is an empty string, it will match any value if the key is set.
|
||||||
|
//
|
||||||
|
// Variables can define an optional regexp pattern to be matched:
|
||||||
|
//
|
||||||
|
// - {name} matches anything until the next slash.
|
||||||
|
//
|
||||||
|
// - {name:pattern} matches the given regexp pattern.
|
||||||
|
func (r *Route) Queries(pairs ...string) *Route { |
||||||
|
length := len(pairs) |
||||||
|
if length%2 != 0 { |
||||||
|
r.err = fmt.Errorf( |
||||||
|
"mux: number of parameters must be multiple of 2, got %v", pairs) |
||||||
|
return nil |
||||||
|
} |
||||||
|
for i := 0; i < length; i += 2 { |
||||||
|
if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], false, false, true); r.err != nil { |
||||||
|
return r |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
// Schemes --------------------------------------------------------------------
|
||||||
|
|
||||||
|
// schemeMatcher matches the request against URL schemes.
|
||||||
|
type schemeMatcher []string |
||||||
|
|
||||||
|
func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool { |
||||||
|
return matchInArray(m, r.URL.Scheme) |
||||||
|
} |
||||||
|
|
||||||
|
// Schemes adds a matcher for URL schemes.
|
||||||
|
// It accepts a sequence of schemes to be matched, e.g.: "http", "https".
|
||||||
|
func (r *Route) Schemes(schemes ...string) *Route { |
||||||
|
for k, v := range schemes { |
||||||
|
schemes[k] = strings.ToLower(v) |
||||||
|
} |
||||||
|
return r.addMatcher(schemeMatcher(schemes)) |
||||||
|
} |
||||||
|
|
||||||
|
// BuildVarsFunc --------------------------------------------------------------
|
||||||
|
|
||||||
|
// BuildVarsFunc is the function signature used by custom build variable
|
||||||
|
// functions (which can modify route variables before a route's URL is built).
|
||||||
|
type BuildVarsFunc func(map[string]string) map[string]string |
||||||
|
|
||||||
|
// BuildVarsFunc adds a custom function to be used to modify build variables
|
||||||
|
// before a route's URL is built.
|
||||||
|
func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route { |
||||||
|
r.buildVarsFunc = f |
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
// Subrouter ------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Subrouter creates a subrouter for the route.
|
||||||
|
//
|
||||||
|
// It will test the inner routes only if the parent route matched. For example:
|
||||||
|
//
|
||||||
|
// r := mux.NewRouter()
|
||||||
|
// s := r.Host("www.example.com").Subrouter()
|
||||||
|
// s.HandleFunc("/products/", ProductsHandler)
|
||||||
|
// s.HandleFunc("/products/{key}", ProductHandler)
|
||||||
|
// s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler)
|
||||||
|
//
|
||||||
|
// Here, the routes registered in the subrouter won't be tested if the host
|
||||||
|
// doesn't match.
|
||||||
|
func (r *Route) Subrouter() *Router { |
||||||
|
router := &Router{parent: r, strictSlash: r.strictSlash} |
||||||
|
r.addMatcher(router) |
||||||
|
return router |
||||||
|
} |
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// URL building
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// URL builds a URL for the route.
|
||||||
|
//
|
||||||
|
// It accepts a sequence of key/value pairs for the route variables. For
|
||||||
|
// example, given this route:
|
||||||
|
//
|
||||||
|
// r := mux.NewRouter()
|
||||||
|
// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
|
||||||
|
// Name("article")
|
||||||
|
//
|
||||||
|
// ...a URL for it can be built using:
|
||||||
|
//
|
||||||
|
// url, err := r.Get("article").URL("category", "technology", "id", "42")
|
||||||
|
//
|
||||||
|
// ...which will return an url.URL with the following path:
|
||||||
|
//
|
||||||
|
// "/articles/technology/42"
|
||||||
|
//
|
||||||
|
// This also works for host variables:
|
||||||
|
//
|
||||||
|
// r := mux.NewRouter()
|
||||||
|
// r.Host("{subdomain}.domain.com").
|
||||||
|
// HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
|
||||||
|
// Name("article")
|
||||||
|
//
|
||||||
|
// // url.String() will be "http://news.domain.com/articles/technology/42"
|
||||||
|
// url, err := r.Get("article").URL("subdomain", "news",
|
||||||
|
// "category", "technology",
|
||||||
|
// "id", "42")
|
||||||
|
//
|
||||||
|
// All variables defined in the route are required, and their values must
|
||||||
|
// conform to the corresponding patterns.
|
||||||
|
func (r *Route) URL(pairs ...string) (*url.URL, error) { |
||||||
|
if r.err != nil { |
||||||
|
return nil, r.err |
||||||
|
} |
||||||
|
if r.regexp == nil { |
||||||
|
return nil, errors.New("mux: route doesn't have a host or path") |
||||||
|
} |
||||||
|
values, err := r.prepareVars(pairs...) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
var scheme, host, path string |
||||||
|
if r.regexp.host != nil { |
||||||
|
// Set a default scheme.
|
||||||
|
scheme = "http" |
||||||
|
if host, err = r.regexp.host.url(values); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
if r.regexp.path != nil { |
||||||
|
if path, err = r.regexp.path.url(values); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
return &url.URL{ |
||||||
|
Scheme: scheme, |
||||||
|
Host: host, |
||||||
|
Path: path, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
// URLHost builds the host part of the URL for a route. See Route.URL().
|
||||||
|
//
|
||||||
|
// The route must have a host defined.
|
||||||
|
func (r *Route) URLHost(pairs ...string) (*url.URL, error) { |
||||||
|
if r.err != nil { |
||||||
|
return nil, r.err |
||||||
|
} |
||||||
|
if r.regexp == nil || r.regexp.host == nil { |
||||||
|
return nil, errors.New("mux: route doesn't have a host") |
||||||
|
} |
||||||
|
values, err := r.prepareVars(pairs...) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
host, err := r.regexp.host.url(values) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return &url.URL{ |
||||||
|
Scheme: "http", |
||||||
|
Host: host, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
// URLPath builds the path part of the URL for a route. See Route.URL().
|
||||||
|
//
|
||||||
|
// The route must have a path defined.
|
||||||
|
func (r *Route) URLPath(pairs ...string) (*url.URL, error) { |
||||||
|
if r.err != nil { |
||||||
|
return nil, r.err |
||||||
|
} |
||||||
|
if r.regexp == nil || r.regexp.path == nil { |
||||||
|
return nil, errors.New("mux: route doesn't have a path") |
||||||
|
} |
||||||
|
values, err := r.prepareVars(pairs...) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
path, err := r.regexp.path.url(values) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return &url.URL{ |
||||||
|
Path: path, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetPathTemplate returns the template used to build the
|
||||||
|
// route match.
|
||||||
|
// This is useful for building simple REST API documentation and for instrumentation
|
||||||
|
// against third-party services.
|
||||||
|
// An error will be returned if the route does not define a path.
|
||||||
|
func (r *Route) GetPathTemplate() (string, error) { |
||||||
|
if r.err != nil { |
||||||
|
return "", r.err |
||||||
|
} |
||||||
|
if r.regexp == nil || r.regexp.path == nil { |
||||||
|
return "", errors.New("mux: route doesn't have a path") |
||||||
|
} |
||||||
|
return r.regexp.path.template, nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetHostTemplate returns the template used to build the
|
||||||
|
// route match.
|
||||||
|
// This is useful for building simple REST API documentation and for instrumentation
|
||||||
|
// against third-party services.
|
||||||
|
// An error will be returned if the route does not define a host.
|
||||||
|
func (r *Route) GetHostTemplate() (string, error) { |
||||||
|
if r.err != nil { |
||||||
|
return "", r.err |
||||||
|
} |
||||||
|
if r.regexp == nil || r.regexp.host == nil { |
||||||
|
return "", errors.New("mux: route doesn't have a host") |
||||||
|
} |
||||||
|
return r.regexp.host.template, nil |
||||||
|
} |
||||||
|
|
||||||
|
// prepareVars converts the route variable pairs into a map. If the route has a
|
||||||
|
// BuildVarsFunc, it is invoked.
|
||||||
|
func (r *Route) prepareVars(pairs ...string) (map[string]string, error) { |
||||||
|
m, err := mapFromPairsToString(pairs...) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return r.buildVars(m), nil |
||||||
|
} |
||||||
|
|
||||||
|
func (r *Route) buildVars(m map[string]string) map[string]string { |
||||||
|
if r.parent != nil { |
||||||
|
m = r.parent.buildVars(m) |
||||||
|
} |
||||||
|
if r.buildVarsFunc != nil { |
||||||
|
m = r.buildVarsFunc(m) |
||||||
|
} |
||||||
|
return m |
||||||
|
} |
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// parentRoute
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// parentRoute allows routes to know about parent host and path definitions.
|
||||||
|
type parentRoute interface { |
||||||
|
getNamedRoutes() map[string]*Route |
||||||
|
getRegexpGroup() *routeRegexpGroup |
||||||
|
buildVars(map[string]string) map[string]string |
||||||
|
} |
||||||
|
|
||||||
|
// getNamedRoutes returns the map where named routes are registered.
|
||||||
|
func (r *Route) getNamedRoutes() map[string]*Route { |
||||||
|
if r.parent == nil { |
||||||
|
// During tests router is not always set.
|
||||||
|
r.parent = NewRouter() |
||||||
|
} |
||||||
|
return r.parent.getNamedRoutes() |
||||||
|
} |
||||||
|
|
||||||
|
// getRegexpGroup returns regexp definitions from this route.
|
||||||
|
func (r *Route) getRegexpGroup() *routeRegexpGroup { |
||||||
|
if r.regexp == nil { |
||||||
|
if r.parent == nil { |
||||||
|
// During tests router is not always set.
|
||||||
|
r.parent = NewRouter() |
||||||
|
} |
||||||
|
regexp := r.parent.getRegexpGroup() |
||||||
|
if regexp == nil { |
||||||
|
r.regexp = new(routeRegexpGroup) |
||||||
|
} else { |
||||||
|
// Copy.
|
||||||
|
r.regexp = &routeRegexpGroup{ |
||||||
|
host: regexp.host, |
||||||
|
path: regexp.path, |
||||||
|
queries: regexp.queries, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return r.regexp |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
Copyright (c) 2012 Rodrigo Moraes. All rights reserved. |
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without |
||||||
|
modification, are permitted provided that the following conditions are |
||||||
|
met: |
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright |
||||||
|
notice, this list of conditions and the following disclaimer. |
||||||
|
* Redistributions in binary form must reproduce the above |
||||||
|
copyright notice, this list of conditions and the following disclaimer |
||||||
|
in the documentation and/or other materials provided with the |
||||||
|
distribution. |
||||||
|
* Neither the name of Google Inc. nor the names of its |
||||||
|
contributors may be used to endorse or promote products derived from |
||||||
|
this software without specific prior written permission. |
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@ -0,0 +1,78 @@ |
|||||||
|
securecookie |
||||||
|
============ |
||||||
|
[![GoDoc](https://godoc.org/github.com/gorilla/securecookie?status.svg)](https://godoc.org/github.com/gorilla/securecookie) [![Build Status](https://travis-ci.org/gorilla/securecookie.png?branch=master)](https://travis-ci.org/gorilla/securecookie) |
||||||
|
|
||||||
|
securecookie encodes and decodes authenticated and optionally encrypted |
||||||
|
cookie values. |
||||||
|
|
||||||
|
Secure cookies can't be forged, because their values are validated using HMAC. |
||||||
|
When encrypted, the content is also inaccessible to malicious eyes. It is still |
||||||
|
recommended that sensitive data not be stored in cookies, and that HTTPS be used |
||||||
|
to prevent cookie [replay attacks](https://en.wikipedia.org/wiki/Replay_attack). |
||||||
|
|
||||||
|
## Examples |
||||||
|
|
||||||
|
To use it, first create a new SecureCookie instance: |
||||||
|
|
||||||
|
```go |
||||||
|
// Hash keys should be at least 32 bytes long |
||||||
|
var hashKey = []byte("very-secret") |
||||||
|
// Block keys should be 16 bytes (AES-128) or 32 bytes (AES-256) long. |
||||||
|
// Shorter keys may weaken the encryption used. |
||||||
|
var blockKey = []byte("a-lot-secret") |
||||||
|
var s = securecookie.New(hashKey, blockKey) |
||||||
|
``` |
||||||
|
|
||||||
|
The hashKey is required, used to authenticate the cookie value using HMAC. |
||||||
|
It is recommended to use a key with 32 or 64 bytes. |
||||||
|
|
||||||
|
The blockKey is optional, used to encrypt the cookie value -- set it to nil |
||||||
|
to not use encryption. If set, the length must correspond to the block size |
||||||
|
of the encryption algorithm. For AES, used by default, valid lengths are |
||||||
|
16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. |
||||||
|
|
||||||
|
Strong keys can be created using the convenience function GenerateRandomKey(). |
||||||
|
|
||||||
|
Once a SecureCookie instance is set, use it to encode a cookie value: |
||||||
|
|
||||||
|
```go |
||||||
|
func SetCookieHandler(w http.ResponseWriter, r *http.Request) { |
||||||
|
value := map[string]string{ |
||||||
|
"foo": "bar", |
||||||
|
} |
||||||
|
if encoded, err := s.Encode("cookie-name", value); err == nil { |
||||||
|
cookie := &http.Cookie{ |
||||||
|
Name: "cookie-name", |
||||||
|
Value: encoded, |
||||||
|
Path: "/", |
||||||
|
Secure: true, |
||||||
|
HttpOnly: true, |
||||||
|
} |
||||||
|
http.SetCookie(w, cookie) |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Later, use the same SecureCookie instance to decode and validate a cookie |
||||||
|
value: |
||||||
|
|
||||||
|
```go |
||||||
|
func ReadCookieHandler(w http.ResponseWriter, r *http.Request) { |
||||||
|
if cookie, err := r.Cookie("cookie-name"); err == nil { |
||||||
|
value := make(map[string]string) |
||||||
|
if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil { |
||||||
|
fmt.Fprintf(w, "The value of foo is %q", value["foo"]) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
We stored a map[string]string, but secure cookies can hold any value that |
||||||
|
can be encoded using `encoding/gob`. To store custom types, they must be |
||||||
|
registered first using gob.Register(). For basic types this is not needed; |
||||||
|
it works out of the box. An optional JSON encoder that uses `encoding/json` is |
||||||
|
available for types compatible with JSON. |
||||||
|
|
||||||
|
## License |
||||||
|
|
||||||
|
BSD licensed. See the LICENSE file for details. |
@ -0,0 +1,61 @@ |
|||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
/* |
||||||
|
Package securecookie encodes and decodes authenticated and optionally |
||||||
|
encrypted cookie values. |
||||||
|
|
||||||
|
Secure cookies can't be forged, because their values are validated using HMAC. |
||||||
|
When encrypted, the content is also inaccessible to malicious eyes. |
||||||
|
|
||||||
|
To use it, first create a new SecureCookie instance: |
||||||
|
|
||||||
|
var hashKey = []byte("very-secret") |
||||||
|
var blockKey = []byte("a-lot-secret") |
||||||
|
var s = securecookie.New(hashKey, blockKey) |
||||||
|
|
||||||
|
The hashKey is required, used to authenticate the cookie value using HMAC. |
||||||
|
It is recommended to use a key with 32 or 64 bytes. |
||||||
|
|
||||||
|
The blockKey is optional, used to encrypt the cookie value -- set it to nil |
||||||
|
to not use encryption. If set, the length must correspond to the block size |
||||||
|
of the encryption algorithm. For AES, used by default, valid lengths are |
||||||
|
16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. |
||||||
|
|
||||||
|
Strong keys can be created using the convenience function GenerateRandomKey(). |
||||||
|
|
||||||
|
Once a SecureCookie instance is set, use it to encode a cookie value: |
||||||
|
|
||||||
|
func SetCookieHandler(w http.ResponseWriter, r *http.Request) { |
||||||
|
value := map[string]string{ |
||||||
|
"foo": "bar", |
||||||
|
} |
||||||
|
if encoded, err := s.Encode("cookie-name", value); err == nil { |
||||||
|
cookie := &http.Cookie{ |
||||||
|
Name: "cookie-name", |
||||||
|
Value: encoded, |
||||||
|
Path: "/", |
||||||
|
} |
||||||
|
http.SetCookie(w, cookie) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Later, use the same SecureCookie instance to decode and validate a cookie |
||||||
|
value: |
||||||
|
|
||||||
|
func ReadCookieHandler(w http.ResponseWriter, r *http.Request) { |
||||||
|
if cookie, err := r.Cookie("cookie-name"); err == nil { |
||||||
|
value := make(map[string]string) |
||||||
|
if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil { |
||||||
|
fmt.Fprintf(w, "The value of foo is %q", value["foo"]) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
We stored a map[string]string, but secure cookies can hold any value that |
||||||
|
can be encoded using encoding/gob. To store custom types, they must be |
||||||
|
registered first using gob.Register(). For basic types this is not needed; |
||||||
|
it works out of the box. |
||||||
|
*/ |
||||||
|
package securecookie |
@ -0,0 +1,25 @@ |
|||||||
|
// +build gofuzz
|
||||||
|
|
||||||
|
package securecookie |
||||||
|
|
||||||
|
var hashKey = []byte("very-secret12345") |
||||||
|
var blockKey = []byte("a-lot-secret1234") |
||||||
|
var s = New(hashKey, blockKey) |
||||||
|
|
||||||
|
type Cookie struct { |
||||||
|
B bool |
||||||
|
I int |
||||||
|
S string |
||||||
|
} |
||||||
|
|
||||||
|
func Fuzz(data []byte) int { |
||||||
|
datas := string(data) |
||||||
|
var c Cookie |
||||||
|
if err := s.Decode("fuzz", datas, &c); err != nil { |
||||||
|
return 0 |
||||||
|
} |
||||||
|
if _, err := s.Encode("fuzz", c); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
return 1 |
||||||
|
} |
@ -0,0 +1,646 @@ |
|||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package securecookie |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"crypto/aes" |
||||||
|
"crypto/cipher" |
||||||
|
"crypto/hmac" |
||||||
|
"crypto/rand" |
||||||
|
"crypto/sha256" |
||||||
|
"crypto/subtle" |
||||||
|
"encoding/base64" |
||||||
|
"encoding/gob" |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"hash" |
||||||
|
"io" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
// Error is the interface of all errors returned by functions in this library.
|
||||||
|
type Error interface { |
||||||
|
error |
||||||
|
|
||||||
|
// IsUsage returns true for errors indicating the client code probably
|
||||||
|
// uses this library incorrectly. For example, the client may have
|
||||||
|
// failed to provide a valid hash key, or may have failed to configure
|
||||||
|
// the Serializer adequately for encoding value.
|
||||||
|
IsUsage() bool |
||||||
|
|
||||||
|
// IsDecode returns true for errors indicating that a cookie could not
|
||||||
|
// be decoded and validated. Since cookies are usually untrusted
|
||||||
|
// user-provided input, errors of this type should be expected.
|
||||||
|
// Usually, the proper action is simply to reject the request.
|
||||||
|
IsDecode() bool |
||||||
|
|
||||||
|
// IsInternal returns true for unexpected errors occurring in the
|
||||||
|
// securecookie implementation.
|
||||||
|
IsInternal() bool |
||||||
|
|
||||||
|
// Cause, if it returns a non-nil value, indicates that this error was
|
||||||
|
// propagated from some underlying library. If this method returns nil,
|
||||||
|
// this error was raised directly by this library.
|
||||||
|
//
|
||||||
|
// Cause is provided principally for debugging/logging purposes; it is
|
||||||
|
// rare that application logic should perform meaningfully different
|
||||||
|
// logic based on Cause. See, for example, the caveats described on
|
||||||
|
// (MultiError).Cause().
|
||||||
|
Cause() error |
||||||
|
} |
||||||
|
|
||||||
|
// errorType is a bitmask giving the error type(s) of an cookieError value.
|
||||||
|
type errorType int |
||||||
|
|
||||||
|
const ( |
||||||
|
usageError = errorType(1 << iota) |
||||||
|
decodeError |
||||||
|
internalError |
||||||
|
) |
||||||
|
|
||||||
|
type cookieError struct { |
||||||
|
typ errorType |
||||||
|
msg string |
||||||
|
cause error |
||||||
|
} |
||||||
|
|
||||||
|
func (e cookieError) IsUsage() bool { return (e.typ & usageError) != 0 } |
||||||
|
func (e cookieError) IsDecode() bool { return (e.typ & decodeError) != 0 } |
||||||
|
func (e cookieError) IsInternal() bool { return (e.typ & internalError) != 0 } |
||||||
|
|
||||||
|
func (e cookieError) Cause() error { return e.cause } |
||||||
|
|
||||||
|
func (e cookieError) Error() string { |
||||||
|
parts := []string{"securecookie: "} |
||||||
|
if e.msg == "" { |
||||||
|
parts = append(parts, "error") |
||||||
|
} else { |
||||||
|
parts = append(parts, e.msg) |
||||||
|
} |
||||||
|
if c := e.Cause(); c != nil { |
||||||
|
parts = append(parts, " - caused by: ", c.Error()) |
||||||
|
} |
||||||
|
return strings.Join(parts, "") |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
errGeneratingIV = cookieError{typ: internalError, msg: "failed to generate random iv"} |
||||||
|
|
||||||
|
errNoCodecs = cookieError{typ: usageError, msg: "no codecs provided"} |
||||||
|
errHashKeyNotSet = cookieError{typ: usageError, msg: "hash key is not set"} |
||||||
|
errBlockKeyNotSet = cookieError{typ: usageError, msg: "block key is not set"} |
||||||
|
errEncodedValueTooLong = cookieError{typ: usageError, msg: "the value is too long"} |
||||||
|
|
||||||
|
errValueToDecodeTooLong = cookieError{typ: decodeError, msg: "the value is too long"} |
||||||
|
errTimestampInvalid = cookieError{typ: decodeError, msg: "invalid timestamp"} |
||||||
|
errTimestampTooNew = cookieError{typ: decodeError, msg: "timestamp is too new"} |
||||||
|
errTimestampExpired = cookieError{typ: decodeError, msg: "expired timestamp"} |
||||||
|
errDecryptionFailed = cookieError{typ: decodeError, msg: "the value could not be decrypted"} |
||||||
|
errValueNotByte = cookieError{typ: decodeError, msg: "value not a []byte."} |
||||||
|
errValueNotBytePtr = cookieError{typ: decodeError, msg: "value not a pointer to []byte."} |
||||||
|
|
||||||
|
// ErrMacInvalid indicates that cookie decoding failed because the HMAC
|
||||||
|
// could not be extracted and verified. Direct use of this error
|
||||||
|
// variable is deprecated; it is public only for legacy compatibility,
|
||||||
|
// and may be privatized in the future, as it is rarely useful to
|
||||||
|
// distinguish between this error and other Error implementations.
|
||||||
|
ErrMacInvalid = cookieError{typ: decodeError, msg: "the value is not valid"} |
||||||
|
) |
||||||
|
|
||||||
|
// Codec defines an interface to encode and decode cookie values.
|
||||||
|
type Codec interface { |
||||||
|
Encode(name string, value interface{}) (string, error) |
||||||
|
Decode(name, value string, dst interface{}) error |
||||||
|
} |
||||||
|
|
||||||
|
// New returns a new SecureCookie.
|
||||||
|
//
|
||||||
|
// hashKey is required, used to authenticate values using HMAC. Create it using
|
||||||
|
// GenerateRandomKey(). It is recommended to use a key with 32 or 64 bytes.
|
||||||
|
//
|
||||||
|
// blockKey is optional, used to encrypt values. Create it using
|
||||||
|
// GenerateRandomKey(). The key length must correspond to the block size
|
||||||
|
// of the encryption algorithm. For AES, used by default, valid lengths are
|
||||||
|
// 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.
|
||||||
|
// The default encoder used for cookie serialization is encoding/gob.
|
||||||
|
//
|
||||||
|
// Note that keys created using GenerateRandomKey() are not automatically
|
||||||
|
// persisted. New keys will be created when the application is restarted, and
|
||||||
|
// previously issued cookies will not be able to be decoded.
|
||||||
|
func New(hashKey, blockKey []byte) *SecureCookie { |
||||||
|
s := &SecureCookie{ |
||||||
|
hashKey: hashKey, |
||||||
|
blockKey: blockKey, |
||||||
|
hashFunc: sha256.New, |
||||||
|
maxAge: 86400 * 30, |
||||||
|
maxLength: 4096, |
||||||
|
sz: GobEncoder{}, |
||||||
|
} |
||||||
|
if hashKey == nil { |
||||||
|
s.err = errHashKeyNotSet |
||||||
|
} |
||||||
|
if blockKey != nil { |
||||||
|
s.BlockFunc(aes.NewCipher) |
||||||
|
} |
||||||
|
return s |
||||||
|
} |
||||||
|
|
||||||
|
// SecureCookie encodes and decodes authenticated and optionally encrypted
|
||||||
|
// cookie values.
|
||||||
|
type SecureCookie struct { |
||||||
|
hashKey []byte |
||||||
|
hashFunc func() hash.Hash |
||||||
|
blockKey []byte |
||||||
|
block cipher.Block |
||||||
|
maxLength int |
||||||
|
maxAge int64 |
||||||
|
minAge int64 |
||||||
|
err error |
||||||
|
sz Serializer |
||||||
|
// For testing purposes, the function that returns the current timestamp.
|
||||||
|
// If not set, it will use time.Now().UTC().Unix().
|
||||||
|
timeFunc func() int64 |
||||||
|
} |
||||||
|
|
||||||
|
// Serializer provides an interface for providing custom serializers for cookie
|
||||||
|
// values.
|
||||||
|
type Serializer interface { |
||||||
|
Serialize(src interface{}) ([]byte, error) |
||||||
|
Deserialize(src []byte, dst interface{}) error |
||||||
|
} |
||||||
|
|
||||||
|
// GobEncoder encodes cookie values using encoding/gob. This is the simplest
|
||||||
|
// encoder and can handle complex types via gob.Register.
|
||||||
|
type GobEncoder struct{} |
||||||
|
|
||||||
|
// JSONEncoder encodes cookie values using encoding/json. Users who wish to
|
||||||
|
// encode complex types need to satisfy the json.Marshaller and
|
||||||
|
// json.Unmarshaller interfaces.
|
||||||
|
type JSONEncoder struct{} |
||||||
|
|
||||||
|
// NopEncoder does not encode cookie values, and instead simply accepts a []byte
|
||||||
|
// (as an interface{}) and returns a []byte. This is particularly useful when
|
||||||
|
// you encoding an object upstream and do not wish to re-encode it.
|
||||||
|
type NopEncoder struct{} |
||||||
|
|
||||||
|
// MaxLength restricts the maximum length, in bytes, for the cookie value.
|
||||||
|
//
|
||||||
|
// Default is 4096, which is the maximum value accepted by Internet Explorer.
|
||||||
|
func (s *SecureCookie) MaxLength(value int) *SecureCookie { |
||||||
|
s.maxLength = value |
||||||
|
return s |
||||||
|
} |
||||||
|
|
||||||
|
// MaxAge restricts the maximum age, in seconds, for the cookie value.
|
||||||
|
//
|
||||||
|
// Default is 86400 * 30. Set it to 0 for no restriction.
|
||||||
|
func (s *SecureCookie) MaxAge(value int) *SecureCookie { |
||||||
|
s.maxAge = int64(value) |
||||||
|
return s |
||||||
|
} |
||||||
|
|
||||||
|
// MinAge restricts the minimum age, in seconds, for the cookie value.
|
||||||
|
//
|
||||||
|
// Default is 0 (no restriction).
|
||||||
|
func (s *SecureCookie) MinAge(value int) *SecureCookie { |
||||||
|
s.minAge = int64(value) |
||||||
|
return s |
||||||
|
} |
||||||
|
|
||||||
|
// HashFunc sets the hash function used to create HMAC.
|
||||||
|
//
|
||||||
|
// Default is crypto/sha256.New.
|
||||||
|
func (s *SecureCookie) HashFunc(f func() hash.Hash) *SecureCookie { |
||||||
|
s.hashFunc = f |
||||||
|
return s |
||||||
|
} |
||||||
|
|
||||||
|
// BlockFunc sets the encryption function used to create a cipher.Block.
|
||||||
|
//
|
||||||
|
// Default is crypto/aes.New.
|
||||||
|
func (s *SecureCookie) BlockFunc(f func([]byte) (cipher.Block, error)) *SecureCookie { |
||||||
|
if s.blockKey == nil { |
||||||
|
s.err = errBlockKeyNotSet |
||||||
|
} else if block, err := f(s.blockKey); err == nil { |
||||||
|
s.block = block |
||||||
|
} else { |
||||||
|
s.err = cookieError{cause: err, typ: usageError} |
||||||
|
} |
||||||
|
return s |
||||||
|
} |
||||||
|
|
||||||
|
// Encoding sets the encoding/serialization method for cookies.
|
||||||
|
//
|
||||||
|
// Default is encoding/gob. To encode special structures using encoding/gob,
|
||||||
|
// they must be registered first using gob.Register().
|
||||||
|
func (s *SecureCookie) SetSerializer(sz Serializer) *SecureCookie { |
||||||
|
s.sz = sz |
||||||
|
|
||||||
|
return s |
||||||
|
} |
||||||
|
|
||||||
|
// Encode encodes a cookie value.
|
||||||
|
//
|
||||||
|
// It serializes, optionally encrypts, signs with a message authentication code,
|
||||||
|
// and finally encodes the value.
|
||||||
|
//
|
||||||
|
// The name argument is the cookie name. It is stored with the encoded value.
|
||||||
|
// The value argument is the value to be encoded. It can be any value that can
|
||||||
|
// be encoded using the currently selected serializer; see SetSerializer().
|
||||||
|
//
|
||||||
|
// It is the client's responsibility to ensure that value, when encoded using
|
||||||
|
// the current serialization/encryption settings on s and then base64-encoded,
|
||||||
|
// is shorter than the maximum permissible length.
|
||||||
|
func (s *SecureCookie) Encode(name string, value interface{}) (string, error) { |
||||||
|
if s.err != nil { |
||||||
|
return "", s.err |
||||||
|
} |
||||||
|
if s.hashKey == nil { |
||||||
|
s.err = errHashKeyNotSet |
||||||
|
return "", s.err |
||||||
|
} |
||||||
|
var err error |
||||||
|
var b []byte |
||||||
|
// 1. Serialize.
|
||||||
|
if b, err = s.sz.Serialize(value); err != nil { |
||||||
|
return "", cookieError{cause: err, typ: usageError} |
||||||
|
} |
||||||
|
// 2. Encrypt (optional).
|
||||||
|
if s.block != nil { |
||||||
|
if b, err = encrypt(s.block, b); err != nil { |
||||||
|
return "", cookieError{cause: err, typ: usageError} |
||||||
|
} |
||||||
|
} |
||||||
|
b = encode(b) |
||||||
|
// 3. Create MAC for "name|date|value". Extra pipe to be used later.
|
||||||
|
b = []byte(fmt.Sprintf("%s|%d|%s|", name, s.timestamp(), b)) |
||||||
|
mac := createMac(hmac.New(s.hashFunc, s.hashKey), b[:len(b)-1]) |
||||||
|
// Append mac, remove name.
|
||||||
|
b = append(b, mac...)[len(name)+1:] |
||||||
|
// 4. Encode to base64.
|
||||||
|
b = encode(b) |
||||||
|
// 5. Check length.
|
||||||
|
if s.maxLength != 0 && len(b) > s.maxLength { |
||||||
|
return "", errEncodedValueTooLong |
||||||
|
} |
||||||
|
// Done.
|
||||||
|
return string(b), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Decode decodes a cookie value.
|
||||||
|
//
|
||||||
|
// It decodes, verifies a message authentication code, optionally decrypts and
|
||||||
|
// finally deserializes the value.
|
||||||
|
//
|
||||||
|
// The name argument is the cookie name. It must be the same name used when
|
||||||
|
// it was stored. The value argument is the encoded cookie value. The dst
|
||||||
|
// argument is where the cookie will be decoded. It must be a pointer.
|
||||||
|
func (s *SecureCookie) Decode(name, value string, dst interface{}) error { |
||||||
|
if s.err != nil { |
||||||
|
return s.err |
||||||
|
} |
||||||
|
if s.hashKey == nil { |
||||||
|
s.err = errHashKeyNotSet |
||||||
|
return s.err |
||||||
|
} |
||||||
|
// 1. Check length.
|
||||||
|
if s.maxLength != 0 && len(value) > s.maxLength { |
||||||
|
return errValueToDecodeTooLong |
||||||
|
} |
||||||
|
// 2. Decode from base64.
|
||||||
|
b, err := decode([]byte(value)) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
// 3. Verify MAC. Value is "date|value|mac".
|
||||||
|
parts := bytes.SplitN(b, []byte("|"), 3) |
||||||
|
if len(parts) != 3 { |
||||||
|
return ErrMacInvalid |
||||||
|
} |
||||||
|
h := hmac.New(s.hashFunc, s.hashKey) |
||||||
|
b = append([]byte(name+"|"), b[:len(b)-len(parts[2])-1]...) |
||||||
|
if err = verifyMac(h, b, parts[2]); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
// 4. Verify date ranges.
|
||||||
|
var t1 int64 |
||||||
|
if t1, err = strconv.ParseInt(string(parts[0]), 10, 64); err != nil { |
||||||
|
return errTimestampInvalid |
||||||
|
} |
||||||
|
t2 := s.timestamp() |
||||||
|
if s.minAge != 0 && t1 > t2-s.minAge { |
||||||
|
return errTimestampTooNew |
||||||
|
} |
||||||
|
if s.maxAge != 0 && t1 < t2-s.maxAge { |
||||||
|
return errTimestampExpired |
||||||
|
} |
||||||
|
// 5. Decrypt (optional).
|
||||||
|
b, err = decode(parts[1]) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if s.block != nil { |
||||||
|
if b, err = decrypt(s.block, b); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
// 6. Deserialize.
|
||||||
|
if err = s.sz.Deserialize(b, dst); err != nil { |
||||||
|
return cookieError{cause: err, typ: decodeError} |
||||||
|
} |
||||||
|
// Done.
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// timestamp returns the current timestamp, in seconds.
|
||||||
|
//
|
||||||
|
// For testing purposes, the function that generates the timestamp can be
|
||||||
|
// overridden. If not set, it will return time.Now().UTC().Unix().
|
||||||
|
func (s *SecureCookie) timestamp() int64 { |
||||||
|
if s.timeFunc == nil { |
||||||
|
return time.Now().UTC().Unix() |
||||||
|
} |
||||||
|
return s.timeFunc() |
||||||
|
} |
||||||
|
|
||||||
|
// Authentication -------------------------------------------------------------
|
||||||
|
|
||||||
|
// createMac creates a message authentication code (MAC).
|
||||||
|
func createMac(h hash.Hash, value []byte) []byte { |
||||||
|
h.Write(value) |
||||||
|
return h.Sum(nil) |
||||||
|
} |
||||||
|
|
||||||
|
// verifyMac verifies that a message authentication code (MAC) is valid.
|
||||||
|
func verifyMac(h hash.Hash, value []byte, mac []byte) error { |
||||||
|
mac2 := createMac(h, value) |
||||||
|
// Check that both MACs are of equal length, as subtle.ConstantTimeCompare
|
||||||
|
// does not do this prior to Go 1.4.
|
||||||
|
if len(mac) == len(mac2) && subtle.ConstantTimeCompare(mac, mac2) == 1 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return ErrMacInvalid |
||||||
|
} |
||||||
|
|
||||||
|
// Encryption -----------------------------------------------------------------
|
||||||
|
|
||||||
|
// encrypt encrypts a value using the given block in counter mode.
|
||||||
|
//
|
||||||
|
// A random initialization vector (http://goo.gl/zF67k) with the length of the
|
||||||
|
// block size is prepended to the resulting ciphertext.
|
||||||
|
func encrypt(block cipher.Block, value []byte) ([]byte, error) { |
||||||
|
iv := GenerateRandomKey(block.BlockSize()) |
||||||
|
if iv == nil { |
||||||
|
return nil, errGeneratingIV |
||||||
|
} |
||||||
|
// Encrypt it.
|
||||||
|
stream := cipher.NewCTR(block, iv) |
||||||
|
stream.XORKeyStream(value, value) |
||||||
|
// Return iv + ciphertext.
|
||||||
|
return append(iv, value...), nil |
||||||
|
} |
||||||
|
|
||||||
|
// decrypt decrypts a value using the given block in counter mode.
|
||||||
|
//
|
||||||
|
// The value to be decrypted must be prepended by a initialization vector
|
||||||
|
// (http://goo.gl/zF67k) with the length of the block size.
|
||||||
|
func decrypt(block cipher.Block, value []byte) ([]byte, error) { |
||||||
|
size := block.BlockSize() |
||||||
|
if len(value) > size { |
||||||
|
// Extract iv.
|
||||||
|
iv := value[:size] |
||||||
|
// Extract ciphertext.
|
||||||
|
value = value[size:] |
||||||
|
// Decrypt it.
|
||||||
|
stream := cipher.NewCTR(block, iv) |
||||||
|
stream.XORKeyStream(value, value) |
||||||
|
return value, nil |
||||||
|
} |
||||||
|
return nil, errDecryptionFailed |
||||||
|
} |
||||||
|
|
||||||
|
// Serialization --------------------------------------------------------------
|
||||||
|
|
||||||
|
// Serialize encodes a value using gob.
|
||||||
|
func (e GobEncoder) Serialize(src interface{}) ([]byte, error) { |
||||||
|
buf := new(bytes.Buffer) |
||||||
|
enc := gob.NewEncoder(buf) |
||||||
|
if err := enc.Encode(src); err != nil { |
||||||
|
return nil, cookieError{cause: err, typ: usageError} |
||||||
|
} |
||||||
|
return buf.Bytes(), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Deserialize decodes a value using gob.
|
||||||
|
func (e GobEncoder) Deserialize(src []byte, dst interface{}) error { |
||||||
|
dec := gob.NewDecoder(bytes.NewBuffer(src)) |
||||||
|
if err := dec.Decode(dst); err != nil { |
||||||
|
return cookieError{cause: err, typ: decodeError} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Serialize encodes a value using encoding/json.
|
||||||
|
func (e JSONEncoder) Serialize(src interface{}) ([]byte, error) { |
||||||
|
buf := new(bytes.Buffer) |
||||||
|
enc := json.NewEncoder(buf) |
||||||
|
if err := enc.Encode(src); err != nil { |
||||||
|
return nil, cookieError{cause: err, typ: usageError} |
||||||
|
} |
||||||
|
return buf.Bytes(), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Deserialize decodes a value using encoding/json.
|
||||||
|
func (e JSONEncoder) Deserialize(src []byte, dst interface{}) error { |
||||||
|
dec := json.NewDecoder(bytes.NewReader(src)) |
||||||
|
if err := dec.Decode(dst); err != nil { |
||||||
|
return cookieError{cause: err, typ: decodeError} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Serialize passes a []byte through as-is.
|
||||||
|
func (e NopEncoder) Serialize(src interface{}) ([]byte, error) { |
||||||
|
if b, ok := src.([]byte); ok { |
||||||
|
return b, nil |
||||||
|
} |
||||||
|
|
||||||
|
return nil, errValueNotByte |
||||||
|
} |
||||||
|
|
||||||
|
// Deserialize passes a []byte through as-is.
|
||||||
|
func (e NopEncoder) Deserialize(src []byte, dst interface{}) error { |
||||||
|
if dat, ok := dst.(*[]byte); ok { |
||||||
|
*dat = src |
||||||
|
return nil |
||||||
|
} |
||||||
|
return errValueNotBytePtr |
||||||
|
} |
||||||
|
|
||||||
|
// Encoding -------------------------------------------------------------------
|
||||||
|
|
||||||
|
// encode encodes a value using base64.
|
||||||
|
func encode(value []byte) []byte { |
||||||
|
encoded := make([]byte, base64.URLEncoding.EncodedLen(len(value))) |
||||||
|
base64.URLEncoding.Encode(encoded, value) |
||||||
|
return encoded |
||||||
|
} |
||||||
|
|
||||||
|
// decode decodes a cookie using base64.
|
||||||
|
func decode(value []byte) ([]byte, error) { |
||||||
|
decoded := make([]byte, base64.URLEncoding.DecodedLen(len(value))) |
||||||
|
b, err := base64.URLEncoding.Decode(decoded, value) |
||||||
|
if err != nil { |
||||||
|
return nil, cookieError{cause: err, typ: decodeError, msg: "base64 decode failed"} |
||||||
|
} |
||||||
|
return decoded[:b], nil |
||||||
|
} |
||||||
|
|
||||||
|
// Helpers --------------------------------------------------------------------
|
||||||
|
|
||||||
|
// GenerateRandomKey creates a random key with the given length in bytes.
|
||||||
|
// On failure, returns nil.
|
||||||
|
//
|
||||||
|
// Callers should explicitly check for the possibility of a nil return, treat
|
||||||
|
// it as a failure of the system random number generator, and not continue.
|
||||||
|
func GenerateRandomKey(length int) []byte { |
||||||
|
k := make([]byte, length) |
||||||
|
if _, err := io.ReadFull(rand.Reader, k); err != nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return k |
||||||
|
} |
||||||
|
|
||||||
|
// CodecsFromPairs returns a slice of SecureCookie instances.
|
||||||
|
//
|
||||||
|
// It is a convenience function to create a list of codecs for key rotation. Note
|
||||||
|
// that the generated Codecs will have the default options applied: callers
|
||||||
|
// should iterate over each Codec and type-assert the underlying *SecureCookie to
|
||||||
|
// change these.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// codecs := securecookie.CodecsFromPairs(
|
||||||
|
// []byte("new-hash-key"),
|
||||||
|
// []byte("new-block-key"),
|
||||||
|
// []byte("old-hash-key"),
|
||||||
|
// []byte("old-block-key"),
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// // Modify each instance.
|
||||||
|
// for _, s := range codecs {
|
||||||
|
// if cookie, ok := s.(*securecookie.SecureCookie); ok {
|
||||||
|
// cookie.MaxAge(86400 * 7)
|
||||||
|
// cookie.SetSerializer(securecookie.JSONEncoder{})
|
||||||
|
// cookie.HashFunc(sha512.New512_256)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
func CodecsFromPairs(keyPairs ...[]byte) []Codec { |
||||||
|
codecs := make([]Codec, len(keyPairs)/2+len(keyPairs)%2) |
||||||
|
for i := 0; i < len(keyPairs); i += 2 { |
||||||
|
var blockKey []byte |
||||||
|
if i+1 < len(keyPairs) { |
||||||
|
blockKey = keyPairs[i+1] |
||||||
|
} |
||||||
|
codecs[i/2] = New(keyPairs[i], blockKey) |
||||||
|
} |
||||||
|
return codecs |
||||||
|
} |
||||||
|
|
||||||
|
// EncodeMulti encodes a cookie value using a group of codecs.
|
||||||
|
//
|
||||||
|
// The codecs are tried in order. Multiple codecs are accepted to allow
|
||||||
|
// key rotation.
|
||||||
|
//
|
||||||
|
// On error, may return a MultiError.
|
||||||
|
func EncodeMulti(name string, value interface{}, codecs ...Codec) (string, error) { |
||||||
|
if len(codecs) == 0 { |
||||||
|
return "", errNoCodecs |
||||||
|
} |
||||||
|
|
||||||
|
var errors MultiError |
||||||
|
for _, codec := range codecs { |
||||||
|
encoded, err := codec.Encode(name, value) |
||||||
|
if err == nil { |
||||||
|
return encoded, nil |
||||||
|
} |
||||||
|
errors = append(errors, err) |
||||||
|
} |
||||||
|
return "", errors |
||||||
|
} |
||||||
|
|
||||||
|
// DecodeMulti decodes a cookie value using a group of codecs.
|
||||||
|
//
|
||||||
|
// The codecs are tried in order. Multiple codecs are accepted to allow
|
||||||
|
// key rotation.
|
||||||
|
//
|
||||||
|
// On error, may return a MultiError.
|
||||||
|
func DecodeMulti(name string, value string, dst interface{}, codecs ...Codec) error { |
||||||
|
if len(codecs) == 0 { |
||||||
|
return errNoCodecs |
||||||
|
} |
||||||
|
|
||||||
|
var errors MultiError |
||||||
|
for _, codec := range codecs { |
||||||
|
err := codec.Decode(name, value, dst) |
||||||
|
if err == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
errors = append(errors, err) |
||||||
|
} |
||||||
|
return errors |
||||||
|
} |
||||||
|
|
||||||
|
// MultiError groups multiple errors.
|
||||||
|
type MultiError []error |
||||||
|
|
||||||
|
func (m MultiError) IsUsage() bool { return m.any(func(e Error) bool { return e.IsUsage() }) } |
||||||
|
func (m MultiError) IsDecode() bool { return m.any(func(e Error) bool { return e.IsDecode() }) } |
||||||
|
func (m MultiError) IsInternal() bool { return m.any(func(e Error) bool { return e.IsInternal() }) } |
||||||
|
|
||||||
|
// Cause returns nil for MultiError; there is no unique underlying cause in the
|
||||||
|
// general case.
|
||||||
|
//
|
||||||
|
// Note: we could conceivably return a non-nil Cause only when there is exactly
|
||||||
|
// one child error with a Cause. However, it would be brittle for client code
|
||||||
|
// to rely on the arity of causes inside a MultiError, so we have opted not to
|
||||||
|
// provide this functionality. Clients which really wish to access the Causes
|
||||||
|
// of the underlying errors are free to iterate through the errors themselves.
|
||||||
|
func (m MultiError) Cause() error { return nil } |
||||||
|
|
||||||
|
func (m MultiError) Error() string { |
||||||
|
s, n := "", 0 |
||||||
|
for _, e := range m { |
||||||
|
if e != nil { |
||||||
|
if n == 0 { |
||||||
|
s = e.Error() |
||||||
|
} |
||||||
|
n++ |
||||||
|
} |
||||||
|
} |
||||||
|
switch n { |
||||||
|
case 0: |
||||||
|
return "(0 errors)" |
||||||
|
case 1: |
||||||
|
return s |
||||||
|
case 2: |
||||||
|
return s + " (and 1 other error)" |
||||||
|
} |
||||||
|
return fmt.Sprintf("%s (and %d other errors)", s, n-1) |
||||||
|
} |
||||||
|
|
||||||
|
// any returns true if any element of m is an Error for which pred returns true.
|
||||||
|
func (m MultiError) any(pred func(Error) bool) bool { |
||||||
|
for _, e := range m { |
||||||
|
if ourErr, ok := e.(Error); ok && pred(ourErr) { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
Copyright (c) 2012 Rodrigo Moraes. All rights reserved. |
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without |
||||||
|
modification, are permitted provided that the following conditions are |
||||||
|
met: |
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright |
||||||
|
notice, this list of conditions and the following disclaimer. |
||||||
|
* Redistributions in binary form must reproduce the above |
||||||
|
copyright notice, this list of conditions and the following disclaimer |
||||||
|
in the documentation and/or other materials provided with the |
||||||
|
distribution. |
||||||
|
* Neither the name of Google Inc. nor the names of its |
||||||
|
contributors may be used to endorse or promote products derived from |
||||||
|
this software without specific prior written permission. |
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@ -0,0 +1,81 @@ |
|||||||
|
sessions |
||||||
|
======== |
||||||
|
[![GoDoc](https://godoc.org/github.com/gorilla/sessions?status.svg)](https://godoc.org/github.com/gorilla/sessions) [![Build Status](https://travis-ci.org/gorilla/sessions.png?branch=master)](https://travis-ci.org/gorilla/sessions) |
||||||
|
|
||||||
|
gorilla/sessions provides cookie and filesystem sessions and infrastructure for |
||||||
|
custom session backends. |
||||||
|
|
||||||
|
The key features are: |
||||||
|
|
||||||
|
* Simple API: use it as an easy way to set signed (and optionally |
||||||
|
encrypted) cookies. |
||||||
|
* Built-in backends to store sessions in cookies or the filesystem. |
||||||
|
* Flash messages: session values that last until read. |
||||||
|
* Convenient way to switch session persistency (aka "remember me") and set |
||||||
|
other attributes. |
||||||
|
* Mechanism to rotate authentication and encryption keys. |
||||||
|
* Multiple sessions per request, even using different backends. |
||||||
|
* Interfaces and infrastructure for custom session backends: sessions from |
||||||
|
different stores can be retrieved and batch-saved using a common API. |
||||||
|
|
||||||
|
Let's start with an example that shows the sessions API in a nutshell: |
||||||
|
|
||||||
|
```go |
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
"github.com/gorilla/sessions" |
||||||
|
) |
||||||
|
|
||||||
|
var store = sessions.NewCookieStore([]byte("something-very-secret")) |
||||||
|
|
||||||
|
func MyHandler(w http.ResponseWriter, r *http.Request) { |
||||||
|
// Get a session. We're ignoring the error resulted from decoding an |
||||||
|
// existing session: Get() always returns a session, even if empty. |
||||||
|
session, _ := store.Get(r, "session-name") |
||||||
|
// Set some session values. |
||||||
|
session.Values["foo"] = "bar" |
||||||
|
session.Values[42] = 43 |
||||||
|
// Save it before we write to the response/return from the handler. |
||||||
|
session.Save(r, w) |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
First we initialize a session store calling `NewCookieStore()` and passing a |
||||||
|
secret key used to authenticate the session. Inside the handler, we call |
||||||
|
`store.Get()` to retrieve an existing session or a new one. Then we set some |
||||||
|
session values in session.Values, which is a `map[interface{}]interface{}`. |
||||||
|
And finally we call `session.Save()` to save the session in the response. |
||||||
|
|
||||||
|
Important Note: If you aren't using gorilla/mux, you need to wrap your handlers |
||||||
|
with |
||||||
|
[`context.ClearHandler`](http://www.gorillatoolkit.org/pkg/context#ClearHandler) |
||||||
|
as or else you will leak memory! An easy way to do this is to wrap the top-level |
||||||
|
mux when calling http.ListenAndServe: |
||||||
|
|
||||||
|
More examples are available [on the Gorilla |
||||||
|
website](http://www.gorillatoolkit.org/pkg/sessions). |
||||||
|
|
||||||
|
## Store Implementations |
||||||
|
|
||||||
|
Other implementations of the `sessions.Store` interface: |
||||||
|
|
||||||
|
* [github.com/starJammer/gorilla-sessions-arangodb](https://github.com/starJammer/gorilla-sessions-arangodb) - ArangoDB |
||||||
|
* [github.com/yosssi/boltstore](https://github.com/yosssi/boltstore) - Bolt |
||||||
|
* [github.com/srinathgs/couchbasestore](https://github.com/srinathgs/couchbasestore) - Couchbase |
||||||
|
* [github.com/denizeren/dynamostore](https://github.com/denizeren/dynamostore) - Dynamodb on AWS |
||||||
|
* [github.com/bradleypeabody/gorilla-sessions-memcache](https://github.com/bradleypeabody/gorilla-sessions-memcache) - Memcache |
||||||
|
* [github.com/dsoprea/go-appengine-sessioncascade](https://github.com/dsoprea/go-appengine-sessioncascade) - Memcache/Datastore/Context in AppEngine |
||||||
|
* [github.com/kidstuff/mongostore](https://github.com/kidstuff/mongostore) - MongoDB |
||||||
|
* [github.com/srinathgs/mysqlstore](https://github.com/srinathgs/mysqlstore) - MySQL |
||||||
|
* [github.com/EnumApps/clustersqlstore](https://github.com/EnumApps/clustersqlstore) - MySQL Cluster |
||||||
|
* [github.com/antonlindstrom/pgstore](https://github.com/antonlindstrom/pgstore) - PostgreSQL |
||||||
|
* [github.com/boj/redistore](https://github.com/boj/redistore) - Redis |
||||||
|
* [github.com/boj/rethinkstore](https://github.com/boj/rethinkstore) - RethinkDB |
||||||
|
* [github.com/boj/riakstore](https://github.com/boj/riakstore) - Riak |
||||||
|
* [github.com/michaeljs1990/sqlitestore](https://github.com/michaeljs1990/sqlitestore) - SQLite |
||||||
|
* [github.com/wader/gormstore](https://github.com/wader/gormstore) - GORM (MySQL, PostgreSQL, SQLite) |
||||||
|
* [github.com/gernest/qlstore](https://github.com/gernest/qlstore) - ql |
||||||
|
|
||||||
|
## License |
||||||
|
|
||||||
|
BSD licensed. See the LICENSE file for details. |
@ -0,0 +1,199 @@ |
|||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
/* |
||||||
|
Package sessions provides cookie and filesystem sessions and |
||||||
|
infrastructure for custom session backends. |
||||||
|
|
||||||
|
The key features are: |
||||||
|
|
||||||
|
* Simple API: use it as an easy way to set signed (and optionally |
||||||
|
encrypted) cookies. |
||||||
|
* Built-in backends to store sessions in cookies or the filesystem. |
||||||
|
* Flash messages: session values that last until read. |
||||||
|
* Convenient way to switch session persistency (aka "remember me") and set |
||||||
|
other attributes. |
||||||
|
* Mechanism to rotate authentication and encryption keys. |
||||||
|
* Multiple sessions per request, even using different backends. |
||||||
|
* Interfaces and infrastructure for custom session backends: sessions from |
||||||
|
different stores can be retrieved and batch-saved using a common API. |
||||||
|
|
||||||
|
Let's start with an example that shows the sessions API in a nutshell: |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
"github.com/gorilla/sessions" |
||||||
|
) |
||||||
|
|
||||||
|
var store = sessions.NewCookieStore([]byte("something-very-secret")) |
||||||
|
|
||||||
|
func MyHandler(w http.ResponseWriter, r *http.Request) { |
||||||
|
// Get a session. We're ignoring the error resulted from decoding an
|
||||||
|
// existing session: Get() always returns a session, even if empty.
|
||||||
|
session, err := store.Get(r, "session-name") |
||||||
|
if err != nil { |
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Set some session values.
|
||||||
|
session.Values["foo"] = "bar" |
||||||
|
session.Values[42] = 43 |
||||||
|
// Save it before we write to the response/return from the handler.
|
||||||
|
session.Save(r, w) |
||||||
|
} |
||||||
|
|
||||||
|
First we initialize a session store calling NewCookieStore() and passing a |
||||||
|
secret key used to authenticate the session. Inside the handler, we call |
||||||
|
store.Get() to retrieve an existing session or a new one. Then we set some |
||||||
|
session values in session.Values, which is a map[interface{}]interface{}. |
||||||
|
And finally we call session.Save() to save the session in the response. |
||||||
|
|
||||||
|
Note that in production code, we should check for errors when calling |
||||||
|
session.Save(r, w), and either display an error message or otherwise handle it. |
||||||
|
|
||||||
|
Save must be called before writing to the response, otherwise the session |
||||||
|
cookie will not be sent to the client. |
||||||
|
|
||||||
|
Important Note: If you aren't using gorilla/mux, you need to wrap your handlers |
||||||
|
with context.ClearHandler as or else you will leak memory! An easy way to do this |
||||||
|
is to wrap the top-level mux when calling http.ListenAndServe: |
||||||
|
|
||||||
|
http.ListenAndServe(":8080", context.ClearHandler(http.DefaultServeMux)) |
||||||
|
|
||||||
|
The ClearHandler function is provided by the gorilla/context package. |
||||||
|
|
||||||
|
That's all you need to know for the basic usage. Let's take a look at other |
||||||
|
options, starting with flash messages. |
||||||
|
|
||||||
|
Flash messages are session values that last until read. The term appeared with |
||||||
|
Ruby On Rails a few years back. When we request a flash message, it is removed |
||||||
|
from the session. To add a flash, call session.AddFlash(), and to get all |
||||||
|
flashes, call session.Flashes(). Here is an example: |
||||||
|
|
||||||
|
func MyHandler(w http.ResponseWriter, r *http.Request) { |
||||||
|
// Get a session.
|
||||||
|
session, err := store.Get(r, "session-name") |
||||||
|
if err != nil { |
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Get the previously flashes, if any.
|
||||||
|
if flashes := session.Flashes(); len(flashes) > 0 { |
||||||
|
// Use the flash values.
|
||||||
|
} else { |
||||||
|
// Set a new flash.
|
||||||
|
session.AddFlash("Hello, flash messages world!") |
||||||
|
} |
||||||
|
session.Save(r, w) |
||||||
|
} |
||||||
|
|
||||||
|
Flash messages are useful to set information to be read after a redirection, |
||||||
|
like after form submissions. |
||||||
|
|
||||||
|
There may also be cases where you want to store a complex datatype within a |
||||||
|
session, such as a struct. Sessions are serialised using the encoding/gob package, |
||||||
|
so it is easy to register new datatypes for storage in sessions: |
||||||
|
|
||||||
|
import( |
||||||
|
"encoding/gob" |
||||||
|
"github.com/gorilla/sessions" |
||||||
|
) |
||||||
|
|
||||||
|
type Person struct { |
||||||
|
FirstName string |
||||||
|
LastName string |
||||||
|
Email string |
||||||
|
Age int |
||||||
|
} |
||||||
|
|
||||||
|
type M map[string]interface{} |
||||||
|
|
||||||
|
func init() { |
||||||
|
|
||||||
|
gob.Register(&Person{}) |
||||||
|
gob.Register(&M{}) |
||||||
|
} |
||||||
|
|
||||||
|
As it's not possible to pass a raw type as a parameter to a function, gob.Register() |
||||||
|
relies on us passing it a value of the desired type. In the example above we've passed |
||||||
|
it a pointer to a struct and a pointer to a custom type representing a |
||||||
|
map[string]interface. (We could have passed non-pointer values if we wished.) This will |
||||||
|
then allow us to serialise/deserialise values of those types to and from our sessions. |
||||||
|
|
||||||
|
Note that because session values are stored in a map[string]interface{}, there's |
||||||
|
a need to type-assert data when retrieving it. We'll use the Person struct we registered above: |
||||||
|
|
||||||
|
func MyHandler(w http.ResponseWriter, r *http.Request) { |
||||||
|
session, err := store.Get(r, "session-name") |
||||||
|
if err != nil { |
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Retrieve our struct and type-assert it
|
||||||
|
val := session.Values["person"] |
||||||
|
var person = &Person{} |
||||||
|
if person, ok := val.(*Person); !ok { |
||||||
|
// Handle the case that it's not an expected type
|
||||||
|
} |
||||||
|
|
||||||
|
// Now we can use our person object
|
||||||
|
} |
||||||
|
|
||||||
|
By default, session cookies last for a month. This is probably too long for |
||||||
|
some cases, but it is easy to change this and other attributes during |
||||||
|
runtime. Sessions can be configured individually or the store can be |
||||||
|
configured and then all sessions saved using it will use that configuration. |
||||||
|
We access session.Options or store.Options to set a new configuration. The |
||||||
|
fields are basically a subset of http.Cookie fields. Let's change the |
||||||
|
maximum age of a session to one week: |
||||||
|
|
||||||
|
session.Options = &sessions.Options{ |
||||||
|
Path: "/", |
||||||
|
MaxAge: 86400 * 7, |
||||||
|
HttpOnly: true, |
||||||
|
} |
||||||
|
|
||||||
|
Sometimes we may want to change authentication and/or encryption keys without |
||||||
|
breaking existing sessions. The CookieStore supports key rotation, and to use |
||||||
|
it you just need to set multiple authentication and encryption keys, in pairs, |
||||||
|
to be tested in order: |
||||||
|
|
||||||
|
var store = sessions.NewCookieStore( |
||||||
|
[]byte("new-authentication-key"), |
||||||
|
[]byte("new-encryption-key"), |
||||||
|
[]byte("old-authentication-key"), |
||||||
|
[]byte("old-encryption-key"), |
||||||
|
) |
||||||
|
|
||||||
|
New sessions will be saved using the first pair. Old sessions can still be |
||||||
|
read because the first pair will fail, and the second will be tested. This |
||||||
|
makes it easy to "rotate" secret keys and still be able to validate existing |
||||||
|
sessions. Note: for all pairs the encryption key is optional; set it to nil |
||||||
|
or omit it and and encryption won't be used. |
||||||
|
|
||||||
|
Multiple sessions can be used in the same request, even with different |
||||||
|
session backends. When this happens, calling Save() on each session |
||||||
|
individually would be cumbersome, so we have a way to save all sessions |
||||||
|
at once: it's sessions.Save(). Here's an example: |
||||||
|
|
||||||
|
var store = sessions.NewCookieStore([]byte("something-very-secret")) |
||||||
|
|
||||||
|
func MyHandler(w http.ResponseWriter, r *http.Request) { |
||||||
|
// Get a session and set a value.
|
||||||
|
session1, _ := store.Get(r, "session-one") |
||||||
|
session1.Values["foo"] = "bar" |
||||||
|
// Get another session and set another value.
|
||||||
|
session2, _ := store.Get(r, "session-two") |
||||||
|
session2.Values[42] = 43 |
||||||
|
// Save all sessions.
|
||||||
|
sessions.Save(r, w) |
||||||
|
} |
||||||
|
|
||||||
|
This is possible because when we call Get() from a session store, it adds the |
||||||
|
session to a common registry. Save() uses it to save all registered sessions. |
||||||
|
*/ |
||||||
|
package sessions |
@ -0,0 +1,102 @@ |
|||||||
|
// This file contains code adapted from the Go standard library
|
||||||
|
// https://github.com/golang/go/blob/39ad0fd0789872f9469167be7fe9578625ff246e/src/net/http/lex.go
|
||||||
|
|
||||||
|
package sessions |
||||||
|
|
||||||
|
import "strings" |
||||||
|
|
||||||
|
var isTokenTable = [127]bool{ |
||||||
|
'!': true, |
||||||
|
'#': true, |
||||||
|
'$': true, |
||||||
|
'%': true, |
||||||
|
'&': true, |
||||||
|
'\'': true, |
||||||
|
'*': true, |
||||||
|
'+': true, |
||||||
|
'-': true, |
||||||
|
'.': true, |
||||||
|
'0': true, |
||||||
|
'1': true, |
||||||
|
'2': true, |
||||||
|
'3': true, |
||||||
|
'4': true, |
||||||
|
'5': true, |
||||||
|
'6': true, |
||||||
|
'7': true, |
||||||
|
'8': true, |
||||||
|
'9': true, |
||||||
|
'A': true, |
||||||
|
'B': true, |
||||||
|
'C': true, |
||||||
|
'D': true, |
||||||
|
'E': true, |
||||||
|
'F': true, |
||||||
|
'G': true, |
||||||
|
'H': true, |
||||||
|
'I': true, |
||||||
|
'J': true, |
||||||
|
'K': true, |
||||||
|
'L': true, |
||||||
|
'M': true, |
||||||
|
'N': true, |
||||||
|
'O': true, |
||||||
|
'P': true, |
||||||
|
'Q': true, |
||||||
|
'R': true, |
||||||
|
'S': true, |
||||||
|
'T': true, |
||||||
|
'U': true, |
||||||
|
'W': true, |
||||||
|
'V': true, |
||||||
|
'X': true, |
||||||
|
'Y': true, |
||||||
|
'Z': true, |
||||||
|
'^': true, |
||||||
|
'_': true, |
||||||
|
'`': true, |
||||||
|
'a': true, |
||||||
|
'b': true, |
||||||
|
'c': true, |
||||||
|
'd': true, |
||||||
|
'e': true, |
||||||
|
'f': true, |
||||||
|
'g': true, |
||||||
|
'h': true, |
||||||
|
'i': true, |
||||||
|
'j': true, |
||||||
|
'k': true, |
||||||
|
'l': true, |
||||||
|
'm': true, |
||||||
|
'n': true, |
||||||
|
'o': true, |
||||||
|
'p': true, |
||||||
|
'q': true, |
||||||
|
'r': true, |
||||||
|
's': true, |
||||||
|
't': true, |
||||||
|
'u': true, |
||||||
|
'v': true, |
||||||
|
'w': true, |
||||||
|
'x': true, |
||||||
|
'y': true, |
||||||
|
'z': true, |
||||||
|
'|': true, |
||||||
|
'~': true, |
||||||
|
} |
||||||
|
|
||||||
|
func isToken(r rune) bool { |
||||||
|
i := int(r) |
||||||
|
return i < len(isTokenTable) && isTokenTable[i] |
||||||
|
} |
||||||
|
|
||||||
|
func isNotToken(r rune) bool { |
||||||
|
return !isToken(r) |
||||||
|
} |
||||||
|
|
||||||
|
func isCookieNameValid(raw string) bool { |
||||||
|
if raw == "" { |
||||||
|
return false |
||||||
|
} |
||||||
|
return strings.IndexFunc(raw, isNotToken) < 0 |
||||||
|
} |
@ -0,0 +1,241 @@ |
|||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package sessions |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/gob" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/gorilla/context" |
||||||
|
) |
||||||
|
|
||||||
|
// Default flashes key.
|
||||||
|
const flashesKey = "_flash" |
||||||
|
|
||||||
|
// Options --------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Options stores configuration for a session or session store.
|
||||||
|
//
|
||||||
|
// Fields are a subset of http.Cookie fields.
|
||||||
|
type Options struct { |
||||||
|
Path string |
||||||
|
Domain string |
||||||
|
// MaxAge=0 means no 'Max-Age' attribute specified.
|
||||||
|
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'.
|
||||||
|
// MaxAge>0 means Max-Age attribute present and given in seconds.
|
||||||
|
MaxAge int |
||||||
|
Secure bool |
||||||
|
HttpOnly bool |
||||||
|
} |
||||||
|
|
||||||
|
// Session --------------------------------------------------------------------
|
||||||
|
|
||||||
|
// NewSession is called by session stores to create a new session instance.
|
||||||
|
func NewSession(store Store, name string) *Session { |
||||||
|
return &Session{ |
||||||
|
Values: make(map[interface{}]interface{}), |
||||||
|
store: store, |
||||||
|
name: name, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Session stores the values and optional configuration for a session.
|
||||||
|
type Session struct { |
||||||
|
// The ID of the session, generated by stores. It should not be used for
|
||||||
|
// user data.
|
||||||
|
ID string |
||||||
|
// Values contains the user-data for the session.
|
||||||
|
Values map[interface{}]interface{} |
||||||
|
Options *Options |
||||||
|
IsNew bool |
||||||
|
store Store |
||||||
|
name string |
||||||
|
} |
||||||
|
|
||||||
|
// Flashes returns a slice of flash messages from the session.
|
||||||
|
//
|
||||||
|
// A single variadic argument is accepted, and it is optional: it defines
|
||||||
|
// the flash key. If not defined "_flash" is used by default.
|
||||||
|
func (s *Session) Flashes(vars ...string) []interface{} { |
||||||
|
var flashes []interface{} |
||||||
|
key := flashesKey |
||||||
|
if len(vars) > 0 { |
||||||
|
key = vars[0] |
||||||
|
} |
||||||
|
if v, ok := s.Values[key]; ok { |
||||||
|
// Drop the flashes and return it.
|
||||||
|
delete(s.Values, key) |
||||||
|
flashes = v.([]interface{}) |
||||||
|
} |
||||||
|
return flashes |
||||||
|
} |
||||||
|
|
||||||
|
// AddFlash adds a flash message to the session.
|
||||||
|
//
|
||||||
|
// A single variadic argument is accepted, and it is optional: it defines
|
||||||
|
// the flash key. If not defined "_flash" is used by default.
|
||||||
|
func (s *Session) AddFlash(value interface{}, vars ...string) { |
||||||
|
key := flashesKey |
||||||
|
if len(vars) > 0 { |
||||||
|
key = vars[0] |
||||||
|
} |
||||||
|
var flashes []interface{} |
||||||
|
if v, ok := s.Values[key]; ok { |
||||||
|
flashes = v.([]interface{}) |
||||||
|
} |
||||||
|
s.Values[key] = append(flashes, value) |
||||||
|
} |
||||||
|
|
||||||
|
// Save is a convenience method to save this session. It is the same as calling
|
||||||
|
// store.Save(request, response, session). You should call Save before writing to
|
||||||
|
// the response or returning from the handler.
|
||||||
|
func (s *Session) Save(r *http.Request, w http.ResponseWriter) error { |
||||||
|
return s.store.Save(r, w, s) |
||||||
|
} |
||||||
|
|
||||||
|
// Name returns the name used to register the session.
|
||||||
|
func (s *Session) Name() string { |
||||||
|
return s.name |
||||||
|
} |
||||||
|
|
||||||
|
// Store returns the session store used to register the session.
|
||||||
|
func (s *Session) Store() Store { |
||||||
|
return s.store |
||||||
|
} |
||||||
|
|
||||||
|
// Registry -------------------------------------------------------------------
|
||||||
|
|
||||||
|
// sessionInfo stores a session tracked by the registry.
|
||||||
|
type sessionInfo struct { |
||||||
|
s *Session |
||||||
|
e error |
||||||
|
} |
||||||
|
|
||||||
|
// contextKey is the type used to store the registry in the context.
|
||||||
|
type contextKey int |
||||||
|
|
||||||
|
// registryKey is the key used to store the registry in the context.
|
||||||
|
const registryKey contextKey = 0 |
||||||
|
|
||||||
|
// GetRegistry returns a registry instance for the current request.
|
||||||
|
func GetRegistry(r *http.Request) *Registry { |
||||||
|
registry := context.Get(r, registryKey) |
||||||
|
if registry != nil { |
||||||
|
return registry.(*Registry) |
||||||
|
} |
||||||
|
newRegistry := &Registry{ |
||||||
|
request: r, |
||||||
|
sessions: make(map[string]sessionInfo), |
||||||
|
} |
||||||
|
context.Set(r, registryKey, newRegistry) |
||||||
|
return newRegistry |
||||||
|
} |
||||||
|
|
||||||
|
// Registry stores sessions used during a request.
|
||||||
|
type Registry struct { |
||||||
|
request *http.Request |
||||||
|
sessions map[string]sessionInfo |
||||||
|
} |
||||||
|
|
||||||
|
// Get registers and returns a session for the given name and session store.
|
||||||
|
//
|
||||||
|
// It returns a new session if there are no sessions registered for the name.
|
||||||
|
func (s *Registry) Get(store Store, name string) (session *Session, err error) { |
||||||
|
if !isCookieNameValid(name) { |
||||||
|
return nil, fmt.Errorf("sessions: invalid character in cookie name: %s", name) |
||||||
|
} |
||||||
|
if info, ok := s.sessions[name]; ok { |
||||||
|
session, err = info.s, info.e |
||||||
|
} else { |
||||||
|
session, err = store.New(s.request, name) |
||||||
|
session.name = name |
||||||
|
s.sessions[name] = sessionInfo{s: session, e: err} |
||||||
|
} |
||||||
|
session.store = store |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Save saves all sessions registered for the current request.
|
||||||
|
func (s *Registry) Save(w http.ResponseWriter) error { |
||||||
|
var errMulti MultiError |
||||||
|
for name, info := range s.sessions { |
||||||
|
session := info.s |
||||||
|
if session.store == nil { |
||||||
|
errMulti = append(errMulti, fmt.Errorf( |
||||||
|
"sessions: missing store for session %q", name)) |
||||||
|
} else if err := session.store.Save(s.request, w, session); err != nil { |
||||||
|
errMulti = append(errMulti, fmt.Errorf( |
||||||
|
"sessions: error saving session %q -- %v", name, err)) |
||||||
|
} |
||||||
|
} |
||||||
|
if errMulti != nil { |
||||||
|
return errMulti |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Helpers --------------------------------------------------------------------
|
||||||
|
|
||||||
|
func init() { |
||||||
|
gob.Register([]interface{}{}) |
||||||
|
} |
||||||
|
|
||||||
|
// Save saves all sessions used during the current request.
|
||||||
|
func Save(r *http.Request, w http.ResponseWriter) error { |
||||||
|
return GetRegistry(r).Save(w) |
||||||
|
} |
||||||
|
|
||||||
|
// NewCookie returns an http.Cookie with the options set. It also sets
|
||||||
|
// the Expires field calculated based on the MaxAge value, for Internet
|
||||||
|
// Explorer compatibility.
|
||||||
|
func NewCookie(name, value string, options *Options) *http.Cookie { |
||||||
|
cookie := &http.Cookie{ |
||||||
|
Name: name, |
||||||
|
Value: value, |
||||||
|
Path: options.Path, |
||||||
|
Domain: options.Domain, |
||||||
|
MaxAge: options.MaxAge, |
||||||
|
Secure: options.Secure, |
||||||
|
HttpOnly: options.HttpOnly, |
||||||
|
} |
||||||
|
if options.MaxAge > 0 { |
||||||
|
d := time.Duration(options.MaxAge) * time.Second |
||||||
|
cookie.Expires = time.Now().Add(d) |
||||||
|
} else if options.MaxAge < 0 { |
||||||
|
// Set it to the past to expire now.
|
||||||
|
cookie.Expires = time.Unix(1, 0) |
||||||
|
} |
||||||
|
return cookie |
||||||
|
} |
||||||
|
|
||||||
|
// Error ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
// MultiError stores multiple errors.
|
||||||
|
//
|
||||||
|
// Borrowed from the App Engine SDK.
|
||||||
|
type MultiError []error |
||||||
|
|
||||||
|
func (m MultiError) Error() string { |
||||||
|
s, n := "", 0 |
||||||
|
for _, e := range m { |
||||||
|
if e != nil { |
||||||
|
if n == 0 { |
||||||
|
s = e.Error() |
||||||
|
} |
||||||
|
n++ |
||||||
|
} |
||||||
|
} |
||||||
|
switch n { |
||||||
|
case 0: |
||||||
|
return "(0 errors)" |
||||||
|
case 1: |
||||||
|
return s |
||||||
|
case 2: |
||||||
|
return s + " (and 1 other error)" |
||||||
|
} |
||||||
|
return fmt.Sprintf("%s (and %d other errors)", s, n-1) |
||||||
|
} |
@ -0,0 +1,295 @@ |
|||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package sessions |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/base32" |
||||||
|
"io/ioutil" |
||||||
|
"net/http" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"github.com/gorilla/securecookie" |
||||||
|
) |
||||||
|
|
||||||
|
// Store is an interface for custom session stores.
|
||||||
|
//
|
||||||
|
// See CookieStore and FilesystemStore for examples.
|
||||||
|
type Store interface { |
||||||
|
// Get should return a cached session.
|
||||||
|
Get(r *http.Request, name string) (*Session, error) |
||||||
|
|
||||||
|
// New should create and return a new session.
|
||||||
|
//
|
||||||
|
// Note that New should never return a nil session, even in the case of
|
||||||
|
// an error if using the Registry infrastructure to cache the session.
|
||||||
|
New(r *http.Request, name string) (*Session, error) |
||||||
|
|
||||||
|
// Save should persist session to the underlying store implementation.
|
||||||
|
Save(r *http.Request, w http.ResponseWriter, s *Session) error |
||||||
|
} |
||||||
|
|
||||||
|
// CookieStore ----------------------------------------------------------------
|
||||||
|
|
||||||
|
// NewCookieStore returns a new CookieStore.
|
||||||
|
//
|
||||||
|
// Keys are defined in pairs to allow key rotation, but the common case is
|
||||||
|
// to set a single authentication key and optionally an encryption key.
|
||||||
|
//
|
||||||
|
// The first key in a pair is used for authentication and the second for
|
||||||
|
// encryption. The encryption key can be set to nil or omitted in the last
|
||||||
|
// pair, but the authentication key is required in all pairs.
|
||||||
|
//
|
||||||
|
// It is recommended to use an authentication key with 32 or 64 bytes.
|
||||||
|
// The encryption key, if set, must be either 16, 24, or 32 bytes to select
|
||||||
|
// AES-128, AES-192, or AES-256 modes.
|
||||||
|
//
|
||||||
|
// Use the convenience function securecookie.GenerateRandomKey() to create
|
||||||
|
// strong keys.
|
||||||
|
func NewCookieStore(keyPairs ...[]byte) *CookieStore { |
||||||
|
cs := &CookieStore{ |
||||||
|
Codecs: securecookie.CodecsFromPairs(keyPairs...), |
||||||
|
Options: &Options{ |
||||||
|
Path: "/", |
||||||
|
MaxAge: 86400 * 30, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
cs.MaxAge(cs.Options.MaxAge) |
||||||
|
return cs |
||||||
|
} |
||||||
|
|
||||||
|
// CookieStore stores sessions using secure cookies.
|
||||||
|
type CookieStore struct { |
||||||
|
Codecs []securecookie.Codec |
||||||
|
Options *Options // default configuration
|
||||||
|
} |
||||||
|
|
||||||
|
// Get returns a session for the given name after adding it to the registry.
|
||||||
|
//
|
||||||
|
// It returns a new session if the sessions doesn't exist. Access IsNew on
|
||||||
|
// the session to check if it is an existing session or a new one.
|
||||||
|
//
|
||||||
|
// It returns a new session and an error if the session exists but could
|
||||||
|
// not be decoded.
|
||||||
|
func (s *CookieStore) Get(r *http.Request, name string) (*Session, error) { |
||||||
|
return GetRegistry(r).Get(s, name) |
||||||
|
} |
||||||
|
|
||||||
|
// New returns a session for the given name without adding it to the registry.
|
||||||
|
//
|
||||||
|
// The difference between New() and Get() is that calling New() twice will
|
||||||
|
// decode the session data twice, while Get() registers and reuses the same
|
||||||
|
// decoded session after the first call.
|
||||||
|
func (s *CookieStore) New(r *http.Request, name string) (*Session, error) { |
||||||
|
session := NewSession(s, name) |
||||||
|
opts := *s.Options |
||||||
|
session.Options = &opts |
||||||
|
session.IsNew = true |
||||||
|
var err error |
||||||
|
if c, errCookie := r.Cookie(name); errCookie == nil { |
||||||
|
err = securecookie.DecodeMulti(name, c.Value, &session.Values, |
||||||
|
s.Codecs...) |
||||||
|
if err == nil { |
||||||
|
session.IsNew = false |
||||||
|
} |
||||||
|
} |
||||||
|
return session, err |
||||||
|
} |
||||||
|
|
||||||
|
// Save adds a single session to the response.
|
||||||
|
func (s *CookieStore) Save(r *http.Request, w http.ResponseWriter, |
||||||
|
session *Session) error { |
||||||
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, |
||||||
|
s.Codecs...) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options)) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// MaxAge sets the maximum age for the store and the underlying cookie
|
||||||
|
// implementation. Individual sessions can be deleted by setting Options.MaxAge
|
||||||
|
// = -1 for that session.
|
||||||
|
func (s *CookieStore) MaxAge(age int) { |
||||||
|
s.Options.MaxAge = age |
||||||
|
|
||||||
|
// Set the maxAge for each securecookie instance.
|
||||||
|
for _, codec := range s.Codecs { |
||||||
|
if sc, ok := codec.(*securecookie.SecureCookie); ok { |
||||||
|
sc.MaxAge(age) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// FilesystemStore ------------------------------------------------------------
|
||||||
|
|
||||||
|
var fileMutex sync.RWMutex |
||||||
|
|
||||||
|
// NewFilesystemStore returns a new FilesystemStore.
|
||||||
|
//
|
||||||
|
// The path argument is the directory where sessions will be saved. If empty
|
||||||
|
// it will use os.TempDir().
|
||||||
|
//
|
||||||
|
// See NewCookieStore() for a description of the other parameters.
|
||||||
|
func NewFilesystemStore(path string, keyPairs ...[]byte) *FilesystemStore { |
||||||
|
if path == "" { |
||||||
|
path = os.TempDir() |
||||||
|
} |
||||||
|
fs := &FilesystemStore{ |
||||||
|
Codecs: securecookie.CodecsFromPairs(keyPairs...), |
||||||
|
Options: &Options{ |
||||||
|
Path: "/", |
||||||
|
MaxAge: 86400 * 30, |
||||||
|
}, |
||||||
|
path: path, |
||||||
|
} |
||||||
|
|
||||||
|
fs.MaxAge(fs.Options.MaxAge) |
||||||
|
return fs |
||||||
|
} |
||||||
|
|
||||||
|
// FilesystemStore stores sessions in the filesystem.
|
||||||
|
//
|
||||||
|
// It also serves as a reference for custom stores.
|
||||||
|
//
|
||||||
|
// This store is still experimental and not well tested. Feedback is welcome.
|
||||||
|
type FilesystemStore struct { |
||||||
|
Codecs []securecookie.Codec |
||||||
|
Options *Options // default configuration
|
||||||
|
path string |
||||||
|
} |
||||||
|
|
||||||
|
// MaxLength restricts the maximum length of new sessions to l.
|
||||||
|
// If l is 0 there is no limit to the size of a session, use with caution.
|
||||||
|
// The default for a new FilesystemStore is 4096.
|
||||||
|
func (s *FilesystemStore) MaxLength(l int) { |
||||||
|
for _, c := range s.Codecs { |
||||||
|
if codec, ok := c.(*securecookie.SecureCookie); ok { |
||||||
|
codec.MaxLength(l) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Get returns a session for the given name after adding it to the registry.
|
||||||
|
//
|
||||||
|
// See CookieStore.Get().
|
||||||
|
func (s *FilesystemStore) Get(r *http.Request, name string) (*Session, error) { |
||||||
|
return GetRegistry(r).Get(s, name) |
||||||
|
} |
||||||
|
|
||||||
|
// New returns a session for the given name without adding it to the registry.
|
||||||
|
//
|
||||||
|
// See CookieStore.New().
|
||||||
|
func (s *FilesystemStore) New(r *http.Request, name string) (*Session, error) { |
||||||
|
session := NewSession(s, name) |
||||||
|
opts := *s.Options |
||||||
|
session.Options = &opts |
||||||
|
session.IsNew = true |
||||||
|
var err error |
||||||
|
if c, errCookie := r.Cookie(name); errCookie == nil { |
||||||
|
err = securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...) |
||||||
|
if err == nil { |
||||||
|
err = s.load(session) |
||||||
|
if err == nil { |
||||||
|
session.IsNew = false |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return session, err |
||||||
|
} |
||||||
|
|
||||||
|
// Save adds a single session to the response.
|
||||||
|
//
|
||||||
|
// If the Options.MaxAge of the session is <= 0 then the session file will be
|
||||||
|
// deleted from the store path. With this process it enforces the properly
|
||||||
|
// session cookie handling so no need to trust in the cookie management in the
|
||||||
|
// web browser.
|
||||||
|
func (s *FilesystemStore) Save(r *http.Request, w http.ResponseWriter, |
||||||
|
session *Session) error { |
||||||
|
// Delete if max-age is <= 0
|
||||||
|
if session.Options.MaxAge <= 0 { |
||||||
|
if err := s.erase(session); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
http.SetCookie(w, NewCookie(session.Name(), "", session.Options)) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
if session.ID == "" { |
||||||
|
// Because the ID is used in the filename, encode it to
|
||||||
|
// use alphanumeric characters only.
|
||||||
|
session.ID = strings.TrimRight( |
||||||
|
base32.StdEncoding.EncodeToString( |
||||||
|
securecookie.GenerateRandomKey(32)), "=") |
||||||
|
} |
||||||
|
if err := s.save(session); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, |
||||||
|
s.Codecs...) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options)) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// MaxAge sets the maximum age for the store and the underlying cookie
|
||||||
|
// implementation. Individual sessions can be deleted by setting Options.MaxAge
|
||||||
|
// = -1 for that session.
|
||||||
|
func (s *FilesystemStore) MaxAge(age int) { |
||||||
|
s.Options.MaxAge = age |
||||||
|
|
||||||
|
// Set the maxAge for each securecookie instance.
|
||||||
|
for _, codec := range s.Codecs { |
||||||
|
if sc, ok := codec.(*securecookie.SecureCookie); ok { |
||||||
|
sc.MaxAge(age) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// save writes encoded session.Values to a file.
|
||||||
|
func (s *FilesystemStore) save(session *Session) error { |
||||||
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, |
||||||
|
s.Codecs...) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
filename := filepath.Join(s.path, "session_"+session.ID) |
||||||
|
fileMutex.Lock() |
||||||
|
defer fileMutex.Unlock() |
||||||
|
return ioutil.WriteFile(filename, []byte(encoded), 0600) |
||||||
|
} |
||||||
|
|
||||||
|
// load reads a file and decodes its content into session.Values.
|
||||||
|
func (s *FilesystemStore) load(session *Session) error { |
||||||
|
filename := filepath.Join(s.path, "session_"+session.ID) |
||||||
|
fileMutex.RLock() |
||||||
|
defer fileMutex.RUnlock() |
||||||
|
fdata, err := ioutil.ReadFile(filename) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err = securecookie.DecodeMulti(session.Name(), string(fdata), |
||||||
|
&session.Values, s.Codecs...); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// delete session file
|
||||||
|
func (s *FilesystemStore) erase(session *Session) error { |
||||||
|
filename := filepath.Join(s.path, "session_"+session.ID) |
||||||
|
|
||||||
|
fileMutex.RLock() |
||||||
|
defer fileMutex.RUnlock() |
||||||
|
|
||||||
|
err := os.Remove(filename) |
||||||
|
return err |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
Copyright (c) 2014 Mark Bates |
||||||
|
|
||||||
|
MIT License |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining |
||||||
|
a copy of this software and associated documentation files (the |
||||||
|
"Software"), to deal in the Software without restriction, including |
||||||
|
without limitation the rights to use, copy, modify, merge, publish, |
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to |
||||||
|
permit persons to whom the Software is furnished to do so, subject to |
||||||
|
the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be |
||||||
|
included in all copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
@ -0,0 +1,143 @@ |
|||||||
|
# Goth: Multi-Provider Authentication for Go [![GoDoc](https://godoc.org/github.com/markbates/goth?status.svg)](https://godoc.org/github.com/markbates/goth) [![Build Status](https://travis-ci.org/markbates/goth.svg)](https://travis-ci.org/markbates/goth) |
||||||
|
|
||||||
|
Package goth provides a simple, clean, and idiomatic way to write authentication |
||||||
|
packages for Go web applications. |
||||||
|
|
||||||
|
Unlike other similar packages, Goth, lets you write OAuth, OAuth2, or any other |
||||||
|
protocol providers, as long as they implement the `Provider` and `Session` interfaces. |
||||||
|
|
||||||
|
This package was inspired by [https://github.com/intridea/omniauth](https://github.com/intridea/omniauth). |
||||||
|
|
||||||
|
## Installation |
||||||
|
|
||||||
|
```text |
||||||
|
$ go get github.com/markbates/goth |
||||||
|
``` |
||||||
|
|
||||||
|
## Supported Providers |
||||||
|
|
||||||
|
* Amazon |
||||||
|
* Auth0 |
||||||
|
* Bitbucket |
||||||
|
* Box |
||||||
|
* Cloud Foundry |
||||||
|
* Dailymotion |
||||||
|
* Deezer |
||||||
|
* Digital Ocean |
||||||
|
* Discord |
||||||
|
* Dropbox |
||||||
|
* Facebook |
||||||
|
* Fitbit |
||||||
|
* GitHub |
||||||
|
* Gitlab |
||||||
|
* Google+ |
||||||
|
* Heroku |
||||||
|
* InfluxCloud |
||||||
|
* Instagram |
||||||
|
* Intercom |
||||||
|
* Lastfm |
||||||
|
* Linkedin |
||||||
|
* Meetup |
||||||
|
* OneDrive |
||||||
|
* OpenID Connect (auto discovery) |
||||||
|
* Paypal |
||||||
|
* SalesForce |
||||||
|
* Slack |
||||||
|
* Soundcloud |
||||||
|
* Spotify |
||||||
|
* Steam |
||||||
|
* Stripe |
||||||
|
* Twitch |
||||||
|
* Twitter |
||||||
|
* Uber |
||||||
|
* Wepay |
||||||
|
* Yahoo |
||||||
|
* Yammer |
||||||
|
|
||||||
|
## Examples |
||||||
|
|
||||||
|
See the [examples](examples) folder for a working application that lets users authenticate |
||||||
|
through Twitter, Facebook, Google Plus etc. |
||||||
|
|
||||||
|
To run the example either clone the source from GitHub |
||||||
|
|
||||||
|
```text |
||||||
|
$ git clone git@github.com:markbates/goth.git |
||||||
|
``` |
||||||
|
or use |
||||||
|
```text |
||||||
|
$ go get github.com/markbates/goth |
||||||
|
``` |
||||||
|
```text |
||||||
|
$ cd goth/examples |
||||||
|
$ go get -v |
||||||
|
$ go build |
||||||
|
$ ./examples |
||||||
|
``` |
||||||
|
|
||||||
|
Now open up your browser and go to [http://localhost:3000](http://localhost:3000) to see the example. |
||||||
|
|
||||||
|
To actually use the different providers, please make sure you configure them given the system environments as defined in the examples/main.go file |
||||||
|
|
||||||
|
## Issues |
||||||
|
|
||||||
|
Issues always stand a significantly better chance of getting fixed if the are accompanied by a |
||||||
|
pull request. |
||||||
|
|
||||||
|
## Contributing |
||||||
|
|
||||||
|
Would I love to see more providers? Certainly! Would you love to contribute one? Hopefully, yes! |
||||||
|
|
||||||
|
1. Fork it |
||||||
|
2. Create your feature branch (git checkout -b my-new-feature) |
||||||
|
3. Write Tests! |
||||||
|
4. Commit your changes (git commit -am 'Add some feature') |
||||||
|
5. Push to the branch (git push origin my-new-feature) |
||||||
|
6. Create new Pull Request |
||||||
|
|
||||||
|
## Contributors |
||||||
|
|
||||||
|
* Mark Bates |
||||||
|
* Tyler Bunnell |
||||||
|
* Corey McGrillis |
||||||
|
* willemvd |
||||||
|
* Rakesh Goyal |
||||||
|
* Andy Grunwald |
||||||
|
* Glenn Walker |
||||||
|
* Kevin Fitzpatrick |
||||||
|
* Ben Tranter |
||||||
|
* Sharad Ganapathy |
||||||
|
* Andrew Chilton |
||||||
|
* sharadgana |
||||||
|
* Aurorae |
||||||
|
* Craig P Jolicoeur |
||||||
|
* Zac Bergquist |
||||||
|
* Geoff Franks |
||||||
|
* Raphael Geronimi |
||||||
|
* Noah Shibley |
||||||
|
* lumost |
||||||
|
* oov |
||||||
|
* Felix Lamouroux |
||||||
|
* Rafael Quintela |
||||||
|
* Tyler |
||||||
|
* DenSm |
||||||
|
* Samy KACIMI |
||||||
|
* dante gray |
||||||
|
* Noah |
||||||
|
* Jacob Walker |
||||||
|
* Marin Martinic |
||||||
|
* Roy |
||||||
|
* Omni Adams |
||||||
|
* Sasa Brankovic |
||||||
|
* dkhamsing |
||||||
|
* Dante Swift |
||||||
|
* Attila Domokos |
||||||
|
* Albin Gilles |
||||||
|
* Syed Zubairuddin |
||||||
|
* Johnny Boursiquot |
||||||
|
* Jerome Touffe-Blin |
||||||
|
* bryanl |
||||||
|
* Masanobu YOSHIOKA |
||||||
|
* Jonathan Hall |
||||||
|
* HaiMing.Yin |
||||||
|
* Sairam Kunala |
@ -0,0 +1,10 @@ |
|||||||
|
/* |
||||||
|
Package goth provides a simple, clean, and idiomatic way to write authentication |
||||||
|
packages for Go web applications. |
||||||
|
|
||||||
|
This package was inspired by https://github.com/intridea/omniauth.
|
||||||
|
|
||||||
|
See the examples folder for a working application that lets users authenticate |
||||||
|
through Twitter or Facebook. |
||||||
|
*/ |
||||||
|
package goth |
@ -0,0 +1,219 @@ |
|||||||
|
/* |
||||||
|
Package gothic wraps common behaviour when using Goth. This makes it quick, and easy, to get up |
||||||
|
and running with Goth. Of course, if you want complete control over how things flow, in regards |
||||||
|
to the authentication process, feel free and use Goth directly. |
||||||
|
|
||||||
|
See https://github.com/markbates/goth/examples/main.go to see this in action.
|
||||||
|
*/ |
||||||
|
package gothic |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"os" |
||||||
|
|
||||||
|
"github.com/gorilla/mux" |
||||||
|
"github.com/gorilla/sessions" |
||||||
|
"github.com/markbates/goth" |
||||||
|
) |
||||||
|
|
||||||
|
// SessionName is the key used to access the session store.
|
||||||
|
const SessionName = "_gothic_session" |
||||||
|
|
||||||
|
// Store can/should be set by applications using gothic. The default is a cookie store.
|
||||||
|
var Store sessions.Store |
||||||
|
var defaultStore sessions.Store |
||||||
|
|
||||||
|
var keySet = false |
||||||
|
|
||||||
|
func init() { |
||||||
|
key := []byte(os.Getenv("SESSION_SECRET")) |
||||||
|
keySet = len(key) != 0 |
||||||
|
Store = sessions.NewCookieStore([]byte(key)) |
||||||
|
defaultStore = Store |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
BeginAuthHandler is a convienence handler for starting the authentication process. |
||||||
|
It expects to be able to get the name of the provider from the query parameters |
||||||
|
as either "provider" or ":provider". |
||||||
|
|
||||||
|
BeginAuthHandler will redirect the user to the appropriate authentication end-point |
||||||
|
for the requested provider. |
||||||
|
|
||||||
|
See https://github.com/markbates/goth/examples/main.go to see this in action.
|
||||||
|
*/ |
||||||
|
func BeginAuthHandler(res http.ResponseWriter, req *http.Request) { |
||||||
|
url, err := GetAuthURL(res, req) |
||||||
|
if err != nil { |
||||||
|
res.WriteHeader(http.StatusBadRequest) |
||||||
|
fmt.Fprintln(res, err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
http.Redirect(res, req, url, http.StatusTemporaryRedirect) |
||||||
|
} |
||||||
|
|
||||||
|
// SetState sets the state string associated with the given request.
|
||||||
|
// If no state string is associated with the request, one will be generated.
|
||||||
|
// This state is sent to the provider and can be retrieved during the
|
||||||
|
// callback.
|
||||||
|
var SetState = func(req *http.Request) string { |
||||||
|
state := req.URL.Query().Get("state") |
||||||
|
if len(state) > 0 { |
||||||
|
return state |
||||||
|
} |
||||||
|
|
||||||
|
return "state" |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// GetState gets the state returned by the provider during the callback.
|
||||||
|
// This is used to prevent CSRF attacks, see
|
||||||
|
// http://tools.ietf.org/html/rfc6749#section-10.12
|
||||||
|
var GetState = func(req *http.Request) string { |
||||||
|
return req.URL.Query().Get("state") |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
GetAuthURL starts the authentication process with the requested provided. |
||||||
|
It will return a URL that should be used to send users to. |
||||||
|
|
||||||
|
It expects to be able to get the name of the provider from the query parameters |
||||||
|
as either "provider" or ":provider". |
||||||
|
|
||||||
|
I would recommend using the BeginAuthHandler instead of doing all of these steps |
||||||
|
yourself, but that's entirely up to you. |
||||||
|
*/ |
||||||
|
func GetAuthURL(res http.ResponseWriter, req *http.Request) (string, error) { |
||||||
|
|
||||||
|
if !keySet && defaultStore == Store { |
||||||
|
fmt.Println("goth/gothic: no SESSION_SECRET environment variable is set. The default cookie store is not available and any calls will fail. Ignore this warning if you are using a different store.") |
||||||
|
} |
||||||
|
|
||||||
|
providerName, err := GetProviderName(req) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
provider, err := goth.GetProvider(providerName) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
sess, err := provider.BeginAuth(SetState(req)) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
url, err := sess.GetAuthURL() |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
err = storeInSession(providerName, sess.Marshal(), req, res) |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
return url, err |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
CompleteUserAuth does what it says on the tin. It completes the authentication |
||||||
|
process and fetches all of the basic information about the user from the provider. |
||||||
|
|
||||||
|
It expects to be able to get the name of the provider from the query parameters |
||||||
|
as either "provider" or ":provider". |
||||||
|
|
||||||
|
See https://github.com/markbates/goth/examples/main.go to see this in action.
|
||||||
|
*/ |
||||||
|
var CompleteUserAuth = func(res http.ResponseWriter, req *http.Request) (goth.User, error) { |
||||||
|
|
||||||
|
if !keySet && defaultStore == Store { |
||||||
|
fmt.Println("goth/gothic: no SESSION_SECRET environment variable is set. The default cookie store is not available and any calls will fail. Ignore this warning if you are using a different store.") |
||||||
|
} |
||||||
|
|
||||||
|
providerName, err := GetProviderName(req) |
||||||
|
if err != nil { |
||||||
|
return goth.User{}, err |
||||||
|
} |
||||||
|
|
||||||
|
provider, err := goth.GetProvider(providerName) |
||||||
|
if err != nil { |
||||||
|
return goth.User{}, err |
||||||
|
} |
||||||
|
|
||||||
|
value, err := getFromSession(providerName, req) |
||||||
|
if err != nil { |
||||||
|
return goth.User{}, err |
||||||
|
} |
||||||
|
|
||||||
|
sess, err := provider.UnmarshalSession(value) |
||||||
|
if err != nil { |
||||||
|
return goth.User{}, err |
||||||
|
} |
||||||
|
|
||||||
|
user, err := provider.FetchUser(sess) |
||||||
|
if err == nil { |
||||||
|
// user can be found with existing session data
|
||||||
|
return user, err |
||||||
|
} |
||||||
|
|
||||||
|
// get new token and retry fetch
|
||||||
|
_, err = sess.Authorize(provider, req.URL.Query()) |
||||||
|
if err != nil { |
||||||
|
return goth.User{}, err |
||||||
|
} |
||||||
|
|
||||||
|
err = storeInSession(providerName, sess.Marshal(), req, res) |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
return goth.User{}, err |
||||||
|
} |
||||||
|
|
||||||
|
return provider.FetchUser(sess) |
||||||
|
} |
||||||
|
|
||||||
|
// GetProviderName is a function used to get the name of a provider
|
||||||
|
// for a given request. By default, this provider is fetched from
|
||||||
|
// the URL query string. If you provide it in a different way,
|
||||||
|
// assign your own function to this variable that returns the provider
|
||||||
|
// name for your request.
|
||||||
|
var GetProviderName = getProviderName |
||||||
|
|
||||||
|
func getProviderName(req *http.Request) (string, error) { |
||||||
|
provider := req.URL.Query().Get("provider") |
||||||
|
if provider == "" { |
||||||
|
if p, ok := mux.Vars(req)["provider"]; ok { |
||||||
|
return p, nil |
||||||
|
} |
||||||
|
} |
||||||
|
if provider == "" { |
||||||
|
provider = req.URL.Query().Get(":provider") |
||||||
|
} |
||||||
|
if provider == "" { |
||||||
|
return provider, errors.New("you must select a provider") |
||||||
|
} |
||||||
|
return provider, nil |
||||||
|
} |
||||||
|
|
||||||
|
func storeInSession(key string, value string, req *http.Request, res http.ResponseWriter) error { |
||||||
|
session, _ := Store.Get(req, key + SessionName) |
||||||
|
|
||||||
|
session.Values[key] = value |
||||||
|
|
||||||
|
return session.Save(req, res) |
||||||
|
} |
||||||
|
|
||||||
|
func getFromSession(key string, req *http.Request) (string, error) { |
||||||
|
session, _ := Store.Get(req, key + SessionName) |
||||||
|
|
||||||
|
value := session.Values[key] |
||||||
|
if value == nil { |
||||||
|
return "", errors.New("could not find a matching session for this request") |
||||||
|
} |
||||||
|
|
||||||
|
return value.(string), nil |
||||||
|
} |
@ -0,0 +1,75 @@ |
|||||||
|
package goth |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"golang.org/x/net/context" |
||||||
|
"golang.org/x/oauth2" |
||||||
|
) |
||||||
|
|
||||||
|
// Provider needs to be implemented for each 3rd party authentication provider
|
||||||
|
// e.g. Facebook, Twitter, etc...
|
||||||
|
type Provider interface { |
||||||
|
Name() string |
||||||
|
SetName(name string) |
||||||
|
BeginAuth(state string) (Session, error) |
||||||
|
UnmarshalSession(string) (Session, error) |
||||||
|
FetchUser(Session) (User, error) |
||||||
|
Debug(bool) |
||||||
|
RefreshToken(refreshToken string) (*oauth2.Token, error) //Get new access token based on the refresh token
|
||||||
|
RefreshTokenAvailable() bool //Refresh token is provided by auth provider or not
|
||||||
|
} |
||||||
|
|
||||||
|
const NoAuthUrlErrorMessage = "an AuthURL has not been set" |
||||||
|
|
||||||
|
// Providers is list of known/available providers.
|
||||||
|
type Providers map[string]Provider |
||||||
|
|
||||||
|
var providers = Providers{} |
||||||
|
|
||||||
|
// UseProviders adds a list of available providers for use with Goth.
|
||||||
|
// Can be called multiple times. If you pass the same provider more
|
||||||
|
// than once, the last will be used.
|
||||||
|
func UseProviders(viders ...Provider) { |
||||||
|
for _, provider := range viders { |
||||||
|
providers[provider.Name()] = provider |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// GetProviders returns a list of all the providers currently in use.
|
||||||
|
func GetProviders() Providers { |
||||||
|
return providers |
||||||
|
} |
||||||
|
|
||||||
|
// GetProvider returns a previously created provider. If Goth has not
|
||||||
|
// been told to use the named provider it will return an error.
|
||||||
|
func GetProvider(name string) (Provider, error) { |
||||||
|
provider := providers[name] |
||||||
|
if provider == nil { |
||||||
|
return nil, fmt.Errorf("no provider for %s exists", name) |
||||||
|
} |
||||||
|
return provider, nil |
||||||
|
} |
||||||
|
|
||||||
|
// ClearProviders will remove all providers currently in use.
|
||||||
|
// This is useful, mostly, for testing purposes.
|
||||||
|
func ClearProviders() { |
||||||
|
providers = Providers{} |
||||||
|
} |
||||||
|
|
||||||
|
// ContextForClient provides a context for use with oauth2.
|
||||||
|
func ContextForClient(h *http.Client) context.Context { |
||||||
|
if h == nil { |
||||||
|
return oauth2.NoContext |
||||||
|
} |
||||||
|
return context.WithValue(oauth2.NoContext, oauth2.HTTPClient, h) |
||||||
|
} |
||||||
|
|
||||||
|
// HTTPClientWithFallBack to be used in all fetch operations.
|
||||||
|
func HTTPClientWithFallBack(h *http.Client) *http.Client { |
||||||
|
if h != nil { |
||||||
|
return h |
||||||
|
} |
||||||
|
return http.DefaultClient |
||||||
|
} |
@ -0,0 +1,224 @@ |
|||||||
|
// Package github implements the OAuth2 protocol for authenticating users through Github.
|
||||||
|
// This package can be used as a reference implementation of an OAuth2 provider for Goth.
|
||||||
|
package github |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/json" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"io/ioutil" |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/markbates/goth" |
||||||
|
"golang.org/x/oauth2" |
||||||
|
) |
||||||
|
|
||||||
|
// These vars define the Authentication, Token, and API URLS for GitHub. If
|
||||||
|
// using GitHub enterprise you should change these values before calling New.
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// github.AuthURL = "https://github.acme.com/login/oauth/authorize
|
||||||
|
// github.TokenURL = "https://github.acme.com/login/oauth/access_token
|
||||||
|
// github.ProfileURL = "https://github.acme.com/api/v3/user
|
||||||
|
// github.EmailURL = "https://github.acme.com/api/v3/user/emails
|
||||||
|
var ( |
||||||
|
AuthURL = "https://github.com/login/oauth/authorize" |
||||||
|
TokenURL = "https://github.com/login/oauth/access_token" |
||||||
|
ProfileURL = "https://api.github.com/user" |
||||||
|
EmailURL = "https://api.github.com/user/emails" |
||||||
|
) |
||||||
|
|
||||||
|
// New creates a new Github provider, and sets up important connection details.
|
||||||
|
// You should always call `github.New` to get a new Provider. Never try to create
|
||||||
|
// one manually.
|
||||||
|
func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { |
||||||
|
p := &Provider{ |
||||||
|
ClientKey: clientKey, |
||||||
|
Secret: secret, |
||||||
|
CallbackURL: callbackURL, |
||||||
|
providerName: "github", |
||||||
|
} |
||||||
|
p.config = newConfig(p, scopes) |
||||||
|
return p |
||||||
|
} |
||||||
|
|
||||||
|
// Provider is the implementation of `goth.Provider` for accessing Github.
|
||||||
|
type Provider struct { |
||||||
|
ClientKey string |
||||||
|
Secret string |
||||||
|
CallbackURL string |
||||||
|
HTTPClient *http.Client |
||||||
|
config *oauth2.Config |
||||||
|
providerName string |
||||||
|
} |
||||||
|
|
||||||
|
// Name is the name used to retrieve this provider later.
|
||||||
|
func (p *Provider) Name() string { |
||||||
|
return p.providerName |
||||||
|
} |
||||||
|
|
||||||
|
// SetName is to update the name of the provider (needed in case of multiple providers of 1 type)
|
||||||
|
func (p *Provider) SetName(name string) { |
||||||
|
p.providerName = name |
||||||
|
} |
||||||
|
|
||||||
|
func (p *Provider) Client() *http.Client { |
||||||
|
return goth.HTTPClientWithFallBack(p.HTTPClient) |
||||||
|
} |
||||||
|
|
||||||
|
// Debug is a no-op for the github package.
|
||||||
|
func (p *Provider) Debug(debug bool) {} |
||||||
|
|
||||||
|
// BeginAuth asks Github for an authentication end-point.
|
||||||
|
func (p *Provider) BeginAuth(state string) (goth.Session, error) { |
||||||
|
url := p.config.AuthCodeURL(state) |
||||||
|
session := &Session{ |
||||||
|
AuthURL: url, |
||||||
|
} |
||||||
|
return session, nil |
||||||
|
} |
||||||
|
|
||||||
|
// FetchUser will go to Github and access basic information about the user.
|
||||||
|
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { |
||||||
|
sess := session.(*Session) |
||||||
|
user := goth.User{ |
||||||
|
AccessToken: sess.AccessToken, |
||||||
|
Provider: p.Name(), |
||||||
|
} |
||||||
|
|
||||||
|
if user.AccessToken == "" { |
||||||
|
// data is not yet retrieved since accessToken is still empty
|
||||||
|
return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) |
||||||
|
} |
||||||
|
|
||||||
|
response, err := p.Client().Get(ProfileURL + "?access_token=" + url.QueryEscape(sess.AccessToken)) |
||||||
|
if err != nil { |
||||||
|
return user, err |
||||||
|
} |
||||||
|
defer response.Body.Close() |
||||||
|
|
||||||
|
if response.StatusCode != http.StatusOK { |
||||||
|
return user, fmt.Errorf("GitHub API responded with a %d trying to fetch user information", response.StatusCode) |
||||||
|
} |
||||||
|
|
||||||
|
bits, err := ioutil.ReadAll(response.Body) |
||||||
|
if err != nil { |
||||||
|
return user, err |
||||||
|
} |
||||||
|
|
||||||
|
err = json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData) |
||||||
|
if err != nil { |
||||||
|
return user, err |
||||||
|
} |
||||||
|
|
||||||
|
err = userFromReader(bytes.NewReader(bits), &user) |
||||||
|
if err != nil { |
||||||
|
return user, err |
||||||
|
} |
||||||
|
|
||||||
|
if user.Email == "" { |
||||||
|
for _, scope := range p.config.Scopes { |
||||||
|
if strings.TrimSpace(scope) == "user" || strings.TrimSpace(scope) == "user:email" { |
||||||
|
user.Email, err = getPrivateMail(p, sess) |
||||||
|
if err != nil { |
||||||
|
return user, err |
||||||
|
} |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return user, err |
||||||
|
} |
||||||
|
|
||||||
|
func userFromReader(reader io.Reader, user *goth.User) error { |
||||||
|
u := struct { |
||||||
|
ID int `json:"id"` |
||||||
|
Email string `json:"email"` |
||||||
|
Bio string `json:"bio"` |
||||||
|
Name string `json:"name"` |
||||||
|
Login string `json:"login"` |
||||||
|
Picture string `json:"avatar_url"` |
||||||
|
Location string `json:"location"` |
||||||
|
}{} |
||||||
|
|
||||||
|
err := json.NewDecoder(reader).Decode(&u) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
user.Name = u.Name |
||||||
|
user.NickName = u.Login |
||||||
|
user.Email = u.Email |
||||||
|
user.Description = u.Bio |
||||||
|
user.AvatarURL = u.Picture |
||||||
|
user.UserID = strconv.Itoa(u.ID) |
||||||
|
user.Location = u.Location |
||||||
|
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
func getPrivateMail(p *Provider, sess *Session) (email string, err error) { |
||||||
|
response, err := p.Client().Get(EmailURL + "?access_token=" + url.QueryEscape(sess.AccessToken)) |
||||||
|
if err != nil { |
||||||
|
if response != nil { |
||||||
|
response.Body.Close() |
||||||
|
} |
||||||
|
return email, err |
||||||
|
} |
||||||
|
defer response.Body.Close() |
||||||
|
|
||||||
|
if response.StatusCode != http.StatusOK { |
||||||
|
return email, fmt.Errorf("GitHub API responded with a %d trying to fetch user email", response.StatusCode) |
||||||
|
} |
||||||
|
|
||||||
|
var mailList = []struct { |
||||||
|
Email string `json:"email"` |
||||||
|
Primary bool `json:"primary"` |
||||||
|
Verified bool `json:"verified"` |
||||||
|
}{} |
||||||
|
err = json.NewDecoder(response.Body).Decode(&mailList) |
||||||
|
if err != nil { |
||||||
|
return email, err |
||||||
|
} |
||||||
|
for _, v := range mailList { |
||||||
|
if v.Primary && v.Verified { |
||||||
|
return v.Email, nil |
||||||
|
} |
||||||
|
} |
||||||
|
// can't get primary email - shouldn't be possible
|
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func newConfig(provider *Provider, scopes []string) *oauth2.Config { |
||||||
|
c := &oauth2.Config{ |
||||||
|
ClientID: provider.ClientKey, |
||||||
|
ClientSecret: provider.Secret, |
||||||
|
RedirectURL: provider.CallbackURL, |
||||||
|
Endpoint: oauth2.Endpoint{ |
||||||
|
AuthURL: AuthURL, |
||||||
|
TokenURL: TokenURL, |
||||||
|
}, |
||||||
|
Scopes: []string{}, |
||||||
|
} |
||||||
|
|
||||||
|
for _, scope := range scopes { |
||||||
|
c.Scopes = append(c.Scopes, scope) |
||||||
|
} |
||||||
|
|
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
//RefreshToken refresh token is not provided by github
|
||||||
|
func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { |
||||||
|
return nil, errors.New("Refresh token is not provided by github") |
||||||
|
} |
||||||
|
|
||||||
|
//RefreshTokenAvailable refresh token is not provided by github
|
||||||
|
func (p *Provider) RefreshTokenAvailable() bool { |
||||||
|
return false |
||||||
|
} |
@ -0,0 +1,56 @@ |
|||||||
|
package github |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"errors" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/markbates/goth" |
||||||
|
) |
||||||
|
|
||||||
|
// Session stores data during the auth process with Github.
|
||||||
|
type Session struct { |
||||||
|
AuthURL string |
||||||
|
AccessToken string |
||||||
|
} |
||||||
|
|
||||||
|
// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Github provider.
|
||||||
|
func (s Session) GetAuthURL() (string, error) { |
||||||
|
if s.AuthURL == "" { |
||||||
|
return "", errors.New(goth.NoAuthUrlErrorMessage) |
||||||
|
} |
||||||
|
return s.AuthURL, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Authorize the session with Github and return the access token to be stored for future use.
|
||||||
|
func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { |
||||||
|
p := provider.(*Provider) |
||||||
|
token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code")) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
if !token.Valid() { |
||||||
|
return "", errors.New("Invalid token received from provider") |
||||||
|
} |
||||||
|
|
||||||
|
s.AccessToken = token.AccessToken |
||||||
|
return token.AccessToken, err |
||||||
|
} |
||||||
|
|
||||||
|
// Marshal the session into a string
|
||||||
|
func (s Session) Marshal() string { |
||||||
|
b, _ := json.Marshal(s) |
||||||
|
return string(b) |
||||||
|
} |
||||||
|
|
||||||
|
func (s Session) String() string { |
||||||
|
return s.Marshal() |
||||||
|
} |
||||||
|
|
||||||
|
// UnmarshalSession will unmarshal a JSON string into a session.
|
||||||
|
func (p *Provider) UnmarshalSession(data string) (goth.Session, error) { |
||||||
|
sess := &Session{} |
||||||
|
err := json.NewDecoder(strings.NewReader(data)).Decode(sess) |
||||||
|
return sess, err |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
package goth |
||||||
|
|
||||||
|
// Params is used to pass data to sessions for authorization. An existing
|
||||||
|
// implementation, and the one most likely to be used, is `url.Values`.
|
||||||
|
type Params interface { |
||||||
|
Get(string) string |
||||||
|
} |
||||||
|
|
||||||
|
// Session needs to be implemented as part of the provider package.
|
||||||
|
// It will be marshaled and persisted between requests to "tie"
|
||||||
|
// the start and the end of the authorization process with a
|
||||||
|
// 3rd party provider.
|
||||||
|
type Session interface { |
||||||
|
// GetAuthURL returns the URL for the authentication end-point for the provider.
|
||||||
|
GetAuthURL() (string, error) |
||||||
|
// Marshal generates a string representation of the Session for storing between requests.
|
||||||
|
Marshal() string |
||||||
|
// Authorize should validate the data from the provider and return an access token
|
||||||
|
// that can be stored for later access to the provider.
|
||||||
|
Authorize(Provider, Params) (string, error) |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
package goth |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/gob" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
func init() { |
||||||
|
gob.Register(User{}) |
||||||
|
} |
||||||
|
|
||||||
|
// User contains the information common amongst most OAuth and OAuth2 providers.
|
||||||
|
// All of the "raw" datafrom the provider can be found in the `RawData` field.
|
||||||
|
type User struct { |
||||||
|
RawData map[string]interface{} |
||||||
|
Provider string |
||||||
|
Email string |
||||||
|
Name string |
||||||
|
FirstName string |
||||||
|
LastName string |
||||||
|
NickName string |
||||||
|
Description string |
||||||
|
UserID string |
||||||
|
AvatarURL string |
||||||
|
Location string |
||||||
|
AccessToken string |
||||||
|
AccessTokenSecret string |
||||||
|
RefreshToken string |
||||||
|
ExpiresAt time.Time |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
# This source code refers to The Go Authors for copyright purposes. |
||||||
|
# The master list of authors is in the main Go distribution, |
||||||
|
# visible at http://tip.golang.org/AUTHORS. |
@ -0,0 +1,31 @@ |
|||||||
|
# Contributing to Go |
||||||
|
|
||||||
|
Go is an open source project. |
||||||
|
|
||||||
|
It is the work of hundreds of contributors. We appreciate your help! |
||||||
|
|
||||||
|
|
||||||
|
## Filing issues |
||||||
|
|
||||||
|
When [filing an issue](https://github.com/golang/oauth2/issues), make sure to answer these five questions: |
||||||
|
|
||||||
|
1. What version of Go are you using (`go version`)? |
||||||
|
2. What operating system and processor architecture are you using? |
||||||
|
3. What did you do? |
||||||
|
4. What did you expect to see? |
||||||
|
5. What did you see instead? |
||||||
|
|
||||||
|
General questions should go to the [golang-nuts mailing list](https://groups.google.com/group/golang-nuts) instead of the issue tracker. |
||||||
|
The gophers there will answer or ask you to file an issue if you've tripped over a bug. |
||||||
|
|
||||||
|
## Contributing code |
||||||
|
|
||||||
|
Please read the [Contribution Guidelines](https://golang.org/doc/contribute.html) |
||||||
|
before sending patches. |
||||||
|
|
||||||
|
**We do not accept GitHub pull requests** |
||||||
|
(we use [Gerrit](https://code.google.com/p/gerrit/) instead for code review). |
||||||
|
|
||||||
|
Unless otherwise noted, the Go source files are distributed under |
||||||
|
the BSD-style license found in the LICENSE file. |
||||||
|
|
@ -0,0 +1,3 @@ |
|||||||
|
# This source code was written by the Go contributors. |
||||||
|
# The master list of contributors is in the main Go distribution, |
||||||
|
# visible at http://tip.golang.org/CONTRIBUTORS. |
@ -0,0 +1,27 @@ |
|||||||
|
Copyright (c) 2009 The oauth2 Authors. All rights reserved. |
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without |
||||||
|
modification, are permitted provided that the following conditions are |
||||||
|
met: |
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright |
||||||
|
notice, this list of conditions and the following disclaimer. |
||||||
|
* Redistributions in binary form must reproduce the above |
||||||
|
copyright notice, this list of conditions and the following disclaimer |
||||||
|
in the documentation and/or other materials provided with the |
||||||
|
distribution. |
||||||
|
* Neither the name of Google Inc. nor the names of its |
||||||
|
contributors may be used to endorse or promote products derived from |
||||||
|
this software without specific prior written permission. |
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@ -0,0 +1,65 @@ |
|||||||
|
# OAuth2 for Go |
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/golang/oauth2.svg?branch=master)](https://travis-ci.org/golang/oauth2) |
||||||
|
[![GoDoc](https://godoc.org/golang.org/x/oauth2?status.svg)](https://godoc.org/golang.org/x/oauth2) |
||||||
|
|
||||||
|
oauth2 package contains a client implementation for OAuth 2.0 spec. |
||||||
|
|
||||||
|
## Installation |
||||||
|
|
||||||
|
~~~~ |
||||||
|
go get golang.org/x/oauth2 |
||||||
|
~~~~ |
||||||
|
|
||||||
|
See godoc for further documentation and examples. |
||||||
|
|
||||||
|
* [godoc.org/golang.org/x/oauth2](http://godoc.org/golang.org/x/oauth2) |
||||||
|
* [godoc.org/golang.org/x/oauth2/google](http://godoc.org/golang.org/x/oauth2/google) |
||||||
|
|
||||||
|
|
||||||
|
## App Engine |
||||||
|
|
||||||
|
In change 96e89be (March 2015) we removed the `oauth2.Context2` type in favor |
||||||
|
of the [`context.Context`](https://golang.org/x/net/context#Context) type from |
||||||
|
the `golang.org/x/net/context` package |
||||||
|
|
||||||
|
This means its no longer possible to use the "Classic App Engine" |
||||||
|
`appengine.Context` type with the `oauth2` package. (You're using |
||||||
|
Classic App Engine if you import the package `"appengine"`.) |
||||||
|
|
||||||
|
To work around this, you may use the new `"google.golang.org/appengine"` |
||||||
|
package. This package has almost the same API as the `"appengine"` package, |
||||||
|
but it can be fetched with `go get` and used on "Managed VMs" and well as |
||||||
|
Classic App Engine. |
||||||
|
|
||||||
|
See the [new `appengine` package's readme](https://github.com/golang/appengine#updating-a-go-app-engine-app) |
||||||
|
for information on updating your app. |
||||||
|
|
||||||
|
If you don't want to update your entire app to use the new App Engine packages, |
||||||
|
you may use both sets of packages in parallel, using only the new packages |
||||||
|
with the `oauth2` package. |
||||||
|
|
||||||
|
import ( |
||||||
|
"golang.org/x/net/context" |
||||||
|
"golang.org/x/oauth2" |
||||||
|
"golang.org/x/oauth2/google" |
||||||
|
newappengine "google.golang.org/appengine" |
||||||
|
newurlfetch "google.golang.org/appengine/urlfetch" |
||||||
|
|
||||||
|
"appengine" |
||||||
|
) |
||||||
|
|
||||||
|
func handler(w http.ResponseWriter, r *http.Request) { |
||||||
|
var c appengine.Context = appengine.NewContext(r) |
||||||
|
c.Infof("Logging a message with the old package") |
||||||
|
|
||||||
|
var ctx context.Context = newappengine.NewContext(r) |
||||||
|
client := &http.Client{ |
||||||
|
Transport: &oauth2.Transport{ |
||||||
|
Source: google.AppEngineTokenSource(ctx, "scope"), |
||||||
|
Base: &newurlfetch.Transport{Context: ctx}, |
||||||
|
}, |
||||||
|
} |
||||||
|
client.Get("...") |
||||||
|
} |
||||||
|
|
@ -0,0 +1,25 @@ |
|||||||
|
// Copyright 2014 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build appengine
|
||||||
|
|
||||||
|
// App Engine hooks.
|
||||||
|
|
||||||
|
package oauth2 |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"golang.org/x/net/context" |
||||||
|
"golang.org/x/oauth2/internal" |
||||||
|
"google.golang.org/appengine/urlfetch" |
||||||
|
) |
||||||
|
|
||||||
|
func init() { |
||||||
|
internal.RegisterContextClientFunc(contextClientAppEngine) |
||||||
|
} |
||||||
|
|
||||||
|
func contextClientAppEngine(ctx context.Context) (*http.Client, error) { |
||||||
|
return urlfetch.Client(ctx), nil |
||||||
|
} |
@ -0,0 +1,76 @@ |
|||||||
|
// Copyright 2014 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package internal contains support packages for oauth2 package.
|
||||||
|
package internal |
||||||
|
|
||||||
|
import ( |
||||||
|
"bufio" |
||||||
|
"crypto/rsa" |
||||||
|
"crypto/x509" |
||||||
|
"encoding/pem" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
// ParseKey converts the binary contents of a private key file
|
||||||
|
// to an *rsa.PrivateKey. It detects whether the private key is in a
|
||||||
|
// PEM container or not. If so, it extracts the the private key
|
||||||
|
// from PEM container before conversion. It only supports PEM
|
||||||
|
// containers with no passphrase.
|
||||||
|
func ParseKey(key []byte) (*rsa.PrivateKey, error) { |
||||||
|
block, _ := pem.Decode(key) |
||||||
|
if block != nil { |
||||||
|
key = block.Bytes |
||||||
|
} |
||||||
|
parsedKey, err := x509.ParsePKCS8PrivateKey(key) |
||||||
|
if err != nil { |
||||||
|
parsedKey, err = x509.ParsePKCS1PrivateKey(key) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("private key should be a PEM or plain PKSC1 or PKCS8; parse error: %v", err) |
||||||
|
} |
||||||
|
} |
||||||
|
parsed, ok := parsedKey.(*rsa.PrivateKey) |
||||||
|
if !ok { |
||||||
|
return nil, errors.New("private key is invalid") |
||||||
|
} |
||||||
|
return parsed, nil |
||||||
|
} |
||||||
|
|
||||||
|
func ParseINI(ini io.Reader) (map[string]map[string]string, error) { |
||||||
|
result := map[string]map[string]string{ |
||||||
|
"": map[string]string{}, // root section
|
||||||
|
} |
||||||
|
scanner := bufio.NewScanner(ini) |
||||||
|
currentSection := "" |
||||||
|
for scanner.Scan() { |
||||||
|
line := strings.TrimSpace(scanner.Text()) |
||||||
|
if strings.HasPrefix(line, ";") { |
||||||
|
// comment.
|
||||||
|
continue |
||||||
|
} |
||||||
|
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { |
||||||
|
currentSection = strings.TrimSpace(line[1 : len(line)-1]) |
||||||
|
result[currentSection] = map[string]string{} |
||||||
|
continue |
||||||
|
} |
||||||
|
parts := strings.SplitN(line, "=", 2) |
||||||
|
if len(parts) == 2 && parts[0] != "" { |
||||||
|
result[currentSection][strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) |
||||||
|
} |
||||||
|
} |
||||||
|
if err := scanner.Err(); err != nil { |
||||||
|
return nil, fmt.Errorf("error scanning ini: %v", err) |
||||||
|
} |
||||||
|
return result, nil |
||||||
|
} |
||||||
|
|
||||||
|
func CondVal(v string) []string { |
||||||
|
if v == "" { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return []string{v} |
||||||
|
} |
@ -0,0 +1,227 @@ |
|||||||
|
// Copyright 2014 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package internal contains support packages for oauth2 package.
|
||||||
|
package internal |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"io/ioutil" |
||||||
|
"mime" |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
"golang.org/x/net/context" |
||||||
|
) |
||||||
|
|
||||||
|
// Token represents the crendentials used to authorize
|
||||||
|
// the requests to access protected resources on the OAuth 2.0
|
||||||
|
// provider's backend.
|
||||||
|
//
|
||||||
|
// This type is a mirror of oauth2.Token and exists to break
|
||||||
|
// an otherwise-circular dependency. Other internal packages
|
||||||
|
// should convert this Token into an oauth2.Token before use.
|
||||||
|
type Token struct { |
||||||
|
// AccessToken is the token that authorizes and authenticates
|
||||||
|
// the requests.
|
||||||
|
AccessToken string |
||||||
|
|
||||||
|
// TokenType is the type of token.
|
||||||
|
// The Type method returns either this or "Bearer", the default.
|
||||||
|
TokenType string |
||||||
|
|
||||||
|
// RefreshToken is a token that's used by the application
|
||||||
|
// (as opposed to the user) to refresh the access token
|
||||||
|
// if it expires.
|
||||||
|
RefreshToken string |
||||||
|
|
||||||
|
// Expiry is the optional expiration time of the access token.
|
||||||
|
//
|
||||||
|
// If zero, TokenSource implementations will reuse the same
|
||||||
|
// token forever and RefreshToken or equivalent
|
||||||
|
// mechanisms for that TokenSource will not be used.
|
||||||
|
Expiry time.Time |
||||||
|
|
||||||
|
// Raw optionally contains extra metadata from the server
|
||||||
|
// when updating a token.
|
||||||
|
Raw interface{} |
||||||
|
} |
||||||
|
|
||||||
|
// tokenJSON is the struct representing the HTTP response from OAuth2
|
||||||
|
// providers returning a token in JSON form.
|
||||||
|
type tokenJSON struct { |
||||||
|
AccessToken string `json:"access_token"` |
||||||
|
TokenType string `json:"token_type"` |
||||||
|
RefreshToken string `json:"refresh_token"` |
||||||
|
ExpiresIn expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number
|
||||||
|
Expires expirationTime `json:"expires"` // broken Facebook spelling of expires_in
|
||||||
|
} |
||||||
|
|
||||||
|
func (e *tokenJSON) expiry() (t time.Time) { |
||||||
|
if v := e.ExpiresIn; v != 0 { |
||||||
|
return time.Now().Add(time.Duration(v) * time.Second) |
||||||
|
} |
||||||
|
if v := e.Expires; v != 0 { |
||||||
|
return time.Now().Add(time.Duration(v) * time.Second) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
type expirationTime int32 |
||||||
|
|
||||||
|
func (e *expirationTime) UnmarshalJSON(b []byte) error { |
||||||
|
var n json.Number |
||||||
|
err := json.Unmarshal(b, &n) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
i, err := n.Int64() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
*e = expirationTime(i) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
var brokenAuthHeaderProviders = []string{ |
||||||
|
"https://accounts.google.com/", |
||||||
|
"https://api.dropbox.com/", |
||||||
|
"https://api.dropboxapi.com/", |
||||||
|
"https://api.instagram.com/", |
||||||
|
"https://api.netatmo.net/", |
||||||
|
"https://api.odnoklassniki.ru/", |
||||||
|
"https://api.pushbullet.com/", |
||||||
|
"https://api.soundcloud.com/", |
||||||
|
"https://api.twitch.tv/", |
||||||
|
"https://app.box.com/", |
||||||
|
"https://connect.stripe.com/", |
||||||
|
"https://login.microsoftonline.com/", |
||||||
|
"https://login.salesforce.com/", |
||||||
|
"https://oauth.sandbox.trainingpeaks.com/", |
||||||
|
"https://oauth.trainingpeaks.com/", |
||||||
|
"https://oauth.vk.com/", |
||||||
|
"https://openapi.baidu.com/", |
||||||
|
"https://slack.com/", |
||||||
|
"https://test-sandbox.auth.corp.google.com", |
||||||
|
"https://test.salesforce.com/", |
||||||
|
"https://user.gini.net/", |
||||||
|
"https://www.douban.com/", |
||||||
|
"https://www.googleapis.com/", |
||||||
|
"https://www.linkedin.com/", |
||||||
|
"https://www.strava.com/oauth/", |
||||||
|
"https://www.wunderlist.com/oauth/", |
||||||
|
"https://api.patreon.com/", |
||||||
|
"https://sandbox.codeswholesale.com/oauth/token", |
||||||
|
"https://api.codeswholesale.com/oauth/token", |
||||||
|
} |
||||||
|
|
||||||
|
func RegisterBrokenAuthHeaderProvider(tokenURL string) { |
||||||
|
brokenAuthHeaderProviders = append(brokenAuthHeaderProviders, tokenURL) |
||||||
|
} |
||||||
|
|
||||||
|
// providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL
|
||||||
|
// implements the OAuth2 spec correctly
|
||||||
|
// See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
|
||||||
|
// In summary:
|
||||||
|
// - Reddit only accepts client secret in the Authorization header
|
||||||
|
// - Dropbox accepts either it in URL param or Auth header, but not both.
|
||||||
|
// - Google only accepts URL param (not spec compliant?), not Auth header
|
||||||
|
// - Stripe only accepts client secret in Auth header with Bearer method, not Basic
|
||||||
|
func providerAuthHeaderWorks(tokenURL string) bool { |
||||||
|
for _, s := range brokenAuthHeaderProviders { |
||||||
|
if strings.HasPrefix(tokenURL, s) { |
||||||
|
// Some sites fail to implement the OAuth2 spec fully.
|
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Assume the provider implements the spec properly
|
||||||
|
// otherwise. We can add more exceptions as they're
|
||||||
|
// discovered. We will _not_ be adding configurable hooks
|
||||||
|
// to this package to let users select server bugs.
|
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values) (*Token, error) { |
||||||
|
hc, err := ContextClient(ctx) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
v.Set("client_id", clientID) |
||||||
|
bustedAuth := !providerAuthHeaderWorks(tokenURL) |
||||||
|
if bustedAuth && clientSecret != "" { |
||||||
|
v.Set("client_secret", clientSecret) |
||||||
|
} |
||||||
|
req, err := http.NewRequest("POST", tokenURL, strings.NewReader(v.Encode())) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") |
||||||
|
if !bustedAuth { |
||||||
|
req.SetBasicAuth(clientID, clientSecret) |
||||||
|
} |
||||||
|
r, err := hc.Do(req) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer r.Body.Close() |
||||||
|
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20)) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) |
||||||
|
} |
||||||
|
if code := r.StatusCode; code < 200 || code > 299 { |
||||||
|
return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", r.Status, body) |
||||||
|
} |
||||||
|
|
||||||
|
var token *Token |
||||||
|
content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) |
||||||
|
switch content { |
||||||
|
case "application/x-www-form-urlencoded", "text/plain": |
||||||
|
vals, err := url.ParseQuery(string(body)) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
token = &Token{ |
||||||
|
AccessToken: vals.Get("access_token"), |
||||||
|
TokenType: vals.Get("token_type"), |
||||||
|
RefreshToken: vals.Get("refresh_token"), |
||||||
|
Raw: vals, |
||||||
|
} |
||||||
|
e := vals.Get("expires_in") |
||||||
|
if e == "" { |
||||||
|
// TODO(jbd): Facebook's OAuth2 implementation is broken and
|
||||||
|
// returns expires_in field in expires. Remove the fallback to expires,
|
||||||
|
// when Facebook fixes their implementation.
|
||||||
|
e = vals.Get("expires") |
||||||
|
} |
||||||
|
expires, _ := strconv.Atoi(e) |
||||||
|
if expires != 0 { |
||||||
|
token.Expiry = time.Now().Add(time.Duration(expires) * time.Second) |
||||||
|
} |
||||||
|
default: |
||||||
|
var tj tokenJSON |
||||||
|
if err = json.Unmarshal(body, &tj); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
token = &Token{ |
||||||
|
AccessToken: tj.AccessToken, |
||||||
|
TokenType: tj.TokenType, |
||||||
|
RefreshToken: tj.RefreshToken, |
||||||
|
Expiry: tj.expiry(), |
||||||
|
Raw: make(map[string]interface{}), |
||||||
|
} |
||||||
|
json.Unmarshal(body, &token.Raw) // no error checks for optional fields
|
||||||
|
} |
||||||
|
// Don't overwrite `RefreshToken` with an empty value
|
||||||
|
// if this was a token refreshing request.
|
||||||
|
if token.RefreshToken == "" { |
||||||
|
token.RefreshToken = v.Get("refresh_token") |
||||||
|
} |
||||||
|
return token, nil |
||||||
|
} |
@ -0,0 +1,69 @@ |
|||||||
|
// Copyright 2014 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package internal contains support packages for oauth2 package.
|
||||||
|
package internal |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"golang.org/x/net/context" |
||||||
|
) |
||||||
|
|
||||||
|
// HTTPClient is the context key to use with golang.org/x/net/context's
|
||||||
|
// WithValue function to associate an *http.Client value with a context.
|
||||||
|
var HTTPClient ContextKey |
||||||
|
|
||||||
|
// ContextKey is just an empty struct. It exists so HTTPClient can be
|
||||||
|
// an immutable public variable with a unique type. It's immutable
|
||||||
|
// because nobody else can create a ContextKey, being unexported.
|
||||||
|
type ContextKey struct{} |
||||||
|
|
||||||
|
// ContextClientFunc is a func which tries to return an *http.Client
|
||||||
|
// given a Context value. If it returns an error, the search stops
|
||||||
|
// with that error. If it returns (nil, nil), the search continues
|
||||||
|
// down the list of registered funcs.
|
||||||
|
type ContextClientFunc func(context.Context) (*http.Client, error) |
||||||
|
|
||||||
|
var contextClientFuncs []ContextClientFunc |
||||||
|
|
||||||
|
func RegisterContextClientFunc(fn ContextClientFunc) { |
||||||
|
contextClientFuncs = append(contextClientFuncs, fn) |
||||||
|
} |
||||||
|
|
||||||
|
func ContextClient(ctx context.Context) (*http.Client, error) { |
||||||
|
if ctx != nil { |
||||||
|
if hc, ok := ctx.Value(HTTPClient).(*http.Client); ok { |
||||||
|
return hc, nil |
||||||
|
} |
||||||
|
} |
||||||
|
for _, fn := range contextClientFuncs { |
||||||
|
c, err := fn(ctx) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if c != nil { |
||||||
|
return c, nil |
||||||
|
} |
||||||
|
} |
||||||
|
return http.DefaultClient, nil |
||||||
|
} |
||||||
|
|
||||||
|
func ContextTransport(ctx context.Context) http.RoundTripper { |
||||||
|
hc, err := ContextClient(ctx) |
||||||
|
// This is a rare error case (somebody using nil on App Engine).
|
||||||
|
if err != nil { |
||||||
|
return ErrorTransport{err} |
||||||
|
} |
||||||
|
return hc.Transport |
||||||
|
} |
||||||
|
|
||||||
|
// ErrorTransport returns the specified error on RoundTrip.
|
||||||
|
// This RoundTripper should be used in rare error cases where
|
||||||
|
// error handling can be postponed to response handling time.
|
||||||
|
type ErrorTransport struct{ Err error } |
||||||
|
|
||||||
|
func (t ErrorTransport) RoundTrip(*http.Request) (*http.Response, error) { |
||||||
|
return nil, t.Err |
||||||
|
} |
@ -0,0 +1,341 @@ |
|||||||
|
// Copyright 2014 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package oauth2 provides support for making
|
||||||
|
// OAuth2 authorized and authenticated HTTP requests.
|
||||||
|
// It can additionally grant authorization with Bearer JWT.
|
||||||
|
package oauth2 // import "golang.org/x/oauth2"
|
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"errors" |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"golang.org/x/net/context" |
||||||
|
"golang.org/x/oauth2/internal" |
||||||
|
) |
||||||
|
|
||||||
|
// NoContext is the default context you should supply if not using
|
||||||
|
// your own context.Context (see https://golang.org/x/net/context).
|
||||||
|
//
|
||||||
|
// Deprecated: Use context.Background() or context.TODO() instead.
|
||||||
|
var NoContext = context.TODO() |
||||||
|
|
||||||
|
// RegisterBrokenAuthHeaderProvider registers an OAuth2 server
|
||||||
|
// identified by the tokenURL prefix as an OAuth2 implementation
|
||||||
|
// which doesn't support the HTTP Basic authentication
|
||||||
|
// scheme to authenticate with the authorization server.
|
||||||
|
// Once a server is registered, credentials (client_id and client_secret)
|
||||||
|
// will be passed as query parameters rather than being present
|
||||||
|
// in the Authorization header.
|
||||||
|
// See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
|
||||||
|
func RegisterBrokenAuthHeaderProvider(tokenURL string) { |
||||||
|
internal.RegisterBrokenAuthHeaderProvider(tokenURL) |
||||||
|
} |
||||||
|
|
||||||
|
// Config describes a typical 3-legged OAuth2 flow, with both the
|
||||||
|
// client application information and the server's endpoint URLs.
|
||||||
|
// For the client credentials 2-legged OAuth2 flow, see the clientcredentials
|
||||||
|
// package (https://golang.org/x/oauth2/clientcredentials).
|
||||||
|
type Config struct { |
||||||
|
// ClientID is the application's ID.
|
||||||
|
ClientID string |
||||||
|
|
||||||
|
// ClientSecret is the application's secret.
|
||||||
|
ClientSecret string |
||||||
|
|
||||||
|
// Endpoint contains the resource server's token endpoint
|
||||||
|
// URLs. These are constants specific to each server and are
|
||||||
|
// often available via site-specific packages, such as
|
||||||
|
// google.Endpoint or github.Endpoint.
|
||||||
|
Endpoint Endpoint |
||||||
|
|
||||||
|
// RedirectURL is the URL to redirect users going through
|
||||||
|
// the OAuth flow, after the resource owner's URLs.
|
||||||
|
RedirectURL string |
||||||
|
|
||||||
|
// Scope specifies optional requested permissions.
|
||||||
|
Scopes []string |
||||||
|
} |
||||||
|
|
||||||
|
// A TokenSource is anything that can return a token.
|
||||||
|
type TokenSource interface { |
||||||
|
// Token returns a token or an error.
|
||||||
|
// Token must be safe for concurrent use by multiple goroutines.
|
||||||
|
// The returned Token must not be modified.
|
||||||
|
Token() (*Token, error) |
||||||
|
} |
||||||
|
|
||||||
|
// Endpoint contains the OAuth 2.0 provider's authorization and token
|
||||||
|
// endpoint URLs.
|
||||||
|
type Endpoint struct { |
||||||
|
AuthURL string |
||||||
|
TokenURL string |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
// AccessTypeOnline and AccessTypeOffline are options passed
|
||||||
|
// to the Options.AuthCodeURL method. They modify the
|
||||||
|
// "access_type" field that gets sent in the URL returned by
|
||||||
|
// AuthCodeURL.
|
||||||
|
//
|
||||||
|
// Online is the default if neither is specified. If your
|
||||||
|
// application needs to refresh access tokens when the user
|
||||||
|
// is not present at the browser, then use offline. This will
|
||||||
|
// result in your application obtaining a refresh token the
|
||||||
|
// first time your application exchanges an authorization
|
||||||
|
// code for a user.
|
||||||
|
AccessTypeOnline AuthCodeOption = SetAuthURLParam("access_type", "online") |
||||||
|
AccessTypeOffline AuthCodeOption = SetAuthURLParam("access_type", "offline") |
||||||
|
|
||||||
|
// ApprovalForce forces the users to view the consent dialog
|
||||||
|
// and confirm the permissions request at the URL returned
|
||||||
|
// from AuthCodeURL, even if they've already done so.
|
||||||
|
ApprovalForce AuthCodeOption = SetAuthURLParam("approval_prompt", "force") |
||||||
|
) |
||||||
|
|
||||||
|
// An AuthCodeOption is passed to Config.AuthCodeURL.
|
||||||
|
type AuthCodeOption interface { |
||||||
|
setValue(url.Values) |
||||||
|
} |
||||||
|
|
||||||
|
type setParam struct{ k, v string } |
||||||
|
|
||||||
|
func (p setParam) setValue(m url.Values) { m.Set(p.k, p.v) } |
||||||
|
|
||||||
|
// SetAuthURLParam builds an AuthCodeOption which passes key/value parameters
|
||||||
|
// to a provider's authorization endpoint.
|
||||||
|
func SetAuthURLParam(key, value string) AuthCodeOption { |
||||||
|
return setParam{key, value} |
||||||
|
} |
||||||
|
|
||||||
|
// AuthCodeURL returns a URL to OAuth 2.0 provider's consent page
|
||||||
|
// that asks for permissions for the required scopes explicitly.
|
||||||
|
//
|
||||||
|
// State is a token to protect the user from CSRF attacks. You must
|
||||||
|
// always provide a non-zero string and validate that it matches the
|
||||||
|
// the state query parameter on your redirect callback.
|
||||||
|
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
|
||||||
|
//
|
||||||
|
// Opts may include AccessTypeOnline or AccessTypeOffline, as well
|
||||||
|
// as ApprovalForce.
|
||||||
|
func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string { |
||||||
|
var buf bytes.Buffer |
||||||
|
buf.WriteString(c.Endpoint.AuthURL) |
||||||
|
v := url.Values{ |
||||||
|
"response_type": {"code"}, |
||||||
|
"client_id": {c.ClientID}, |
||||||
|
"redirect_uri": internal.CondVal(c.RedirectURL), |
||||||
|
"scope": internal.CondVal(strings.Join(c.Scopes, " ")), |
||||||
|
"state": internal.CondVal(state), |
||||||
|
} |
||||||
|
for _, opt := range opts { |
||||||
|
opt.setValue(v) |
||||||
|
} |
||||||
|
if strings.Contains(c.Endpoint.AuthURL, "?") { |
||||||
|
buf.WriteByte('&') |
||||||
|
} else { |
||||||
|
buf.WriteByte('?') |
||||||
|
} |
||||||
|
buf.WriteString(v.Encode()) |
||||||
|
return buf.String() |
||||||
|
} |
||||||
|
|
||||||
|
// PasswordCredentialsToken converts a resource owner username and password
|
||||||
|
// pair into a token.
|
||||||
|
//
|
||||||
|
// Per the RFC, this grant type should only be used "when there is a high
|
||||||
|
// degree of trust between the resource owner and the client (e.g., the client
|
||||||
|
// is part of the device operating system or a highly privileged application),
|
||||||
|
// and when other authorization grant types are not available."
|
||||||
|
// See https://tools.ietf.org/html/rfc6749#section-4.3 for more info.
|
||||||
|
//
|
||||||
|
// The HTTP client to use is derived from the context.
|
||||||
|
// If nil, http.DefaultClient is used.
|
||||||
|
func (c *Config) PasswordCredentialsToken(ctx context.Context, username, password string) (*Token, error) { |
||||||
|
return retrieveToken(ctx, c, url.Values{ |
||||||
|
"grant_type": {"password"}, |
||||||
|
"username": {username}, |
||||||
|
"password": {password}, |
||||||
|
"scope": internal.CondVal(strings.Join(c.Scopes, " ")), |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// Exchange converts an authorization code into a token.
|
||||||
|
//
|
||||||
|
// It is used after a resource provider redirects the user back
|
||||||
|
// to the Redirect URI (the URL obtained from AuthCodeURL).
|
||||||
|
//
|
||||||
|
// The HTTP client to use is derived from the context.
|
||||||
|
// If a client is not provided via the context, http.DefaultClient is used.
|
||||||
|
//
|
||||||
|
// The code will be in the *http.Request.FormValue("code"). Before
|
||||||
|
// calling Exchange, be sure to validate FormValue("state").
|
||||||
|
func (c *Config) Exchange(ctx context.Context, code string) (*Token, error) { |
||||||
|
return retrieveToken(ctx, c, url.Values{ |
||||||
|
"grant_type": {"authorization_code"}, |
||||||
|
"code": {code}, |
||||||
|
"redirect_uri": internal.CondVal(c.RedirectURL), |
||||||
|
"scope": internal.CondVal(strings.Join(c.Scopes, " ")), |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// Client returns an HTTP client using the provided token.
|
||||||
|
// The token will auto-refresh as necessary. The underlying
|
||||||
|
// HTTP transport will be obtained using the provided context.
|
||||||
|
// The returned client and its Transport should not be modified.
|
||||||
|
func (c *Config) Client(ctx context.Context, t *Token) *http.Client { |
||||||
|
return NewClient(ctx, c.TokenSource(ctx, t)) |
||||||
|
} |
||||||
|
|
||||||
|
// TokenSource returns a TokenSource that returns t until t expires,
|
||||||
|
// automatically refreshing it as necessary using the provided context.
|
||||||
|
//
|
||||||
|
// Most users will use Config.Client instead.
|
||||||
|
func (c *Config) TokenSource(ctx context.Context, t *Token) TokenSource { |
||||||
|
tkr := &tokenRefresher{ |
||||||
|
ctx: ctx, |
||||||
|
conf: c, |
||||||
|
} |
||||||
|
if t != nil { |
||||||
|
tkr.refreshToken = t.RefreshToken |
||||||
|
} |
||||||
|
return &reuseTokenSource{ |
||||||
|
t: t, |
||||||
|
new: tkr, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// tokenRefresher is a TokenSource that makes "grant_type"=="refresh_token"
|
||||||
|
// HTTP requests to renew a token using a RefreshToken.
|
||||||
|
type tokenRefresher struct { |
||||||
|
ctx context.Context // used to get HTTP requests
|
||||||
|
conf *Config |
||||||
|
refreshToken string |
||||||
|
} |
||||||
|
|
||||||
|
// WARNING: Token is not safe for concurrent access, as it
|
||||||
|
// updates the tokenRefresher's refreshToken field.
|
||||||
|
// Within this package, it is used by reuseTokenSource which
|
||||||
|
// synchronizes calls to this method with its own mutex.
|
||||||
|
func (tf *tokenRefresher) Token() (*Token, error) { |
||||||
|
if tf.refreshToken == "" { |
||||||
|
return nil, errors.New("oauth2: token expired and refresh token is not set") |
||||||
|
} |
||||||
|
|
||||||
|
tk, err := retrieveToken(tf.ctx, tf.conf, url.Values{ |
||||||
|
"grant_type": {"refresh_token"}, |
||||||
|
"refresh_token": {tf.refreshToken}, |
||||||
|
}) |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if tf.refreshToken != tk.RefreshToken { |
||||||
|
tf.refreshToken = tk.RefreshToken |
||||||
|
} |
||||||
|
return tk, err |
||||||
|
} |
||||||
|
|
||||||
|
// reuseTokenSource is a TokenSource that holds a single token in memory
|
||||||
|
// and validates its expiry before each call to retrieve it with
|
||||||
|
// Token. If it's expired, it will be auto-refreshed using the
|
||||||
|
// new TokenSource.
|
||||||
|
type reuseTokenSource struct { |
||||||
|
new TokenSource // called when t is expired.
|
||||||
|
|
||||||
|
mu sync.Mutex // guards t
|
||||||
|
t *Token |
||||||
|
} |
||||||
|
|
||||||
|
// Token returns the current token if it's still valid, else will
|
||||||
|
// refresh the current token (using r.Context for HTTP client
|
||||||
|
// information) and return the new one.
|
||||||
|
func (s *reuseTokenSource) Token() (*Token, error) { |
||||||
|
s.mu.Lock() |
||||||
|
defer s.mu.Unlock() |
||||||
|
if s.t.Valid() { |
||||||
|
return s.t, nil |
||||||
|
} |
||||||
|
t, err := s.new.Token() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
s.t = t |
||||||
|
return t, nil |
||||||
|
} |
||||||
|
|
||||||
|
// StaticTokenSource returns a TokenSource that always returns the same token.
|
||||||
|
// Because the provided token t is never refreshed, StaticTokenSource is only
|
||||||
|
// useful for tokens that never expire.
|
||||||
|
func StaticTokenSource(t *Token) TokenSource { |
||||||
|
return staticTokenSource{t} |
||||||
|
} |
||||||
|
|
||||||
|
// staticTokenSource is a TokenSource that always returns the same Token.
|
||||||
|
type staticTokenSource struct { |
||||||
|
t *Token |
||||||
|
} |
||||||
|
|
||||||
|
func (s staticTokenSource) Token() (*Token, error) { |
||||||
|
return s.t, nil |
||||||
|
} |
||||||
|
|
||||||
|
// HTTPClient is the context key to use with golang.org/x/net/context's
|
||||||
|
// WithValue function to associate an *http.Client value with a context.
|
||||||
|
var HTTPClient internal.ContextKey |
||||||
|
|
||||||
|
// NewClient creates an *http.Client from a Context and TokenSource.
|
||||||
|
// The returned client is not valid beyond the lifetime of the context.
|
||||||
|
//
|
||||||
|
// As a special case, if src is nil, a non-OAuth2 client is returned
|
||||||
|
// using the provided context. This exists to support related OAuth2
|
||||||
|
// packages.
|
||||||
|
func NewClient(ctx context.Context, src TokenSource) *http.Client { |
||||||
|
if src == nil { |
||||||
|
c, err := internal.ContextClient(ctx) |
||||||
|
if err != nil { |
||||||
|
return &http.Client{Transport: internal.ErrorTransport{Err: err}} |
||||||
|
} |
||||||
|
return c |
||||||
|
} |
||||||
|
return &http.Client{ |
||||||
|
Transport: &Transport{ |
||||||
|
Base: internal.ContextTransport(ctx), |
||||||
|
Source: ReuseTokenSource(nil, src), |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// ReuseTokenSource returns a TokenSource which repeatedly returns the
|
||||||
|
// same token as long as it's valid, starting with t.
|
||||||
|
// When its cached token is invalid, a new token is obtained from src.
|
||||||
|
//
|
||||||
|
// ReuseTokenSource is typically used to reuse tokens from a cache
|
||||||
|
// (such as a file on disk) between runs of a program, rather than
|
||||||
|
// obtaining new tokens unnecessarily.
|
||||||
|
//
|
||||||
|
// The initial token t may be nil, in which case the TokenSource is
|
||||||
|
// wrapped in a caching version if it isn't one already. This also
|
||||||
|
// means it's always safe to wrap ReuseTokenSource around any other
|
||||||
|
// TokenSource without adverse effects.
|
||||||
|
func ReuseTokenSource(t *Token, src TokenSource) TokenSource { |
||||||
|
// Don't wrap a reuseTokenSource in itself. That would work,
|
||||||
|
// but cause an unnecessary number of mutex operations.
|
||||||
|
// Just build the equivalent one.
|
||||||
|
if rt, ok := src.(*reuseTokenSource); ok { |
||||||
|
if t == nil { |
||||||
|
// Just use it directly.
|
||||||
|
return rt |
||||||
|
} |
||||||
|
src = rt.new |
||||||
|
} |
||||||
|
return &reuseTokenSource{ |
||||||
|
t: t, |
||||||
|
new: src, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,158 @@ |
|||||||
|
// Copyright 2014 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package oauth2 |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
"golang.org/x/net/context" |
||||||
|
"golang.org/x/oauth2/internal" |
||||||
|
) |
||||||
|
|
||||||
|
// expiryDelta determines how earlier a token should be considered
|
||||||
|
// expired than its actual expiration time. It is used to avoid late
|
||||||
|
// expirations due to client-server time mismatches.
|
||||||
|
const expiryDelta = 10 * time.Second |
||||||
|
|
||||||
|
// Token represents the crendentials used to authorize
|
||||||
|
// the requests to access protected resources on the OAuth 2.0
|
||||||
|
// provider's backend.
|
||||||
|
//
|
||||||
|
// Most users of this package should not access fields of Token
|
||||||
|
// directly. They're exported mostly for use by related packages
|
||||||
|
// implementing derivative OAuth2 flows.
|
||||||
|
type Token struct { |
||||||
|
// AccessToken is the token that authorizes and authenticates
|
||||||
|
// the requests.
|
||||||
|
AccessToken string `json:"access_token"` |
||||||
|
|
||||||
|
// TokenType is the type of token.
|
||||||
|
// The Type method returns either this or "Bearer", the default.
|
||||||
|
TokenType string `json:"token_type,omitempty"` |
||||||
|
|
||||||
|
// RefreshToken is a token that's used by the application
|
||||||
|
// (as opposed to the user) to refresh the access token
|
||||||
|
// if it expires.
|
||||||
|
RefreshToken string `json:"refresh_token,omitempty"` |
||||||
|
|
||||||
|
// Expiry is the optional expiration time of the access token.
|
||||||
|
//
|
||||||
|
// If zero, TokenSource implementations will reuse the same
|
||||||
|
// token forever and RefreshToken or equivalent
|
||||||
|
// mechanisms for that TokenSource will not be used.
|
||||||
|
Expiry time.Time `json:"expiry,omitempty"` |
||||||
|
|
||||||
|
// raw optionally contains extra metadata from the server
|
||||||
|
// when updating a token.
|
||||||
|
raw interface{} |
||||||
|
} |
||||||
|
|
||||||
|
// Type returns t.TokenType if non-empty, else "Bearer".
|
||||||
|
func (t *Token) Type() string { |
||||||
|
if strings.EqualFold(t.TokenType, "bearer") { |
||||||
|
return "Bearer" |
||||||
|
} |
||||||
|
if strings.EqualFold(t.TokenType, "mac") { |
||||||
|
return "MAC" |
||||||
|
} |
||||||
|
if strings.EqualFold(t.TokenType, "basic") { |
||||||
|
return "Basic" |
||||||
|
} |
||||||
|
if t.TokenType != "" { |
||||||
|
return t.TokenType |
||||||
|
} |
||||||
|
return "Bearer" |
||||||
|
} |
||||||
|
|
||||||
|
// SetAuthHeader sets the Authorization header to r using the access
|
||||||
|
// token in t.
|
||||||
|
//
|
||||||
|
// This method is unnecessary when using Transport or an HTTP Client
|
||||||
|
// returned by this package.
|
||||||
|
func (t *Token) SetAuthHeader(r *http.Request) { |
||||||
|
r.Header.Set("Authorization", t.Type()+" "+t.AccessToken) |
||||||
|
} |
||||||
|
|
||||||
|
// WithExtra returns a new Token that's a clone of t, but using the
|
||||||
|
// provided raw extra map. This is only intended for use by packages
|
||||||
|
// implementing derivative OAuth2 flows.
|
||||||
|
func (t *Token) WithExtra(extra interface{}) *Token { |
||||||
|
t2 := new(Token) |
||||||
|
*t2 = *t |
||||||
|
t2.raw = extra |
||||||
|
return t2 |
||||||
|
} |
||||||
|
|
||||||
|
// Extra returns an extra field.
|
||||||
|
// Extra fields are key-value pairs returned by the server as a
|
||||||
|
// part of the token retrieval response.
|
||||||
|
func (t *Token) Extra(key string) interface{} { |
||||||
|
if raw, ok := t.raw.(map[string]interface{}); ok { |
||||||
|
return raw[key] |
||||||
|
} |
||||||
|
|
||||||
|
vals, ok := t.raw.(url.Values) |
||||||
|
if !ok { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
v := vals.Get(key) |
||||||
|
switch s := strings.TrimSpace(v); strings.Count(s, ".") { |
||||||
|
case 0: // Contains no "."; try to parse as int
|
||||||
|
if i, err := strconv.ParseInt(s, 10, 64); err == nil { |
||||||
|
return i |
||||||
|
} |
||||||
|
case 1: // Contains a single "."; try to parse as float
|
||||||
|
if f, err := strconv.ParseFloat(s, 64); err == nil { |
||||||
|
return f |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return v |
||||||
|
} |
||||||
|
|
||||||
|
// expired reports whether the token is expired.
|
||||||
|
// t must be non-nil.
|
||||||
|
func (t *Token) expired() bool { |
||||||
|
if t.Expiry.IsZero() { |
||||||
|
return false |
||||||
|
} |
||||||
|
return t.Expiry.Add(-expiryDelta).Before(time.Now()) |
||||||
|
} |
||||||
|
|
||||||
|
// Valid reports whether t is non-nil, has an AccessToken, and is not expired.
|
||||||
|
func (t *Token) Valid() bool { |
||||||
|
return t != nil && t.AccessToken != "" && !t.expired() |
||||||
|
} |
||||||
|
|
||||||
|
// tokenFromInternal maps an *internal.Token struct into
|
||||||
|
// a *Token struct.
|
||||||
|
func tokenFromInternal(t *internal.Token) *Token { |
||||||
|
if t == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return &Token{ |
||||||
|
AccessToken: t.AccessToken, |
||||||
|
TokenType: t.TokenType, |
||||||
|
RefreshToken: t.RefreshToken, |
||||||
|
Expiry: t.Expiry, |
||||||
|
raw: t.Raw, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// retrieveToken takes a *Config and uses that to retrieve an *internal.Token.
|
||||||
|
// This token is then mapped from *internal.Token into an *oauth2.Token which is returned along
|
||||||
|
// with an error..
|
||||||
|
func retrieveToken(ctx context.Context, c *Config, v url.Values) (*Token, error) { |
||||||
|
tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.TokenURL, v) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return tokenFromInternal(tk), nil |
||||||
|
} |
@ -0,0 +1,132 @@ |
|||||||
|
// Copyright 2014 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package oauth2 |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"io" |
||||||
|
"net/http" |
||||||
|
"sync" |
||||||
|
) |
||||||
|
|
||||||
|
// Transport is an http.RoundTripper that makes OAuth 2.0 HTTP requests,
|
||||||
|
// wrapping a base RoundTripper and adding an Authorization header
|
||||||
|
// with a token from the supplied Sources.
|
||||||
|
//
|
||||||
|
// Transport is a low-level mechanism. Most code will use the
|
||||||
|
// higher-level Config.Client method instead.
|
||||||
|
type Transport struct { |
||||||
|
// Source supplies the token to add to outgoing requests'
|
||||||
|
// Authorization headers.
|
||||||
|
Source TokenSource |
||||||
|
|
||||||
|
// Base is the base RoundTripper used to make HTTP requests.
|
||||||
|
// If nil, http.DefaultTransport is used.
|
||||||
|
Base http.RoundTripper |
||||||
|
|
||||||
|
mu sync.Mutex // guards modReq
|
||||||
|
modReq map[*http.Request]*http.Request // original -> modified
|
||||||
|
} |
||||||
|
|
||||||
|
// RoundTrip authorizes and authenticates the request with an
|
||||||
|
// access token. If no token exists or token is expired,
|
||||||
|
// tries to refresh/fetch a new token.
|
||||||
|
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { |
||||||
|
if t.Source == nil { |
||||||
|
return nil, errors.New("oauth2: Transport's Source is nil") |
||||||
|
} |
||||||
|
token, err := t.Source.Token() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
req2 := cloneRequest(req) // per RoundTripper contract
|
||||||
|
token.SetAuthHeader(req2) |
||||||
|
t.setModReq(req, req2) |
||||||
|
res, err := t.base().RoundTrip(req2) |
||||||
|
if err != nil { |
||||||
|
t.setModReq(req, nil) |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
res.Body = &onEOFReader{ |
||||||
|
rc: res.Body, |
||||||
|
fn: func() { t.setModReq(req, nil) }, |
||||||
|
} |
||||||
|
return res, nil |
||||||
|
} |
||||||
|
|
||||||
|
// CancelRequest cancels an in-flight request by closing its connection.
|
||||||
|
func (t *Transport) CancelRequest(req *http.Request) { |
||||||
|
type canceler interface { |
||||||
|
CancelRequest(*http.Request) |
||||||
|
} |
||||||
|
if cr, ok := t.base().(canceler); ok { |
||||||
|
t.mu.Lock() |
||||||
|
modReq := t.modReq[req] |
||||||
|
delete(t.modReq, req) |
||||||
|
t.mu.Unlock() |
||||||
|
cr.CancelRequest(modReq) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (t *Transport) base() http.RoundTripper { |
||||||
|
if t.Base != nil { |
||||||
|
return t.Base |
||||||
|
} |
||||||
|
return http.DefaultTransport |
||||||
|
} |
||||||
|
|
||||||
|
func (t *Transport) setModReq(orig, mod *http.Request) { |
||||||
|
t.mu.Lock() |
||||||
|
defer t.mu.Unlock() |
||||||
|
if t.modReq == nil { |
||||||
|
t.modReq = make(map[*http.Request]*http.Request) |
||||||
|
} |
||||||
|
if mod == nil { |
||||||
|
delete(t.modReq, orig) |
||||||
|
} else { |
||||||
|
t.modReq[orig] = mod |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// cloneRequest returns a clone of the provided *http.Request.
|
||||||
|
// The clone is a shallow copy of the struct and its Header map.
|
||||||
|
func cloneRequest(r *http.Request) *http.Request { |
||||||
|
// shallow copy of the struct
|
||||||
|
r2 := new(http.Request) |
||||||
|
*r2 = *r |
||||||
|
// deep copy of the Header
|
||||||
|
r2.Header = make(http.Header, len(r.Header)) |
||||||
|
for k, s := range r.Header { |
||||||
|
r2.Header[k] = append([]string(nil), s...) |
||||||
|
} |
||||||
|
return r2 |
||||||
|
} |
||||||
|
|
||||||
|
type onEOFReader struct { |
||||||
|
rc io.ReadCloser |
||||||
|
fn func() |
||||||
|
} |
||||||
|
|
||||||
|
func (r *onEOFReader) Read(p []byte) (n int, err error) { |
||||||
|
n, err = r.rc.Read(p) |
||||||
|
if err == io.EOF { |
||||||
|
r.runFunc() |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (r *onEOFReader) Close() error { |
||||||
|
err := r.rc.Close() |
||||||
|
r.runFunc() |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
func (r *onEOFReader) runFunc() { |
||||||
|
if fn := r.fn; fn != nil { |
||||||
|
fn() |
||||||
|
r.fn = nil |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue