Documentation
¶
Overview ¶
Package middleware provides the Gin middleware used by the tingly-box server: HTTP access logging, CORS, authentication, response compression, per-route rate limiting, and IO-deadline management for streaming routes.
Global vs per-route ¶
Only a thin stack runs globally on every request (server.setupMiddleware in internal/server/server_routes.go):
Request │ ├─ gin.Recovery — panic → 500, prevents process crash ├─ MemoryLog — structured HTTP access log + in-memory ring buffer └─ CORS — Access-Control-* headers
Auth, rate limiting, gzip, and IO-timeout clearing are NOT global — each is applied to the specific route group that needs it at registration time.
Components ¶
MemoryLog (memory_log.go)
Logs one structured entry per request — method, path, status, latency, error — to the multi-mode logger (text + JSON files via pkg/obs.MultiLogger) and an in-memory ring buffer (50 entries per source, pkg/obs.defaultMemorySinkEntries).
For AI-routed requests, the entry is enriched with routing metadata after the handler returns. These fields are written into the gin context by SetTrackingContext (internal/protocolserver/tracking_context.go) and read back here:
- request_model — model name the client requested
- routed_model — model name actually forwarded to the provider
- routed_provider — provider name selected by the routing pipeline
- api_style — provider API style (e.g. openai, anthropic)
- base_url — provider API base URL
- scenario — agent scenario (e.g. "claude_code", "openai")
- lb_service_id — load-balancer service id chosen for this request
- lb_tactic — load-balancer tactic name
Non-AI routes (system/management APIs) produce no routing fields.
The access log deliberately records no request/response bodies. Mirroring bodies here (wrapping c.Request.Body / c.Writer) is unstable — it interferes with streaming, Flush/Hijack, and large or Expect-100-continue uploads — for little gain. Bodies that matter for diagnosis are recorded where they are understood: the handler, and the model_request client stage (correlated to this entry by request_id).
AuthMiddleware (auth.go)
Two distinct auth modes, each applied to its route group at registration:
UserAuthMiddleware — web-UI / management routes; validates a static bearer token from config; on success sets user_id to the default admin (db.DefaultAdminUserID) so usage records have a stable owner.
ModelAuthMiddleware — AI-endpoint routes; supports three methods in priority order: 1. JWT API tokens (multi-tenant, "tb-share-*" prefix, validated from DB) 2. Global config model token ("tingly-box-*" prefix) 3. Enterprise context JWT (X-TBE-Context-JWT header, HS256/RS256) A token carrying the "sk-tbe-" virtual-key prefix is rejected here with a pointer to the dedicated /tbe/* endpoints.
CORS (cors.go)
Applies permissive Access-Control-Allow-* headers required for the single-page web UI. Preflight OPTIONS requests are handled and short- circuited before auth runs.
Gzip (gzip.go)
Per-route response compression for endpoints that can return large JSON (usage stats, time series, records). Registered via swagger.WithMiddleware(middleware.Gzip()); never on streaming/SSE routes.
RateLimit (ratelimit.go)
A fixed-window failed-attempt blocker keyed by client IP: after maxAttempts POSTs within windowSize the IP is blocked for blockDuration. It is scoped to specific auth paths passed to RateLimitMiddleware. Note: it is a failed- attempt limiter, not a token bucket, and it is not wired into the default route stack today.
ClearServerIOTimeouts (io_timeout.go)
Applied to the AI protocol route groups (/tingly/:scenario and /tingly/:scenario/v1, see internal/protocolserver/routes.go) only. Clears the per-connection read/write deadlines armed by http.Server's ReadTimeout/WriteTimeout so long-running SSE streams and large request bodies are bounded by the upstream provider timeout and client disconnect, not by wall-clock from request start (issue #1384).
Index ¶
- func BaseURLFromRequest(c *gin.Context, defaultPort int) string
- func CORS() gin.HandlerFunc
- func CORSWithConfig(config CORSConfig) gin.HandlerFunc
- func ClearServerIOTimeouts() gin.HandlerFunc
- func Gzip() gin.HandlerFunc
- func RateLimitMiddleware(rl *RateLimiter, authPaths ...string) gin.HandlerFunc
- type APITokenStore
- type AuthMiddleware
- type CORSConfig
- type ErrorDetail
- type ErrorResponse
- type MemoryLog
- func (m *MemoryLog) Clear()
- func (m *MemoryLog) GetEntries() []*logrus.Entry
- func (m *MemoryLog) GetEntriesByLevel(level logrus.Level) []*logrus.Entry
- func (m *MemoryLog) GetEntriesSince(since time.Time) []*logrus.Entry
- func (m *MemoryLog) GetLatestEntries(n int) []*logrus.Entry
- func (m *MemoryLog) Middleware() gin.HandlerFunc
- func (m *MemoryLog) Size() int
- type RateLimiter
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BaseURLFromRequest ¶
BaseURLFromRequest returns the base URL the client used to reach the server, honoring the X-Forwarded-Proto header set by reverse proxies. defaultPort is appended when the request Host carries no explicit port. This is the URL echoed back to clients (e.g. baked into generated agent configs), so it must reflect what the user actually connected to rather than the bind address.
func CORSWithConfig ¶
func CORSWithConfig(config CORSConfig) gin.HandlerFunc
CORSWithConfig returns a CORS middleware handler with custom configuration
func ClearServerIOTimeouts ¶
func ClearServerIOTimeouts() gin.HandlerFunc
ClearServerIOTimeouts removes the per-connection read/write deadlines that http.Server arms from its ReadTimeout/WriteTimeout for the current request.
The server-wide WriteTimeout is armed once, when the request headers are read, and is never extended by subsequent writes. AI sampling requests are bounded by the upstream provider timeout (provider.Timeout, default 1800s) plus failover attempts — not by wall-clock from request start — so any SSE stream that outlives WriteTimeout gets its TCP connection killed mid-stream and the client sees EOF without a terminal event (Codex: "stream closed before response.completed", issue #1384). ReadTimeout similarly caps reading the request body, which agentic clients fill with the entire conversation (tens of MB) on every turn.
Applied per-group to the AI protocol endpoints only; management/UI routes keep the server-wide protection. Request lifetime on these routes remains bounded by the upstream timeout and by client-disconnect cancellation of the request context.
func Gzip ¶
func Gzip() gin.HandlerFunc
Gzip returns gin middleware that gzip-compresses the response body when the client accepts it. Intended for endpoints that can return large JSON payloads (usage stats, time series, records) — register it per-route via swagger.WithMiddleware(middleware.Gzip()) rather than wrapping the handler directly, so it composes through the normal auth/CORS middleware chain instead of bypassing it. Do not use it on streaming/SSE endpoints.
func RateLimitMiddleware ¶
func RateLimitMiddleware(rl *RateLimiter, authPaths ...string) gin.HandlerFunc
RateLimitMiddleware returns a Gin middleware for rate limiting This is specifically for auth endpoints (handshake, execute)
Types ¶
type APITokenStore ¶
type APITokenStore interface {
ValidateToken(tokenID string) (*db.APITokenRecord, error)
UpdateLastUsed(tokenID string) error
}
APITokenStore interface for token validation (abstracted for testability)
type AuthMiddleware ¶
type AuthMiddleware struct {
// contains filtered or unexported fields
}
AuthMiddleware provides authentication middleware for different types of authentication
func NewAuthMiddleware ¶
func NewAuthMiddleware(cfg *config.Config, jwtManager *auth.JWTManager, apiTokenManager *auth.APITokenManager, apiTokenStore APITokenStore) *AuthMiddleware
NewAuthMiddleware creates a new authentication middleware
func (*AuthMiddleware) ModelAuthMiddleware ¶
func (am *AuthMiddleware) ModelAuthMiddleware() gin.HandlerFunc
ModelAuthMiddleware middleware for OpenAI and Anthropic API authentication The auth will support both `Authorization` and `X-Api-Key` Supports three authentication methods (in order of precedence): 1. JWT API tokens (when multi-tenant is enabled) 2. Global config model token (backward compatibility) 3. Enterprise context JWT (X-TBE-Context-JWT header)
func (*AuthMiddleware) UserAuthMiddleware ¶
func (am *AuthMiddleware) UserAuthMiddleware() gin.HandlerFunc
UserAuthMiddleware middleware for UI and control API authentication
type CORSConfig ¶
type CORSConfig struct {
AllowOrigins string
AllowMethods string
AllowHeaders string
ExposeHeaders string
MaxAge int
HandlePreflight bool
}
CORSConfig defines the configuration for CORS middleware
type ErrorDetail ¶
type ErrorDetail struct {
Message string `json:"message"`
Type string `json:"type"`
Code string `json:"code,omitempty"`
}
ErrorDetail represents error details
type ErrorResponse ¶
type ErrorResponse struct {
Error ErrorDetail `json:"error"`
}
ErrorResponse represents an error response
type MemoryLog ¶
type MemoryLog struct {
// contains filtered or unexported fields
}
MemoryLog is the HTTP access log for the whole request chain. It records one structured entry per request — method, path, status, latency, error, and (for AI routes) routing metadata — correlated across stages by a request_id. Entries go to the multi-mode logger (text + JSON files) and an in-memory ring buffer for the logs API.
It deliberately does NOT capture request/response bodies. Opportunistically mirroring bodies here (wrapping c.Request.Body / c.Writer) is unstable — it interferes with streaming, Flush/Hijack, and large/Expect-100-continue uploads — for little gain: the bodies that matter for diagnosis are recorded where they are understood (the handler and the model_request client stage).
func NewMemoryLogMiddleware ¶
func NewMemoryLogMiddleware(multiLogger *obs.MultiLogger) *MemoryLog
NewMemoryLogMiddleware creates the HTTP access log middleware.
func (*MemoryLog) GetEntries ¶
GetEntries returns all log entries from memory in chronological order
func (*MemoryLog) GetEntriesByLevel ¶
GetEntriesByLevel returns log entries from memory matching the specified level
func (*MemoryLog) GetEntriesSince ¶
GetEntriesSince returns log entries from memory after the specified time
func (*MemoryLog) GetLatestEntries ¶
GetLatestEntries returns the newest N log entries from memory
func (*MemoryLog) Middleware ¶
func (m *MemoryLog) Middleware() gin.HandlerFunc
Middleware returns a Gin middleware compatible with gin.Logger() It logs all HTTP requests to both the multi-mode logger and memory
type RateLimiter ¶
type RateLimiter struct {
// contains filtered or unexported fields
}
RateLimiter provides rate limiting functionality
func NewRateLimiter ¶
func NewRateLimiter(maxAttempts int, windowSize, blockDuration time.Duration) *RateLimiter
NewRateLimiter creates a new rate limiter
func (*RateLimiter) Cleanup ¶
func (rl *RateLimiter) Cleanup()
cleanup runs periodically to remove expired entries
func (*RateLimiter) GetStats ¶
func (rl *RateLimiter) GetStats() map[string]interface{}
GetStats returns rate limiting statistics
func (*RateLimiter) ResetIP ¶
func (rl *RateLimiter) ResetIP(ip string)
ResetIP resets the rate limit for a specific IP (admin use only)