84 lines
2.8 KiB
Go
84 lines
2.8 KiB
Go
// Package httpcommon hosts cross-router HTTP middleware shared by the
|
|
// Game Lobby Service public and internal listeners.
|
|
package httpcommon
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base32"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"galaxy/lobby/internal/logging"
|
|
)
|
|
|
|
// RequestIDHeader is the canonical HTTP header used to carry a
|
|
// caller-supplied request id across service hops.
|
|
const RequestIDHeader = "X-Request-Id"
|
|
|
|
// requestIDTokenBytes controls the entropy of generated request ids. Eight
|
|
// bytes produce a 13-character base32 token, well above what is needed to
|
|
// keep collisions vanishingly rare within any single service's logs.
|
|
const requestIDTokenBytes = 8
|
|
|
|
// requestIDMaxLength caps the length of caller-supplied request ids so a
|
|
// hostile or buggy upstream cannot blow up logs and trace attributes.
|
|
const requestIDMaxLength = 128
|
|
|
|
// base32NoPadding mirrors the encoding used elsewhere in the lobby module
|
|
// (see `internal/adapters/idgen`) so generated ids stay visually similar.
|
|
var base32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding)
|
|
|
|
// RequestID is the HTTP middleware that materialises the per-request
|
|
// `request_id` for downstream loggers. It reads the X-Request-Id header
|
|
// (case-insensitively); when the header is absent, malformed, or longer
|
|
// than requestIDMaxLength it generates a fresh token from crypto/rand.
|
|
// The id is stored on the request context via logging.WithRequestID and
|
|
// echoed back on the response header.
|
|
func RequestID(next http.Handler) http.Handler {
|
|
if next == nil {
|
|
panic("httpcommon: nil next handler")
|
|
}
|
|
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
requestID := normalizeRequestID(request.Header.Get(RequestIDHeader))
|
|
if requestID == "" {
|
|
requestID = generateRequestID()
|
|
}
|
|
|
|
writer.Header().Set(RequestIDHeader, requestID)
|
|
|
|
ctx := logging.WithRequestID(request.Context(), requestID)
|
|
next.ServeHTTP(writer, request.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
// normalizeRequestID returns a trimmed copy of value when it satisfies the
|
|
// per-request constraints, otherwise the empty string. The empty return
|
|
// signals that the middleware must generate a fresh id.
|
|
func normalizeRequestID(value string) string {
|
|
trimmed := strings.TrimSpace(value)
|
|
if trimmed == "" {
|
|
return ""
|
|
}
|
|
if len(trimmed) > requestIDMaxLength {
|
|
return ""
|
|
}
|
|
for _, r := range trimmed {
|
|
if r < 0x20 || r == 0x7f {
|
|
return ""
|
|
}
|
|
}
|
|
return trimmed
|
|
}
|
|
|
|
// generateRequestID returns a fresh opaque id derived from crypto/rand.
|
|
// Errors from the random source are vanishingly unlikely; the helper
|
|
// returns the literal "fallback" on the impossible path so the middleware
|
|
// remains panic-free.
|
|
func generateRequestID() string {
|
|
buf := make([]byte, requestIDTokenBytes)
|
|
if _, err := rand.Read(buf); err != nil {
|
|
return "rid-fallback"
|
|
}
|
|
return "rid-" + strings.ToLower(base32NoPadding.EncodeToString(buf))
|
|
}
|