From de1fc76165620da99e2edbaf5720bcf2e0936e35 Mon Sep 17 00:00:00 2001 From: Kentaro Kuribayashi Date: Sat, 16 Aug 2014 01:12:13 +0900 Subject: [PATCH 01/18] Make authenticate process abstract to allow users to choose other OAuth providers. --- authenticator.go | 121 ++++++++++++++++++++++++++++++++++++++++++++++ conf.go | 20 +++++--- config_sample.yml | 7 +-- config_test.go | 6 ++- httpd.go | 80 ++---------------------------- 5 files changed, 146 insertions(+), 88 deletions(-) create mode 100644 authenticator.go diff --git a/authenticator.go b/authenticator.go new file mode 100644 index 0000000..f41a3f5 --- /dev/null +++ b/authenticator.go @@ -0,0 +1,121 @@ +package main + +import ( + "encoding/json" + "github.com/go-martini/martini" + gooauth2 "github.com/golang/oauth2" + "github.com/martini-contrib/oauth2" + "log" + "net/http" + "strings" +) + +type Authenticator interface { + Authenticate([]string, martini.Context, oauth2.Tokens, http.ResponseWriter, *http.Request) +} + +func NewAuthenticator(conf *Conf) Authenticator { + var a Authenticator + + if conf.Auth.Info.Service == "google" { + oauth := oauth2.Google(&gooauth2.Options{ + ClientID: conf.Auth.Info.ClientId, + ClientSecret: conf.Auth.Info.ClientSecret, + RedirectURL: conf.Auth.Info.RedirectURL, + Scopes: []string{"email"}, + }) + a = &GoogleAuth{oauth} + } else if conf.Auth.Info.Service == "github" { + oauth := oauth2.Github(&gooauth2.Options{ + ClientID: conf.Auth.Info.ClientId, + ClientSecret: conf.Auth.Info.ClientSecret, + RedirectURL: conf.Auth.Info.RedirectURL, + Scopes: []string{"user:email"}, + }) + a = &GitHubAuth{oauth} + } else { + panic("unsupported authentication method") + } + + return a +} + +type GoogleAuth struct { + martini.Handler +} + +func (a *GoogleAuth) Authenticate(domain []string, c martini.Context, tokens oauth2.Tokens, w http.ResponseWriter, r *http.Request) { + extra := tokens.ExtraData() + if _, ok := extra["id_token"]; ok == false { + log.Printf("id_token not found") + forbidden(w) + return + } + + keys := strings.Split(extra["id_token"], ".") + if len(keys) < 2 { + log.Printf("invalid id_token") + forbidden(w) + return + } + + data, err := base64Decode(keys[1]) + if err != nil { + log.Printf("failed to decode base64: %s", err.Error()) + forbidden(w) + return + } + + var info map[string]interface{} + if err := json.Unmarshal(data, &info); err != nil { + log.Printf("failed to decode json: %s", err.Error()) + forbidden(w) + return + } + + if email, ok := info["email"].(string); ok { + var user *User + if len(domain) > 0 { + for _, d := range domain { + if strings.Contains(d, "@") { + if d == email { + user = &User{email} + } + } else { + if strings.HasSuffix(email, "@"+d) { + user = &User{email} + break + } + } + } + } else { + user = &User{email} + } + + if user != nil { + log.Printf("user %s logged in", email) + c.Map(user) + } else { + log.Printf("email doesn't allow: %s", email) + forbidden(w) + return + } + } else { + log.Printf("email not found") + forbidden(w) + return + } +} + +type GitHubAuth struct { + martini.Handler +} + +func (a *GitHubAuth) Authenticate(domain []string, c martini.Context, tokens oauth2.Tokens, w http.ResponseWriter, r *http.Request) { + +} + +func forbidden(w http.ResponseWriter) { + w.WriteHeader(403) + w.Write([]byte("Access denied")) +} diff --git a/conf.go b/conf.go index fd08139..80705e3 100644 --- a/conf.go +++ b/conf.go @@ -22,14 +22,15 @@ type SSLConf struct { type AuthConf struct { Session AuthSessionConf `yaml:"session"` - Google AuthGoogleConf `yaml:"google"` + Info AuthInfoConf `yaml:"info"` } type AuthSessionConf struct { Key string `yaml:"key"` } -type AuthGoogleConf struct { +type AuthInfoConf struct { + Service string `yaml:"service"` ClientId string `yaml:"client_id"` ClientSecret string `yaml:"client_secret"` RedirectURL string `yaml:"redirect_url"` @@ -59,14 +60,17 @@ func ParseConf(path string) (*Conf, error) { if c.Auth.Session.Key == "" { return nil, errors.New("auth.session.key config is required") } - if c.Auth.Google.ClientId == "" { - return nil, errors.New("auth.google.client_id config is required") + if c.Auth.Info.Service == "" { + return nil, errors.New("auth.info.service config is required") } - if c.Auth.Google.ClientSecret == "" { - return nil, errors.New("auth.google.client_secret config is required") + if c.Auth.Info.ClientId == "" { + return nil, errors.New("auth.info.client_id config is required") } - if c.Auth.Google.RedirectURL == "" { - return nil, errors.New("auth.google.redirect_url config is required") + if c.Auth.Info.ClientSecret == "" { + return nil, errors.New("auth.info.client_secret config is required") + } + if c.Auth.Info.RedirectURL == "" { + return nil, errors.New("auth.info.redirect_url config is required") } if c.Htdocs == "" { diff --git a/config_sample.yml b/config_sample.yml index 9cabbe2..5ba3e76 100644 --- a/config_sample.yml +++ b/config_sample.yml @@ -11,11 +11,12 @@ auth: # authentication key for cookie store key: secret123 - google: - # your google app keys + info: + service: google + # your app keys for the service client_id: your client id client_secret: your client secret - # your google app redirect_url: path is always "/oauth2callback" + # your app redirect_url for the service: if the service is Google, path is always "/oauth2callback" redirect_url: https://yourapp.example.com/oauth2callback # # restrict domain. (optional) diff --git a/config_test.go b/config_test.go index 71d301b..8aee0e8 100644 --- a/config_test.go +++ b/config_test.go @@ -23,7 +23,8 @@ auth: session: key: secret - google: + info: + service: 'google' client_id: 'secret client id' client_secret: 'secret client secret' redirect_url: 'http://example.com/oauth2callback' @@ -66,7 +67,8 @@ auth: session: key: secret - google: + info: + service: 'google' client_id: 'secret client id' client_secret: 'secret client secret' redirect_url: 'http://example.com/oauth2callback' diff --git a/httpd.go b/httpd.go index ce27924..b558b2d 100644 --- a/httpd.go +++ b/httpd.go @@ -10,10 +10,8 @@ import ( "strings" "encoding/base64" - "encoding/json" "github.com/go-martini/martini" - gooauth2 "github.com/golang/oauth2" "github.com/martini-contrib/oauth2" "github.com/martini-contrib/sessions" "path/filepath" @@ -33,17 +31,13 @@ func NewServer(conf *Conf) *Server { func (s *Server) Run() error { m := martini.Classic() + a := NewAuthenticator(s.Conf) m.Use(sessions.Sessions("session", sessions.NewCookieStore([]byte(s.Conf.Auth.Session.Key)))) - m.Use(oauth2.Google(&gooauth2.Options{ - ClientID: s.Conf.Auth.Google.ClientId, - ClientSecret: s.Conf.Auth.Google.ClientSecret, - RedirectURL: s.Conf.Auth.Google.RedirectURL, - Scopes: []string{"email"}, - })) + m.Use(a) m.Use(loginRequired()) - m.Use(restrictDomain(s.Conf.Domain)) + m.Use(restrictDomain(s.Conf.Domain, a)) for i := range s.Conf.Proxies { p := s.Conf.Proxies[i] @@ -90,11 +84,6 @@ func (s *Server) Run() error { } } -func forbidden(w http.ResponseWriter) { - w.WriteHeader(403) - w.Write([]byte("Access denied")) -} - func isWebsocket(r *http.Request) bool { if strings.ToLower(r.Header.Get("Connection")) == "upgrade" && strings.ToLower(r.Header.Get("Upgrade")) == "websocket" { @@ -169,73 +158,14 @@ func base64Decode(s string) ([]byte, error) { return base64.URLEncoding.DecodeString(s) } -func restrictDomain(domain []string) martini.Handler { +func restrictDomain(domain []string, authenticator Authenticator) martini.Handler { return func(c martini.Context, tokens oauth2.Tokens, w http.ResponseWriter, r *http.Request) { // skip websocket if isWebsocket(r) { return } - extra := tokens.ExtraData() - if _, ok := extra["id_token"]; ok == false { - log.Printf("id_token not found") - forbidden(w) - return - } - - keys := strings.Split(extra["id_token"], ".") - if len(keys) < 2 { - log.Printf("invalid id_token") - forbidden(w) - return - } - - data, err := base64Decode(keys[1]) - if err != nil { - log.Printf("failed to decode base64: %s", err.Error()) - forbidden(w) - return - } - - var info map[string]interface{} - if err := json.Unmarshal(data, &info); err != nil { - log.Printf("failed to decode json: %s", err.Error()) - forbidden(w) - return - } - - if email, ok := info["email"].(string); ok { - var user *User - if len(domain) > 0 { - for _, d := range domain { - if strings.Contains(d, "@") { - if d == email { - user = &User{email} - } - } else { - if strings.HasSuffix(email, "@"+d) { - user = &User{email} - break - } - } - } - } else { - user = &User{email} - } - - if user != nil { - log.Printf("user %s logged in", email) - c.Map(user) - } else { - log.Printf("email doesn't allow: %s", email) - forbidden(w) - return - } - } else { - log.Printf("email not found") - forbidden(w) - return - } + authenticator.Authenticate(domain, c, tokens, w, r) } } From db45545a68aa12f6356580951e459e8e9662f6b5 Mon Sep 17 00:00:00 2001 From: Kentaro Kuribayashi Date: Sat, 16 Aug 2014 01:24:28 +0900 Subject: [PATCH 02/18] Adjust a type for handler --- authenticator.go | 25 +++++++++++++++++-------- httpd.go | 2 +- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/authenticator.go b/authenticator.go index f41a3f5..702d83a 100644 --- a/authenticator.go +++ b/authenticator.go @@ -12,36 +12,45 @@ import ( type Authenticator interface { Authenticate([]string, martini.Context, oauth2.Tokens, http.ResponseWriter, *http.Request) + Handler() martini.Handler } func NewAuthenticator(conf *Conf) Authenticator { - var a Authenticator + var authenticator Authenticator if conf.Auth.Info.Service == "google" { - oauth := oauth2.Google(&gooauth2.Options{ + handler := oauth2.Google(&gooauth2.Options{ ClientID: conf.Auth.Info.ClientId, ClientSecret: conf.Auth.Info.ClientSecret, RedirectURL: conf.Auth.Info.RedirectURL, Scopes: []string{"email"}, }) - a = &GoogleAuth{oauth} + authenticator = &GoogleAuth{&BaseAuth{handler}} } else if conf.Auth.Info.Service == "github" { - oauth := oauth2.Github(&gooauth2.Options{ + handler := oauth2.Github(&gooauth2.Options{ ClientID: conf.Auth.Info.ClientId, ClientSecret: conf.Auth.Info.ClientSecret, RedirectURL: conf.Auth.Info.RedirectURL, Scopes: []string{"user:email"}, }) - a = &GitHubAuth{oauth} + authenticator = &GitHubAuth{&BaseAuth{handler}} } else { panic("unsupported authentication method") } - return a + return authenticator +} + +type BaseAuth struct { + handler martini.Handler +} + +func (b *BaseAuth) Handler() martini.Handler { + return b.handler } type GoogleAuth struct { - martini.Handler + *BaseAuth } func (a *GoogleAuth) Authenticate(domain []string, c martini.Context, tokens oauth2.Tokens, w http.ResponseWriter, r *http.Request) { @@ -108,7 +117,7 @@ func (a *GoogleAuth) Authenticate(domain []string, c martini.Context, tokens oau } type GitHubAuth struct { - martini.Handler + *BaseAuth } func (a *GitHubAuth) Authenticate(domain []string, c martini.Context, tokens oauth2.Tokens, w http.ResponseWriter, r *http.Request) { diff --git a/httpd.go b/httpd.go index b558b2d..cd66241 100644 --- a/httpd.go +++ b/httpd.go @@ -34,7 +34,7 @@ func (s *Server) Run() error { a := NewAuthenticator(s.Conf) m.Use(sessions.Sessions("session", sessions.NewCookieStore([]byte(s.Conf.Auth.Session.Key)))) - m.Use(a) + m.Use(a.Handler()) m.Use(loginRequired()) m.Use(restrictDomain(s.Conf.Domain, a)) From f5a8fdbf1d899844579b8d66db9cedeccc6d82c4 Mon Sep 17 00:00:00 2001 From: Kentaro Kuribayashi Date: Sat, 16 Aug 2014 04:23:05 +0900 Subject: [PATCH 03/18] Add GitHub authentication based on organizations --- README.md | 57 ++++++++++++++++++++++++++++++++++++++++-------- authenticator.go | 46 ++++++++++++++++++++++++++++++++++++-- conf.go | 12 +++++----- config_test.go | 12 +++++----- httpd.go | 6 ++--- 5 files changed, 107 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 77d8280..2a48d1a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # "gate" for your private resources -gate is a static file server and reverse proxy integrated with google account authentications. +gate is a static file server and reverse proxy integrated with Google/GitHub account authentication. -With gate, you can safely serve your private resources with your company google apps authenticaiton. +With gate, you can safely serve your private resources with your company Google Apps/GitHub authenticaiton. ## Usage @@ -16,25 +16,27 @@ With gate, you can safely serve your private resources with your company google # address to bind address: :9999 -# # ssl keys (optional) -# ssl: -# cert: ./ssl/ssl.cer -# key: ./ssl/ssl.key +# ssl keys (optional) +ssl: + cert: ./ssl/ssl.cer + key: ./ssl/ssl.key auth: session: # authentication key for cookie store key: secret123 - google: + info: + service: google # your google app keys client_id: your client id client_secret: your client secret # your google app redirect_url: path is always "/oauth2callback" redirect_url: https://yourapp.example.com/oauth2callback -# # restrict domain. (optional) -# domain: yourdomain.com +# restrict domain. (optional) +conditions: + - yourdomain.com # document root for static files htdocs: ./ @@ -48,7 +50,44 @@ proxy: - path: /influxdb dest: http://127.0.0.1:8086 strip_path: yes +``` + +## Authentication Strategy + +gate now supports Google Apps and GitHub to authenticate users. + +### Example config for Google + +```yaml +auth: + info: + service: google + client_id: your client id + client_secret: your client secret + redirect_url: https://yourapp.example.com/oauth2callback + +# restrict domain. (optional) +conditions: + - example.com + - you@example.com +``` + +### Example config for GitHub + +Unlike the example of Google Apps above, if the `service` is GitHub, gate uses whether request user is a member of organization designated like below: + +```yaml +auth: + info: + service: github + client_id: your client id + client_secret: your client secret + redirect_url: https://yourapp.example.com/oauth2callback +# restrict organization (optional) +conditions: + - foo_organization + - bar_organization ``` ## License diff --git a/authenticator.go b/authenticator.go index 702d83a..0e476d8 100644 --- a/authenticator.go +++ b/authenticator.go @@ -5,6 +5,7 @@ import ( "github.com/go-martini/martini" gooauth2 "github.com/golang/oauth2" "github.com/martini-contrib/oauth2" + "io/ioutil" "log" "net/http" "strings" @@ -31,7 +32,7 @@ func NewAuthenticator(conf *Conf) Authenticator { ClientID: conf.Auth.Info.ClientId, ClientSecret: conf.Auth.Info.ClientSecret, RedirectURL: conf.Auth.Info.RedirectURL, - Scopes: []string{"user:email"}, + Scopes: []string{"read:org"}, }) authenticator = &GitHubAuth{&BaseAuth{handler}} } else { @@ -120,8 +121,49 @@ type GitHubAuth struct { *BaseAuth } -func (a *GitHubAuth) Authenticate(domain []string, c martini.Context, tokens oauth2.Tokens, w http.ResponseWriter, r *http.Request) { +func (a *GitHubAuth) Authenticate(organizations []string, c martini.Context, tokens oauth2.Tokens, w http.ResponseWriter, r *http.Request) { + if len(organizations) > 0 { + req, err := http.NewRequest("GET", "https://api.github.com/user/orgs", nil) + if err != nil { + log.Printf("failed to create a request to retrieve organizations: %s", err) + forbidden(w) + } + + req.SetBasicAuth(tokens.Access(), "x-oauth-basic") + + client := http.Client{} + res, err := client.Do(req) + if err != nil { + log.Printf("failed to retrieve organizations: %s", err) + forbidden(w) + } + + data, err := ioutil.ReadAll(res.Body) + res.Body.Close() + + if err != nil { + log.Printf("failed to read body of GitHub response: %s", err) + forbidden(w) + } + var info []map[string]interface{} + if err := json.Unmarshal(data, &info); err != nil { + log.Printf("failed to decode json: %s", err.Error()) + forbidden(w) + return + } + + for _, targetOrg := range info { + for _, conditionOrg := range organizations { + if targetOrg["login"] == conditionOrg { + return + } + } + } + + log.Print("not a member of designated organizations") + forbidden(w) + } } func forbidden(w http.ResponseWriter) { diff --git a/conf.go b/conf.go index 80705e3..ccf019c 100644 --- a/conf.go +++ b/conf.go @@ -7,12 +7,12 @@ import ( ) type Conf struct { - Addr string `yaml:"address"` - SSL SSLConf `yaml:"ssl"` - Auth AuthConf `yaml:"auth"` - Domain []string `yaml:"domain"` - Proxies []ProxyConf `yaml:"proxy"` - Htdocs string `yaml:"htdocs"` + Addr string `yaml:"address"` + SSL SSLConf `yaml:"ssl"` + Auth AuthConf `yaml:"auth"` + Conditions []string `yaml:"conditions"` + Proxies []ProxyConf `yaml:"proxy"` + Htdocs string `yaml:"htdocs"` } type SSLConf struct { diff --git a/config_test.go b/config_test.go index 8aee0e8..4ded875 100644 --- a/config_test.go +++ b/config_test.go @@ -50,7 +50,7 @@ proxy: } } -func TestParseMultiDomain(t *testing.T) { +func TestParseMultiConditions(t *testing.T) { f, err := ioutil.TempFile("", "") if err != nil { t.Error(err) @@ -80,7 +80,7 @@ proxy: dest: http://example.com/bar strip_path: yes -domain: +conditions: - 'example1.com' - 'example2.com' ` @@ -93,12 +93,12 @@ domain: t.Error(err) } - if len(conf.Domain) != 2 { - t.Errorf("unexpected domains num: %d", len(conf.Domain)) + if len(conf.Conditions) != 2 { + t.Errorf("unexpected conditions num: %d", len(conf.Conditions)) } - if conf.Domain[0] != "example1.com" || conf.Domain[1] != "example2.com" { - t.Errorf("unexpected domains: %+v", conf.Domain) + if conf.Conditions[0] != "example1.com" || conf.Conditions[1] != "example2.com" { + t.Errorf("unexpected conditions: %+v", conf.Conditions) } } diff --git a/httpd.go b/httpd.go index cd66241..e0e2a15 100644 --- a/httpd.go +++ b/httpd.go @@ -37,7 +37,7 @@ func (s *Server) Run() error { m.Use(a.Handler()) m.Use(loginRequired()) - m.Use(restrictDomain(s.Conf.Domain, a)) + m.Use(restrictByConditions(s.Conf.Conditions, a)) for i := range s.Conf.Proxies { p := s.Conf.Proxies[i] @@ -158,14 +158,14 @@ func base64Decode(s string) ([]byte, error) { return base64.URLEncoding.DecodeString(s) } -func restrictDomain(domain []string, authenticator Authenticator) martini.Handler { +func restrictByConditions(conditions []string, authenticator Authenticator) martini.Handler { return func(c martini.Context, tokens oauth2.Tokens, w http.ResponseWriter, r *http.Request) { // skip websocket if isWebsocket(r) { return } - authenticator.Authenticate(domain, c, tokens, w, r) + authenticator.Authenticate(conditions, c, tokens, w, r) } } From c3834c0991e6fedf64f21b6b5fb4cb34fb7cc203 Mon Sep 17 00:00:00 2001 From: Kentaro Kuribayashi Date: Sat, 16 Aug 2014 18:44:39 +0900 Subject: [PATCH 04/18] Change name condition to restrictions --- README.md | 37 ++++++++++++++++++++----------------- conf.go | 12 ++++++------ config_sample.yml | 10 ++++++---- config_test.go | 12 ++++++------ httpd.go | 6 +++--- 5 files changed, 41 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 2a48d1a..bd9c295 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # "gate" for your private resources -gate is a static file server and reverse proxy integrated with Google/GitHub account authentication. +gate is a static file server and reverse proxy integrated with OAuth2 account authentication. -With gate, you can safely serve your private resources with your company Google Apps/GitHub authenticaiton. +With gate, you can safely serve your private resources based on whether or not request user is a member of your company's Google Apps or GitHub organizations. ## Usage @@ -16,10 +16,10 @@ With gate, you can safely serve your private resources with your company Google # address to bind address: :9999 -# ssl keys (optional) -ssl: - cert: ./ssl/ssl.cer - key: ./ssl/ssl.key +# # ssl keys (optional) +# ssl: +# cert: ./ssl/ssl.cer +# key: ./ssl/ssl.key auth: session: @@ -27,16 +27,19 @@ auth: key: secret123 info: + # oauth2 provider name (`google` or `github`) service: google - # your google app keys + # your app keys for the service client_id: your client id client_secret: your client secret - # your google app redirect_url: path is always "/oauth2callback" + # your app redirect_url for the service: if the service is Google, path is always "/oauth2callback" redirect_url: https://yourapp.example.com/oauth2callback -# restrict domain. (optional) -conditions: - - yourdomain.com +# # restrict user request. (optional) +# restrictions: +# - yourdomain.com # domain of your Google App (Google) +# - example@gmail.com # specific email address (same as above) +# - your_company_org # organization name (GitHub) # document root for static files htdocs: ./ @@ -66,10 +69,10 @@ auth: client_secret: your client secret redirect_url: https://yourapp.example.com/oauth2callback -# restrict domain. (optional) -conditions: - - example.com - - you@example.com +# restrict user request. (optional) +restrictions: + - yourdomain.com # domain of your Google App + - example@gmail.com # specific email address ``` ### Example config for GitHub @@ -84,8 +87,8 @@ auth: client_secret: your client secret redirect_url: https://yourapp.example.com/oauth2callback -# restrict organization (optional) -conditions: +# restrict user request. (optional) +restrictions: - foo_organization - bar_organization ``` diff --git a/conf.go b/conf.go index ccf019c..9e29841 100644 --- a/conf.go +++ b/conf.go @@ -7,12 +7,12 @@ import ( ) type Conf struct { - Addr string `yaml:"address"` - SSL SSLConf `yaml:"ssl"` - Auth AuthConf `yaml:"auth"` - Conditions []string `yaml:"conditions"` - Proxies []ProxyConf `yaml:"proxy"` - Htdocs string `yaml:"htdocs"` + Addr string `yaml:"address"` + SSL SSLConf `yaml:"ssl"` + Auth AuthConf `yaml:"auth"` + Restrictions []string `yaml:"restrictions"` + Proxies []ProxyConf `yaml:"proxy"` + Htdocs string `yaml:"htdocs"` } type SSLConf struct { diff --git a/config_sample.yml b/config_sample.yml index 5ba3e76..7fd6788 100644 --- a/config_sample.yml +++ b/config_sample.yml @@ -12,6 +12,7 @@ auth: key: secret123 info: + # oauth2 provider name (`google` or `github`) service: google # your app keys for the service client_id: your client id @@ -19,10 +20,11 @@ auth: # your app redirect_url for the service: if the service is Google, path is always "/oauth2callback" redirect_url: https://yourapp.example.com/oauth2callback -# # restrict domain. (optional) -# domain: -# - yourdomain.com # restrict by domain -# - example@gmail.com # or specific address +# # restrict user request. (optional) +# restrictions: +# - yourdomain.com # domain of your Google App (Google) +# - example@gmail.com # specific email address (same as above) +# - your_company_org # organization name (GitHub) # document root for static files htdocs: ./ diff --git a/config_test.go b/config_test.go index 4ded875..a68bd18 100644 --- a/config_test.go +++ b/config_test.go @@ -50,7 +50,7 @@ proxy: } } -func TestParseMultiConditions(t *testing.T) { +func TestParseMultiRestrictions(t *testing.T) { f, err := ioutil.TempFile("", "") if err != nil { t.Error(err) @@ -80,7 +80,7 @@ proxy: dest: http://example.com/bar strip_path: yes -conditions: +restrictions: - 'example1.com' - 'example2.com' ` @@ -93,12 +93,12 @@ conditions: t.Error(err) } - if len(conf.Conditions) != 2 { - t.Errorf("unexpected conditions num: %d", len(conf.Conditions)) + if len(conf.Restrictions) != 2 { + t.Errorf("unexpected restrictions num: %d", len(conf.Restrictions)) } - if conf.Conditions[0] != "example1.com" || conf.Conditions[1] != "example2.com" { - t.Errorf("unexpected conditions: %+v", conf.Conditions) + if conf.Restrictions[0] != "example1.com" || conf.Restrictions[1] != "example2.com" { + t.Errorf("unexpected restrictions: %+v", conf.Restrictions) } } diff --git a/httpd.go b/httpd.go index e0e2a15..26ed043 100644 --- a/httpd.go +++ b/httpd.go @@ -37,7 +37,7 @@ func (s *Server) Run() error { m.Use(a.Handler()) m.Use(loginRequired()) - m.Use(restrictByConditions(s.Conf.Conditions, a)) + m.Use(restrictRequest(s.Conf.Restrictions, a)) for i := range s.Conf.Proxies { p := s.Conf.Proxies[i] @@ -158,14 +158,14 @@ func base64Decode(s string) ([]byte, error) { return base64.URLEncoding.DecodeString(s) } -func restrictByConditions(conditions []string, authenticator Authenticator) martini.Handler { +func restrictRequest(restrictions []string, authenticator Authenticator) martini.Handler { return func(c martini.Context, tokens oauth2.Tokens, w http.ResponseWriter, r *http.Request) { // skip websocket if isWebsocket(r) { return } - authenticator.Authenticate(conditions, c, tokens, w, r) + authenticator.Authenticate(restrictions, c, tokens, w, r) } } From 2240b1472a53d3d4b2dff4bde35f05a818d8c23f Mon Sep 17 00:00:00 2001 From: Kentaro Kuribayashi Date: Sat, 16 Aug 2014 18:52:58 +0900 Subject: [PATCH 05/18] Trivial change --- authenticator.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/authenticator.go b/authenticator.go index 0e476d8..bb0e745 100644 --- a/authenticator.go +++ b/authenticator.go @@ -153,9 +153,9 @@ func (a *GitHubAuth) Authenticate(organizations []string, c martini.Context, tok return } - for _, targetOrg := range info { - for _, conditionOrg := range organizations { - if targetOrg["login"] == conditionOrg { + for _, userOrg := range info { + for _, org := range organizations { + if userOrg["login"] == org { return } } From 2b2529948460dbbde373a020257984cd63b6f501 Mon Sep 17 00:00:00 2001 From: Kentaro Kuribayashi Date: Sat, 16 Aug 2014 19:00:11 +0900 Subject: [PATCH 06/18] Make sure to return --- authenticator.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/authenticator.go b/authenticator.go index bb0e745..c626623 100644 --- a/authenticator.go +++ b/authenticator.go @@ -127,6 +127,7 @@ func (a *GitHubAuth) Authenticate(organizations []string, c martini.Context, tok if err != nil { log.Printf("failed to create a request to retrieve organizations: %s", err) forbidden(w) + return } req.SetBasicAuth(tokens.Access(), "x-oauth-basic") @@ -136,6 +137,7 @@ func (a *GitHubAuth) Authenticate(organizations []string, c martini.Context, tok if err != nil { log.Printf("failed to retrieve organizations: %s", err) forbidden(w) + return } data, err := ioutil.ReadAll(res.Body) @@ -144,6 +146,7 @@ func (a *GitHubAuth) Authenticate(organizations []string, c martini.Context, tok if err != nil { log.Printf("failed to read body of GitHub response: %s", err) forbidden(w) + return } var info []map[string]interface{} @@ -163,6 +166,7 @@ func (a *GitHubAuth) Authenticate(organizations []string, c martini.Context, tok log.Print("not a member of designated organizations") forbidden(w) + return } } From 2634ac004e6d5b8ac78d350e3778b3db0c4c5b9a Mon Sep 17 00:00:00 2001 From: Shuhei Tanuma Date: Tue, 19 Aug 2014 13:47:26 +0900 Subject: [PATCH 07/18] add github enterprise support --- authenticator.go | 20 +++++++++++++++----- conf.go | 9 +++++++++ config_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/authenticator.go b/authenticator.go index c626623..d229793 100644 --- a/authenticator.go +++ b/authenticator.go @@ -9,6 +9,7 @@ import ( "log" "net/http" "strings" + "fmt" ) type Authenticator interface { @@ -26,15 +27,15 @@ func NewAuthenticator(conf *Conf) Authenticator { RedirectURL: conf.Auth.Info.RedirectURL, Scopes: []string{"email"}, }) - authenticator = &GoogleAuth{&BaseAuth{handler}} + authenticator = &GoogleAuth{&BaseAuth{handler, conf}} } else if conf.Auth.Info.Service == "github" { - handler := oauth2.Github(&gooauth2.Options{ + handler := GithubGeneral(&gooauth2.Options{ ClientID: conf.Auth.Info.ClientId, ClientSecret: conf.Auth.Info.ClientSecret, RedirectURL: conf.Auth.Info.RedirectURL, Scopes: []string{"read:org"}, - }) - authenticator = &GitHubAuth{&BaseAuth{handler}} + }, conf) + authenticator = &GitHubAuth{&BaseAuth{handler, conf}} } else { panic("unsupported authentication method") } @@ -42,8 +43,17 @@ func NewAuthenticator(conf *Conf) Authenticator { return authenticator } +// Currently, martini-contrib/oauth2 doesn't support github enterprise directly. +func GithubGeneral(opts *gooauth2.Options, conf *Conf) martini.Handler { + authUrl := fmt.Sprintf("%s/login/oauth/authorize", conf.Auth.Info.Endpoint) + tokenUrl := fmt.Sprintf("%s/login/oauth/access_token", conf.Auth.Info.Endpoint) + + return oauth2.NewOAuth2Provider(opts, authUrl, tokenUrl) +} + type BaseAuth struct { handler martini.Handler + conf *Conf } func (b *BaseAuth) Handler() martini.Handler { @@ -123,7 +133,7 @@ type GitHubAuth struct { func (a *GitHubAuth) Authenticate(organizations []string, c martini.Context, tokens oauth2.Tokens, w http.ResponseWriter, r *http.Request) { if len(organizations) > 0 { - req, err := http.NewRequest("GET", "https://api.github.com/user/orgs", nil) + req, err := http.NewRequest("GET", fmt.Sprintf("%s/user/orgs", a.conf.Auth.Info.Endpoint), nil) if err != nil { log.Printf("failed to create a request to retrieve organizations: %s", err) forbidden(w) diff --git a/conf.go b/conf.go index 9e29841..aa424b5 100644 --- a/conf.go +++ b/conf.go @@ -34,6 +34,8 @@ type AuthInfoConf struct { ClientId string `yaml:"client_id"` ClientSecret string `yaml:"client_secret"` RedirectURL string `yaml:"redirect_url"` + Endpoint string `yaml:"endpoint"` + ApiEndpoint string `yaml:"api_endpoint"` } type ProxyConf struct { @@ -77,5 +79,12 @@ func ParseConf(path string) (*Conf, error) { c.Htdocs = "." } + if c.Auth.Info.Service == "github" && c.Auth.Info.Endpoint == "" { + c.Auth.Info.Endpoint = "https://github.com" + } + if c.Auth.Info.Service == "github" && c.Auth.Info.ApiEndpoint == "" { + c.Auth.Info.ApiEndpoint = "https://api.github.com" + } + return c, nil } diff --git a/config_test.go b/config_test.go index a68bd18..f07d122 100644 --- a/config_test.go +++ b/config_test.go @@ -102,3 +102,42 @@ restrictions: } } +func TestParseGithubServiceShouldSetDefaultValue(t *testing.T) { + f, err := ioutil.TempFile("", "") + if err != nil { + t.Error(err) + } + defer func() { + f.Close() + os.Remove(f.Name()) + }() + + data := `--- +address: ":9999" + +auth: + session: + key: secret + + info: + service: 'github' + client_id: 'secret client id' + client_secret: 'secret client secret' + redirect_url: 'http://example.com/oauth2callback' +` + if err := ioutil.WriteFile(f.Name(), []byte(data), 0644); err != nil { + t.Error(err) + } + + conf, err := ParseConf(f.Name()) + if err != nil { + t.Error(err) + } + + if conf.Auth.Info.Endpoint != "https://github.com" { + t.Errorf("unexpected endpoint address: %s", conf.Auth.Info.Endpoint) + } + if conf.Auth.Info.ApiEndpoint != "https://api.github.com" { + t.Errorf("unexpected api endpoint address: %s", conf.Auth.Info.ApiEndpoint) + } +} From abd7083681944546b3dd4fd2979da78320cba6a2 Mon Sep 17 00:00:00 2001 From: Daisuke Murase Date: Wed, 20 Aug 2014 12:16:09 +0900 Subject: [PATCH 08/18] add github:e config example in README --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index bd9c295..a376e49 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,21 @@ restrictions: - bar_organization ``` +#### github:e support + +GitHub Enterprise is also supported. To authenticate via github enterprise, add api endpoint information to config like following: + +```yaml +auth: + info: + service: github + client_id: your client id + client_secret: your client secret + redirect_url: https://yourapp.example.com/oauth2callback + endpoint: https://github.yourcompany.com + api_endpoint: https://api.github.yourcompany.com +``` + ## License MIT From 357ec8d6b0a1f7df54b123a6753bb31fde92ffb0 Mon Sep 17 00:00:00 2001 From: Daisuke Murase Date: Wed, 20 Aug 2014 12:24:01 +0900 Subject: [PATCH 09/18] update instruction --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a376e49..7db3344 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,10 @@ With gate, you can safely serve your private resources based on whether or not r ## Usage -1. rename `config_sample.yml` to `config.yml` -2. edit `config.yml` to fit your environment -3. run `gate` +1. Download [binary](https://github.com/typester/gate/releases) or `go get` +2. rename `config_sample.yml` to `config.yml` +3. edit `config.yml` to fit your environment +4. run `gate` ## Example config From d2da13b3019c9243a26276df54d4774ac4c762bc Mon Sep 17 00:00:00 2001 From: Daisuke Murase Date: Wed, 20 Aug 2014 12:26:04 +0900 Subject: [PATCH 10/18] change api_endpoint example in doc --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7db3344..704bd9f 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ auth: client_secret: your client secret redirect_url: https://yourapp.example.com/oauth2callback endpoint: https://github.yourcompany.com - api_endpoint: https://api.github.yourcompany.com + api_endpoint: https://github.yourcompany.com/api ``` ## License From f27d017606055011acaf2fcd63bf3b2a09a4de6a Mon Sep 17 00:00:00 2001 From: Shuhei Tanuma Date: Wed, 20 Aug 2014 13:57:55 +0900 Subject: [PATCH 11/18] github: fix organization --- authenticator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticator.go b/authenticator.go index d229793..77a6d1d 100644 --- a/authenticator.go +++ b/authenticator.go @@ -133,7 +133,7 @@ type GitHubAuth struct { func (a *GitHubAuth) Authenticate(organizations []string, c martini.Context, tokens oauth2.Tokens, w http.ResponseWriter, r *http.Request) { if len(organizations) > 0 { - req, err := http.NewRequest("GET", fmt.Sprintf("%s/user/orgs", a.conf.Auth.Info.Endpoint), nil) + req, err := http.NewRequest("GET", fmt.Sprintf("%s/user/orgs", a.conf.Auth.Info.ApiEndpoint), nil) if err != nil { log.Printf("failed to create a request to retrieve organizations: %s", err) forbidden(w) From 4d0dd19c9e0f2b9871067e58ff17ff184ba25981 Mon Sep 17 00:00:00 2001 From: FUJIWARA Shunichiro Date: Mon, 25 Aug 2014 18:09:24 +0900 Subject: [PATCH 12/18] name based virtual host support --- README.md | 23 ++++++++++++++++ conf.go | 4 ++- httpd.go | 80 ++++++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 96 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 704bd9f..eaf30b7 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,29 @@ auth: api_endpoint: https://github.yourcompany.com/api ``` +## Name Based Virtual Host + +An example of "Name Based Viatual Host" setting. + +```yaml +auth: + session: + # authentication key for cookie store + key: secret123 + # domain of virtual hosts base host + cookie_domain: gate.example.com + +# proxy definitions +proxy: + - path: / + host: elasticsearch.gate.example.com + dest: http://127.0.0.1:9200 + + - path: / + host: influxdb.gate.example.com + dest: http://127.0.0.1:8086 +``` + ## License MIT diff --git a/conf.go b/conf.go index aa424b5..b9c80d6 100644 --- a/conf.go +++ b/conf.go @@ -26,7 +26,8 @@ type AuthConf struct { } type AuthSessionConf struct { - Key string `yaml:"key"` + Key string `yaml:"key"` + CookieDomain string `yaml:"cookie_domain"` } type AuthInfoConf struct { @@ -42,6 +43,7 @@ type ProxyConf struct { Path string `yaml:"path"` Dest string `yaml:"dest"` Strip bool `yaml:"strip_path"` + Host string `yaml:"host"` } func ParseConf(path string) (*Conf, error) { diff --git a/httpd.go b/httpd.go index 26ed043..25a44f2 100644 --- a/httpd.go +++ b/httpd.go @@ -25,6 +25,17 @@ type User struct { Email string } +type Backend struct { + Host string + URL *url.URL + Strip bool + StripPath string +} + +const ( + BackendHostHeader = "X-Gate-Backend-Host" +) + func NewServer(conf *Conf) *Server { return &Server{conf} } @@ -33,12 +44,19 @@ func (s *Server) Run() error { m := martini.Classic() a := NewAuthenticator(s.Conf) - m.Use(sessions.Sessions("session", sessions.NewCookieStore([]byte(s.Conf.Auth.Session.Key)))) + cookieStore := sessions.NewCookieStore([]byte(s.Conf.Auth.Session.Key)) + if domain := s.Conf.Auth.Session.CookieDomain; domain != "" { + cookieStore.Options(sessions.Options{Domain: domain}) + } + m.Use(sessions.Sessions("session", cookieStore)) m.Use(a.Handler()) m.Use(loginRequired()) m.Use(restrictRequest(s.Conf.Restrictions, a)) + backendsFor := make(map[string][]Backend) + backendIndex := make([]string, len(s.Conf.Proxies)) + for i := range s.Conf.Proxies { p := s.Conf.Proxies[i] @@ -55,15 +73,24 @@ func (s *Server) Run() error { if err != nil { return err } + backendsFor[p.Path] = append(backendsFor[p.Path], Backend{ + Host: p.Host, + URL: u, + Strip: p.Strip, + StripPath: strip_path, + }) + backendIndex[i] = p.Path + log.Printf("register proxy host:%s path:%s dest:%s strip_path:%v", p.Host, strip_path, u.String(), p.Strip) + } - proxy := httputil.NewSingleHostReverseProxy(u) - if p.Strip { - m.Any(p.Path, http.StripPrefix(strip_path, proxyHandleWrapper(u, proxy))) - } else { - m.Any(p.Path, proxyHandleWrapper(u, proxy)) + registered := make(map[string]bool) + for _, path := range backendIndex { + if registered[path] { + continue } - - log.Printf("register proxy path:%s dest:%s", strip_path, u.String()) + proxy := newVirtualHostReverseProxy(backendsFor[path]) + m.Any(path, proxyHandleWrapper(proxy)) + registered[path] = true } path, err := filepath.Abs(s.Conf.Htdocs) @@ -84,6 +111,34 @@ func (s *Server) Run() error { } } +func newVirtualHostReverseProxy(backends []Backend) http.Handler { + bmap := make(map[string]Backend) + for _, b := range backends { + bmap[b.Host] = b + } + defaultBackend, ok := bmap[""] + if !ok { + defaultBackend = backends[0] + } + + director := func(req *http.Request) { + b, ok := bmap[req.Host] + if !ok { + b = defaultBackend + } + req.URL.Scheme = b.URL.Scheme + req.URL.Host = b.URL.Host + if b.Strip { + if p := strings.TrimPrefix(req.URL.Path, b.StripPath); len(p) < len(req.URL.Path) { + req.URL.Path = "/" + p + } + } + req.Header.Set(BackendHostHeader, req.URL.Host) + log.Println("backend url", req.URL.String()) + } + return &httputil.ReverseProxy{Director: director} +} + func isWebsocket(r *http.Request) bool { if strings.ToLower(r.Header.Get("Connection")) == "upgrade" && strings.ToLower(r.Header.Get("Upgrade")) == "websocket" { @@ -93,11 +148,16 @@ func isWebsocket(r *http.Request) bool { } } -func proxyHandleWrapper(u *url.URL, handler http.Handler) http.Handler { +func proxyHandleWrapper(handler http.Handler) http.Handler { + proxy, _ := handler.(*httputil.ReverseProxy) + director := proxy.Director + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // websocket? if isWebsocket(r) { - target := u.Host + director(r) // rewrite request headers for backend + target := r.Header.Get(BackendHostHeader) + if strings.HasPrefix(r.URL.Path, "/") == false { r.URL.Path = "/" + r.URL.Path } From 307bee167b3c8f9ec3399cc54ccd485d91fbe194 Mon Sep 17 00:00:00 2001 From: Syohei YOSHIDA Date: Tue, 2 Sep 2014 13:09:46 +0900 Subject: [PATCH 13/18] Wait 2 goroutines Original code, one goroutine may fails in following case. 1 one goroutine sends error to channel 2 waited function receives it from channel 3 waited function finishes 4 waited function closes 'nc' and 'd' in defer 5 other goroutine fails io.Copy because 'nc' and 'd' are already closed. --- httpd.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/httpd.go b/httpd.go index 26ed043..317daa4 100644 --- a/httpd.go +++ b/httpd.go @@ -137,7 +137,9 @@ func proxyHandleWrapper(u *url.URL, handler http.Handler) http.Handler { } go cp(d, nc) go cp(nc, d) - <-errc + for i := 0; i < cap(errc); i++ { + <-errc + } } else { handler.ServeHTTP(w, r) } From 9a9e49f5facd565f8edecfa8d55c9ef1ec2f006a Mon Sep 17 00:00:00 2001 From: FUJIWARA Shunichiro Date: Fri, 5 Sep 2014 17:54:31 +0900 Subject: [PATCH 14/18] add noAuthServiceName. skip OAuth for testing. --- authenticator.go | 4 ++-- conf.go | 4 ++++ httpd.go | 10 ++++++---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/authenticator.go b/authenticator.go index d229793..5bcc422 100644 --- a/authenticator.go +++ b/authenticator.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "fmt" "github.com/go-martini/martini" gooauth2 "github.com/golang/oauth2" "github.com/martini-contrib/oauth2" @@ -9,7 +10,6 @@ import ( "log" "net/http" "strings" - "fmt" ) type Authenticator interface { @@ -53,7 +53,7 @@ func GithubGeneral(opts *gooauth2.Options, conf *Conf) martini.Handler { type BaseAuth struct { handler martini.Handler - conf *Conf + conf *Conf } func (b *BaseAuth) Handler() martini.Handler { diff --git a/conf.go b/conf.go index b9c80d6..9324762 100644 --- a/conf.go +++ b/conf.go @@ -6,6 +6,10 @@ import ( "io/ioutil" ) +const ( + noAuthServiceName = "nothing" // for testing only (undocumented) +) + type Conf struct { Addr string `yaml:"address"` SSL SSLConf `yaml:"ssl"` diff --git a/httpd.go b/httpd.go index 25a44f2..d351eb3 100644 --- a/httpd.go +++ b/httpd.go @@ -42,17 +42,19 @@ func NewServer(conf *Conf) *Server { func (s *Server) Run() error { m := martini.Classic() - a := NewAuthenticator(s.Conf) cookieStore := sessions.NewCookieStore([]byte(s.Conf.Auth.Session.Key)) if domain := s.Conf.Auth.Session.CookieDomain; domain != "" { cookieStore.Options(sessions.Options{Domain: domain}) } m.Use(sessions.Sessions("session", cookieStore)) - m.Use(a.Handler()) - m.Use(loginRequired()) - m.Use(restrictRequest(s.Conf.Restrictions, a)) + if s.Conf.Auth.Info.Service != noAuthServiceName { + a := NewAuthenticator(s.Conf) + m.Use(a.Handler()) + m.Use(loginRequired()) + m.Use(restrictRequest(s.Conf.Restrictions, a)) + } backendsFor := make(map[string][]Backend) backendIndex := make([]string, len(s.Conf.Proxies)) From 4d1030dfca8aa9b5fc5821bf7b0e3a16f1e30f45 Mon Sep 17 00:00:00 2001 From: FUJIWARA Shunichiro Date: Fri, 5 Sep 2014 18:06:53 +0900 Subject: [PATCH 15/18] add tests --- config_test.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/config_test.go b/config_test.go index f07d122..431a977 100644 --- a/config_test.go +++ b/config_test.go @@ -141,3 +141,64 @@ auth: t.Errorf("unexpected api endpoint address: %s", conf.Auth.Info.ApiEndpoint) } } + +func TestParseNamebasedVhosts(t *testing.T) { + f, err := ioutil.TempFile("", "") + if err != nil { + t.Error(err) + } + defer func() { + f.Close() + os.Remove(f.Name()) + }() + + data := `--- +address: ":9999" + +auth: + session: + key: secret + cookie_domain: example.com + + info: + service: 'google' + client_id: 'secret client id' + client_secret: 'secret client secret' + redirect_url: 'http://example.com/oauth2callback' + +htdocs: ./ + +proxy: + - path: / + host: elasticsearch.example.com + dest: http://127.0.0.1:9200 + - path: / + host: influxdb.example.com + dest: http://127.0.0.1:8086 +` + if err := ioutil.WriteFile(f.Name(), []byte(data), 0644); err != nil { + t.Error(err) + } + + conf, err := ParseConf(f.Name()) + if err != nil { + t.Error(err) + } + + if conf.Auth.Session.CookieDomain != "example.com" { + t.Errorf("unexpected cookie_domain: %s", conf.Auth.Session.CookieDomain) + } + + if len(conf.Proxies) != 2 { + t.Errorf("insufficient proxy definions") + } + es := conf.Proxies[0] + if es.Path != "/" || es.Host != "elasticsearch.example.com" || es.Dest != "http://127.0.0.1:9200" { + t.Errorf("unexpected proxy[0]: %#v", es) + } + + ifdb := conf.Proxies[1] + if ifdb.Path != "/" || ifdb.Host != "influxdb.example.com" || ifdb.Dest != "http://127.0.0.1:8086" { + t.Errorf("unexpected proxy[1]: %#v", ifdb) + } +} From 7d07d2ab7c2753dc5217b08137548ce3d70764b9 Mon Sep 17 00:00:00 2001 From: FUJIWARA Shunichiro Date: Mon, 8 Sep 2014 16:38:42 +0900 Subject: [PATCH 16/18] add httpd_test.go --- httpd_test.go | 177 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 httpd_test.go diff --git a/httpd_test.go b/httpd_test.go new file mode 100644 index 0000000..d23c1f4 --- /dev/null +++ b/httpd_test.go @@ -0,0 +1,177 @@ +package main + +import ( + "fmt" + "io/ioutil" + "net/http" + "os" + "testing" + "time" +) + +func TestPrepareFoo(t *testing.T) { + http.HandleFunc("/foo/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "hello Foo\n") + }) + go func() { + err := http.ListenAndServe(":10001", nil) + if err != nil { + t.Error(err) + } + }() + time.Sleep(1 * time.Second) +} + +func TestPrepareBar(t *testing.T) { + http.HandleFunc("/bar/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "hello Bar\n") + }) + go func() { + err := http.ListenAndServe(":10002", nil) + if err != nil { + t.Error(err) + } + }() + time.Sleep(1 * time.Second) +} + +func TestRunHTTPd(t *testing.T) { + f, err := ioutil.TempFile("", "") + if err != nil { + t.Error(err) + } + defer func() { + f.Close() + os.Remove(f.Name()) + }() + data := ` +address: "127.0.0.1:9999" +auth: + session: + key: dummy + info: + service: nothing + client_id: dummy + client_secret: dummy + redirect_url: "http://example.com/oauth2callback" +proxy: + - path: /foo + dest: http://127.0.0.1:10001 + strip_path: no + + - path: /bar + dest: http://127.0.0.1:10002 + strip_path: no +` + if err := ioutil.WriteFile(f.Name(), []byte(data), 0644); err != nil { + t.Error(err) + } + conf, err := ParseConf(f.Name()) + if err != nil { + t.Error(err) + } + server := NewServer(conf) + if server == nil { + t.Error("NewServer failed") + } + go server.Run() + time.Sleep(1 * time.Second) + + // backend foo + if res, err := http.Get("http://127.0.0.1:9999/foo/"); err == nil { + defer res.Body.Close() + body, _ := ioutil.ReadAll(res.Body) + if string(body) != "hello Foo\n" { + t.Errorf("unexpected foo body %s", body) + } + } else { + t.Error(err) + } + + // backend bar + if res, err := http.Get("http://127.0.0.1:9999/bar/"); err == nil { + defer res.Body.Close() + body, _ := ioutil.ReadAll(res.Body) + if string(body) != "hello Bar\n" { + t.Errorf("unexpected bar body %s", body) + } + } else { + t.Error(err) + } +} + +func TestRunVhost(t *testing.T) { + f, err := ioutil.TempFile("", "") + if err != nil { + t.Error(err) + } + defer func() { + f.Close() + os.Remove(f.Name()) + }() + data := ` +address: "127.0.0.1:10000" +auth: + session: + key: dummy + cookie_domain: example.com + info: + service: nothing + client_id: dummy + client_secret: dummy + redirect_url: "http://example.com/oauth2callback" +proxy: + - path: / + dest: http://127.0.0.1:10001 + strip_path: no + host: foo.example.com + + - path: / + dest: http://127.0.0.1:10002 + strip_path: no + host: bar.example.com +` + if err := ioutil.WriteFile(f.Name(), []byte(data), 0644); err != nil { + t.Error(err) + } + conf, err := ParseConf(f.Name()) + if err != nil { + t.Error(err) + } + server := NewServer(conf) + if server == nil { + t.Error("NewServer failed") + } + go server.Run() + time.Sleep(1 * time.Second) + + var req *http.Request + client := &http.Client{} + + // backend foo + req, _ = http.NewRequest("GET", "http://127.0.0.1:10000/foo/", nil) + req.Header.Add("Host", "foo.example.com") + if res, err := client.Do(req); err == nil { + defer res.Body.Close() + body, _ := ioutil.ReadAll(res.Body) + if string(body) != "hello Foo\n" { + t.Errorf("unexpected foo body %s", body) + } + } else { + t.Error(err) + } + + // backend bar + req, _ = http.NewRequest("GET", "http://127.0.0.1:10000/bar/", nil) + req.Header.Add("Host", "bar.example.com") + if res, err := http.Get("http://127.0.0.1:10000/bar/"); err == nil { + defer res.Body.Close() + body, _ := ioutil.ReadAll(res.Body) + if string(body) != "hello Bar\n" { + t.Errorf("unexpected bar body %s", body) + } + } else { + t.Error(err) + } + +} From 467892559fffbb1ea4ff7eb4d956085f773b14b1 Mon Sep 17 00:00:00 2001 From: "Shota Fukumori (sora_h)" Date: Wed, 29 Oct 2014 18:10:16 +0900 Subject: [PATCH 17/18] Allow customize oauth2 paths We have `/login` in backend servers, so it shouldn't be taken by the Gate. --- conf.go | 24 ++++++++++++++++++++++ config_test.go | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 2 ++ 3 files changed, 81 insertions(+) diff --git a/conf.go b/conf.go index aa424b5..93bf220 100644 --- a/conf.go +++ b/conf.go @@ -4,6 +4,7 @@ import ( "errors" "gopkg.in/yaml.v1" "io/ioutil" + "github.com/martini-contrib/oauth2" ) type Conf struct { @@ -12,6 +13,7 @@ type Conf struct { Auth AuthConf `yaml:"auth"` Restrictions []string `yaml:"restrictions"` Proxies []ProxyConf `yaml:"proxy"` + Paths PathConf `yaml:"paths"` Htdocs string `yaml:"htdocs"` } @@ -44,6 +46,13 @@ type ProxyConf struct { Strip bool `yaml:"strip_path"` } +type PathConf struct { + Login string `yaml:"login"` + Logout string `yaml:"logout"` + Callback string `yaml:"callback"` + Error string `yaml:"error"` +} + func ParseConf(path string) (*Conf, error) { data, err := ioutil.ReadFile(path) if err != nil { @@ -88,3 +97,18 @@ func ParseConf(path string) (*Conf, error) { return c, nil } + +func (c *Conf) SetOAuth2Paths() { + if c.Paths.Login != "" { + oauth2.PathLogin = c.Paths.Login + } + if c.Paths.Logout != "" { + oauth2.PathLogout = c.Paths.Logout + } + if c.Paths.Callback != "" { + oauth2.PathCallback = c.Paths.Callback + } + if c.Paths.Error != "" { + oauth2.PathError = c.Paths.Error + } +} diff --git a/config_test.go b/config_test.go index f07d122..bffbe58 100644 --- a/config_test.go +++ b/config_test.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "os" "testing" + "github.com/martini-contrib/oauth2" ) func TestParse(t *testing.T) { @@ -141,3 +142,57 @@ auth: t.Errorf("unexpected api endpoint address: %s", conf.Auth.Info.ApiEndpoint) } } + +func TestPathConf(t *testing.T) { + f, err := ioutil.TempFile("", "") + if err != nil { + t.Error(err) + } + defer func() { + f.Close() + os.Remove(f.Name()) + }() + + data := `--- +address: ":9999" + +auth: + session: + key: secret + + info: + service: 'github' + client_id: 'secret client id' + client_secret: 'secret client secret' + redirect_url: 'http://example.com/_gate_callback' + +paths: + login: "/_gate_login" + logout: "/_gate_logout" + callback: "/_gate_callback" + error: "/_gate_error" +` + if err := ioutil.WriteFile(f.Name(), []byte(data), 0644); err != nil { + t.Error(err) + } + + conf, err := ParseConf(f.Name()) + if err != nil { + t.Error(err) + } + + conf.SetOAuth2Paths() + + if oauth2.PathLogin != "/_gate_login" { + t.Errorf("unexpected oauth2.PathLogin: %s", oauth2.PathLogin) + } + if oauth2.PathLogout != "/_gate_logout" { + t.Errorf("unexpected oauth2.PathLogout: %s", oauth2.PathLogout) + } + if oauth2.PathCallback != "/_gate_callback" { + t.Errorf("unexpected oauth2.PathCallback: %s", oauth2.PathCallback) + } + if oauth2.PathError != "/_gate_error" { + t.Errorf("unexpected oauth2.PathError: %s", oauth2.PathError) + } +} diff --git a/main.go b/main.go index 721095f..5650ae3 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,8 @@ func main() { panic(err) } + conf.SetOAuth2Paths() + server := NewServer(conf) log.Fatal(server.Run()) } From 7241e8606ff1e7677cd4d9e5ceba5843c74c4852 Mon Sep 17 00:00:00 2001 From: catatsuy Date: Wed, 5 Nov 2014 13:48:22 +0900 Subject: [PATCH 18/18] redirect if there is no / at the end of the URL I can't access the page if I forget to give / at the end of the URL This is unkind --- httpd.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/httpd.go b/httpd.go index f0feab7..abc4fc9 100644 --- a/httpd.go +++ b/httpd.go @@ -1,20 +1,19 @@ package main import ( + "encoding/base64" "io" "log" "net" "net/http" "net/http/httputil" "net/url" + "path/filepath" "strings" - "encoding/base64" - "github.com/go-martini/martini" "github.com/martini-contrib/oauth2" "github.com/martini-contrib/sessions" - "path/filepath" ) type Server struct { @@ -58,11 +57,14 @@ func (s *Server) Run() error { backendsFor := make(map[string][]Backend) backendIndex := make([]string, len(s.Conf.Proxies)) + rawPaths := make([]string, len(s.Conf.Proxies)) for i := range s.Conf.Proxies { p := s.Conf.Proxies[i] + rawPath := "" if strings.HasSuffix(p.Path, "/") == false { + rawPath = p.Path p.Path += "/" } strip_path := p.Path @@ -82,17 +84,24 @@ func (s *Server) Run() error { StripPath: strip_path, }) backendIndex[i] = p.Path + rawPaths[i] = rawPath log.Printf("register proxy host:%s path:%s dest:%s strip_path:%v", p.Host, strip_path, u.String(), p.Strip) } registered := make(map[string]bool) - for _, path := range backendIndex { + for i, path := range backendIndex { if registered[path] { continue } proxy := newVirtualHostReverseProxy(backendsFor[path]) m.Any(path, proxyHandleWrapper(proxy)) registered[path] = true + rawPath := rawPaths[i] + if rawPath != "" { + m.Get(rawPath, func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, rawPath+"/", http.StatusFound) + }) + } } path, err := filepath.Abs(s.Conf.Htdocs)