diff --git a/README.md b/README.md index 23eccf5..5f4d36e 100644 --- a/README.md +++ b/README.md @@ -73,25 +73,26 @@ configuration. But if you need to customize its behavior, there are a few environment variables that you can set. | Variable Name | Description | Default Value | -|-----------------------|---------------------------------------------------------|---------------| -| `TLS_DOMAIN` | Comma-separated list of domain names to use for TLS provisioning. If not set, TLS will be disabled. | None | -| `TARGET_PORT` | The port that your Puma server should run on. Thruster will set `PORT` to this value when starting your server. | 3000 | -| `CACHE_SIZE` | The size of the HTTP cache in bytes. | 64MB | -| `MAX_CACHE_ITEM_SIZE` | The maximum size of a single item in the HTTP cache in bytes. | 1MB | -| `X_SENDFILE_ENABLED` | Whether to enable X-Sendfile support. Set to `0` or `false` to disable. | Enabled | -| `MAX_REQUEST_BODY` | The maximum size of a request body in bytes. Requests larger than this size will be refused; `0` means no maximum size is enforced. | `0` | -| `STORAGE_PATH` | The path to store Thruster's internal state. Provisioned TLS certificates will be stored here, so that they will not need to be requested every time your application is started. | `./storage/thruster` | -| `BAD_GATEWAY_PAGE` | Path to an HTML file to serve when the backend server returns a 502 Bad Gateway error. If there is no file at the specific path, Thruster will serve an empty 502 response instead. Because Thruster boots very quickly, a custom page can be a useful way to show that your application is starting up. | `./public/502.html` | -| `HTTP_PORT` | The port to listen on for HTTP traffic. | 80 | -| `HTTPS_PORT` | The port to listen on for HTTPS traffic. | 443 | -| `HTTP_IDLE_TIMEOUT` | The maximum time in seconds that a client can be idle before the connection is closed. | 60 | -| `HTTP_READ_TIMEOUT` | The maximum time in seconds that a client can take to send the request headers and body. | 30 | -| `HTTP_WRITE_TIMEOUT` | The maximum time in seconds during which the client must read the response. | 30 | -| `ACME_DIRECTORY` | The URL of the ACME directory to use for TLS certificate provisioning. | `https://acme-v02.api.letsencrypt.org/directory` (Let's Encrypt production) | -| `EAB_KID` | The EAB key identifier to use when provisioning TLS certificates, if required. | None | -| `EAB_HMAC_KEY` | The Base64-encoded EAB HMAC key to use when provisioning TLS certificates, if required. | None | -| `FORWARD_HEADERS` | Whether to forward X-Forwarded-* headers from the client. | Disabled when running with TLS; enabled otherwise | -| `DEBUG` | Set to `1` or `true` to enable debug logging. | Disabled | +|-----------------------------|---------------------------------------------------------|---------------| +| `TLS_DOMAIN` | Comma-separated list of domain names to use for TLS provisioning. If not set, TLS will be disabled. | None | +| `TARGET_PORT` | The port that your Puma server should run on. Thruster will set `PORT` to this value when starting your server. | 3000 | +| `CACHE_SIZE` | The size of the HTTP cache in bytes. | 64MB | +| `MAX_CACHE_ITEM_SIZE` | The maximum size of a single item in the HTTP cache in bytes. | 1MB | +| `GZIP_COMPRESSION_ENABLED` | Whether to enable gzip compression for static assets. Set to `0` or `false` to disable. | Enabled | +| `X_SENDFILE_ENABLED` | Whether to enable X-Sendfile support. Set to `0` or `false` to disable. | Enabled | +| `MAX_REQUEST_BODY` | The maximum size of a request body in bytes. Requests larger than this size will be refused; `0` means no maximum size is enforced. | `0` | +| `STORAGE_PATH` | The path to store Thruster's internal state. Provisioned TLS certificates will be stored here, so that they will not need to be requested every time your application is started. | `./storage/thruster` | +| `BAD_GATEWAY_PAGE` | Path to an HTML file to serve when the backend server returns a 502 Bad Gateway error. If there is no file at the specific path, Thruster will serve an empty 502 response instead. Because Thruster boots very quickly, a custom page can be a useful way to show that your application is starting up. | `./public/502.html` | +| `HTTP_PORT` | The port to listen on for HTTP traffic. | 80 | +| `HTTPS_PORT` | The port to listen on for HTTPS traffic. | 443 | +| `HTTP_IDLE_TIMEOUT` | The maximum time in seconds that a client can be idle before the connection is closed. | 60 | +| `HTTP_READ_TIMEOUT` | The maximum time in seconds that a client can take to send the request headers and body. | 30 | +| `HTTP_WRITE_TIMEOUT` | The maximum time in seconds during which the client must read the response. | 30 | +| `ACME_DIRECTORY` | The URL of the ACME directory to use for TLS certificate provisioning. | `https://acme-v02.api.letsencrypt.org/directory` (Let's Encrypt production) | +| `EAB_KID` | The EAB key identifier to use when provisioning TLS certificates, if required. | None | +| `EAB_HMAC_KEY` | The Base64-encoded EAB HMAC key to use when provisioning TLS certificates, if required. | None | +| `FORWARD_HEADERS` | Whether to forward X-Forwarded-* headers from the client. | Disabled when running with TLS; enabled otherwise | +| `DEBUG` | Set to `1` or `true` to enable debug logging. | Disabled | To prevent naming clashes with your application's own environment variables, Thruster's environment variables can optionally be prefixed with `THRUSTER_`. diff --git a/internal/config.go b/internal/config.go index d0d2ce2..655f9eb 100644 --- a/internal/config.go +++ b/internal/config.go @@ -41,10 +41,11 @@ type Config struct { UpstreamCommand string UpstreamArgs []string - CacheSizeBytes int - MaxCacheItemSizeBytes int - XSendfileEnabled bool - MaxRequestBody int + CacheSizeBytes int + MaxCacheItemSizeBytes int + XSendfileEnabled bool + GzipCompressionEnabled bool + MaxRequestBody int TLSDomains []string ACMEDirectoryURL string @@ -79,10 +80,11 @@ func NewConfig() (*Config, error) { UpstreamCommand: os.Args[1], UpstreamArgs: os.Args[2:], - CacheSizeBytes: getEnvInt("CACHE_SIZE", defaultCacheSize), - MaxCacheItemSizeBytes: getEnvInt("MAX_CACHE_ITEM_SIZE", defaultMaxCacheItemSizeBytes), - XSendfileEnabled: getEnvBool("X_SENDFILE_ENABLED", true), - MaxRequestBody: getEnvInt("MAX_REQUEST_BODY", defaultMaxRequestBody), + CacheSizeBytes: getEnvInt("CACHE_SIZE", defaultCacheSize), + MaxCacheItemSizeBytes: getEnvInt("MAX_CACHE_ITEM_SIZE", defaultMaxCacheItemSizeBytes), + XSendfileEnabled: getEnvBool("X_SENDFILE_ENABLED", true), + GzipCompressionEnabled: getEnvBool("GZIP_COMPRESSION_ENABLED", true), + MaxRequestBody: getEnvInt("MAX_REQUEST_BODY", defaultMaxRequestBody), TLSDomains: getEnvStrings("TLS_DOMAIN", []string{}), ACMEDirectoryURL: getEnvString("ACME_DIRECTORY", defaultACMEDirectoryURL), diff --git a/internal/config_test.go b/internal/config_test.go index c8415d9..7446693 100644 --- a/internal/config_test.go +++ b/internal/config_test.go @@ -113,6 +113,7 @@ func TestConfig_override_defaults_with_env_vars(t *testing.T) { usingEnvVar(t, "CACHE_SIZE", "256") usingEnvVar(t, "HTTP_READ_TIMEOUT", "5") usingEnvVar(t, "X_SENDFILE_ENABLED", "0") + usingEnvVar(t, "GZIP_COMPRESSION_ENABLED", "0") usingEnvVar(t, "DEBUG", "1") usingEnvVar(t, "ACME_DIRECTORY", "https://acme-staging-v02.api.letsencrypt.org/directory") @@ -123,6 +124,7 @@ func TestConfig_override_defaults_with_env_vars(t *testing.T) { assert.Equal(t, 256, c.CacheSizeBytes) assert.Equal(t, 5*time.Second, c.HttpReadTimeout) assert.Equal(t, false, c.XSendfileEnabled) + assert.Equal(t, false, c.GzipCompressionEnabled) assert.Equal(t, slog.LevelDebug, c.LogLevel) assert.Equal(t, "https://acme-staging-v02.api.letsencrypt.org/directory", c.ACMEDirectoryURL) } diff --git a/internal/handler.go b/internal/handler.go index 11feac9..c9fbafa 100644 --- a/internal/handler.go +++ b/internal/handler.go @@ -15,6 +15,7 @@ type HandlerOptions struct { maxRequestBody int targetUrl *url.URL xSendfileEnabled bool + gzipCompressionEnabled bool forwardHeaders bool } @@ -22,7 +23,9 @@ func NewHandler(options HandlerOptions) http.Handler { handler := NewProxyHandler(options.targetUrl, options.badGatewayPage, options.forwardHeaders) handler = NewCacheHandler(options.cache, options.maxCacheableResponseBody, handler) handler = NewSendfileHandler(options.xSendfileEnabled, handler) - handler = gzhttp.GzipHandler(handler) + if options.gzipCompressionEnabled { + handler = gzhttp.GzipHandler(handler) + } if options.maxRequestBody > 0 { handler = http.MaxBytesHandler(handler, int64(options.maxRequestBody)) diff --git a/internal/handler_test.go b/internal/handler_test.go index a3485b6..77d2584 100644 --- a/internal/handler_test.go +++ b/internal/handler_test.go @@ -33,6 +33,30 @@ func TestHandlerGzipCompression_when_proxying(t *testing.T) { assert.Less(t, transferredSize, fixtureLength("loremipsum.txt")) } +func TestNotHandlerGzipCompression_when_disabled(t *testing.T) { + fixtureLength := strconv.FormatInt(fixtureLength("loremipsum.txt"), 10) + + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Length", fixtureLength) + w.Write(fixtureContent("loremipsum.txt")) + })) + defer upstream.Close() + + options := handlerOptions(upstream.URL) + options.gzipCompressionEnabled = false + h := NewHandler(options) + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + r.Header.Set("Accept-Encoding", "gzip") + h.ServeHTTP(w, r) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Header().Get("Content-Type"), "text/plain") + assert.Empty(t, w.Header().Get("Content-Encoding")) + assert.Equal(t, fixtureLength, w.Header().Get("Content-Length")) +} + func TestHandlerGzipCompression_is_not_applied_when_not_requested(t *testing.T) { fixtureLength := strconv.FormatInt(fixtureLength("loremipsum.txt"), 10) @@ -248,6 +272,7 @@ func handlerOptions(targetUrl string) HandlerOptions { cache: NewMemoryCache(defaultCacheSize, defaultMaxCacheItemSizeBytes), targetUrl: url, xSendfileEnabled: true, + gzipCompressionEnabled: true, maxCacheableResponseBody: 1024, badGatewayPage: "", forwardHeaders: true, diff --git a/internal/service.go b/internal/service.go index 5418eb0..04fee25 100644 --- a/internal/service.go +++ b/internal/service.go @@ -22,6 +22,7 @@ func (s *Service) Run() int { cache: s.cache(), targetUrl: s.targetUrl(), xSendfileEnabled: s.config.XSendfileEnabled, + gzipCompressionEnabled: s.config.GzipCompressionEnabled, maxCacheableResponseBody: s.config.MaxCacheItemSizeBytes, maxRequestBody: s.config.MaxRequestBody, badGatewayPage: s.config.BadGatewayPage,