package feature import ( "context" "net/http" "net/http/httputil" "net/url" "go.uber.org/zap" ) // HTTPProxy is an HTTP proxy that's guided by a feature flag. If the feature flag // presented to it is enabled, it will perform the proxying behavior. Otherwise // it will be a no-op. type HTTPProxy struct { proxy *httputil.ReverseProxy logger *zap.Logger enabler ProxyEnabler } // NewHTTPProxy returns a new Proxy. func NewHTTPProxy(dest *url.URL, logger *zap.Logger, enabler ProxyEnabler) *HTTPProxy { return &HTTPProxy{ proxy: newReverseProxy(dest, enabler.Key()), logger: logger, enabler: enabler, } } // Do performs the proxying. It returns whether or not the request was proxied. func (p *HTTPProxy) Do(w http.ResponseWriter, r *http.Request) bool { if p.enabler.Enabled(r.Context()) { p.proxy.ServeHTTP(w, r) return true } return false } const ( // headerProxyFlag is the HTTP header for enriching the request and response // with the feature flag key that precipitated the proxying behavior. headerProxyFlag = "X-Platform-Proxy-Flag" ) // newReverseProxy creates a new single-host reverse proxy. func newReverseProxy(dest *url.URL, enablerKey string) *httputil.ReverseProxy { proxy := httputil.NewSingleHostReverseProxy(dest) defaultDirector := proxy.Director proxy.Director = func(r *http.Request) { defaultDirector(r) r.Header.Set(headerProxyFlag, enablerKey) // Override r.Host to prevent us sending this request back to ourselves. // A bug in the stdlib causes this value to be preferred over the // r.URL.Host (which is set in the default Director) if r.Host isn't // empty (which it isn't). // https://github.com/golang/go/issues/28168 r.Host = dest.Host } proxy.ModifyResponse = func(r *http.Response) error { r.Header.Set(headerProxyFlag, enablerKey) return nil } return proxy } // ProxyEnabler is a boolean feature flag. type ProxyEnabler interface { Key() string Enabled(ctx context.Context, fs ...Flagger) bool }