From e1c3e938cc80a2827d2b84767189102eda0f5d35 Mon Sep 17 00:00:00 2001 From: Joel Speed Date: Tue, 26 May 2020 19:53:10 +0100 Subject: [PATCH] Add upstream package with Proxy server implementation --- pkg/upstream/file.go | 32 +++++++++ pkg/upstream/http.go | 157 +++++++++++++++++++++++++++++++++++++++++ pkg/upstream/proxy.go | 90 +++++++++++++++++++++++ pkg/upstream/static.go | 34 +++++++++ 4 files changed, 313 insertions(+) create mode 100644 pkg/upstream/file.go create mode 100644 pkg/upstream/http.go create mode 100644 pkg/upstream/proxy.go create mode 100644 pkg/upstream/static.go diff --git a/pkg/upstream/file.go b/pkg/upstream/file.go new file mode 100644 index 00000000..ec7492c5 --- /dev/null +++ b/pkg/upstream/file.go @@ -0,0 +1,32 @@ +package upstream + +import "net/http" + +const fileScheme = "file" + +// newFileServer creates a new fileServer that can serve requests +// to a file system location. +func newFileServer(id, path, fileSystemPath string) http.Handler { + return &fileServer{ + upstream: id, + handler: newFileServerForPath(path, fileSystemPath), + } +} + +// newFileServerForPath creates a http.Handler to serve files from the filesystem +func newFileServerForPath(path string, filesystemPath string) http.Handler { + return http.StripPrefix(path, http.FileServer(http.Dir(filesystemPath))) +} + +// fileServer represents a single filesystem upstream proxy +type fileServer struct { + upstream string + handler http.Handler +} + +// ServeHTTP proxies requests to the upstream provider while signing the +// request headers +func (u *fileServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + rw.Header().Set("GAP-Upstream-Address", u.upstream) + u.handler.ServeHTTP(rw, req) +} diff --git a/pkg/upstream/http.go b/pkg/upstream/http.go new file mode 100644 index 00000000..0c209103 --- /dev/null +++ b/pkg/upstream/http.go @@ -0,0 +1,157 @@ +package upstream + +import ( + "crypto/tls" + "net/http" + "net/http/httputil" + "net/url" + "strings" + + "github.com/mbland/hmacauth" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" + "github.com/yhat/wsutil" +) + +const ( + // SignatureHeader is the name of the request header containing the GAP Signature + // Part of hmacauth + SignatureHeader = "GAP-Signature" + + httpScheme = "http" + httpsScheme = "https" +) + +// SignatureHeaders contains the headers to be signed by the hmac algorithm +// Part of hmacauth +var SignatureHeaders = []string{ + "Content-Length", + "Content-Md5", + "Content-Type", + "Date", + "Authorization", + "X-Forwarded-User", + "X-Forwarded-Email", + "X-Forwarded-Preferred-User", + "X-Forwarded-Access-Token", + "Cookie", + "Gap-Auth", +} + +// newHTTPUpstreamProxy creates a new httpUpstreamProxy that can serve requests +// to a single upstream host. +func newHTTPUpstreamProxy(upstream options.Upstream, u *url.URL, sigData *options.SignatureData, errorHandler ProxyErrorHandler) http.Handler { + // Set path to empty so that request paths start at the server root + u.Path = "" + + // Create a ReverseProxy + proxy := newReverseProxy(u, upstream, errorHandler) + + // Set up a WebSocket proxy if required + var wsProxy http.Handler + if upstream.ProxyWebSockets { + wsProxy = newWebSocketReverseProxy(u, upstream.InsecureSkipTLSVerify) + } + + var auth hmacauth.HmacAuth + if sigData != nil { + auth = hmacauth.NewHmacAuth(sigData.Hash, []byte(sigData.Key), SignatureHeader, SignatureHeaders) + } + + return &httpUpstreamProxy{ + upstream: upstream.ID, + handler: proxy, + wsHandler: wsProxy, + auth: auth, + } +} + +// httpUpstreamProxy represents a single HTTP(S) upstream proxy +type httpUpstreamProxy struct { + upstream string + handler http.Handler + wsHandler http.Handler + auth hmacauth.HmacAuth +} + +// ServeHTTP proxies requests to the upstream provider while signing the +// request headers +func (h *httpUpstreamProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + rw.Header().Set("GAP-Upstream-Address", h.upstream) + if h.auth != nil { + req.Header.Set("GAP-Auth", rw.Header().Get("GAP-Auth")) + h.auth.SignRequest(req) + } + if h.wsHandler != nil && strings.EqualFold(req.Header.Get("Connection"), "upgrade") && req.Header.Get("Upgrade") == "websocket" { + h.wsHandler.ServeHTTP(rw, req) + } else { + h.handler.ServeHTTP(rw, req) + } +} + +// newReverseProxy creates a new reverse proxy for proxying requests to upstream +// servers based on the upstream configuration provided. +// The proxy should render an error page if there are failures connecting to the +// upstream server. +func newReverseProxy(target *url.URL, upstream options.Upstream, errorHandler ProxyErrorHandler) http.Handler { + proxy := httputil.NewSingleHostReverseProxy(target) + + // Configure options on the SingleHostReverseProxy + proxy.FlushInterval = *upstream.FlushInterval + if upstream.InsecureSkipTLSVerify { + proxy.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + } + + // Set the request director based on the PassHostHeader option + if !upstream.PassHostHeader { + setProxyUpstreamHostHeader(proxy, target) + } else { + setProxyDirector(proxy) + } + + // Set the error handler so that upstream connection failures render the + // error page instead of sending a empty response + if errorHandler != nil { + proxy.ErrorHandler = errorHandler + } + return proxy +} + +// setProxyUpstreamHostHeader sets the proxy.Director so that upstream requests +// receive a host header matching the target URL. +func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) { + director := proxy.Director + proxy.Director = func(req *http.Request) { + director(req) + // use RequestURI so that we aren't unescaping encoded slashes in the request path + req.Host = target.Host + req.URL.Opaque = req.RequestURI + req.URL.RawQuery = "" + } +} + +// setProxyDirector sets the proxy.Director so that request URIs are escaped +// when proxying to usptream servers. +func setProxyDirector(proxy *httputil.ReverseProxy) { + director := proxy.Director + proxy.Director = func(req *http.Request) { + director(req) + // use RequestURI so that we aren't unescaping encoded slashes in the request path + req.URL.Opaque = req.RequestURI + req.URL.RawQuery = "" + } +} + +// newWebSocketReverseProxy creates a new reverse proxy for proxying websocket connections. +func newWebSocketReverseProxy(u *url.URL, skipTLSVerify bool) http.Handler { + // This should create the correct scheme for insecure vs secure connections + wsScheme := "ws" + strings.TrimPrefix(u.Scheme, "http") + wsURL := &url.URL{Scheme: wsScheme, Host: u.Host} + + wsProxy := wsutil.NewSingleHostReverseProxy(wsURL) + if skipTLSVerify { + wsProxy.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } + return wsProxy +} diff --git a/pkg/upstream/proxy.go b/pkg/upstream/proxy.go new file mode 100644 index 00000000..6c7c581b --- /dev/null +++ b/pkg/upstream/proxy.go @@ -0,0 +1,90 @@ +package upstream + +import ( + "fmt" + "html/template" + "net/http" + "net/url" + + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" +) + +// ProxyErrorHandler is a function that will be used to render error pages when +// HTTP proxies fail to connect to upstream servers. +type ProxyErrorHandler func(http.ResponseWriter, *http.Request, error) + +// NewProxy creates a new multiUpstreamProxy that can serve requests directed to +// multiple upstreams. +func NewProxy(upstreams options.Upstreams, sigData *options.SignatureData, errorHandler ProxyErrorHandler) (http.Handler, error) { + m := &multiUpstreamProxy{ + serveMux: http.NewServeMux(), + } + + for _, upstream := range upstreams { + if upstream.Static { + m.registerStaticResponseHandler(upstream) + continue + } + + u, err := url.Parse(upstream.URI) + if err != nil { + return nil, fmt.Errorf("error parsing URI for upstream %q: %w", upstream.ID, err) + } + switch u.Scheme { + case fileScheme: + m.registerFileServer(upstream, u) + case httpScheme, httpsScheme: + m.registerHTTPUpstreamProxy(upstream, u, sigData, errorHandler) + default: + return nil, fmt.Errorf("unknown scheme for upstream %q: %q", upstream.ID, u.Scheme) + } + } + return m, nil +} + +// multiUpstreamProxy will serve requests directed to multiple upstream servers +// registered in the serverMux. +type multiUpstreamProxy struct { + serveMux *http.ServeMux +} + +// ServerHTTP handles HTTP requests. +func (m *multiUpstreamProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + m.serveMux.ServeHTTP(rw, req) +} + +// registerStaticResponseHandler registers a static response handler with at the given path. +func (m *multiUpstreamProxy) registerStaticResponseHandler(upstream options.Upstream) { + m.serveMux.Handle(upstream.Path, newStaticResponseHandler(upstream.ID, upstream.StaticCode)) +} + +// registerFileServer registers a new fileServer based on the configuration given. +func (m *multiUpstreamProxy) registerFileServer(upstream options.Upstream, u *url.URL) { + logger.Printf("mapping path %q => file system %q", upstream.Path, u.Path) + m.serveMux.Handle(upstream.Path, newFileServer(upstream.ID, upstream.Path, u.Path)) +} + +// registerHTTPUpstreamProxy registers a new httpUpstreamProxy based on the configuration given. +func (m *multiUpstreamProxy) registerHTTPUpstreamProxy(upstream options.Upstream, u *url.URL, sigData *options.SignatureData, errorHandler ProxyErrorHandler) { + logger.Printf("mapping path %q => upstream %q", upstream.Path, upstream.URI) + m.serveMux.Handle(upstream.Path, newHTTPUpstreamProxy(upstream, u, sigData, errorHandler)) +} + +// NewProxyErrorHandler creates a ProxyErrorHandler using the template given. +func NewProxyErrorHandler(errorTemplate *template.Template, proxyPrefix string) ProxyErrorHandler { + return func(rw http.ResponseWriter, req *http.Request, proxyErr error) { + logger.Printf("Error proxying to upstream server: %v", proxyErr) + rw.WriteHeader(http.StatusBadGateway) + data := struct { + Title string + Message string + ProxyPrefix string + }{ + Title: "Bad Gateway", + Message: "Error proxying to upstream server", + ProxyPrefix: proxyPrefix, + } + errorTemplate.Execute(rw, data) + } +} diff --git a/pkg/upstream/static.go b/pkg/upstream/static.go new file mode 100644 index 00000000..0a061421 --- /dev/null +++ b/pkg/upstream/static.go @@ -0,0 +1,34 @@ +package upstream + +import ( + "fmt" + "net/http" +) + +const defaultStaticResponseCode = 200 + +// newStaticResponseHandler creates a new staticResponseHandler that serves a +// a static response code. +func newStaticResponseHandler(upstream string, code *int) http.Handler { + if code == nil { + c := defaultStaticResponseCode + code = &c + } + return &staticResponseHandler{ + code: *code, + upstream: upstream, + } +} + +// staticResponseHandler responds with a static response with the given response code. +type staticResponseHandler struct { + code int + upstream string +} + +// ServeHTTP serves a static response. +func (s *staticResponseHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + rw.Header().Set("GAP-Upstream-Address", s.upstream) + rw.WriteHeader(s.code) + fmt.Fprintf(rw, "Authenticated") +}