65 lines
2.2 KiB
Go
65 lines
2.2 KiB
Go
package ratelimit
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"net/http"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/internal/apiserver"
|
|
)
|
|
|
|
// KeyFunc bestimmt den Rate-Limit-Schluessel fuer eine Anfrage — typischerweise
|
|
// der Tenant-Slug aus apiserver.RequestContext, siehe KeyByTenant. Als
|
|
// eigenstaendiger Typ austauschbar (z.B. spaeter API-Token-basiert), ohne
|
|
// internal/apiserver aendern zu muessen (Kein Umbau angrenzender Bereiche).
|
|
type KeyFunc func(r *http.Request) (string, bool)
|
|
|
|
// KeyByTenant liest den Tenant aus dem von internal/apiserver.authAndTenantContext
|
|
// gesetzten RequestContext (IAM-02/API-01) — dieselbe Middleware-Kette,
|
|
// keine zweite Authentifizierung.
|
|
func KeyByTenant(r *http.Request) (string, bool) {
|
|
rc, ok := apiserver.FromContext(r.Context())
|
|
if !ok || rc.TenantSlug == "" {
|
|
return "", false
|
|
}
|
|
return rc.TenantSlug, true
|
|
}
|
|
|
|
// Middleware setzt sich VOR den eigentlichen Handler (nach Auth+Tenant-
|
|
// Kontext, siehe KeyByTenant) und lehnt Anfragen ueber dem konfigurierten
|
|
// Limit mit 429 + Retry-After ab (Akzeptanzkriterium 3).
|
|
func Middleware(store *Store, keyFn KeyFunc, next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
key, ok := keyFn(r)
|
|
if !ok {
|
|
// Kein Schluessel ermittelbar (z.B. kein Tenant-Kontext) — die
|
|
// Auth-Pruefung selbst ist Sache von authAndTenantContext, hier
|
|
// wird nur nicht limitiert, wenn schon kein Kontext vorliegt.
|
|
next(w, r)
|
|
return
|
|
}
|
|
|
|
result, err := store.Allow(r.Context(), key)
|
|
if err != nil {
|
|
apiserver.WriteError(w, http.StatusInternalServerError, "rate_limit_error", "rate-limit-pruefung fehlgeschlagen")
|
|
return
|
|
}
|
|
|
|
w.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%d", result.Limit))
|
|
w.Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", result.Remaining))
|
|
|
|
if !result.Allowed {
|
|
retryAfterSeconds := int(math.Ceil(result.RetryAfter.Seconds()))
|
|
if retryAfterSeconds < 1 {
|
|
retryAfterSeconds = 1
|
|
}
|
|
w.Header().Set("Retry-After", fmt.Sprintf("%d", retryAfterSeconds))
|
|
apiserver.WriteError(w, http.StatusTooManyRequests, "rate_limit_exceeded",
|
|
"zu viele anfragen, bitte spaeter erneut versuchen")
|
|
return
|
|
}
|
|
|
|
next(w, r)
|
|
}
|
|
}
|