All checks were successful
check / check (push) Successful in 3m6s
The `method` label was recorded as `r.Method` verbatim. net/http
accepts any RFC 9110 token as a method and passes it through, so the
label was bounded at nothing: 300 requests to `/` carrying random
12-character method tokens took a live scrape from 81 lines to 7,606,
unauthenticated and on a route with no rate limiter. This is the same
remote memory-exhaustion vector the handler label carried, reached
through a second dimension.
The fix goes in the same recorder seam that bounds the handler label,
which is renamed to reflect that it now bounds both. A method the
router can route is kept verbatim, so real methods stay
distinguishable; anything else carries the existing `(unmatched)`
sentinel, deliberately the same spelling rather than a second one for
the same idea. The bound is ten values.
The retained set is restated against the net/http constants because
chi's own methodMap is unexported. A token outside it can only ever
produce chi's 405, so folding those together loses nothing a scrape
could have used.
README's Metrics section gains the inbound HTTP metrics, the bound on
each of their labels, and the aggregate
`http_requests_inflight{handler="(all)"}` semantics.
150 lines
4.2 KiB
Go
150 lines
4.2 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
|
|
httpmetrics "github.com/slok/go-http-metrics/metrics"
|
|
)
|
|
|
|
// MetricsMiddlewareForTest builds the metrics recording middleware
|
|
// against a caller-supplied recorder, so a test can gather from its
|
|
// own Prometheus registry rather than the process-wide default one
|
|
// that Middleware.Metrics uses.
|
|
func MetricsMiddlewareForTest(
|
|
rec httpmetrics.Recorder,
|
|
) func(http.Handler) http.Handler {
|
|
return metricsMiddleware(rec)
|
|
}
|
|
|
|
// UnmatchedRouteConst exposes the sentinel that stands in for a
|
|
// request matching no route pattern.
|
|
const UnmatchedRouteConst = unmatchedRoute
|
|
|
|
// InflightHandlerConst exposes the fixed handler label on the
|
|
// inflight gauge.
|
|
const InflightHandlerConst = inflightHandler
|
|
|
|
// UnmatchedMethodConst exposes the sentinel that stands in for a
|
|
// method the router can never route.
|
|
const UnmatchedMethodConst = unmatchedMethod
|
|
|
|
// NewLoggingResponseWriterForTest wraps newLoggingResponseWriter
|
|
// for use in external test packages.
|
|
func NewLoggingResponseWriterForTest(
|
|
w http.ResponseWriter,
|
|
) *loggingResponseWriter {
|
|
return newLoggingResponseWriter(w)
|
|
}
|
|
|
|
// LoggingResponseWriterStatusCode returns the status code
|
|
// captured by the loggingResponseWriter.
|
|
func LoggingResponseWriterStatusCode(
|
|
lrw *loggingResponseWriter,
|
|
) int {
|
|
return lrw.statusCode
|
|
}
|
|
|
|
// IPFromHostPort exposes ipFromHostPort for testing.
|
|
func IPFromHostPort(hp string) string {
|
|
return ipFromHostPort(hp)
|
|
}
|
|
|
|
// ClientKeyForTest exposes clientKey for testing.
|
|
func ClientKeyForTest(m *Middleware, r *http.Request) string {
|
|
return m.clientKey(r)
|
|
}
|
|
|
|
// IsClientTLS exposes isClientTLS for testing.
|
|
func IsClientTLS(r *http.Request) bool {
|
|
return isClientTLS(r)
|
|
}
|
|
|
|
// LoginRateLimitConst exposes the loginRateLimit constant: the
|
|
// number of FAILED login attempts one client may make against one
|
|
// submitted username per interval.
|
|
const LoginRateLimitConst = loginRateLimit
|
|
|
|
// LoginFailureMaxKeysConst exposes the cap on each of the login
|
|
// guard's key sets.
|
|
const LoginFailureMaxKeysConst = loginFailureMaxKeys
|
|
|
|
// PasswordVerifyConcurrencyConst exposes the bound on concurrent
|
|
// Argon2id verifications.
|
|
const PasswordVerifyConcurrencyConst = passwordVerifyConcurrency
|
|
|
|
// PasswordVerifyMaxWaitersConst exposes the bound on how many
|
|
// requests may queue for a verification slot.
|
|
const PasswordVerifyMaxWaitersConst = passwordVerifyMaxWaiters
|
|
|
|
// LoginGuard is the login failure counter and verification
|
|
// semaphore, exposed for direct testing.
|
|
type LoginGuard = loginGuard
|
|
|
|
// NewLoginGuardForTest builds a guard with test-sized parameters.
|
|
func NewLoginGuardForTest(
|
|
limit int,
|
|
interval time.Duration,
|
|
maxKeys, concurrency, maxWaiters int,
|
|
wait time.Duration,
|
|
) *LoginGuard {
|
|
return newLoginGuard(
|
|
limit, interval, maxKeys, concurrency, maxWaiters, wait,
|
|
)
|
|
}
|
|
|
|
// QueuedWaitersForTest reports how many requests are currently
|
|
// queued for a verification slot.
|
|
func (g *LoginGuard) QueuedWaitersForTest() int {
|
|
return len(g.queue)
|
|
}
|
|
|
|
// SetNowForTest replaces the guard's clock.
|
|
func (g *LoginGuard) SetNowForTest(now func() time.Time) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
|
|
g.now = now
|
|
}
|
|
|
|
// FailForTest exposes fail.
|
|
func (g *LoginGuard) FailForTest(clientKey, username string) bool {
|
|
return g.fail(clientKey, username)
|
|
}
|
|
|
|
// SucceedForTest exposes succeed.
|
|
func (g *LoginGuard) SucceedForTest(clientKey, username string) {
|
|
g.succeed(clientKey, username)
|
|
}
|
|
|
|
// AcquireForTest exposes acquire.
|
|
func (g *LoginGuard) AcquireForTest(
|
|
ctx context.Context,
|
|
) (func(), bool) {
|
|
return g.acquire(ctx)
|
|
}
|
|
|
|
// TrackedKeysForTest reports how many failure counters the guard
|
|
// holds, per-username and per-address respectively.
|
|
func (g *LoginGuard) TrackedKeysForTest() (int, int) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
|
|
return len(g.byUser), len(g.byAddr)
|
|
}
|
|
|
|
// PasswordChangeRateLimitConst exposes the
|
|
// passwordChangeRateLimit constant.
|
|
const PasswordChangeRateLimitConst = passwordChangeRateLimit
|
|
|
|
// ReceiverAggregateMultiplierConst exposes the
|
|
// receiverAggregateMultiplier constant.
|
|
const ReceiverAggregateMultiplierConst = receiverAggregateMultiplier
|
|
|
|
// ReceiverAggregateLimitForTest exposes receiverAggregateLimit for
|
|
// testing.
|
|
func ReceiverAggregateLimitForTest(perEntrypoint int) int {
|
|
return receiverAggregateLimit(perEntrypoint)
|
|
}
|