diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b4a00b..d5de003 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,7 @@ - [#159](https://github.com/pusher/oauth2_proxy/pull/159) Add option to skip the OIDC provider verified email check: `--insecure-oidc-allow-unverified-email` - [#210](https://github.com/pusher/oauth2_proxy/pull/210) Update base image from Alpine 3.9 to 3.10 (@steakunderscore) - [#211](https://github.com/pusher/oauth2_proxy/pull/211) Switch from dep to go modules (@steakunderscore) +- [#204](https://github.com/pusher/oauth2_proxy/pull/204) Add subdomain-based routing. It is now possible to route based on domain name and path. # v3.2.0 diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index 5bef512..4509bcb 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -123,6 +123,17 @@ Static file paths are configured as a file:// URL. `file:///var/www/static/` wil Multiple upstreams can either be configured by supplying a comma separated list to the `-upstream` parameter, supplying the parameter multiple times or provinding a list in the [config file](#config-file). When multiple upstreams are used routing to them will be based on the path they are set up with. +Subdomain-based routing is possible by prepending a subdomain followed by a `|` to the upstream URL, like this: + +``` +test |http://127.0.0.1:8082/ +other|http://127.0.0.1:8083/ +other|http://127.0.0.1:8084/path/ + |http://127.0.0.1:8085/ +``` + +Assuming cookie domain is set to `.example.com`, the requests `test.example.com`, `other.example.com`, `other.example.com/path/` and `example.com` will all be routed to different upstreams. Any spacing around the `|` separator is ignored. + ### Environment variables Every command line argument can be specified as an environment variable by diff --git a/oauthproxy.go b/oauthproxy.go index 365a14e..6b9273f 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -84,7 +84,7 @@ type OAuthProxy struct { SignInMessage string HtpasswdFile *HtpasswdFile DisplayHtpasswdForm bool - serveMux http.Handler + serveMuxes map[string]http.Handler SetXAuthRequest bool PassBasicAuth bool SkipProviderButton bool @@ -193,35 +193,46 @@ func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.Hma // NewOAuthProxy creates a new instance of OOuthProxy from the options provided func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { - serveMux := http.NewServeMux() + serveMuxes := map[string]http.Handler{} var auth hmacauth.HmacAuth if sigData := opts.signatureData; sigData != nil { auth = hmacauth.NewHmacAuth(sigData.hash, []byte(sigData.key), SignatureHeader, SignatureHeaders) } - for _, u := range opts.proxyURLs { - path := u.Path - switch u.Scheme { - case httpScheme, httpsScheme: - logger.Printf("mapping path %q => upstream %q", path, u) - proxy := NewWebSocketOrRestReverseProxy(u, opts, auth) - serveMux.Handle(path, proxy) + for host, urls := range opts.proxyURLs { + if host != "*" && strings.HasPrefix(opts.CookieDomain, ".") && !strings.HasSuffix(host, opts.CookieDomain) { + if host == "" { + host = opts.CookieDomain[1:] + } else { + host = host + opts.CookieDomain + } + } + serveMux := http.NewServeMux() + serveMuxes[host] = serveMux + for _, u := range urls { + path := u.Path + switch u.Scheme { + case httpScheme, httpsScheme: + logger.Printf("mapping path %q for %q => upstream %q", path, host, u) + proxy := NewWebSocketOrRestReverseProxy(u, opts, auth) + serveMux.Handle(path, proxy) - case "file": - if u.Fragment != "" { - path = u.Fragment + case "file": + if u.Fragment != "" { + path = u.Fragment + } + logger.Printf("mapping path %q => file system %q", path, u.Path) + proxy := NewFileServer(path, u.Path) + uProxy := UpstreamProxy{ + upstream: path, + handler: proxy, + wsHandler: nil, + auth: nil, + } + serveMux.Handle(path, &uProxy) + default: + panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme)) } - logger.Printf("mapping path %q => file system %q", path, u.Path) - proxy := NewFileServer(path, u.Path) - uProxy := UpstreamProxy{ - upstream: path, - handler: proxy, - wsHandler: nil, - auth: nil, - } - serveMux.Handle(path, &uProxy) - default: - panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme)) } } for _, u := range opts.CompiledRegex { @@ -245,6 +256,12 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { refresh = fmt.Sprintf("after %s", opts.CookieRefresh) } + whitelistDomains := opts.WhitelistDomains + if len(serveMuxes) >= 2 && len(whitelistDomains) == 0 && opts.CookieDomain != "" { + // by default the cookie-domain is allowed + whitelistDomains = append(whitelistDomains, opts.CookieDomain) + } + logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domain:%s path:%s refresh:%s", opts.CookieName, opts.CookieSecure, opts.CookieHTTPOnly, opts.CookieExpire, opts.CookieDomain, opts.CookiePath, refresh) return &OAuthProxy{ @@ -270,9 +287,9 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { ProxyPrefix: opts.ProxyPrefix, provider: opts.provider, sessionStore: opts.sessionStore, - serveMux: serveMux, + serveMuxes: serveMuxes, redirectURL: redirectURL, - whitelistDomains: opts.WhitelistDomains, + whitelistDomains: whitelistDomains, skipAuthRegex: opts.SkipAuthRegex, skipAuthPreflight: opts.SkipAuthPreflight, skipJwtBearerTokens: opts.SkipJwtBearerTokens, @@ -481,6 +498,9 @@ func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) } redirect = req.Form.Get("rd") + if len(p.serveMuxes) >= 2 && strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") { + redirect = "https://" + req.Host + redirect + } if !p.IsValidRedirect(redirect) { redirect = req.URL.Path if strings.HasPrefix(redirect, p.ProxyPrefix) { @@ -543,7 +563,7 @@ func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { case path == p.PingPath: p.PingPage(rw) case p.IsWhitelistedRequest(req): - p.serveMux.ServeHTTP(rw, req) + p.serveRequest(rw, req) case path == p.SignInPath: p.SignIn(rw, req) case path == p.SignOutPath: @@ -687,6 +707,25 @@ func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) rw.WriteHeader(http.StatusAccepted) } +// serveRequest actually proxies the request to the upstream +func (p *OAuthProxy) serveRequest(rw http.ResponseWriter, req *http.Request) { + domain := req.Host + if h, _, err := net.SplitHostPort(domain); err == nil { + domain = h + } + var serveMux http.Handler + if m, ok := p.serveMuxes[domain]; ok { + serveMux = m + } else { + serveMux = p.serveMuxes["*"] + } + if serveMux == nil { + p.ErrorPage(rw, 404, "Page not found", "Invalid Host") + return + } + serveMux.ServeHTTP(rw, req) +} + // Proxy proxies the user request if the user is authenticated else it prompts // them to authenticate func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { @@ -695,7 +734,7 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { case nil: // we are authenticated p.addHeadersForProxying(rw, req, session) - p.serveMux.ServeHTTP(rw, req) + p.serveRequest(rw, req) case ErrNeedsLogin: // we need to send the user to a login screen diff --git a/options.go b/options.go index 03f5ff3..33d4606 100644 --- a/options.go +++ b/options.go @@ -119,7 +119,7 @@ type Options struct { // internal values that are set after config validation redirectURL *url.URL - proxyURLs []*url.URL + proxyURLs map[string][]*url.URL CompiledRegex []*regexp.Regexp provider providers.Provider sessionStore sessionsapi.SessionStore @@ -284,7 +284,14 @@ func (o *Options) Validate() error { o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs) + domainUpstreamRegex := regexp.MustCompile(`^\s*([a-z0-9._-]*)\s*\|\s*(.*)$`) + o.proxyURLs = map[string][]*url.URL{} for _, u := range o.Upstreams { + subdomain := "*" + m := domainUpstreamRegex.FindStringSubmatch(u) + if m != nil { + subdomain, u = m[1], m[2] + } upstreamURL, err := url.Parse(u) if err != nil { msgs = append(msgs, fmt.Sprintf("error parsing upstream: %s", err)) @@ -292,7 +299,7 @@ func (o *Options) Validate() error { if upstreamURL.Path == "" { upstreamURL.Path = "/" } - o.proxyURLs = append(o.proxyURLs, upstreamURL) + o.proxyURLs[subdomain] = append(o.proxyURLs[subdomain], upstreamURL) } } diff --git a/options_test.go b/options_test.go index 171ff36..25ea7a3 100644 --- a/options_test.go +++ b/options_test.go @@ -86,11 +86,25 @@ func TestRedirectURL(t *testing.T) { func TestProxyURLs(t *testing.T) { o := testOptions() o.Upstreams = append(o.Upstreams, "http://127.0.0.1:8081") + o.Upstreams = append(o.Upstreams, "|http://127.0.0.1:8082") + o.Upstreams = append(o.Upstreams, " |http://127.0.0.1:8083/x/") + o.Upstreams = append(o.Upstreams, "sub.domain|http://127.0.0.1:8082/abc") + o.Upstreams = append(o.Upstreams, " sub.domain\t | http://127.0.0.1:8083/abc/") assert.Equal(t, nil, o.Validate()) - expected := []*url.URL{ - {Scheme: "http", Host: "127.0.0.1:8080", Path: "/"}, - // note the '/' was added - {Scheme: "http", Host: "127.0.0.1:8081", Path: "/"}, + expected := map[string][]*url.URL{ + "*": { + {Scheme: "http", Host: "127.0.0.1:8080", Path: "/"}, + // note the '/' was added + {Scheme: "http", Host: "127.0.0.1:8081", Path: "/"}, + }, + "": { + {Scheme: "http", Host: "127.0.0.1:8082", Path: "/"}, + {Scheme: "http", Host: "127.0.0.1:8083", Path: "/x/"}, + }, + "sub.domain": { + {Scheme: "http", Host: "127.0.0.1:8082", Path: "/abc"}, + {Scheme: "http", Host: "127.0.0.1:8083", Path: "/abc/"}, + }, } assert.Equal(t, expected, o.proxyURLs) }