From 76fa56777fa64cf3cdb585ad55536617cd2561ce Mon Sep 17 00:00:00 2001 From: Umputun Date: Sat, 11 Sep 2021 14:38:56 -0500 Subject: [PATCH] add ability to drop incoming headers #108 In some cases proxy should sanitize incoming headers. --drop-header and $DROP_HEADERS set list of headers (keys) and those headers removed from the request. --- .github/workflows/ci.yml | 4 ++-- README.md | 7 +++++-- app/main.go | 6 ++++-- app/mgmt/metrics.go | 2 +- app/proxy/handlers.go | 13 ++++++++++--- app/proxy/handlers_test.go | 8 +++++++- app/proxy/proxy.go | 25 +++++++++++++------------ 7 files changed, 42 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81ed835..c60d99d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: set up go 1.16 + - name: set up go 1.17 uses: actions/setup-go@v2 with: - go-version: 1.16 + go-version: 1.17 id: go - name: checkout diff --git a/README.md b/README.md index 32e62b6..8a2ce54 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Reproxy is a simple edge HTTP(s) server / reverse proxy supporting various provi - Docker container distribution - Built-in static assets server with optional "SPA friendly" mode - Support for redirect rules -- Optional limiter for the overall activity as well as for user's activity +- Optional limiter for the overall activity as well as for user's activity - Live health check and fail-over/load-balancing - Management server with routes info and prometheus metrics - Plugins support via RPC to implement custom functionality @@ -225,6 +225,7 @@ supported codes: - `--gzip` enables gzip compression for responses. - `--max=N` allows to set the maximum size of request (default 64k). Setting it to `0` disables the size check. - `--header` sets extra header(s) added to each proxied response. +- `--drop-header` drops headers from incoming request. For example this is how it can be done with the docker compose: @@ -324,7 +325,8 @@ This is the list of all options supporting multiple elements: -l, --listen= listen on host:port (default: 0.0.0.0:8080/8443 under docker, 127.0.0.1:80/443 without) [$LISTEN] -m, --max= max request size (default: 64K) [$MAX_SIZE] -g, --gzip enable gz compression [$GZIP] - -x, --header= proxy headers + -x, --header= outgoing proxy headers to add + --drop-header= incoming headers to drop [$DROP_HEADERS] --lb-type=[random|failover] load balancer type (default: random) [$LB_TYPE] --signature enable reproxy signature headers [$SIGNATURE] --dbg debug mode [$DEBUG] @@ -407,6 +409,7 @@ plugin: Help Options: -h, --help Show this help message + ``` ## Status diff --git a/app/main.go b/app/main.go index c0b88e0..9ba9fec 100644 --- a/app/main.go +++ b/app/main.go @@ -33,7 +33,8 @@ var opts struct { Listen string `short:"l" long:"listen" env:"LISTEN" description:"listen on host:port (default: 0.0.0.0:8080/8443 under docker, 127.0.0.1:80/443 without)"` MaxSize string `short:"m" long:"max" env:"MAX_SIZE" default:"64K" description:"max request size"` GzipEnabled bool `short:"g" long:"gzip" env:"GZIP" description:"enable gz compression"` - ProxyHeaders []string `short:"x" long:"header" description:"proxy headers"` // env HEADER split in code to allow , inside "" + ProxyHeaders []string `short:"x" long:"header" description:"outgoing proxy headers to add"` // env HEADER split in code to allow , inside "" + DropHeaders []string `long:"drop-header" env:"DROP_HEADERS" description:"incoming headers to drop" env-delim:","` LBType string `long:"lb-type" env:"LB_TYPE" description:"load balancer type" choice:"random" choice:"failover" default:"random"` //nolint @@ -239,6 +240,7 @@ func run() error { GzEnabled: opts.GzipEnabled, SSLConfig: sslConfig, ProxyHeaders: proxyHeaders, + DropHeader: opts.DropHeaders, AccessLog: accessLog, StdOutEnabled: opts.Logger.StdOut, Signature: opts.Signature, @@ -258,7 +260,7 @@ func run() error { Reporter: errReporter, PluginConductor: makePluginConductor(ctx), ThrottleSystem: opts.Throttle.System * 3, - ThottleUser: opts.Throttle.User, + ThrottleUser: opts.Throttle.User, } err = px.Run(ctx) diff --git a/app/mgmt/metrics.go b/app/mgmt/metrics.go index 951f985..c393f98 100644 --- a/app/mgmt/metrics.go +++ b/app/mgmt/metrics.go @@ -45,7 +45,7 @@ func NewMetrics() *Metrics { Buckets: []float64{0.01, 0.1, 0.5, 1, 2, 3, 5}, }, []string{"path"}) - prometheus.Unregister(prometheus.NewGoCollector()) + prometheus.Unregister(prometheus.NewGoCollector()) //nolint if err := prometheus.Register(res.totalRequests); err != nil { log.Printf("[WARN] can't register prometheus totalRequests, %v", err) diff --git a/app/proxy/handlers.go b/app/proxy/handlers.go index d424115..7334479 100644 --- a/app/proxy/handlers.go +++ b/app/proxy/handlers.go @@ -14,21 +14,28 @@ import ( "github.com/umputun/reproxy/app/discovery" ) -func headersHandler(headers []string) func(next http.Handler) http.Handler { +func headersHandler(addHeaders, dropHeaders []string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if len(headers) == 0 { + if len(addHeaders) == 0 && len(dropHeaders) == 0 { next.ServeHTTP(w, r) return } - for _, h := range headers { + // add headers to response + for _, h := range addHeaders { elems := strings.Split(h, ":") if len(elems) != 2 { continue } w.Header().Set(strings.TrimSpace(elems[0]), strings.TrimSpace(elems[1])) } + + // drop headers from request + for _, h := range dropHeaders { + r.Header.Del(h) + } + next.ServeHTTP(w, r) }) } diff --git a/app/proxy/handlers_test.go b/app/proxy/handlers_test.go index 7aff1bd..f9c28f8 100644 --- a/app/proxy/handlers_test.go +++ b/app/proxy/handlers_test.go @@ -18,11 +18,17 @@ import ( func Test_headersHandler(t *testing.T) { wr := httptest.NewRecorder() - handler := headersHandler([]string{"k1:v1", "k2:v2"})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := headersHandler([]string{"k1:v1", "k2:v2"}, []string{"r1", "r2"})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Logf("req: %v", r) + assert.Equal(t, "", r.Header.Get("r1"), "r1 header dropped") + assert.Equal(t, "", r.Header.Get("r2"), "r2 header dropped") + assert.Equal(t, "rv3", r.Header.Get("r3"), "r3 kept") })) req, err := http.NewRequest("GET", "http://example.com", nil) require.NoError(t, err) + req.Header.Set("r1", "rv1") + req.Header.Set("r2", "rv2") + req.Header.Set("r3", "rv3") handler.ServeHTTP(wr, req) assert.Equal(t, "v1", wr.Result().Header.Get("k1")) assert.Equal(t, "v2", wr.Result().Header.Get("k2")) diff --git a/app/proxy/proxy.go b/app/proxy/proxy.go index 91b16bc..1b235be 100644 --- a/app/proxy/proxy.go +++ b/app/proxy/proxy.go @@ -32,6 +32,7 @@ type Http struct { // nolint golint MaxBodySize int64 GzEnabled bool ProxyHeaders []string + DropHeader []string SSLConfig SSLConfig Version string AccessLog io.Writer @@ -45,7 +46,7 @@ type Http struct { // nolint golint LBSelector func(len int) int ThrottleSystem int - ThottleUser int + ThrottleUser int } // Matcher source info (server and route) to the destination url @@ -110,17 +111,17 @@ func (h *Http) Run(ctx context.Context) error { }() handler := R.Wrap(h.proxyHandler(), - R.Recoverer(log.Default()), // recover on errors - signatureHandler(h.Signature, h.Version), // send app signature - h.pingHandler, // respond to /ping - h.healthMiddleware, // respond to /health - h.matchHandler, // set matched routes to context - limiterSystemHandler(h.ThrottleSystem), // limit total requests/sec - limiterUserHandler(h.ThottleUser), // req/seq per user/route match - h.mgmtHandler(), // handles /metrics and /routes for prometheus - h.pluginHandler(), // prc to external plugins - headersHandler(h.ProxyHeaders), // set response headers - accessLogHandler(h.AccessLog), // apache-format log file + R.Recoverer(log.Default()), // recover on errors + signatureHandler(h.Signature, h.Version), // send app signature + h.pingHandler, // respond to /ping + h.healthMiddleware, // respond to /health + h.matchHandler, // set matched routes to context + limiterSystemHandler(h.ThrottleSystem), // limit total requests/sec + limiterUserHandler(h.ThrottleUser), // req/seq per user/route match + h.mgmtHandler(), // handles /metrics and /routes for prometheus + h.pluginHandler(), // prc to external plugins + headersHandler(h.ProxyHeaders, h.DropHeader), // add response headers and delete some request headers + accessLogHandler(h.AccessLog), // apache-format log file stdoutLogHandler(h.StdOutEnabled, logger.New(logger.Log(log.Default()), logger.Prefix("[INFO]")).Handler), maxReqSizeHandler(h.MaxBodySize), // limit request max size gzipHandler(h.GzEnabled), // gzip response