All checks were successful
check / check (push) Successful in 3m41s
The plaintext listener bound `:PORT`, so it answered on every interface with no way to say otherwise. That published the admin UI and the unauthenticated receiver in cleartext beside whatever TLS proxy was in front of them, reachable from any host that could route to the machine. BIND_ADDRESS now selects the address. The binary defaults to 127.0.0.1, which is the safe answer for a bare host: reaching webhooker from elsewhere becomes a deliberate act. The image sets 0.0.0.0, which is the correct answer inside a container, where the network namespace is already the boundary and exposure is decided by the publish flag instead — so `-p 127.0.0.1:8080:8080` is what the README shows. Existing container deployments are unaffected. Only IP address literals are accepted: hostnames, host:port and CIDR blocks abort startup naming the variable and the value, and a literal that is not an address of this host fails at listen and exits non-zero. The http.Server is now built in New rather than in the serving goroutine, and sentryEnabled is atomic. Both fields were written by the serving goroutine and read by the fx stop hook with nothing ordering them, and the OnStart hook returns before that goroutine has necessarily run: cleanShutdown could dereference a nil httpServer on an early SIGTERM, and both reads raced. No test started and stopped the server, so nothing observed it. Closes #226. README gains a "Deployment behind a reverse proxy" section: a working nginx server block, and the five things that are silent when wrong — bind or firewall the app port, WEBHOOKER_ENVIRONMENT=prod, TRUSTED_PROXIES, Host as $http_host rather than $host, and keeping the proxy's access log because webhooker's own records only the proxy.
104 lines
3.1 KiB
Go
104 lines
3.1 KiB
Go
package server
|
|
|
|
import (
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"github.com/getsentry/sentry-go"
|
|
"sneak.berlin/go/webhooker/internal/config"
|
|
"sneak.berlin/go/webhooker/internal/handlers"
|
|
"sneak.berlin/go/webhooker/internal/middleware"
|
|
)
|
|
|
|
// MaxFormBodySizeForTest exposes the form body cap so tests can
|
|
// build requests that sit exactly at, below, and above it.
|
|
const MaxFormBodySizeForTest = maxFormBodySize
|
|
|
|
// ScrubSentryRequestForTest exposes the BeforeSend hook that
|
|
// enableSentry installs, so a test can assert on what it leaves in an
|
|
// event without standing up a Sentry client.
|
|
func ScrubSentryRequestForTest(
|
|
event *sentry.Event,
|
|
hint *sentry.EventHint,
|
|
) *sentry.Event {
|
|
return scrubSentryRequest(event, hint)
|
|
}
|
|
|
|
// SentryClientOptionsForTest exposes the exact options enableSentry
|
|
// initialises the SDK with, so a test can capture events through the
|
|
// production hook wiring rather than a hand-built equivalent.
|
|
func SentryClientOptionsForTest(
|
|
dsn, release string,
|
|
) sentry.ClientOptions {
|
|
return sentryClientOptions(dsn, release)
|
|
}
|
|
|
|
// NewRouterForTest builds the real route tree via SetupRoutes with
|
|
// the supplied middleware and handlers, bypassing the fx lifecycle
|
|
// and the HTTP listener. Tests use it so that route-group middleware
|
|
// registration order is exercised exactly as it ships, rather than
|
|
// against a hand-rebuilt chain that could drift from routes.go.
|
|
func NewRouterForTest(
|
|
log *slog.Logger,
|
|
cfg *config.Config,
|
|
mw *middleware.Middleware,
|
|
h *handlers.Handlers,
|
|
) http.Handler {
|
|
s := &Server{
|
|
log: log,
|
|
mw: mw,
|
|
h: h,
|
|
params: ServerParams{Config: cfg},
|
|
}
|
|
s.SetupRoutes()
|
|
|
|
return s.router
|
|
}
|
|
|
|
// ListenAddrForTest exposes the address the HTTP listener binds for
|
|
// a given Config, so the rendering of host and port — IPv6
|
|
// bracketing above all — can be pinned without standing up a
|
|
// listener.
|
|
func ListenAddrForTest(cfg *config.Config) string {
|
|
s := &Server{params: ServerParams{Config: cfg}}
|
|
|
|
return s.listenAddr()
|
|
}
|
|
|
|
// ProbePattern is the route NewRouterWithProbeForTest adds to the
|
|
// production route tree.
|
|
const ProbePattern = "/probe"
|
|
|
|
// NewRouterWithProbeForTest builds the production route tree exactly
|
|
// as NewRouterForTest does and then registers probe at ProbePattern,
|
|
// so a test can drive a handler that panics through the shipped
|
|
// global middleware chain rather than a hand-assembled one. Nothing
|
|
// about the chain is rebuilt here: the probe is an extra leaf under
|
|
// the same Use() registrations every other route gets.
|
|
//
|
|
// sentryEnabled selects whether the sentryhttp handler is registered,
|
|
// which in production a configured SENTRY_DSN decides. It is a
|
|
// parameter because the relationship between that handler's Repanic
|
|
// option and the recoverer registered outside it is the thing a test
|
|
// has to be able to pin.
|
|
func NewRouterWithProbeForTest(
|
|
log *slog.Logger,
|
|
cfg *config.Config,
|
|
mw *middleware.Middleware,
|
|
h *handlers.Handlers,
|
|
sentryEnabled bool,
|
|
probe http.HandlerFunc,
|
|
) http.Handler {
|
|
s := &Server{
|
|
log: log,
|
|
mw: mw,
|
|
h: h,
|
|
params: ServerParams{Config: cfg},
|
|
}
|
|
s.sentryEnabled.Store(sentryEnabled)
|
|
s.SetupRoutes()
|
|
s.router.Handle(ProbePattern, probe)
|
|
|
|
return s.router
|
|
}
|