check / check (push) Successful in 3m26s
Each client address may make 60 requests to /metrics a minute, through the same httprate middleware and TRUSTED_PROXIES resolution the report route uses, with an allowance of its own. The limit runs before the basic auth, so past it the answer is 429 and the password is not checked. backend/README.md says so; a test uses up one client's allowance on wrong passwords, gets 429 with the right one, and checks that another client behind the same nginx still gets in. Model: opus-5-5
86 lines
2.8 KiB
Go
86 lines
2.8 KiB
Go
package server
|
|
|
|
import (
|
|
"time"
|
|
|
|
sentryhttp "github.com/getsentry/sentry-go/http"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/collectors"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
)
|
|
|
|
const (
|
|
requestTimeout = 60 * time.Second
|
|
|
|
// maxRequestBodyBytes caps every request body. A route group
|
|
// can mount s.mw.MaxBodyBytes with a smaller value to lower
|
|
// its bound, but cannot raise it: this cap runs first.
|
|
maxRequestBodyBytes int64 = 1 << 20 // 1 MiB
|
|
|
|
// metricsRequestsPerMinute is how many requests to /metrics each
|
|
// client address may make a minute, whatever their credentials. A
|
|
// scraper polling every 2 seconds sends half of it, which httprate
|
|
// never refuses.
|
|
metricsRequestsPerMinute = 60
|
|
)
|
|
|
|
// SetupRoutes configures the chi router with middleware and
|
|
// all application routes.
|
|
func (s *Server) SetupRoutes() {
|
|
s.router = chi.NewRouter()
|
|
|
|
s.router.Use(s.mw.Recoverer())
|
|
s.router.Use(middleware.RequestID)
|
|
s.router.Use(s.mw.Logging())
|
|
s.router.Use(s.mw.SecurityHeaders())
|
|
s.router.Use(s.mw.CORS(s.params.Config.CORSAllowedOrigins))
|
|
s.router.Use(s.mw.MaxBodyBytes(maxRequestBodyBytes))
|
|
s.router.Use(middleware.Timeout(requestTimeout))
|
|
|
|
// Sentry reports a panic, then panics again, so that s.mw.Recoverer
|
|
// still answers 500.
|
|
if s.params.Config.SentryDSN != "" {
|
|
s.router.Use(sentryhttp.New(sentryhttp.Options{Repanic: true}).Handle)
|
|
}
|
|
|
|
// The metrics go in a registry of this server's own, not in
|
|
// Prometheus' default one, which takes them only once per process.
|
|
registry := prometheus.NewRegistry()
|
|
registry.MustRegister(
|
|
collectors.NewGoCollector(),
|
|
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
|
|
)
|
|
|
|
// Requests are measured only once chi has matched them to one of
|
|
// these routes, by path and method. The metrics are labelled with
|
|
// both, which any client can make up, so measuring every request
|
|
// would let clients add labels without bound. A Route here would
|
|
// be matched by its path prefix alone, so each path is given in
|
|
// full.
|
|
s.router.Group(func(r chi.Router) {
|
|
// config.New refuses one of the two credentials without the
|
|
// other.
|
|
if s.params.Config.MetricsUsername != "" {
|
|
r.Use(s.mw.Metrics(registry))
|
|
}
|
|
|
|
r.Get("/.well-known/healthcheck", s.h.HandleHealthCheck())
|
|
|
|
r.With(s.mw.RateLimit(s.params.Config.ReportsPerMinute)).
|
|
Post("/api/v1/reports", s.h.HandleReport())
|
|
})
|
|
|
|
// The rate limit comes before the basic auth, so a client past it
|
|
// gets 429 and its password is not checked.
|
|
if s.params.Config.MetricsUsername != "" {
|
|
s.router.With(
|
|
s.mw.RateLimit(metricsRequestsPerMinute),
|
|
s.mw.MetricsAuth(),
|
|
).Get("/metrics", promhttp.HandlerFor(
|
|
registry, promhttp.HandlerOpts{},
|
|
).ServeHTTP)
|
|
}
|
|
}
|