internal/pkg/api/router.go (128 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.
package api
import (
"net/http"
"regexp"
"strings"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/zerolog"
"go.elastic.co/apm/module/apmchiv5/v2"
"go.elastic.co/apm/v2"
"github.com/elastic/fleet-server/v7/internal/pkg/config"
"github.com/elastic/fleet-server/v7/internal/pkg/limit"
"github.com/elastic/fleet-server/v7/internal/pkg/logger"
)
func newRouter(cfg *config.ServerLimits, si ServerInterface, tracer *apm.Tracer) http.Handler {
r := chi.NewRouter()
if tracer != nil {
r.Use(apmchiv5.Middleware(apmchiv5.WithTracer(tracer)))
}
r.Use(logger.Middleware) // Attach middlewares to router directly so the occur before any request parsing/validation
r.Use(middleware.Recoverer)
if cfg.MaxConnections > 0 {
r.Use(middleware.Throttle(cfg.MaxConnections))
}
r.Use(Limiter(cfg).middleware)
return HandlerWithOptions(si, ChiServerOptions{
BaseRouter: r,
ErrorHandlerFunc: ErrorResp,
Middlewares: []MiddlewareFunc{NewAPIVersion().middleware},
// TODO auth as middleware? - here it takes place after chi router adds scope annotations to the request ctx
})
}
// limiter wraps routes with metrics and rate limits.
//
// auth is handled elsewhere.
type limiter struct {
checkin *limit.Limiter
artifact *limit.Limiter
enroll *limit.Limiter
ack *limit.Limiter
status *limit.Limiter
uploadBegin *limit.Limiter
uploadChunk *limit.Limiter
uploadComplete *limit.Limiter
deliverFile *limit.Limiter
getPGPKey *limit.Limiter
auditUnenroll *limit.Limiter
}
func Limiter(cfg *config.ServerLimits) *limiter {
return &limiter{
checkin: limit.NewLimiter(&cfg.CheckinLimit),
artifact: limit.NewLimiter(&cfg.ArtifactLimit),
enroll: limit.NewLimiter(&cfg.EnrollLimit),
ack: limit.NewLimiter(&cfg.AckLimit),
status: limit.NewLimiter(&cfg.StatusLimit),
uploadBegin: limit.NewLimiter(&cfg.UploadStartLimit),
uploadChunk: limit.NewLimiter(&cfg.UploadChunkLimit),
uploadComplete: limit.NewLimiter(&cfg.UploadEndLimit),
deliverFile: limit.NewLimiter(&cfg.DeliverFileLimit),
getPGPKey: limit.NewLimiter(&cfg.GetPGPKey),
auditUnenroll: limit.NewLimiter(&cfg.AuditUnenrollLimit),
}
}
var pgpReg = regexp.MustCompile(`\/api\/agents\/upgrades\/[0-9]+\.[0-9]+\.[0-9]+\/pgp-public-key`)
// pathToOperation determines the endpoint passed on the request path.
// idealy we would be able to use chi's route context, but it is not ready this early in the stack
//
//nolint:goconst // using const values here makes it harder to read
func pathToOperation(path string) string {
path = strings.TrimSuffix(path, "/")
if path == "/api/status" {
return "status"
}
if path == "/api/fleet/uploads" {
return "uploadBegin"
}
if pgpReg.MatchString(path) {
return "getPGPKey"
}
if strings.HasPrefix(path, "/api/fleet/") {
pp := strings.Split(strings.TrimPrefix(path, "/"), "/")
if len(pp) == 4 {
if pp[2] == "agents" {
return "enroll"
} else if pp[2] == "uploads" {
return "uploadComplete"
} else if pp[2] == "file" {
return "deliverFile"
}
} else if len(pp) == 5 {
if pp[2] == "agents" {
if pp[4] == "acks" || pp[4] == "checkin" {
return pp[4]
}
} else if pp[2] == "uploads" {
return "uploadChunk"
} else if pp[2] == "artifacts" {
return "artifact"
}
} else if len(pp) == 6 && pp[2] == "agents" && pp[4] == "audit" {
return "audit-" + pp[5]
}
}
return ""
}
func (l *limiter) middleware(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
switch pathToOperation(r.URL.Path) {
case "enroll":
l.enroll.Wrap("enroll", &cntEnroll, zerolog.DebugLevel)(next).ServeHTTP(w, r)
case "acks":
l.ack.Wrap("acks", &cntAcks, zerolog.DebugLevel)(next).ServeHTTP(w, r)
case "checkin":
l.checkin.Wrap("checkin", &cntCheckin, zerolog.WarnLevel)(next).ServeHTTP(w, r)
case "artifact":
l.artifact.Wrap("artifact", &cntArtifacts, zerolog.DebugLevel)(next).ServeHTTP(w, r)
case "uploadBegin":
l.uploadBegin.Wrap("uploadBegin", &cntUploadStart, zerolog.DebugLevel)(next).ServeHTTP(w, r)
case "uploadComplete":
l.uploadComplete.Wrap("uploadComplete", &cntUploadEnd, zerolog.DebugLevel)(next).ServeHTTP(w, r)
case "uploadChunk":
l.uploadChunk.Wrap("uploadChunk", &cntUploadChunk, zerolog.DebugLevel)(next).ServeHTTP(w, r)
case "deliverFile":
l.deliverFile.Wrap("deliverFile", &cntFileDeliv, zerolog.DebugLevel)(next).ServeHTTP(w, r)
case "getPGPKey":
l.getPGPKey.Wrap("getPGPKey", &cntGetPGP, zerolog.DebugLevel)(next).ServeHTTP(w, r)
case "status":
l.status.Wrap("status", &cntStatus, zerolog.DebugLevel)(next).ServeHTTP(w, r)
case "audit-unenroll":
l.auditUnenroll.Wrap("audit-unenroll", &cntAuditUnenroll, zerolog.DebugLevel)(next).ServeHTTP(w, r)
default:
// no tracking or limits
next.ServeHTTP(w, r)
}
}
return http.HandlerFunc(fn)
}