Implement subdomain-based routing

This commit is contained in:
rustyx 2019-08-11 13:18:41 +02:00
parent a91cce7ab9
commit cd93f4d947
5 changed files with 105 additions and 33 deletions

View File

@ -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` - [#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) - [#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) - [#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 # v3.2.0

View File

@ -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. 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 ### Environment variables
Every command line argument can be specified as an environment variable by Every command line argument can be specified as an environment variable by

View File

@ -84,7 +84,7 @@ type OAuthProxy struct {
SignInMessage string SignInMessage string
HtpasswdFile *HtpasswdFile HtpasswdFile *HtpasswdFile
DisplayHtpasswdForm bool DisplayHtpasswdForm bool
serveMux http.Handler serveMuxes map[string]http.Handler
SetXAuthRequest bool SetXAuthRequest bool
PassBasicAuth bool PassBasicAuth bool
SkipProviderButton 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 // NewOAuthProxy creates a new instance of OOuthProxy from the options provided
func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
serveMux := http.NewServeMux() serveMuxes := map[string]http.Handler{}
var auth hmacauth.HmacAuth var auth hmacauth.HmacAuth
if sigData := opts.signatureData; sigData != nil { if sigData := opts.signatureData; sigData != nil {
auth = hmacauth.NewHmacAuth(sigData.hash, []byte(sigData.key), auth = hmacauth.NewHmacAuth(sigData.hash, []byte(sigData.key),
SignatureHeader, SignatureHeaders) SignatureHeader, SignatureHeaders)
} }
for _, u := range opts.proxyURLs { for host, urls := range opts.proxyURLs {
path := u.Path if host != "*" && strings.HasPrefix(opts.CookieDomain, ".") && !strings.HasSuffix(host, opts.CookieDomain) {
switch u.Scheme { if host == "" {
case httpScheme, httpsScheme: host = opts.CookieDomain[1:]
logger.Printf("mapping path %q => upstream %q", path, u) } else {
proxy := NewWebSocketOrRestReverseProxy(u, opts, auth) host = host + opts.CookieDomain
serveMux.Handle(path, proxy) }
}
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": case "file":
if u.Fragment != "" { if u.Fragment != "" {
path = 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 { 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) 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) 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{ return &OAuthProxy{
@ -270,9 +287,9 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
ProxyPrefix: opts.ProxyPrefix, ProxyPrefix: opts.ProxyPrefix,
provider: opts.provider, provider: opts.provider,
sessionStore: opts.sessionStore, sessionStore: opts.sessionStore,
serveMux: serveMux, serveMuxes: serveMuxes,
redirectURL: redirectURL, redirectURL: redirectURL,
whitelistDomains: opts.WhitelistDomains, whitelistDomains: whitelistDomains,
skipAuthRegex: opts.SkipAuthRegex, skipAuthRegex: opts.SkipAuthRegex,
skipAuthPreflight: opts.SkipAuthPreflight, skipAuthPreflight: opts.SkipAuthPreflight,
skipJwtBearerTokens: opts.SkipJwtBearerTokens, skipJwtBearerTokens: opts.SkipJwtBearerTokens,
@ -481,6 +498,9 @@ func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error)
} }
redirect = req.Form.Get("rd") 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) { if !p.IsValidRedirect(redirect) {
redirect = req.URL.Path redirect = req.URL.Path
if strings.HasPrefix(redirect, p.ProxyPrefix) { if strings.HasPrefix(redirect, p.ProxyPrefix) {
@ -543,7 +563,7 @@ func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
case path == p.PingPath: case path == p.PingPath:
p.PingPage(rw) p.PingPage(rw)
case p.IsWhitelistedRequest(req): case p.IsWhitelistedRequest(req):
p.serveMux.ServeHTTP(rw, req) p.serveRequest(rw, req)
case path == p.SignInPath: case path == p.SignInPath:
p.SignIn(rw, req) p.SignIn(rw, req)
case path == p.SignOutPath: case path == p.SignOutPath:
@ -687,6 +707,25 @@ func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request)
rw.WriteHeader(http.StatusAccepted) 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 // Proxy proxies the user request if the user is authenticated else it prompts
// them to authenticate // them to authenticate
func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { 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: case nil:
// we are authenticated // we are authenticated
p.addHeadersForProxying(rw, req, session) p.addHeadersForProxying(rw, req, session)
p.serveMux.ServeHTTP(rw, req) p.serveRequest(rw, req)
case ErrNeedsLogin: case ErrNeedsLogin:
// we need to send the user to a login screen // we need to send the user to a login screen

View File

@ -119,7 +119,7 @@ type Options struct {
// internal values that are set after config validation // internal values that are set after config validation
redirectURL *url.URL redirectURL *url.URL
proxyURLs []*url.URL proxyURLs map[string][]*url.URL
CompiledRegex []*regexp.Regexp CompiledRegex []*regexp.Regexp
provider providers.Provider provider providers.Provider
sessionStore sessionsapi.SessionStore sessionStore sessionsapi.SessionStore
@ -284,7 +284,14 @@ func (o *Options) Validate() error {
o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs) 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 { for _, u := range o.Upstreams {
subdomain := "*"
m := domainUpstreamRegex.FindStringSubmatch(u)
if m != nil {
subdomain, u = m[1], m[2]
}
upstreamURL, err := url.Parse(u) upstreamURL, err := url.Parse(u)
if err != nil { if err != nil {
msgs = append(msgs, fmt.Sprintf("error parsing upstream: %s", err)) msgs = append(msgs, fmt.Sprintf("error parsing upstream: %s", err))
@ -292,7 +299,7 @@ func (o *Options) Validate() error {
if upstreamURL.Path == "" { if upstreamURL.Path == "" {
upstreamURL.Path = "/" upstreamURL.Path = "/"
} }
o.proxyURLs = append(o.proxyURLs, upstreamURL) o.proxyURLs[subdomain] = append(o.proxyURLs[subdomain], upstreamURL)
} }
} }

View File

@ -86,11 +86,25 @@ func TestRedirectURL(t *testing.T) {
func TestProxyURLs(t *testing.T) { func TestProxyURLs(t *testing.T) {
o := testOptions() o := testOptions()
o.Upstreams = append(o.Upstreams, "http://127.0.0.1:8081") 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()) assert.Equal(t, nil, o.Validate())
expected := []*url.URL{ 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:8080", Path: "/"},
{Scheme: "http", Host: "127.0.0.1:8081", 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) assert.Equal(t, expected, o.proxyURLs)
} }