Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ environment variables that you can set.
| `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_HEALTH_PATH` | The http health path to check before start port listening. | None |
| `HTTP_HEALTH_HOST` | The http health host to check before start port listening. | 127.0.0.1 |
| `HTTP_HEALTH_INTERVAL` | The http health path check interval (seconds). | 1 |
| `HTTP_HEALTH_TIMEOUT` | The http health path check timeout (seconds). | 1 |
| `HTTP_HEALTH_DEADLINE` | The http health path deadline interval (seconds), after which thruster will exit with error, if no success response. | 120 |
| `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 |
Expand All @@ -103,6 +108,32 @@ Thruster's environment variables can optionally be prefixed with `THRUSTER_`.
For example, `TLS_DOMAIN` can also be written as `THRUSTER_TLS_DOMAIN`. Whenever
a prefixed variable is set, it will take precedence over the unprefixed version.

### HTTP_HEALTH_PATH and rails

When using `HTTP_HEALTH_PATH` for health check, this endpoint should work over HTTP protocol and return 200 status code. In rails you can add in `config/routes.rb` such route:

```ruby
get '/health', to: 'rails/health#show', as: :rails_health_check
```

and add in `config/application.rb` such settings for hosts checks:

```ruby
config.host_authorization = {
exclude: ->(request) { request.path == '/health' }
}
```

If your environment have `config.assume_ssl = true` (not handle http to https redirects), in this case you done. But if you doing http to https redirects on rails side (like need on heroku router), you need also add in `config/application.rb` such settings:

```ruby
config.ssl_options = {
redirect: {
exclude: ->(request) { request.path == '/health' }
}
}
```

## Security

### BREACH Mitigation
Expand Down
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ module github.com/basecamp/thruster
go 1.25.6

require (
github.com/klauspost/compress v1.18.2
github.com/klauspost/compress v1.18.3
github.com/stretchr/testify v1.8.4
golang.org/x/crypto v0.46.0
golang.org/x/net v0.48.0
golang.org/x/crypto v0.47.0
golang.org/x/net v0.49.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/text v0.33.0 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
Expand All @@ -13,12 +13,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
46 changes: 30 additions & 16 deletions internal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@ const (
defaultStoragePath = "./storage/thruster"
defaultBadGatewayPage = "./public/502.html"

defaultHttpPort = 80
defaultHttpsPort = 443
defaultHttpIdleTimeout = 60 * time.Second
defaultHttpReadTimeout = 30 * time.Second
defaultHttpWriteTimeout = 30 * time.Second
defaultHttpPort = 80
defaultHttpsPort = 443
defaultHttpHealthHost = "127.0.0.1"
defaultHttpHealthTimeout = 1 * time.Second
defaultHttpHealthInterval = 1 * time.Second
defaultHttpHealthDeadline = 2 * time.Minute
defaultHttpIdleTimeout = 60 * time.Second
defaultHttpReadTimeout = 30 * time.Second
defaultHttpWriteTimeout = 30 * time.Second

defaultH2CEnabled = false

Expand Down Expand Up @@ -62,11 +66,16 @@ type Config struct {
StoragePath string
BadGatewayPage string

HttpPort int
HttpsPort int
HttpIdleTimeout time.Duration
HttpReadTimeout time.Duration
HttpWriteTimeout time.Duration
HttpPort int
HttpsPort int
HttpHealthHost string
HttpHealthPath string
HttpHealthTimeout time.Duration
HttpHealthInterval time.Duration
HttpHealthDeadline time.Duration
HttpIdleTimeout time.Duration
HttpReadTimeout time.Duration
HttpWriteTimeout time.Duration

H2CEnabled bool

Expand All @@ -89,7 +98,7 @@ func NewConfig() (*Config, error) {
config := &Config{
TargetPort: getEnvInt("TARGET_PORT", defaultTargetPort),
UpstreamCommand: os.Args[1],
UpstreamArgs: os.Args[2:],
UpstreamArgs: append([]string{}, os.Args[2:]...),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prevent UpstreamArgs mutation


CacheSizeBytes: getEnvInt("CACHE_SIZE", defaultCacheSize),
MaxCacheItemSizeBytes: getEnvInt("MAX_CACHE_ITEM_SIZE", defaultMaxCacheItemSizeBytes),
Expand All @@ -106,11 +115,16 @@ func NewConfig() (*Config, error) {
StoragePath: getEnvString("STORAGE_PATH", defaultStoragePath),
BadGatewayPage: getEnvString("BAD_GATEWAY_PAGE", defaultBadGatewayPage),

HttpPort: getEnvInt("HTTP_PORT", defaultHttpPort),
HttpsPort: getEnvInt("HTTPS_PORT", defaultHttpsPort),
HttpIdleTimeout: getEnvDuration("HTTP_IDLE_TIMEOUT", defaultHttpIdleTimeout),
HttpReadTimeout: getEnvDuration("HTTP_READ_TIMEOUT", defaultHttpReadTimeout),
HttpWriteTimeout: getEnvDuration("HTTP_WRITE_TIMEOUT", defaultHttpWriteTimeout),
HttpPort: getEnvInt("HTTP_PORT", defaultHttpPort),
HttpsPort: getEnvInt("HTTPS_PORT", defaultHttpsPort),
HttpHealthHost: getEnvString("HTTP_HEALTH_HOST", defaultHttpHealthHost),
HttpHealthPath: getEnvString("HTTP_HEALTH_PATH", ""),
HttpHealthInterval: getEnvDuration("HTTP_HEALTH_INTERVAL", defaultHttpHealthInterval),
HttpHealthTimeout: getEnvDuration("HTTP_HEALTH_TIMEOUT", defaultHttpHealthTimeout),
HttpHealthDeadline: getEnvDuration("HTTP_HEALTH_DEADLINE", defaultHttpHealthDeadline),
HttpIdleTimeout: getEnvDuration("HTTP_IDLE_TIMEOUT", defaultHttpIdleTimeout),
HttpReadTimeout: getEnvDuration("HTTP_READ_TIMEOUT", defaultHttpReadTimeout),
HttpWriteTimeout: getEnvDuration("HTTP_WRITE_TIMEOUT", defaultHttpWriteTimeout),

H2CEnabled: getEnvBool("H2C_ENABLED", defaultH2CEnabled),

Expand Down
39 changes: 39 additions & 0 deletions internal/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ func TestConfig_defaults(t *testing.T) {
assert.Equal(t, "echo", c.UpstreamCommand)
assert.Equal(t, defaultCacheSize, c.CacheSizeBytes)
assert.Equal(t, slog.LevelInfo, c.LogLevel)
assert.Equal(t, "", c.HttpHealthPath)
assert.Equal(t, "127.0.0.1", c.HttpHealthHost)
assert.Equal(t, 1*time.Second, c.HttpHealthTimeout)
assert.Equal(t, 1*time.Second, c.HttpHealthInterval)
assert.Equal(t, 2*time.Minute, c.HttpHealthDeadline)
assert.Equal(t, false, c.H2CEnabled)
}

Expand All @@ -118,6 +123,11 @@ func TestConfig_override_defaults_with_env_vars(t *testing.T) {
usingEnvVar(t, "DEBUG", "1")
usingEnvVar(t, "ACME_DIRECTORY", "https://acme-staging-v02.api.letsencrypt.org/directory")
usingEnvVar(t, "LOG_REQUESTS", "false")
usingEnvVar(t, "HTTP_HEALTH_PATH", "/health")
usingEnvVar(t, "HTTP_HEALTH_HOST", "localhost")
usingEnvVar(t, "HTTP_HEALTH_INTERVAL", "3")
usingEnvVar(t, "HTTP_HEALTH_TIMEOUT", "4")
usingEnvVar(t, "HTTP_HEALTH_DEADLINE", "60")
usingEnvVar(t, "H2C_ENABLED", "true")
usingEnvVar(t, "GZIP_COMPRESSION_DISABLE_ON_AUTH", "true")
usingEnvVar(t, "GZIP_COMPRESSION_JITTER", "64")
Expand All @@ -132,6 +142,11 @@ func TestConfig_override_defaults_with_env_vars(t *testing.T) {
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)
assert.Equal(t, "/health", c.HttpHealthPath)
assert.Equal(t, "localhost", c.HttpHealthHost)
assert.Equal(t, 3*time.Second, c.HttpHealthInterval)
assert.Equal(t, 4*time.Second, c.HttpHealthTimeout)
assert.Equal(t, 60*time.Second, c.HttpHealthDeadline)
assert.Equal(t, false, c.LogRequests)
assert.Equal(t, true, c.H2CEnabled)
assert.Equal(t, true, c.GzipCompressionDisableOnAuth)
Expand All @@ -146,6 +161,11 @@ func TestConfig_override_defaults_with_env_vars_using_prefix(t *testing.T) {
usingEnvVar(t, "THRUSTER_X_SENDFILE_ENABLED", "0")
usingEnvVar(t, "THRUSTER_DEBUG", "1")
usingEnvVar(t, "THRUSTER_LOG_REQUESTS", "0")
usingEnvVar(t, "THRUSTER_HTTP_HEALTH_PATH", "/health")
usingEnvVar(t, "THRUSTER_HTTP_HEALTH_HOST", "localhost")
usingEnvVar(t, "THRUSTER_HTTP_HEALTH_INTERVAL", "3")
usingEnvVar(t, "THRUSTER_HTTP_HEALTH_TIMEOUT", "4")
usingEnvVar(t, "THRUSTER_HTTP_HEALTH_DEADLINE", "60")
usingEnvVar(t, "THRUSTER_H2C_ENABLED", "1")

c, err := NewConfig()
Expand All @@ -157,6 +177,11 @@ func TestConfig_override_defaults_with_env_vars_using_prefix(t *testing.T) {
assert.Equal(t, false, c.XSendfileEnabled)
assert.Equal(t, slog.LevelDebug, c.LogLevel)
assert.Equal(t, false, c.LogRequests)
assert.Equal(t, "/health", c.HttpHealthPath)
assert.Equal(t, "localhost", c.HttpHealthHost)
assert.Equal(t, 3*time.Second, c.HttpHealthInterval)
assert.Equal(t, 4*time.Second, c.HttpHealthTimeout)
assert.Equal(t, 60*time.Second, c.HttpHealthDeadline)
assert.Equal(t, true, c.H2CEnabled)
}

Expand All @@ -171,6 +196,20 @@ func TestConfig_prefixed_variables_take_precedence_over_non_prefixed(t *testing.
assert.Equal(t, 4000, c.TargetPort)
}

func TestConfig_defaults_are_used_if_strconv_fails(t *testing.T) {
usingProgramArgs(t, "thruster", "echo", "hello")
usingEnvVar(t, "TARGET_PORT", "should-be-an-int")
usingEnvVar(t, "HTTP_IDLE_TIMEOUT", "should-be-a-duration")
usingEnvVar(t, "X_SENDFILE_ENABLED", "should-be-a-bool")

c, err := NewConfig()
require.NoError(t, err)

assert.Equal(t, 3000, c.TargetPort)
assert.Equal(t, 60*time.Second, c.HttpIdleTimeout)
assert.Equal(t, true, c.XSendfileEnabled)
}

func TestConfig_return_error_when_no_upstream_command(t *testing.T) {
usingProgramArgs(t, "thruster")

Expand Down
Loading