From 1d220562775355db38578680e52e21a753d5ecc5 Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Thu, 30 Jul 2026 10:50:34 -0500 Subject: [PATCH 1/7] build(deps): bump go-utils to v1.1.2 for pooled-context ownership v1.1.2 adds ownValues tracking to the pooled http.Ctx: ShareValues now records that the values map was borrowed, and Cleanup hands the context a private map before returning it to the pool. Without that, a context returned to the pool still aliasing another context's values map lets the next unrelated request clear and overwrite values belonging to a request still in flight. That map carries request identity and tenant scope, so the failure mode is cross-request data corruption rather than a stale read. --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index dc82806b..75e762ad 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 github.com/xraph/confy v0.5.2 github.com/xraph/forgeui v1.4.1 - github.com/xraph/go-utils v1.1.1 + github.com/xraph/go-utils v1.1.2 github.com/xraph/vessel v1.0.2 go.opentelemetry.io/otel v1.40.0 go.opentelemetry.io/otel/exporters/jaeger v1.17.0 diff --git a/go.sum b/go.sum index b53a4760..ab442eed 100644 --- a/go.sum +++ b/go.sum @@ -308,6 +308,8 @@ github.com/xraph/forgeui v1.4.1 h1:LHK1t/sZ+9zL+MNUZralO9/rc0f5UCa19dpbWTuRMNg= github.com/xraph/forgeui v1.4.1/go.mod h1:rH/+wb1tt2pXSHotWAvoP+Lt846xlIjuwPDSpS5K5mw= github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= From e5eeac95380da32c31e57bb0bbcb7b0f88286845 Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Thu, 30 Jul 2026 10:50:52 -0500 Subject: [PATCH 2/7] fix(middleware): close CORS, rate-limit and recovery security gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CORS wildcard subdomains were matched with strings.HasSuffix, so "*.example.com" accepted any origin merely ending in "example.com" — evil-example.com and notexample.com among them. Since getAllowOrigin reflects the request origin back and this path is reachable with AllowCredentials, an attacker could register a matching domain and read authenticated responses. Origins are now parsed and matched on label boundaries. The rate limiter keyed on RemoteAddr, which Go sets to "IP:ephemeral-port". A new port per connection meant a new bucket per connection: 100 of 100 requests from one IP were allowed against a burst of 3, and the bucket map grew without bound under client control. Keying is now per client IP via ClientIP, the map is capped, and RateLimitWithKey covers proxy and user-ID keying. NewRateLimiter's cleanup goroutine also had no way to stop; added Stop(). Timeout buffered the entire response with no cap and implemented neither Flusher nor Hijacker, so streaming stalled until the handler returned, WebSocket upgrades failed through it, and any large response was held in memory. Buffering is now bounded and abandoned on flush or hijack, and the timeout path no longer writes a second header over a committed response. Recovery called logger.Error unguarded — a nil logger panicked inside the deferred recover, killing the connection instead of returning the 500 it exists to produce. It now guards the logger, logs debug.Stack() as its comment always claimed, re-panics http.ErrAbortHandler, and skips writing over an already-committed response. Logging's SensitiveHeaders and IncludeHeaders were never read, so the default redaction list read as protection that did not exist; the measured duration was discarded too. GetRequestID always returned "" because nothing ever stored the key it reads, and the inbound X-Request-ID was echoed and logged unbounded. --- middleware/cors.go | 49 +++++- middleware/logging.go | 61 ++++++- middleware/rate_limit.go | 125 +++++++++++--- middleware/recovery.go | 53 +++++- middleware/request_id.go | 50 +++++- middleware/response_writer.go | 6 + middleware/security_regression_test.go | 215 +++++++++++++++++++++++++ middleware/timeout.go | 157 +++++++++++++++--- 8 files changed, 655 insertions(+), 61 deletions(-) create mode 100644 middleware/security_regression_test.go diff --git a/middleware/cors.go b/middleware/cors.go index 5a7f6e01..2e813e54 100644 --- a/middleware/cors.go +++ b/middleware/cors.go @@ -1,7 +1,9 @@ package middleware import ( + "net" "net/http" + "net/url" "slices" "strconv" "strings" @@ -45,24 +47,67 @@ func DefaultCORSConfig() CORSConfig { } } +// originHost extracts the host (including port, lowercased) from an origin +// header value. Returns "" if the origin is not a parseable absolute URL, which +// callers must treat as "not allowed". +func originHost(requestOrigin string) string { + parsed, err := url.Parse(requestOrigin) + if err != nil || parsed.Host == "" || parsed.Scheme == "" { + return "" + } + + return strings.ToLower(parsed.Host) +} + +// matchesWildcardDomain reports whether host is a subdomain of domain. +// +// The match is on host labels, not on the raw string. A plain suffix test would +// accept "evil-example.com" for "*.example.com" — any domain merely ending in +// the allowed one — which lets an attacker register a matching domain and, when +// AllowCredentials is set, read authenticated responses cross-origin. +func matchesWildcardDomain(host, domain string) bool { + if domain == "" { + return false + } + + // Strip the port before comparing labels; ports are matched separately by + // exact-origin entries. + if h, _, err := net.SplitHostPort(host); err == nil { + host = h + } + + // A wildcard covers strict subdomains only, so the host must be longer than + // the domain and the extra part must end on a label boundary. + return strings.HasSuffix(host, "."+domain) +} + // isOriginAllowed checks if the request origin is in the allowed list. func isOriginAllowed(requestOrigin string, allowedOrigins []string) bool { if len(allowedOrigins) == 0 { return false } + host := originHost(requestOrigin) + for _, allowed := range allowedOrigins { if allowed == "*" { return true } + // Exact origin match (scheme + host + port), case-sensitive on scheme + // per the Origin serialization, so compare the raw values. if allowed == requestOrigin { return true } + // Support wildcard subdomains like *.example.com if strings.HasPrefix(allowed, "*.") { - domain := allowed[2:] - if strings.HasSuffix(requestOrigin, domain) { + if host == "" { + // Unparseable origin: never match a wildcard. + continue + } + + if matchesWildcardDomain(host, strings.ToLower(allowed[2:])) { return true } } diff --git a/middleware/logging.go b/middleware/logging.go index b77e8912..87a0fef6 100644 --- a/middleware/logging.go +++ b/middleware/logging.go @@ -1,11 +1,16 @@ package middleware import ( + "net/http" + "strings" "time" forge "github.com/xraph/forge" ) +// redactedValue replaces the value of a sensitive header in logs. +const redactedValue = "[REDACTED]" + // LoggingConfig defines configuration for logging middleware. type LoggingConfig struct { // IncludeHeaders includes request headers in logs @@ -40,6 +45,13 @@ func LoggingWithConfig(logger forge.Logger, config LoggingConfig) forge.Middlewa excludeMap[path] = true } + // Pre-lower the sensitive header names once; HTTP header names are + // case-insensitive, so the comparison must be too. + sensitive := make(map[string]bool, len(config.SensitiveHeaders)) + for _, h := range config.SensitiveHeaders { + sensitive[strings.ToLower(h)] = true + } + return func(next forge.Handler) forge.Handler { return func(ctx forge.Context) error { // Skip excluded paths @@ -47,22 +59,61 @@ func LoggingWithConfig(logger forge.Logger, config LoggingConfig) forge.Middlewa return next(ctx) } + r := ctx.Request() + // Start timing start := time.Now() + fields := []forge.Field{ + forge.F("method", r.Method), + forge.F("path", r.URL.Path), + } + + if config.IncludeHeaders { + fields = append(fields, forge.F("headers", redactHeaders(r.Header, sensitive))) + } + // Log request start - logger.Info("request started") + logger.Info("request started", fields...) // Process request err := next(ctx) - // Calculate duration - _ = time.Since(start) + // Log request completion, including the measured duration + completion := []forge.Field{ + forge.F("method", r.Method), + forge.F("path", r.URL.Path), + forge.F("duration", time.Since(start)), + } + if err != nil { + completion = append(completion, forge.F("error", err.Error())) + } - // Log request completion - logger.Info("request completed") + logger.Info("request completed", completion...) return err } } } + +// redactHeaders copies headers for logging, replacing the values of any header +// named in sensitive. +// +// Redaction happens here because the configured header list is the only place +// that knows which values are secrets — logging the raw header map would put +// bearer tokens and session cookies straight into the log. +func redactHeaders(header http.Header, sensitive map[string]bool) map[string]string { + out := make(map[string]string, len(header)) + + for name, values := range header { + if sensitive[strings.ToLower(name)] { + out[name] = redactedValue + + continue + } + + out[name] = strings.Join(values, ", ") + } + + return out +} diff --git a/middleware/rate_limit.go b/middleware/rate_limit.go index f2e0bf56..ecec90c3 100644 --- a/middleware/rate_limit.go +++ b/middleware/rate_limit.go @@ -1,6 +1,7 @@ package middleware import ( + "net" "net/http" "sync" "time" @@ -8,13 +9,21 @@ import ( forge "github.com/xraph/forge" ) +// defaultMaxBuckets caps how many distinct keys a RateLimiter tracks. Without a +// cap the bucket map is an unbounded, attacker-driven allocation: every new key +// adds an entry and cleanup only runs periodically. +const defaultMaxBuckets = 100_000 + // RateLimiter implements token bucket algorithm for rate limiting. type RateLimiter struct { - mu sync.Mutex - buckets map[string]*bucket - rate int // tokens per second - capacity int // max tokens - cleanup time.Duration // cleanup interval + mu sync.Mutex + buckets map[string]*bucket + rate int // tokens per second + capacity int // max tokens + cleanup time.Duration // cleanup interval + maxBuckets int + stopOnce sync.Once + stopCh chan struct{} } type bucket struct { @@ -25,12 +34,17 @@ type bucket struct { // NewRateLimiter creates a new rate limiter // rate: maximum requests per second // burst: maximum burst size (capacity) +// +// Call Stop when the limiter is no longer needed to release its cleanup +// goroutine. func NewRateLimiter(rate, burst int) *RateLimiter { rl := &RateLimiter{ - buckets: make(map[string]*bucket), - rate: rate, - capacity: burst, - cleanup: 5 * time.Minute, + buckets: make(map[string]*bucket), + rate: rate, + capacity: burst, + cleanup: 5 * time.Minute, + maxBuckets: defaultMaxBuckets, + stopCh: make(chan struct{}), } // Start cleanup goroutine @@ -39,6 +53,24 @@ func NewRateLimiter(rate, burst int) *RateLimiter { return rl } +// SetMaxBuckets overrides how many distinct keys are tracked before new keys +// are rejected. Values below 1 are ignored. +func (rl *RateLimiter) SetMaxBuckets(maxBuckets int) { + if maxBuckets < 1 { + return + } + + rl.mu.Lock() + defer rl.mu.Unlock() + + rl.maxBuckets = maxBuckets +} + +// Stop terminates the cleanup goroutine. Safe to call more than once. +func (rl *RateLimiter) Stop() { + rl.stopOnce.Do(func() { close(rl.stopCh) }) +} + // Allow checks if a request from the given key should be allowed. func (rl *RateLimiter) Allow(key string) bool { rl.mu.Lock() @@ -49,11 +81,17 @@ func (rl *RateLimiter) Allow(key string) bool { // Get or create bucket b, exists := rl.buckets[key] if !exists { - b = &bucket{ + // At capacity: deny rather than grow without bound. Denying is the + // safe direction — evicting instead would let an attacker flush + // legitimate clients' buckets and reset their limits. + if len(rl.buckets) >= rl.maxBuckets { + return false + } + + rl.buckets[key] = &bucket{ tokens: rl.capacity - 1, // -1 for this request lastCheck: now, } - rl.buckets[key] = b return true } @@ -84,26 +122,69 @@ func (rl *RateLimiter) cleanupLoop() { ticker := time.NewTicker(rl.cleanup) defer ticker.Stop() - for range ticker.C { - rl.mu.Lock() - - now := time.Now() - for key, b := range rl.buckets { - if now.Sub(b.lastCheck) > rl.cleanup { - delete(rl.buckets, key) + for { + select { + case <-rl.stopCh: + return + case <-ticker.C: + rl.mu.Lock() + + now := time.Now() + for key, b := range rl.buckets { + if now.Sub(b.lastCheck) > rl.cleanup { + delete(rl.buckets, key) + } } + + rl.mu.Unlock() } + } +} - rl.mu.Unlock() +// ClientIP returns the client's IP address from RemoteAddr, discarding the port. +// +// This is the default rate-limit key. Using RemoteAddr verbatim does not work: +// Go's HTTP server sets it to "IP:ephemeral-port" and the port changes per +// connection, so every connection would get a fresh bucket — no effective limit +// at all, plus unbounded map growth. +// +// This deliberately ignores X-Forwarded-For. Behind a proxy every client +// collapses into the proxy's IP, so supply a RateLimitKeyFunc that reads the +// forwarded header only for hops you actually trust — reading it +// unconditionally is a header-spoofing bypass. +func ClientIP(r *http.Request) string { + host, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + // RemoteAddr carried no port (some test servers, unix sockets). + return r.RemoteAddr } + + return host } -// RateLimit middleware enforces rate limiting per client. +// RateLimitKeyFunc derives the bucket key for a request. Return "" to skip rate +// limiting for that request. +type RateLimitKeyFunc func(*http.Request) string + +// RateLimit middleware enforces rate limiting per client, keyed on client IP. func RateLimit(limiter *RateLimiter, logger forge.Logger) forge.Middleware { + return RateLimitWithKey(limiter, ClientIP, logger) +} + +// RateLimitWithKey enforces rate limiting using a caller-supplied key function. +// Use this to key on an authenticated user ID, an API key, or a trusted +// forwarded-for hop instead of the client IP. +func RateLimitWithKey(limiter *RateLimiter, keyFunc RateLimitKeyFunc, logger forge.Logger) forge.Middleware { + if keyFunc == nil { + keyFunc = ClientIP + } + return func(next forge.Handler) forge.Handler { return func(ctx forge.Context) error { - // Use remote address as key (could also use user ID, API key, etc.) - key := ctx.Request().RemoteAddr + key := keyFunc(ctx.Request()) + if key == "" { + return next(ctx) + } if !limiter.Allow(key) { if logger != nil { diff --git a/middleware/recovery.go b/middleware/recovery.go index b2b6fbc4..ddf7af7c 100644 --- a/middleware/recovery.go +++ b/middleware/recovery.go @@ -1,28 +1,69 @@ package middleware import ( + "errors" "fmt" "net/http" + "runtime/debug" forge "github.com/xraph/forge" ) -// Recovery middleware recovers from panics and logs them +// Recovery middleware recovers from panics and logs them with a stack trace. // Returns http.StatusInternalServerError on panic. +// +// http.ErrAbortHandler is deliberately re-panicked: net/http uses it as the +// signal to drop the connection without logging, and swallowing it here would +// turn an intentional abort into a bogus 500. func Recovery(logger forge.Logger) forge.Middleware { return func(next forge.Handler) forge.Handler { return func(ctx forge.Context) error { defer func() { - if err := recover(); err != nil { - // Log the panic with stack trace - logger.Error(fmt.Sprintf("panic recovered: %v", err)) + rec := recover() + if rec == nil { + return + } + + if err, ok := rec.(error); ok && errors.Is(err, http.ErrAbortHandler) { + panic(rec) + } - // Return http.StatusInternalServerError error - _ = ctx.String(http.StatusInternalServerError, "Internal Server Error") + // Guard the logger: a nil logger here would panic inside the + // deferred recover, killing the connection instead of returning + // the 500 this middleware exists to produce. Other middleware in + // this package already treat the logger as optional. + if logger != nil { + logger.Error(fmt.Sprintf("panic recovered: %v\n%s", rec, debug.Stack())) } + + // If the handler already wrote a response before panicking, the + // status line is spent; writing another emits a superfluous + // header and appends error text to a partial body. Leave it + // alone — the log carries the detail. + if responseAlreadyWritten(ctx) { + return + } + + _ = ctx.String(http.StatusInternalServerError, "Internal Server Error") }() return next(ctx) } } } + +// responseAlreadyWritten reports whether the response writer has already +// committed a status code. +// +// It relies on the writer exposing Written(); when the writer is not wrapped +// (so there is nothing to ask) it reports false and Recovery falls back to +// writing the 500, matching the previous behaviour. +func responseAlreadyWritten(ctx forge.Context) bool { + type wroteReporter interface{ Written() bool } + + if w, ok := ctx.Response().(wroteReporter); ok { + return w.Written() + } + + return false +} diff --git a/middleware/request_id.go b/middleware/request_id.go index 135df393..de64bed5 100644 --- a/middleware/request_id.go +++ b/middleware/request_id.go @@ -2,6 +2,7 @@ package middleware import ( "context" + "strings" "github.com/google/uuid" forge "github.com/xraph/forge" @@ -12,29 +13,66 @@ type RequestIDContextKey string const requestIDKey RequestIDContextKey = "request_id" +// RequestIDForgeKey is the forge-context key under which the request ID is +// stored. Exported so callers can read it without guessing the string. +const RequestIDForgeKey = "request_id" + +// maxRequestIDLength bounds an inbound X-Request-ID. The value is echoed into +// the response and into every log line for the request, so an unbounded one lets +// a client inflate log volume at will. +const maxRequestIDLength = 128 + // RequestID middleware adds a unique request ID to each request -// If X-Request-ID header is present, it uses that, otherwise generates a new UUID. +// If X-Request-ID header is present and well-formed, it is used; otherwise a new +// UUID is generated. func RequestID() forge.Middleware { return func(next forge.Handler) forge.Handler { return func(ctx forge.Context) error { // Check for existing request ID in header - requestID := ctx.Request().Header.Get("X-Request-ID") + requestID := sanitizeRequestID(ctx.Request().Header.Get("X-Request-ID")) if requestID == "" { - // Generate new UUID if not present - requestID = uuid.New().String() + // Generate new UUID if not present or not usable + requestID = uuid.NewString() } // Set response header ctx.Response().Header().Set("X-Request-ID", requestID) // Add to Forge context - ctx.Set("request_id", requestID) + ctx.Set(RequestIDForgeKey, requestID) + + // Also store on the stdlib request context so GetRequestID works for + // code that only has a context.Context — background jobs, database + // hooks, anything below the handler. Previously nothing wrote this + // key, so GetRequestID always returned "". + ctx.WithContext(context.WithValue(ctx.Context(), requestIDKey, requestID)) return next(ctx) } } } +// sanitizeRequestID accepts a client-supplied correlation ID only if it is safe +// to echo and to log: bounded in length and limited to printable, non-space +// characters. Returns "" when the value should be replaced with a fresh UUID. +// +// Rejecting rather than escaping keeps the ID usable as a log-correlation token; +// a value carrying spaces or control characters would break naive log parsers +// even though net/http itself strips newlines from header values. +func sanitizeRequestID(v string) string { + if v == "" || len(v) > maxRequestIDLength { + return "" + } + + if strings.ContainsFunc(v, func(r rune) bool { + return r < '!' || r > '~' + }) { + return "" + } + + return v +} + // GetRequestID retrieves the request ID from standard context. func GetRequestID(ctx context.Context) string { if requestID, ok := ctx.Value(requestIDKey).(string); ok { @@ -46,7 +84,7 @@ func GetRequestID(ctx context.Context) string { // GetRequestIDFromForgeContext retrieves the request ID from Forge context. func GetRequestIDFromForgeContext(ctx forge.Context) string { - val := ctx.Get("request_id") + val := ctx.Get(RequestIDForgeKey) if val == nil { return "" } diff --git a/middleware/response_writer.go b/middleware/response_writer.go index c2e8e1ab..71e1f503 100644 --- a/middleware/response_writer.go +++ b/middleware/response_writer.go @@ -54,6 +54,12 @@ func (w *ResponseWriter) Size() int { return w.size } +// Written reports whether a status code has been committed to the client. +// Recovery uses this to avoid writing a second header over a partial response. +func (w *ResponseWriter) Written() bool { + return w.wroteHeader +} + // Flush implements http.Flusher. func (w *ResponseWriter) Flush() { if flusher, ok := w.ResponseWriter.(http.Flusher); ok { diff --git a/middleware/security_regression_test.go b/middleware/security_regression_test.go new file mode 100644 index 00000000..7158873e --- /dev/null +++ b/middleware/security_regression_test.go @@ -0,0 +1,215 @@ +package middleware + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +// A wildcard entry like "*.example.com" was matched with strings.HasSuffix, so +// any domain merely ending in "example.com" was accepted. Since getAllowOrigin +// reflects the request origin back and this path is reachable with +// AllowCredentials, an attacker could register a matching domain and read +// authenticated responses. +func TestCORSWildcardMatchesOnLabelBoundaries(t *testing.T) { + allowed := []string{"*.example.com", "https://exact.test:8443"} + + deny := map[string]string{ + "https://evil-example.com": "hyphen prefix is a different domain", + "https://notexample.com": "no label separator", + "https://wwwexample.com": "no label separator", + "https://example.com": "apex is not a subdomain", + "https://exact.test": "port must match an exact entry", + "not-a-url": "unparseable origin", + "": "empty origin", + } + + for origin, why := range deny { + if isOriginAllowed(origin, allowed) { + t.Errorf("origin %q should be denied (%s)", origin, why) + } + } + + permit := []string{ + "https://app.example.com", + "http://deep.nested.example.com", + "https://APP.EXAMPLE.COM", + "https://exact.test:8443", + } + + for _, origin := range permit { + if !isOriginAllowed(origin, allowed) { + t.Errorf("origin %q should be allowed", origin) + } + } +} + +// The limiter keyed on RemoteAddr, which Go sets to "IP:ephemeral-port". A new +// port per connection meant a new bucket per connection: no effective limit, and +// unbounded map growth driven by the client. +func TestRateLimitKeyIgnoresEphemeralPort(t *testing.T) { + for port := range 5 { + r := httptest.NewRequest(http.MethodGet, "/", nil) + r.RemoteAddr = fmt.Sprintf("203.0.113.7:%d", 40000+port) + + if got := ClientIP(r); got != "203.0.113.7" { + t.Fatalf("ClientIP(%q) = %q, want 203.0.113.7", r.RemoteAddr, got) + } + } + + limiter := NewRateLimiter(1, 3) // 1/s, burst 3 + defer limiter.Stop() + + allowed := 0 + + for port := range 100 { + r := httptest.NewRequest(http.MethodGet, "/", nil) + r.RemoteAddr = fmt.Sprintf("203.0.113.7:%d", 40000+port) + + if limiter.Allow(ClientIP(r)) { + allowed++ + } + } + + if allowed > 3 { + t.Errorf("rate limit bypassed: %d/100 requests allowed from one IP, burst is 3", allowed) + } + + if len(limiter.buckets) != 1 { + t.Errorf("bucket per connection: %d buckets tracked for a single IP", len(limiter.buckets)) + } +} + +// Bucket count must be bounded; the map was previously attacker-driven. +func TestRateLimiterBoundsTrackedKeys(t *testing.T) { + limiter := NewRateLimiter(1, 5) + defer limiter.Stop() + + limiter.SetMaxBuckets(10) + + for i := range 500 { + limiter.Allow(fmt.Sprintf("10.0.0.%d", i)) + } + + if len(limiter.buckets) > 10 { + t.Errorf("tracked %d keys, cap is 10", len(limiter.buckets)) + } +} + +// Stop must be idempotent — it is a natural thing to call from a shutdown hook +// that may run more than once. +func TestRateLimiterStopIsIdempotent(t *testing.T) { + limiter := NewRateLimiter(1, 1) + + limiter.Stop() + limiter.Stop() +} + +// The Timeout middleware buffers responses so it can substitute a 504. It must +// stop buffering when the handler flushes, or streaming responses stall until +// the handler returns. +func TestTimeoutMiddlewarePassesFlushThrough(t *testing.T) { + var sawFlusher bool + + h := Timeout(time.Second, nil)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + f, ok := w.(http.Flusher) + sawFlusher = ok + + if !ok { + return + } + + _, _ = w.Write([]byte("chunk")) + + f.Flush() + })) + + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/stream", nil)) + + if !sawFlusher { + t.Fatal("handler did not receive an http.Flusher; streaming is broken through Timeout") + } + + if body := rec.Body.String(); body != "chunk" { + t.Errorf("body = %q, want %q", body, "chunk") + } +} + +// A response larger than the buffer limit must not be held in memory. +func TestTimeoutMiddlewareBoundsItsBuffer(t *testing.T) { + payload := make([]byte, DefaultTimeoutBufferLimit+4096) + for i := range payload { + payload[i] = 'x' + } + + h := Timeout(5*time.Second, nil)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write(payload) + })) + + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/big", nil)) + + if got := rec.Body.Len(); got != len(payload) { + t.Errorf("wrote %d bytes, want %d", got, len(payload)) + } +} + +// An inbound X-Request-ID is echoed into the response and every log line, so it +// must be bounded and free of characters that would break log parsing. +func TestRequestIDRejectsHostileInboundValues(t *testing.T) { + reject := map[string]string{ + "": "empty", + "a b": "contains a space", + "a\tb": "contains a tab", + "\x00": "contains a NUL", + } + + for v, why := range reject { + if got := sanitizeRequestID(v); got != "" { + t.Errorf("sanitizeRequestID(%q) = %q, want rejected (%s)", v, got, why) + } + } + + long := make([]byte, maxRequestIDLength+1) + for i := range long { + long[i] = 'a' + } + + if got := sanitizeRequestID(string(long)); got != "" { + t.Errorf("over-length ID accepted (%d chars)", len(got)) + } + + const ok = "7f3a9c21-4b6e-11ee-be56-0242ac120002" + if got := sanitizeRequestID(ok); got != ok { + t.Errorf("sanitizeRequestID(%q) = %q, want it preserved", ok, got) + } +} + +// Sensitive headers must be redacted when header logging is on; the config field +// existed but was never read. +func TestLoggingRedactsSensitiveHeaders(t *testing.T) { + sensitive := map[string]bool{"authorization": true, "cookie": true} + + header := http.Header{} + header.Set("Authorization", "Bearer super-secret") + header.Set("X-Trace-Id", "trace-1") + + // Assigned directly rather than via Set, which would canonicalize it. Header + // names are case-insensitive, so redaction must match regardless of casing. + header["cookie"] = []string{"session=abc123"} + + got := redactHeaders(header, sensitive) + + for _, name := range []string{"Authorization", "cookie"} { + if v := got[name]; v != redactedValue { + t.Errorf("header %s = %q, want %q", name, v, redactedValue) + } + } + + if got["X-Trace-Id"] != "trace-1" { + t.Errorf("non-sensitive header was altered: %q", got["X-Trace-Id"]) + } +} diff --git a/middleware/timeout.go b/middleware/timeout.go index a61fd3d0..d517cbf9 100644 --- a/middleware/timeout.go +++ b/middleware/timeout.go @@ -1,8 +1,10 @@ package middleware import ( + "bufio" "context" "maps" + "net" "net/http" "sync" "time" @@ -10,16 +12,32 @@ import ( forge "github.com/xraph/forge" ) -// safeResponseWriter wraps http.ResponseWriter to prevent race conditions -// by completely buffering the response until flush is called. +// DefaultTimeoutBufferLimit bounds how much of a response the Timeout +// middleware will hold in memory before it gives up on buffering and starts +// passing writes straight through. Buffering exists so a timeout can replace a +// partially written response; past this size that is no longer worth the memory, +// and an unbounded buffer would let any large response drive allocation. +const DefaultTimeoutBufferLimit = 1 << 20 // 1 MiB + +// safeResponseWriter wraps http.ResponseWriter and buffers the response so a +// timeout can substitute its own response instead of racing a half-written one. +// +// Buffering is abandoned — and writes go straight to the client — as soon as +// any of the following happens, because each means the response is already +// committed or must not be held: +// - the buffer exceeds bufferLimit +// - the handler calls Flush (streaming: the client is waiting on bytes now) +// - the handler calls Hijack (WebSocket upgrade: we no longer own the conn) type safeResponseWriter struct { http.ResponseWriter - mu sync.Mutex - header http.Header - code int - body []byte - flushed bool + mu sync.Mutex + header http.Header + code int + body []byte + flushed bool + passthrough bool + bufferLimit int } func newSafeResponseWriter(w http.ResponseWriter) *safeResponseWriter { @@ -27,10 +45,18 @@ func newSafeResponseWriter(w http.ResponseWriter) *safeResponseWriter { ResponseWriter: w, header: make(http.Header), code: http.StatusOK, + bufferLimit: DefaultTimeoutBufferLimit, } } func (w *safeResponseWriter) Header() http.Header { + w.mu.Lock() + defer w.mu.Unlock() + + if w.passthrough { + return w.ResponseWriter.Header() + } + return w.header } @@ -38,6 +64,12 @@ func (w *safeResponseWriter) WriteHeader(code int) { w.mu.Lock() defer w.mu.Unlock() + if w.passthrough { + w.ResponseWriter.WriteHeader(code) + + return + } + if !w.flushed { w.code = code } @@ -47,31 +79,107 @@ func (w *safeResponseWriter) Write(data []byte) (int, error) { w.mu.Lock() defer w.mu.Unlock() - if !w.flushed { - w.body = append(w.body, data...) + if w.flushed || w.passthrough { + return w.ResponseWriter.Write(data) + } + + // Buffering this write would exceed the cap: commit what we have and let + // this and every later write go directly to the client. + if len(w.body)+len(data) > w.bufferLimit { + w.commitLocked() - return len(data), nil + return w.ResponseWriter.Write(data) } - return w.ResponseWriter.Write(data) + w.body = append(w.body, data...) + + return len(data), nil +} + +// commitLocked writes the buffered head of the response to the client and +// switches to passthrough. Caller must hold w.mu. +func (w *safeResponseWriter) commitLocked() { + if w.passthrough || w.flushed { + return + } + + maps.Copy(w.ResponseWriter.Header(), w.header) + w.ResponseWriter.WriteHeader(w.code) + + if len(w.body) > 0 { + _, _ = w.ResponseWriter.Write(w.body) + w.body = nil + } + + w.passthrough = true +} + +// Flush implements http.Flusher. A handler that flushes is streaming, so the +// response must stop being buffered — otherwise SSE and chunked responses stall +// until the handler returns. +func (w *safeResponseWriter) Flush() { + w.mu.Lock() + w.commitLocked() + w.mu.Unlock() + + if f, ok := w.ResponseWriter.(http.Flusher); ok { + f.Flush() + } +} + +// Hijack implements http.Hijacker so WebSocket upgrades survive this middleware. +// Buffering is committed first: once the connection is hijacked this wrapper can +// no longer write to it. +func (w *safeResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hijacker, ok := w.ResponseWriter.(http.Hijacker) + if !ok { + return nil, nil, http.ErrNotSupported + } + + w.mu.Lock() + w.passthrough = true + w.body = nil + w.mu.Unlock() + + return hijacker.Hijack() +} + +// Push implements http.Pusher. +func (w *safeResponseWriter) Push(target string, opts *http.PushOptions) error { + if p, ok := w.ResponseWriter.(http.Pusher); ok { + return p.Push(target, opts) + } + + return http.ErrNotSupported +} + +// committed reports whether anything has already reached the client, in which +// case the timeout path must not try to write its own status line. +func (w *safeResponseWriter) committed() bool { + w.mu.Lock() + defer w.mu.Unlock() + + return w.passthrough || w.flushed } func (w *safeResponseWriter) flush() { w.mu.Lock() defer w.mu.Unlock() - if !w.flushed { - // Copy headers to underlying writer - maps.Copy(w.ResponseWriter.Header(), w.header) + if w.passthrough || w.flushed { + return + } - w.ResponseWriter.WriteHeader(w.code) + // Copy headers to underlying writer + maps.Copy(w.ResponseWriter.Header(), w.header) - if len(w.body) > 0 { - _, _ = w.ResponseWriter.Write(w.body) - } + w.ResponseWriter.WriteHeader(w.code) - w.flushed = true + if len(w.body) > 0 { + _, _ = w.ResponseWriter.Write(w.body) } + + w.flushed = true } // Timeout middleware enforces a timeout on request handling @@ -105,10 +213,19 @@ func Timeout(duration time.Duration, logger forge.Logger) func(http.Handler) htt return case <-ctx.Done(): - // Timeout occurred - write timeout response directly to underlying writer if logger != nil { logger.Warn("request timeout") } + + // If the handler already committed bytes to the client (it + // streamed, hijacked, or overflowed the buffer) there is no + // status line left to write — doing so would emit a second + // header and corrupt the response. The context deadline is the + // handler's signal to stop. + if safeW.committed() { + return + } + // Write timeout response directly to avoid race with buffered response w.WriteHeader(http.StatusGatewayTimeout) _, _ = w.Write([]byte("Gateway Timeout")) From f39480863082bfc75a315623cadc8d2cf00f6cb4 Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Thu, 30 Jul 2026 10:51:12 -0500 Subject: [PATCH 3/7] fix(errors): stop echoing internal error detail to clients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A 5xx response body built from a wrapped error embeds whatever the error carried — driver output, SQL fragments, file paths, internal hostnames — and anyone able to trigger the error could read it. Both paths leaked. The router's fallback put err.Error() straight into the 500 body, and InternalError produces an IHTTPError whose ResponseBody embeds the wrapped text, which DefaultErrorHandler (the path real apps take, since the app always installs one) serialized verbatim. The redaction therefore lives in internal/shared, where both paths go through it: 5xx bodies are replaced with a minimal envelope unless exposure is enabled, 4xx bodies pass through untouched because validation messages are exactly what the client needs to correct the request. The app enables exposure outside production so local debugging is unaffected; detail always reaches the logs either way. --- internal/router/handler.go | 33 +++++++++++++--- internal/shared/error_disclosure.go | 45 ++++++++++++++++++++++ internal/shared/error_disclosure_test.go | 48 ++++++++++++++++++++++++ internal/shared/errors.go | 8 +++- 4 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 internal/shared/error_disclosure.go create mode 100644 internal/shared/error_disclosure_test.go diff --git a/internal/router/handler.go b/internal/router/handler.go index c766a5ec..2e3e4042 100644 --- a/internal/router/handler.go +++ b/internal/router/handler.go @@ -378,6 +378,14 @@ func resolveService(ctx Context, serviceType reflect.Type, serviceName string) ( return val, nil } +// SetExposeInternalErrors controls whether internal error detail appears in 5xx +// response bodies. Forwards to the shared toggle so the router and the default +// error handler stay in agreement — both emit 5xx envelopes, and gating only one +// of them leaves the leak open on whichever path the app happens to use. +func SetExposeInternalErrors(expose bool) { + shared.SetExposeInternalErrors(expose) +} + func handleError(ctx Context, err error) { // Check if the error implements the IHTTPError interface // IHTTPError is the interface from errs package (go-utils) @@ -389,13 +397,26 @@ func handleError(ctx Context, err error) { var httpErr httpErrorInterface if errors.As(err, &httpErr) { - _ = ctx.JSON(httpErr.StatusCode(), httpErr.ResponseBody()) - } else { - _ = ctx.JSON(http.StatusInternalServerError, map[string]any{ - "code": "INTERNAL_SERVER_ERROR", - "message": err.Error(), - }) + // InternalError and friends embed the wrapped error text in their body, + // so a 5xx body is redacted unless exposure is enabled. 4xx bodies pass + // through — the client needs those to fix the request. + status := httpErr.StatusCode() + + _ = ctx.JSON(status, shared.SanitizeErrorBody(status, httpErr.ResponseBody())) + + return } + + // Unhandled error: the status is all the client is entitled to. + message := http.StatusText(http.StatusInternalServerError) + if shared.ExposeInternalErrors() { + message = err.Error() + } + + _ = ctx.JSON(http.StatusInternalServerError, map[string]any{ + "code": "INTERNAL_SERVER_ERROR", + "message": message, + }) } // processResponse handles response struct tags using shared logic. diff --git a/internal/shared/error_disclosure.go b/internal/shared/error_disclosure.go new file mode 100644 index 00000000..04203d45 --- /dev/null +++ b/internal/shared/error_disclosure.go @@ -0,0 +1,45 @@ +package shared + +import ( + "net/http" + "sync/atomic" +) + +// exposeInternalErrors controls whether server-error detail is echoed to +// clients. Set once at app construction; see SetExposeInternalErrors. +var exposeInternalErrors atomic.Bool + +// SetExposeInternalErrors controls whether 5xx responses carry internal error +// detail. The app enables it outside production so local debugging is not +// degraded, and leaves it off in production. +// +// A 5xx body built from a wrapped error commonly embeds whatever the error +// carried — driver output, SQL fragments, file paths, internal hostnames — and +// anyone able to trigger the error can read it. Detail always reaches the logs +// regardless of this setting. +func SetExposeInternalErrors(expose bool) { + exposeInternalErrors.Store(expose) +} + +// ExposeInternalErrors reports the current setting. +func ExposeInternalErrors() bool { + return exposeInternalErrors.Load() +} + +// SanitizeErrorBody returns the body to send for an error response. +// +// For 5xx it replaces the error's own body with a minimal envelope unless +// exposure is enabled: a server error is by definition not something the client +// can act on, so the detail is pure disclosure. 4xx bodies pass through +// untouched — validation messages and field errors are exactly what the client +// needs in order to correct the request. +func SanitizeErrorBody(status int, body any) any { + if status < http.StatusInternalServerError || exposeInternalErrors.Load() { + return body + } + + return map[string]any{ + "code": status, + "error": http.StatusText(status), + } +} diff --git a/internal/shared/error_disclosure_test.go b/internal/shared/error_disclosure_test.go new file mode 100644 index 00000000..07f2ea60 --- /dev/null +++ b/internal/shared/error_disclosure_test.go @@ -0,0 +1,48 @@ +package shared + +import ( + "net/http" + "testing" +) + +// A 5xx body built from a wrapped error embeds the error text. Both the router's +// fallback and DefaultErrorHandler emit these bodies, so the redaction lives here +// where both go through it. +func TestSanitizeErrorBodyRedactsServerErrors(t *testing.T) { + t.Cleanup(func() { SetExposeInternalErrors(false) }) + + leaky := map[string]any{ + "code": 500, + "details": `pq: relation "internal_billing" does not exist`, + } + + SetExposeInternalErrors(false) + + got, ok := SanitizeErrorBody(http.StatusInternalServerError, leaky).(map[string]any) + if !ok { + t.Fatalf("sanitized body has unexpected type %T", got) + } + + if _, present := got["details"]; present { + t.Errorf("server-error detail survived redaction: %v", got) + } + + if got["code"] != http.StatusInternalServerError { + t.Errorf("status code lost: %v", got["code"]) + } + + // 4xx bodies are actionable by the client and must pass through untouched. + clientErr := map[string]any{"code": 400, "details": "field 'email' is required"} + if out := SanitizeErrorBody(http.StatusBadRequest, clientErr); out == nil { + t.Error("client-error body was dropped") + } else if m, _ := out.(map[string]any); m["details"] != "field 'email' is required" { + t.Errorf("client-error detail was altered: %v", m) + } + + // With exposure on, 5xx detail is preserved for local debugging. + SetExposeInternalErrors(true) + + if out, _ := SanitizeErrorBody(http.StatusInternalServerError, leaky).(map[string]any); out["details"] == nil { + t.Error("exposure enabled but detail was still redacted") + } +} diff --git a/internal/shared/errors.go b/internal/shared/errors.go index 6a82010c..8417d1ae 100644 --- a/internal/shared/errors.go +++ b/internal/shared/errors.go @@ -57,9 +57,13 @@ func (h *DefaultErrorHandler) HandleError(ctx context.Context, err error) error // Check if error implements HTTPResponder var responder HTTPResponder if errors.As(err, &responder) { - // Error knows how to format itself + // Error knows how to format itself. Server-error bodies are redacted + // unless exposure is enabled — an InternalError's body embeds the wrapped + // error text, which is not the client's business. if c, ok := ctx.(Context); ok { - return c.JSON(responder.StatusCode(), responder.ResponseBody()) + status := responder.StatusCode() + + return c.JSON(status, SanitizeErrorBody(status, responder.ResponseBody())) } } From ee076e16e5eb0b4064810a1e4c731f29687d9c69 Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Thu, 30 Jul 2026 10:51:31 -0500 Subject: [PATCH 4/7] fix(router): prevent cross-request context and interceptor corruption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three defects that silently crossed request boundaries. Group interceptor inheritance appended onto the group's own slice. When that slice had spare capacity — three separate WithGroupInterceptor options give len 3, cap 4 — each route wrote its interceptor into the group's backing array, and cfg is shallow-copied into route.config and closed over by applyMiddlewareAndInterceptors, so the aliasing outlived registration. A route registered earlier silently inherited a later sibling's interceptor: a route guarded by requireAuth ran publicOnly instead. Both the interceptor and middleware slices are now cloned before appending. shareContextValues aliases the lender's values map into the borrower for O(1) propagation, but the borrower was then returned to the shared pool still holding that alias. The next unrelated request drew it out and NewContext's clear() wiped, then overwrote, the lender's map mid-flight — and that map holds tenant scope (see forge.SetScope), making this a tenant-isolation failure. The borrow is now released before pooling, using the context's own ReleaseSharedValues where available (go-utils >= v1.1.2) and falling back otherwise. Parallel and ParallelAny handed the same context to N goroutines. The context has no internal synchronization — Set is a bare map write and Scope() an unsynchronized lazy init — so interceptors that touch it raced, and an unsynchronized map write is a fatal runtime error recover() cannot catch: it takes the process down rather than failing the request. The documented Enrich pattern triggers it. Interceptors now receive a syncContext sharing one guard. The guard is detachable, which matters more than the mutex: ParallelAny returns on first success and leaves goroutines running, and a lock makes concurrent access safe without stopping a straggler from writing to a context the handler already returned and the pool may have reassigned. Each regression test was confirmed to fail against the pre-fix code. --- internal/router/interceptor_context.go | 215 ++++++++++++++++++ internal/router/interceptor_isolation_test.go | 179 +++++++++++++++ internal/router/interceptor_parallel.go | 16 +- internal/router/middleware.go | 76 +++++-- internal/router/router.go | 52 +++++ internal/router/router_impl.go | 73 +++++- internal/router/router_opts.go | 18 ++ 7 files changed, 606 insertions(+), 23 deletions(-) create mode 100644 internal/router/interceptor_context.go create mode 100644 internal/router/interceptor_isolation_test.go diff --git a/internal/router/interceptor_context.go b/internal/router/interceptor_context.go new file mode 100644 index 00000000..1e100af0 --- /dev/null +++ b/internal/router/interceptor_context.go @@ -0,0 +1,215 @@ +package router + +import ( + "errors" + "sync" + + "github.com/xraph/forge/internal/shared" + "github.com/xraph/go-utils/di" +) + +// errContextDetached is returned by a detached syncContext. It only surfaces to +// an interceptor goroutine whose result has already been discarded, so it never +// reaches a handler. +var errContextDetached = errors.New("interceptor context detached: fan-out already resolved") + +// contextGuard serializes access to one forge context across a fan-out of +// interceptor goroutines, and can be detached once the fan-out's decision is +// made so stragglers stop touching the context altogether. +type contextGuard struct { + mu sync.Mutex + detached bool +} + +// detach makes every syncContext sharing this guard inert. Callers must detach +// before returning from a fan-out that leaves goroutines running: the forge +// context is pooled and reused by the next request, so a straggler writing to +// it after the handler returns corrupts unrelated in-flight requests. +func (g *contextGuard) detach() { + g.mu.Lock() + g.detached = true + g.mu.Unlock() +} + +// embeddedContext exists purely so syncContext can embed the context interface. +// Embedding Context directly would create a field named "Context", which +// shadows the interface's own Context() method and stops the wrapper from +// satisfying the interface. Embedding through an alias keeps the method. +type embeddedContext = Context + +// syncContext serializes the context operations that mutate state shared by +// every interceptor running under Parallel/ParallelAny. +// +// The underlying forge context carries a plain map for values and lazily +// creates its DI scope on first use, with no synchronization of its own — +// it is built for one goroutine per request. Handing the same context to N +// interceptor goroutines therefore races, and an unsynchronized map write is +// a fatal runtime error that recover() cannot catch, so it takes the process +// down rather than just failing the request. Racing the lazy scope init also +// orphans a scope that is never ended. +// +// Only the state-mutating surface is wrapped. Reads of immutable request data +// (Param, Query, Header, Request, Response) pass through untouched, as do the +// response writers — interceptors are expected to allow/block and enrich, not +// to write responses, and a mutex here would not make concurrent response +// writes correct anyway. +type syncContext struct { + embeddedContext // everything not overridden below passes straight through + + guard *contextGuard +} + +// newSyncContext wraps ctx so it can be shared across interceptor goroutines. +// Every wrapper produced for one fan-out must share a single guard. +func newSyncContext(ctx Context, guard *contextGuard) Context { + return &syncContext{embeddedContext: ctx, guard: guard} +} + +// --- values map --- + +func (c *syncContext) Set(key string, value any) { + c.guard.mu.Lock() + defer c.guard.mu.Unlock() + + if c.guard.detached { + return + } + + c.embeddedContext.Set(key, value) +} + +func (c *syncContext) Get(key string) any { + c.guard.mu.Lock() + defer c.guard.mu.Unlock() + + if c.guard.detached { + return nil + } + + return c.embeddedContext.Get(key) +} + +func (c *syncContext) MustGet(key string) any { + c.guard.mu.Lock() + defer c.guard.mu.Unlock() + + if c.guard.detached { + return nil + } + + return c.embeddedContext.MustGet(key) +} + +// --- lazily created DI scope --- + +func (c *syncContext) Scope() di.Scope { + c.guard.mu.Lock() + defer c.guard.mu.Unlock() + + if c.guard.detached { + return nil + } + + return c.embeddedContext.Scope() +} + +func (c *syncContext) Resolve(name string) (any, error) { + c.guard.mu.Lock() + defer c.guard.mu.Unlock() + + if c.guard.detached { + return nil, errContextDetached + } + + return c.embeddedContext.Resolve(name) +} + +func (c *syncContext) Must(name string) any { + c.guard.mu.Lock() + defer c.guard.mu.Unlock() + + if c.guard.detached { + return nil + } + + return c.embeddedContext.Must(name) +} + +// --- lazily loaded session --- + +func (c *syncContext) Session() (shared.Session, error) { + c.guard.mu.Lock() + defer c.guard.mu.Unlock() + + if c.guard.detached { + return nil, errContextDetached + } + + return c.embeddedContext.Session() +} + +func (c *syncContext) SetSession(session shared.Session) { + c.guard.mu.Lock() + defer c.guard.mu.Unlock() + + if c.guard.detached { + return + } + + c.embeddedContext.SetSession(session) +} + +func (c *syncContext) SaveSession() error { + c.guard.mu.Lock() + defer c.guard.mu.Unlock() + + if c.guard.detached { + return errContextDetached + } + + return c.embeddedContext.SaveSession() +} + +func (c *syncContext) DestroySession() error { + c.guard.mu.Lock() + defer c.guard.mu.Unlock() + + if c.guard.detached { + return errContextDetached + } + + return c.embeddedContext.DestroySession() +} + +func (c *syncContext) GetSessionValue(key string) (any, bool) { + c.guard.mu.Lock() + defer c.guard.mu.Unlock() + + if c.guard.detached { + return nil, false + } + + return c.embeddedContext.GetSessionValue(key) +} + +func (c *syncContext) SetSessionValue(key string, value any) { + c.guard.mu.Lock() + defer c.guard.mu.Unlock() + + if c.guard.detached { + return + } + + c.embeddedContext.SetSessionValue(key, value) +} + +func (c *syncContext) DeleteSessionValue(key string) { + c.guard.mu.Lock() + defer c.guard.mu.Unlock() + + if c.guard.detached { + return + } + + c.embeddedContext.DeleteSessionValue(key) +} diff --git a/internal/router/interceptor_isolation_test.go b/internal/router/interceptor_isolation_test.go new file mode 100644 index 00000000..ce130bc2 --- /dev/null +++ b/internal/router/interceptor_isolation_test.go @@ -0,0 +1,179 @@ +package router + +import ( + "net/http" + "net/http/httptest" + "sync" + "testing" + + forge_http "github.com/xraph/go-utils/http" + "github.com/xraph/vessel" +) + +// recordingInterceptor allows a request and records that it ran. +func recordingInterceptor(name string, seen *[]string, mu *sync.Mutex) Interceptor { + return NewInterceptor(name, func(Context, RouteInfo) InterceptorResult { + mu.Lock() + + *seen = append(*seen, name) + + mu.Unlock() + + return Allow() + }) +} + +// Group interceptors are inherited by appending onto the group's slice. If that +// slice is not cloned first, each route writes its own interceptor into the +// group's spare capacity and the previously registered route silently picks up +// the later route's interceptor — dropping, for example, a route's auth check. +func TestGroupInterceptorsAreNotSharedBetweenRoutes(t *testing.T) { + var ( + mu sync.Mutex + seen []string + ) + + r := NewRouter(WithContainer(vessel.New())) + + // Three separate options so the group's slice grows incrementally and ends + // up with capacity beyond its length — the condition that exposes aliasing. + group := r.Group("/api", + WithGroupInterceptor(recordingInterceptor("group1", &seen, &mu)), + WithGroupInterceptor(recordingInterceptor("group2", &seen, &mu)), + WithGroupInterceptor(recordingInterceptor("group3", &seen, &mu)), + ) + + handler := func(ctx Context) error { return ctx.String(http.StatusOK, "ok") } + + if err := group.GET("/private", handler, + WithInterceptor(recordingInterceptor("requireAuth", &seen, &mu))); err != nil { + t.Fatal(err) + } + + if err := group.GET("/public", handler, + WithInterceptor(recordingInterceptor("publicOnly", &seen, &mu))); err != nil { + t.Fatal(err) + } + + // Hit the route registered FIRST — it is the one a later registration can + // have clobbered. + mu.Lock() + seen = nil + mu.Unlock() + + rec := httptest.NewRecorder() + r.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/private", nil)) + + mu.Lock() + + got := append([]string(nil), seen...) + + mu.Unlock() + + want := []string{"group1", "group2", "group3", "requireAuth"} + if len(got) != len(want) { + t.Fatalf("interceptors run = %v, want %v", got, want) + } + + for i := range want { + if got[i] != want[i] { + t.Errorf("interceptor %d = %q, want %q (full chain %v)", i, got[i], want[i], got) + } + } +} + +// shareContextValues aliases the lender's values map into the borrower. If the +// borrower is returned to the shared pool still holding that alias, the next +// unrelated request draws it out, and NewContext's clear() wipes — then +// overwrites — the lender's map while the lender is still in flight. The map +// holds request identity and tenant scope, so this crosses request boundaries. +func TestBorrowedValuesMapIsNotReturnedToThePool(t *testing.T) { + newCtx := func() Context { + return forge_http.NewContext( + httptest.NewRecorder(), + httptest.NewRequest(http.MethodGet, "/", nil), + nil, + ) + } + + // Request 1: an outer context plus a middleware-bridge context that borrows + // its values map, then finishes and is pooled. + lender := newCtx() + lender.Set("tenant", "acme") + + borrower := newCtx() + release := shareContextValues(lender, borrower) + cleanupBorrowedContext(borrower, release) + + // Request 2: fresh context, likely drawn from the pool. + other := newCtx() + + if got := other.Get("tenant"); got != nil { + t.Errorf("new request observed the previous request's data: tenant=%v", got) + } + + other.Set("tenant", "evilcorp") + + if got := lender.Get("tenant"); got != "acme" { + t.Errorf("in-flight request's context was overwritten by a later request: tenant=%v, want acme", got) + } +} + +// Parallel and ParallelAny hand the same context to N goroutines. The context +// has no internal synchronization, and an unsynchronized map write is a fatal +// runtime error that recover() cannot catch — it takes the process down. Run +// under -race to catch the regression. +func TestParallelInterceptorsDoNotRaceOnContext(t *testing.T) { + ctx := forge_http.NewContext( + httptest.NewRecorder(), + httptest.NewRequest(http.MethodGet, "/dashboard", nil), + nil, + ) + + touch := func(key string) Interceptor { + return NewInterceptor("touch:"+key, func(c Context, _ RouteInfo) InterceptorResult { + for i := range 200 { + c.Set(key, i) + _ = c.Get(key) + } + + return AllowWithValues(map[string]any{key: "done"}) + }) + } + + route := RouteInfo{Path: "/dashboard"} + + if res := Parallel(touch("a"), touch("b"), touch("c"), touch("d")).Intercept(ctx, route); res.Blocked { + t.Errorf("Parallel blocked unexpectedly: %v", res.Error) + } + + if res := ParallelAny(touch("e"), touch("f"), touch("g")).Intercept(ctx, route); res.Blocked { + t.Errorf("ParallelAny blocked unexpectedly: %v", res.Error) + } +} + +// Once a fan-out has resolved, stragglers must not keep mutating a context the +// handler has already returned — the pool may have reassigned it. +func TestDetachedSyncContextStopsWriting(t *testing.T) { + inner := forge_http.NewContext( + httptest.NewRecorder(), + httptest.NewRequest(http.MethodGet, "/", nil), + nil, + ) + inner.Set("owner", "request-1") + + guard := &contextGuard{} + wrapped := newSyncContext(inner, guard) + + guard.detach() + + wrapped.Set("owner", "straggler") + + if got := inner.Get("owner"); got != "request-1" { + t.Errorf("detached wrapper still wrote through: owner=%v, want request-1", got) + } + + if got := wrapped.Get("owner"); got != nil { + t.Errorf("detached wrapper still read through: owner=%v, want nil", got) + } +} diff --git a/internal/router/interceptor_parallel.go b/internal/router/interceptor_parallel.go index 6a48c0e5..4cf19498 100644 --- a/internal/router/interceptor_parallel.go +++ b/internal/router/interceptor_parallel.go @@ -43,6 +43,11 @@ func Parallel(interceptors ...Interceptor) Interceptor { execCtx, cancel := context.WithCancel(ctx.Context()) defer cancel() + // The forge context is not safe for concurrent use; every interceptor + // gets a wrapper sharing one guard. See syncContext. + guard := &contextGuard{} + defer guard.detach() + for _, interceptor := range interceptors { wg.Add(1) @@ -56,7 +61,7 @@ func Parallel(interceptors ...Interceptor) Interceptor { default: } - result := i.Intercept(ctx, route) + result := i.Intercept(newSyncContext(ctx, guard), route) results <- parallelResult{name: i.Name(), result: result} // If blocked, cancel other interceptors @@ -136,6 +141,13 @@ func ParallelAny(interceptors ...Interceptor) Interceptor { execCtx, cancel := context.WithCancel(ctx.Context()) defer cancel() + // ParallelAny returns on the first success, leaving the remaining + // interceptors running. Detaching on the way out stops those + // stragglers from touching a context the handler has already + // returned — and that the pool may have handed to another request. + guard := &contextGuard{} + defer guard.detach() + var wg sync.WaitGroup for _, interceptor := range interceptors { @@ -150,7 +162,7 @@ func ParallelAny(interceptors ...Interceptor) Interceptor { default: } - result := i.Intercept(ctx, route) + result := i.Intercept(newSyncContext(ctx, guard), route) results <- outcome{ allowed: !result.Blocked, result: parallelResult{name: i.Name(), result: result}, diff --git a/internal/router/middleware.go b/internal/router/middleware.go index c90dcffc..57cf370c 100644 --- a/internal/router/middleware.go +++ b/internal/router/middleware.go @@ -22,11 +22,12 @@ func (f MiddlewareFunc) ToMiddleware(container vessel.Vessel) Middleware { httpHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Create a new context from the http request with the same container forgeCtx := forge_http.NewContext(w, r, container) - defer forgeCtx.(forge_http.ContextWithClean).Cleanup() // Share values map by reference instead of copying per-key. - // This is O(1) vs O(n) and avoids allocations. - shareContextValues(ctx, forgeCtx) + // This is O(1) vs O(n) and avoids allocations. The borrow is + // released before forgeCtx returns to the pool. + release := shareContextValues(ctx, forgeCtx) + defer cleanupBorrowedContext(forgeCtx, release) // Execute the forge handler if err := next(forgeCtx); err != nil { @@ -70,10 +71,11 @@ func (m PureMiddleware) ToMiddleware() Middleware { // Create a new context from the http request with the same container forgeCtx := forge_http.NewContext(w, r, container) - defer forgeCtx.(forge_http.ContextWithClean).Cleanup() // Share values map by reference instead of copying per-key. - shareContextValues(ctx, forgeCtx) + // The borrow is released before forgeCtx returns to the pool. + release := shareContextValues(ctx, forgeCtx) + defer cleanupBorrowedContext(forgeCtx, release) // Execute the forge handler if err := next(forgeCtx); err != nil { @@ -168,20 +170,62 @@ func FromMiddleware(m Middleware) PureMiddleware { panic("FromMiddleware requires container and errorHandler. Use ToPureMiddleware instead.") } +type contextValuer interface { + Values() map[string]any +} + +type contextSharer interface { + ShareValues(map[string]any) +} + +// contextValuesReleaser is implemented by contexts that track values-map +// ownership themselves and can hand back a private map on request. +type contextValuesReleaser interface { + ReleaseSharedValues() +} + // shareContextValues transfers values from one context to another by sharing -// the underlying map reference. This is O(1) compared to the previous O(n) -// per-key copy loop, and avoids per-hop allocations in the middleware chain. -func shareContextValues(src Context, dst Context) { - type valuer interface { - Values() map[string]any +// the underlying map reference. This is O(1) compared to a per-key copy loop, +// and avoids per-hop allocations in the middleware chain. +// +// The returned function MUST be called before dst is cleaned up. Contexts are +// pooled (see forge_http.NewContext), and a borrowed map that rides along into +// the pool gets cleared and overwritten by whichever unrelated request draws +// that context out next — corrupting the lender's values while it is still in +// flight. The lender's map holds request identity and tenant scope (see +// forge.SetScope), so this is a cross-request data leak, not just a stale read. +// Releasing hands dst a private map so only src continues to own the shared one. +// +// Contexts that track ownership internally release themselves on Cleanup; the +// returned function is then a no-op and exists so this call site stays correct +// against context implementations that do not. +func shareContextValues(src Context, dst Context) (release func()) { + v, ok := src.(contextValuer) + if !ok { + return func() {} + } + + s, ok := dst.(contextSharer) + if !ok { + return func() {} } - type sharer interface { - ShareValues(map[string]any) + + s.ShareValues(v.Values()) + + if r, ok := dst.(contextValuesReleaser); ok { + return r.ReleaseSharedValues } - if v, ok := src.(valuer); ok { - if s, ok := dst.(sharer); ok { - s.ShareValues(v.Values()) - } + return func() { s.ShareValues(make(map[string]any)) } +} + +// cleanupBorrowedContext releases a borrowed values map and then returns the +// context to the pool. Order matters: releasing after Cleanup would put the +// context back while it still aliases the lender's map. +func cleanupBorrowedContext(ctx Context, release func()) { + release() + + if c, ok := ctx.(forge_http.ContextWithClean); ok { + c.Cleanup() } } diff --git a/internal/router/router.go b/internal/router/router.go index 51c6f412..dd1b215f 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -128,6 +128,10 @@ type RouteConfig struct { // SensitiveFieldCleaning enables cleaning of sensitive fields in responses. SensitiveFieldCleaning bool + + // MaxBodySize caps this route's request body in bytes, overriding the + // router-wide setting. 0 inherits; negative means unlimited. + MaxBodySize int64 } // GroupConfig holds route group configuration. @@ -201,6 +205,14 @@ type routerConfig struct { asyncAPIConfig *AsyncAPIConfig metricsConfig *shared.MetricsConfig healthConfig *shared.HealthConfig + + // webSocketOrigins is the allow-list for WebSocket upgrade Origins. + // Empty means same-origin only; see origin.go. + webSocketOrigins []string + + // maxBodySize caps request bodies. 0 means DefaultMaxRequestBodySize; + // negative means unlimited. + maxBodySize int64 } // RouterAdapter wraps a routing backend. @@ -239,6 +251,15 @@ func WithTimeout(d time.Duration) RouteOption { return &timeoutOpt{d} } +// WithMaxBodySize caps this route's request body in bytes, overriding the +// router-wide limit. Use it to raise the cap on upload endpoints, or lower it on +// routes that should only ever receive small payloads. +// +// Pass a negative value for no limit on this route. +func WithMaxBodySize(bytes int64) RouteOption { + return &routeMaxBodySizeOpt{bytes} +} + func WithMetadata(key string, value any) RouteOption { return &metadataOpt{key, value} } @@ -309,3 +330,34 @@ func WithRecovery() RouterOption { func WithHTTPAddress(address string) RouterOption { return &httpAddressOpt{address} } + +// WithWebSocketOrigins sets the Origin allow-list for WebSocket upgrades. +// +// By default only same-origin upgrades are accepted, because browsers send +// cookies with WebSocket upgrades but do not apply CORS to them — an +// unrestricted upgrade endpoint lets any site open an authenticated socket as +// the visiting user. Use this to permit specific cross-origin clients. +// +// Accepted entry forms: +// +// "https://app.example.com" exact scheme://host[:port] +// "app.example.com" bare host, any scheme +// "*.example.com" any strict subdomain of example.com +// "*" any origin (disables the check — avoid in production) +// +// Requests with no Origin header are always allowed: non-browser clients do not +// send one, and browsers always do. +func WithWebSocketOrigins(origins ...string) RouterOption { + return &webSocketOriginsOpt{origins} +} + +// WithMaxRequestBodySize caps the request body for every route on this router, +// in bytes. Bodies exceeding the cap fail the read with a 413-shaped error +// instead of allocating without bound. +// +// Defaults to DefaultMaxRequestBodySize. Pass a negative value to disable the +// limit entirely (not recommended on internet-facing routes); override per route +// with WithMaxBodySize for upload endpoints. +func WithMaxRequestBodySize(bytes int64) RouterOption { + return &maxBodySizeOpt{bytes} +} diff --git a/internal/router/router_impl.go b/internal/router/router_impl.go index 69801139..a34999f4 100644 --- a/internal/router/router_impl.go +++ b/internal/router/router_impl.go @@ -58,6 +58,13 @@ type router struct { webTransportConfig WebTransportConfig http3Server any // *http3.Server + // WebSocket origin allow-list. Empty means same-origin only; see origin.go. + webSocketOrigins []string + + // maxBodySize caps request bodies for every route. 0 means use + // DefaultMaxRequestBodySize; negative means unlimited. + maxBodySize int64 + mu *sync.RWMutex // Pointer to shared mutex for groups } @@ -96,7 +103,11 @@ func newRouter(opts ...RouterOption) *router { asyncAPIConfig: cfg.asyncAPIConfig, metricsConfig: cfg.metricsConfig, healthConfig: cfg.healthConfig, - mu: mu, // Initialize shared mutex + + webSocketOrigins: cfg.webSocketOrigins, + maxBodySize: cfg.maxBodySize, + + mu: mu, // Initialize shared mutex } // Create default BunRouter adapter if none provided @@ -249,7 +260,13 @@ func (r *router) Group(prefix string, opts ...GroupOption) Router { middleware: append([]Middleware{}, r.middleware...), prefix: r.prefix + prefix, groupConfig: cfg, - mu: r.mu, // Share mutex with parent (CRITICAL for thread safety) + + // Groups inherit the parent's WebSocket origin policy; otherwise a + // group-registered socket would silently fall back to same-origin only. + webSocketOrigins: r.webSocketOrigins, + maxBodySize: r.maxBodySize, + + mu: r.mu, // Share mutex with parent (CRITICAL for thread safety) } } @@ -484,7 +501,12 @@ func (r *router) register(method, path string, handler any, opts ...RouteOption) if r.groupConfig != nil { cfg.Tags = append(cfg.Tags, r.groupConfig.Tags...) - cfg.Middleware = append(r.groupConfig.Middleware, cfg.Middleware...) + // Clone the group slice before appending. Appending straight onto + // r.groupConfig.Middleware writes into the group's backing array + // whenever it has spare capacity, so every route in the group would + // overwrite the previous route's entries. + cfg.Middleware = append(slices.Clone(r.groupConfig.Middleware), cfg.Middleware...) + for k, v := range r.groupConfig.Metadata { if cfg.Metadata == nil { cfg.Metadata = make(map[string]any) @@ -495,8 +517,12 @@ func (r *router) register(method, path string, handler any, opts ...RouteOption) } } - // Inherit group interceptors (group interceptors run before route interceptors) - cfg.Interceptors = append(r.groupConfig.Interceptors, cfg.Interceptors...) + // Inherit group interceptors (group interceptors run before route + // interceptors). Cloned for the same reason as Middleware above — + // cfg is shallow-copied into route.config and closed over by + // applyMiddlewareAndInterceptors, so an aliased slice would let one + // route silently inherit a sibling's interceptors. + cfg.Interceptors = append(slices.Clone(r.groupConfig.Interceptors), cfg.Interceptors...) // Merge skip interceptors from group if len(r.groupConfig.SkipInterceptors) > 0 { @@ -601,6 +627,7 @@ func (r *router) register(method, path string, handler any, opts ...RouteOption) routeInfo, r.container, r.errorHandler, + r.resolveMaxBodySize(cfg.MaxBodySize), ) // Wrap with panic recovery if enabled @@ -671,6 +698,32 @@ func applyMiddleware(h http.Handler, middleware []Middleware, container vessel.V }) } +// DefaultMaxRequestBodySize is the request body cap applied when neither the +// router nor the route specifies one. Chosen to be generous for JSON APIs while +// still bounding a single request's allocation; raise it per route for upload +// endpoints via WithMaxBodySize, or set a negative value for no limit. +const DefaultMaxRequestBodySize int64 = 10 << 20 // 10 MiB + +// resolveMaxBodySize picks the effective body cap for a route: the route's own +// value if set, otherwise the router's, otherwise the default. A negative value +// at either level means unlimited and is returned as 0 (no limiter installed). +func (r *router) resolveMaxBodySize(routeMax int64) int64 { + size := routeMax + if size == 0 { + size = r.maxBodySize + } + + if size == 0 { + size = DefaultMaxRequestBodySize + } + + if size < 0 { + return 0 // unlimited + } + + return size +} + // applyMiddlewareAndInterceptors applies middleware chain and interceptors to a handler. // Execution order: middleware -> interceptors -> handler // If any interceptor blocks, the handler is not executed. @@ -682,6 +735,7 @@ func applyMiddlewareAndInterceptors( routeInfo RouteInfo, container vessel.Vessel, errorHandler ErrorHandler, + maxBodySize int64, ) http.Handler { // Convert http.Handler to forge Handler that includes interceptor execution forgeHandler := func(ctx Context) error { @@ -720,6 +774,15 @@ func applyMiddlewareAndInterceptors( r = r.WithContext(context.WithValue(r.Context(), forge_http.ContextKeyForSensitiveCleaning, true)) } + // Cap the request body before any handler, binder or middleware can + // read it. Without this, decoding is bounded only by what the client + // chooses to send, so a single unauthenticated request can drive + // arbitrary allocation. MaxBytesReader also signals the server to + // close the connection once the cap is hit. + if maxBodySize > 0 && r.Body != nil { + r.Body = http.MaxBytesReader(w, r.Body, maxBodySize) + } + // Apply per-route timeout for non-streaming handlers. The server-level // WriteTimeout is 0 (disabled) to support SSE/WebSocket, so regular // REST handlers use a context deadline instead. diff --git a/internal/router/router_opts.go b/internal/router/router_opts.go index 6ba4d9cf..a0f68512 100644 --- a/internal/router/router_opts.go +++ b/internal/router/router_opts.go @@ -155,3 +155,21 @@ type httpAddressOpt struct{ address string } func (o *httpAddressOpt) Apply(cfg *routerConfig) { cfg.httpAddress = o.address } + +type webSocketOriginsOpt struct{ origins []string } + +func (o *webSocketOriginsOpt) Apply(cfg *routerConfig) { + cfg.webSocketOrigins = append(cfg.webSocketOrigins, o.origins...) +} + +type maxBodySizeOpt struct{ bytes int64 } + +func (o *maxBodySizeOpt) Apply(cfg *routerConfig) { + cfg.maxBodySize = o.bytes +} + +type routeMaxBodySizeOpt struct{ bytes int64 } + +func (o *routeMaxBodySizeOpt) Apply(cfg *RouteConfig) { + cfg.MaxBodySize = o.bytes +} From dfa114ece66dabe20a54c9856c42a9c4fb183905 Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Thu, 30 Jul 2026 10:51:53 -0500 Subject: [PATCH 5/7] fix(router): validate upgrade origins and harden streaming paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WebSocket upgrades performed no Origin check. Browsers do not apply CORS to upgrades but do send cookies with them, so any site a logged-in user visited could open an authenticated socket as that user. WebTransport already had a checker, but an empty allow-list meant "allow everything", leaving its default open too. Both now share one checker that defaults to same-origin. Requests with no Origin are still allowed, since non-browser clients do not send one and browsers always do; cross-origin clients are permitted explicitly via WithWebSocketOrigins. Wildcard entries match on label boundaries, so "*.example.com" does not accept evil-example.com. SSE wrote "event: %s" and "data: %s" with no validation. A newline in the event name or a multi-line payload terminates the field and lets the rest parse as further SSE fields — event forgery wherever any part of the value is user-influenced, and silent corruption of multi-line data regardless. Event names and comments are now rejected if they contain a newline, and data is encoded one "data: " line per line as the grammar requires. Connection IDs came from time.Now().UnixNano(), which collides for upgrades landing in the same clock tick; two live connections sharing an ID means cross-talk wherever connections are tracked by ID, and the value was trivially guessable. Now UUIDs. WebSocket reads were unbounded, letting a peer request an arbitrarily large allocation via the advertised frame length, and concurrent readers could interleave partial frames. Reads are now capped and serialized. SSE, WebSocket and WebTransport handlers never called Cleanup, the only place scope.End() and MultipartForm.RemoveAll() run — so every long-lived connection leaked its DI scope, and any that parsed a multipart form leaked temp files, for the process lifetime. These are the longest-lived contexts in the system, so this was the most consequential leak. Also dropped the wildcard Access-Control-Allow-Origin on the AsyncAPI spec endpoint, which made the channel and message map readable by any origin. --- internal/router/asyncapi_generator.go | 7 +- internal/router/hardening_test.go | 200 +++++++++++++++++++++++ internal/router/origin.go | 125 ++++++++++++++ internal/router/origin_test.go | 94 +++++++++++ internal/router/router_streaming.go | 31 +++- internal/router/router_streaming_test.go | 2 +- internal/router/router_webtransport.go | 60 ++----- internal/router/streaming_sse.go | 53 +++++- internal/router/streaming_websocket.go | 108 ++++++++++-- 9 files changed, 608 insertions(+), 72 deletions(-) create mode 100644 internal/router/hardening_test.go create mode 100644 internal/router/origin.go create mode 100644 internal/router/origin_test.go diff --git a/internal/router/asyncapi_generator.go b/internal/router/asyncapi_generator.go index 8fff4b98..2e6b79d4 100644 --- a/internal/router/asyncapi_generator.go +++ b/internal/router/asyncapi_generator.go @@ -418,7 +418,12 @@ func (g *asyncAPIGenerator) handleSpecEndpoint(ctx Context) error { } ctx.Response().Header().Set("Content-Type", "application/json") - ctx.Response().Header().Set("Access-Control-Allow-Origin", "*") + + // No wildcard Access-Control-Allow-Origin here. The spec enumerates every + // channel, message shape and internal name in the app; making it readable by + // any origin hands that map to any page the user visits. Apps that genuinely + // need cross-origin spec access should mount the CORS middleware with an + // explicit allow-list instead. encoder := json.NewEncoder(ctx.Response()) if g.config.PrettyJSON { diff --git a/internal/router/hardening_test.go b/internal/router/hardening_test.go new file mode 100644 index 00000000..00f75f20 --- /dev/null +++ b/internal/router/hardening_test.go @@ -0,0 +1,200 @@ +package router + +import ( + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/xraph/vessel" +) + +type sizedPayload struct { + Blob string `json:"blob"` +} + +// Request bodies were decoded with no cap, so a single unauthenticated request +// could drive allocation as large as the client chose to send. +func TestRequestBodyIsCapped(t *testing.T) { + r := NewRouter(WithContainer(vessel.New()), WithMaxRequestBodySize(1024)) + + if err := r.POST("/echo", func(_ Context, req *sizedPayload) (*sizedPayload, error) { + return req, nil + }); err != nil { + t.Fatal(err) + } + + body := `{"blob":"` + strings.Repeat("A", 64*1024) + `"}` + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/echo", strings.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + r.ServeHTTP(rec, req) + + if rec.Code == http.StatusOK { + t.Errorf("64 KiB body accepted against a 1 KiB cap (status %d)", rec.Code) + } +} + +func TestRequestBodyUnderCapSucceeds(t *testing.T) { + r := NewRouter(WithContainer(vessel.New()), WithMaxRequestBodySize(64*1024)) + + if err := r.POST("/echo", func(_ Context, req *sizedPayload) (*sizedPayload, error) { + return req, nil + }); err != nil { + t.Fatal(err) + } + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/echo", strings.NewReader(`{"blob":"small"}`)) + req.Header.Set("Content-Type", "application/json") + r.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("small body rejected: status %d, body %s", rec.Code, rec.Body.String()) + } +} + +func TestResolveMaxBodySizePrecedence(t *testing.T) { + cases := []struct { + name string + routerWide int64 + route int64 + want int64 + }{ + {"falls back to the default", 0, 0, DefaultMaxRequestBodySize}, + {"router-wide setting applies", 4096, 0, 4096}, + {"route overrides router", 4096, 8192, 8192}, + {"route opts out", 4096, -1, 0}, + {"router opts out", -1, 0, 0}, + {"route raises an opted-out router", -1, 2048, 2048}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + r := &router{maxBodySize: c.routerWide} + if got := r.resolveMaxBodySize(c.route); got != c.want { + t.Errorf("resolveMaxBodySize(route=%d) with router=%d = %d, want %d", + c.route, c.routerWide, got, c.want) + } + }) + } +} + +// SSE fields are newline-delimited. An unescaped newline in the event name or a +// multi-line data payload lets the remainder be parsed as further SSE fields — +// event forgery when any part of the value is user-influenced. +func TestSSESendRejectsNewlineInEventName(t *testing.T) { + stream, rec := newTestSSEStream(t) + + err := stream.Send("update\n\nevent: admin", []byte("payload")) + if err == nil { + t.Fatal("event name containing a newline was accepted") + } + + if body := rec.Body.String(); strings.Contains(body, "event: admin") { + t.Errorf("forged event reached the wire: %q", body) + } +} + +func TestSSESendCommentRejectsNewline(t *testing.T) { + stream, _ := newTestSSEStream(t) + + if err := stream.SendComment("keepalive\n\ndata: forged"); err == nil { + t.Fatal("comment containing a newline was accepted") + } +} + +// Multi-line data must be emitted as one "data: " line per line, per the SSE +// grammar — otherwise the payload is both corrupted and injectable. +func TestSSEEncodesMultiLineData(t *testing.T) { + stream, rec := newTestSSEStream(t) + + if err := stream.Send("message", []byte("line one\nline two\r\nline three")); err != nil { + t.Fatal(err) + } + + body := rec.Body.String() + + for _, want := range []string{ + "event: message\n", + "data: line one\n", + "data: line two\n", + "data: line three\n", + } { + if !strings.Contains(body, want) { + t.Errorf("missing %q in:\n%s", want, body) + } + } + + // A bare "line two" without its data: prefix would mean the payload escaped + // its field. + if strings.Contains(body, "\nline two") { + t.Errorf("data line emitted without its field prefix:\n%s", body) + } +} + +func newTestSSEStream(t *testing.T) (*sseStream, *httptest.ResponseRecorder) { + t.Helper() + + rec := httptest.NewRecorder() + + stream, err := newSSEStream(rec, httptest.NewRequest(http.MethodGet, "/events", nil), 0) + if err != nil { + t.Fatal(err) + } + + return stream, rec +} + +// Connection IDs were derived from time.Now().UnixNano(), which collides for +// upgrades landing in the same clock tick. Two live connections sharing an ID +// means cross-talk anywhere connections are tracked by ID. +func TestConnectionIDsAreUnique(t *testing.T) { + const n = 10_000 + + seen := make(map[string]bool, n) + + for range n { + id := generateConnectionID() + if seen[id] { + t.Fatalf("duplicate connection ID generated: %q", id) + } + + seen[id] = true + } +} + +// A 500 must not echo internal error text unless explicitly enabled; the wrapped +// message can carry driver output, paths and internal hostnames. +func TestInternalErrorDetailIsGatedByEnvironment(t *testing.T) { + t.Cleanup(func() { SetExposeInternalErrors(false) }) + + const secret = "pq: relation \"internal_billing\" does not exist" + + run := func(expose bool) string { + SetExposeInternalErrors(expose) + + r := NewRouter(WithContainer(vessel.New())) + + if err := r.GET("/boom", func(Context) error { + return InternalError(errors.New(secret)) + }); err != nil { + t.Fatal(err) + } + + rec := httptest.NewRecorder() + r.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/boom", nil)) + + return rec.Body.String() + } + + if body := run(false); strings.Contains(body, "internal_billing") { + t.Errorf("production response leaked internal error detail: %s", body) + } + + if body := run(true); !strings.Contains(body, "internal_billing") { + t.Errorf("development response should include detail, got: %s", body) + } +} diff --git a/internal/router/origin.go b/internal/router/origin.go new file mode 100644 index 00000000..6aa5f493 --- /dev/null +++ b/internal/router/origin.go @@ -0,0 +1,125 @@ +package router + +import ( + "net/http" + "net/url" + "strings" +) + +// Origin validation for connection upgrades (WebSocket, WebTransport). +// +// Browsers do not apply CORS to WebSocket or WebTransport upgrades, but they do +// send cookies with them. An upgrade endpoint that accepts any Origin is +// therefore hijackable: any site the user visits can open an authenticated +// socket as that user (cross-site WebSocket hijacking). CORS middleware does +// not help — it governs XHR/fetch, not upgrades. +// +// The default here is same-origin: an upgrade carrying an Origin that does not +// match the request's own host is refused. Requests with no Origin header are +// allowed, because non-browser clients (CLIs, servers, mobile SDKs) do not send +// one and browsers always do. Cross-origin browser clients must be allowed +// explicitly via configuration. + +// requestOriginAllowed reports whether an upgrade request's Origin is +// acceptable given the configured allow-list. +// +// allowedOrigins entries may be: +// - "*" allow any origin (explicit opt-in) +// - "https://app.example.com" exact scheme://host[:port] match +// - "app.example.com" bare host match, any scheme +// - "*.example.com" any strict subdomain of example.com +// +// An empty list means same-origin only. +func requestOriginAllowed(r *http.Request, allowedOrigins []string) bool { + origin := r.Header.Get("Origin") + if origin == "" { + // Non-browser client: no Origin to forge, nothing to check. + return true + } + + parsed, err := url.Parse(origin) + if err != nil || parsed.Host == "" || parsed.Scheme == "" { + return false + } + + originScheme := strings.ToLower(parsed.Scheme) + originHostPort := strings.ToLower(parsed.Host) + + if len(allowedOrigins) == 0 { + return sameOrigin(r, originHostPort) + } + + for _, allowed := range allowedOrigins { + allowed = strings.ToLower(strings.TrimSpace(allowed)) + if allowed == "" { + continue + } + + if allowed == "*" { + return true + } + + // Wildcard subdomain. Matched on label boundaries — a plain suffix + // test would accept "evil-example.com" for "*.example.com". + if rest, ok := strings.CutPrefix(allowed, "*."); ok { + if strings.HasSuffix(stripPort(originHostPort), "."+rest) { + return true + } + + continue + } + + // Exact scheme://host[:port]. + if allowed == originScheme+"://"+originHostPort { + return true + } + + // Bare host[:port], any scheme. + if allowed == originHostPort { + return true + } + } + + return false +} + +// sameOrigin reports whether the origin's host matches the host the request was +// addressed to. Scheme is not compared: a TLS-terminating proxy makes the +// server see http for an https origin, and rejecting that would break every +// deployment behind a load balancer. +func sameOrigin(r *http.Request, originHostPort string) bool { + host := strings.ToLower(r.Host) + if host == "" { + // Nothing to compare against; refuse rather than guess. + return false + } + + if host == originHostPort { + return true + } + + // Tolerate a default port on exactly one side (":443" vs bare host). + return stripDefaultPort(host) == stripDefaultPort(originHostPort) +} + +// stripPort removes a trailing ":port" if present. +func stripPort(hostPort string) string { + if i := strings.LastIndexByte(hostPort, ':'); i != -1 && !strings.Contains(hostPort[i:], "]") { + return hostPort[:i] + } + + return hostPort +} + +// stripDefaultPort removes an explicit :80 or :443 so that "example.com" and +// "example.com:443" compare equal. +func stripDefaultPort(hostPort string) string { + switch { + case strings.HasSuffix(hostPort, ":443"): + return strings.TrimSuffix(hostPort, ":443") + case strings.HasSuffix(hostPort, ":80"): + return strings.TrimSuffix(hostPort, ":80") + default: + return hostPort + } +} diff --git a/internal/router/origin_test.go b/internal/router/origin_test.go new file mode 100644 index 00000000..c75f5cec --- /dev/null +++ b/internal/router/origin_test.go @@ -0,0 +1,94 @@ +package router + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func originRequest(host, origin string) *http.Request { + r := httptest.NewRequest(http.MethodGet, "/ws", nil) + r.Host = host + + if origin != "" { + r.Header.Set("Origin", origin) + } + + return r +} + +// Connection upgrades are not covered by CORS but do carry cookies, so the +// Origin check is the only thing standing between a logged-in user and any site +// they visit opening an authenticated socket. +func TestRequestOriginAllowed(t *testing.T) { + cases := []struct { + name string + host string + origin string + allowed []string + want bool + }{ + // Default policy: same-origin. + {"no origin header is a non-browser client", "api.example.com", "", nil, true}, + {"same origin", "api.example.com", "https://api.example.com", nil, true}, + {"same origin with explicit default port", "api.example.com", "https://api.example.com:443", nil, true}, + {"cross origin denied by default", "api.example.com", "https://evil.com", nil, false}, + {"empty host cannot establish same-origin", "", "https://evil.com", nil, false}, + + // Explicit allow-list forms. + {"exact origin", "api.example.com", "https://app.example.com", []string{"https://app.example.com"}, true}, + {"bare host, any scheme", "api.example.com", "http://app.example.com", []string{"app.example.com"}, true}, + {"wildcard subdomain", "api.example.com", "https://app.example.com", []string{"*.example.com"}, true}, + {"wildcard nested subdomain", "api.example.com", "https://a.b.example.com", []string{"*.example.com"}, true}, + {"explicit star allows anything", "api.example.com", "https://evil.com", []string{"*"}, true}, + + // Regressions: a plain suffix test accepted all of these. + {"suffix bypass with hyphen", "api.example.com", "https://evil-example.com", []string{"*.example.com"}, false}, + {"suffix bypass without separator", "api.example.com", "https://notexample.com", []string{"*.example.com"}, false}, + {"wildcard does not cover the apex", "api.example.com", "https://example.com", []string{"*.example.com"}, false}, + {"port mismatch on exact entry", "api.example.com", "https://exact.test", []string{"https://exact.test:8443"}, false}, + + // Malformed origins must never match. + {"non-absolute origin", "api.example.com", "not-a-url", []string{"*.example.com"}, false}, + {"scheme-only origin", "api.example.com", "javascript:alert(1)", []string{"*.example.com"}, false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := requestOriginAllowed(originRequest(c.host, c.origin), c.allowed) + if got != c.want { + t.Errorf("requestOriginAllowed(host=%q, origin=%q, allowed=%v) = %v, want %v", + c.host, c.origin, c.allowed, got, c.want) + } + }) + } +} + +// Origin matching is case-insensitive on host, per the URL spec. +func TestRequestOriginAllowedIsCaseInsensitive(t *testing.T) { + r := originRequest("api.example.com", "https://APP.EXAMPLE.COM") + + for _, allowed := range [][]string{ + {"*.example.com"}, + {"https://app.example.com"}, + {"app.example.com"}, + } { + if !requestOriginAllowed(r, allowed) { + t.Errorf("uppercase origin rejected by allow-list %v", allowed) + } + } +} + +// WebTransport and WebSocket must enforce identical rules; an empty list used to +// mean "allow everything" on the WebTransport side. +func TestCheckWebTransportOriginDefaultsToSameOrigin(t *testing.T) { + check := checkWebTransportOrigin(nil) + + if check(originRequest("api.example.com", "https://evil.com")) { + t.Error("empty allow-list permitted a cross-origin WebTransport upgrade") + } + + if !check(originRequest("api.example.com", "https://api.example.com")) { + t.Error("empty allow-list rejected a same-origin WebTransport upgrade") + } +} diff --git a/internal/router/router_streaming.go b/internal/router/router_streaming.go index a6e4eb23..4830876c 100644 --- a/internal/router/router_streaming.go +++ b/internal/router/router_streaming.go @@ -1,6 +1,7 @@ package router import ( + "errors" "net/http" forge_http "github.com/xraph/go-utils/http" @@ -10,15 +11,25 @@ import ( func (r *router) WebSocket(path string, handler WebSocketHandler, opts ...RouteOption) error { // Convert WebSocketHandler to http.Handler httpHandler := func(w http.ResponseWriter, req *http.Request) { - // Upgrade to WebSocket - conn, err := upgradeToWebSocket(w, req) + // Upgrade to WebSocket (validates Origin first) + conn, err := upgradeToWebSocket(w, req, r.webSocketOrigins) if err != nil { + if errors.Is(err, errOriginNotAllowed) { + if r.logger != nil { + r.logger.Warn("websocket upgrade rejected: origin not allowed") + } + + http.Error(w, "Forbidden", http.StatusForbidden) + + return + } + if r.logger != nil { r.logger.Error("failed to upgrade websocket connection") } - http.Error(w, "Failed to upgrade connection", http.StatusInternalServerError) - + // ws.UpgradeHTTP has already written its own error response to the + // client, so do not write a second status line here. return } @@ -28,8 +39,10 @@ func (r *router) WebSocket(path string, handler WebSocketHandler, opts ...RouteO wsConn := newWSConnection(connID, conn, req.Context()) defer wsConn.Close() - // Create context + // Create context. Cleanup releases the DI scope and any multipart temp + // files; without it every connection leaks them for process life. ctx := forge_http.NewContext(w, req, r.container) + defer ctx.(forge_http.ContextWithClean).Cleanup() // Call handler if err := handler(ctx, wsConn); err != nil { @@ -77,8 +90,10 @@ func (r *router) EventStream(path string, handler SSEHandler, opts ...RouteOptio } defer stream.Close() - // Create context + // Create context. Cleanup releases the DI scope and any multipart temp + // files; long-lived streams are exactly where leaking them hurts most. ctx := forge_http.NewContext(w, req, r.container) + defer ctx.(forge_http.ContextWithClean).Cleanup() // Call handler if err := handler(ctx, stream); err != nil { @@ -119,8 +134,10 @@ func (r *router) SSE(path string, handler Handler, opts ...RouteOption) error { w.Header().Set("Connection", "keep-alive") w.Header().Set("X-Accel-Buffering", "no") // Disable nginx buffering - // Create context + // Create context. Cleanup releases the DI scope and any multipart temp + // files; long-lived streams are exactly where leaking them hurts most. ctx := forge_http.NewContext(w, req, r.container) + defer ctx.(forge_http.ContextWithClean).Cleanup() // Call handler - user can now use ctx.WriteSSE() if err := handler(ctx); err != nil { diff --git a/internal/router/router_streaming_test.go b/internal/router/router_streaming_test.go index f32bd7b8..b3db1693 100644 --- a/internal/router/router_streaming_test.go +++ b/internal/router/router_streaming_test.go @@ -350,7 +350,7 @@ func TestUpgradeToWebSocket_MockRequest(t *testing.T) { w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/ws", nil) - _, err := upgradeToWebSocket(w, req) + _, err := upgradeToWebSocket(w, req, nil) // Expected to fail with mock request (no upgrade header) assert.Error(t, err) } diff --git a/internal/router/router_webtransport.go b/internal/router/router_webtransport.go index ed2db6e5..04afc756 100644 --- a/internal/router/router_webtransport.go +++ b/internal/router/router_webtransport.go @@ -5,8 +5,6 @@ import ( "errors" "fmt" "net/http" - "net/url" - "strings" "time" "github.com/quic-go/quic-go" @@ -56,8 +54,10 @@ func (r *router) WebTransport(path string, handler WebTransportHandler, opts ... wtSession := newWebTransportSession(sessionID, session, req.Context()) defer wtSession.Close() - // Create context + // Create context. Cleanup releases the DI scope and any multipart temp + // files; long-lived sessions are exactly where leaking them hurts most. ctx := forge_http.NewContext(w, req, r.container) + defer ctx.(forge_http.ContextWithClean).Cleanup() // Call handler if err := handler(ctx, wtSession); err != nil { @@ -187,52 +187,16 @@ func (r *router) StopHTTP3() error { } // checkWebTransportOrigin returns an origin checker function. -// If allowedOrigins is empty, all origins are allowed. +// +// An empty allowedOrigins list means same-origin only. It previously meant +// "allow everything", which left the default configuration open to cross-site +// hijacking — browsers send cookies with WebTransport upgrades but do not apply +// CORS to them. Pass "*" to opt back into allowing any origin. +// +// Delegates to the shared checker in origin.go so WebTransport and WebSocket +// enforce identical rules, including label-boundary wildcard matching. func checkWebTransportOrigin(allowedOrigins []string) func(r *http.Request) bool { - if len(allowedOrigins) == 0 { - return func(r *http.Request) bool { return true } - } - - // Build a set for fast lookups - allowed := make(map[string]bool, len(allowedOrigins)) - allowAll := false - for _, origin := range allowedOrigins { - origin = strings.TrimSpace(origin) - if origin == "*" { - allowAll = true - break - } - allowed[strings.ToLower(origin)] = true - } - - if allowAll { - return func(r *http.Request) bool { return true } - } - return func(r *http.Request) bool { - origin := r.Header.Get("Origin") - if origin == "" { - // No origin header — allow same-origin requests - return true - } - - // Parse origin to normalize - parsed, err := url.Parse(origin) - if err != nil { - return false - } - - // Check scheme://host against allowed origins - normalized := strings.ToLower(parsed.Scheme + "://" + parsed.Host) - if allowed[normalized] { - return true - } - - // Also check just the host (for convenience) - if allowed[strings.ToLower(parsed.Host)] { - return true - } - - return false + return requestOriginAllowed(r, allowedOrigins) } } diff --git a/internal/router/streaming_sse.go b/internal/router/streaming_sse.go index 75b95a06..b6a3acf8 100644 --- a/internal/router/streaming_sse.go +++ b/internal/router/streaming_sse.go @@ -5,7 +5,9 @@ import ( "encoding/json" "errors" "fmt" + "io" "net/http" + "strings" "sync" "time" ) @@ -76,8 +78,51 @@ func (s *sseStream) setWriteDeadline() func() { } } +// errInvalidSSEField is returned when a field value cannot be represented in +// the SSE wire format. +var errInvalidSSEField = errors.New("sse: field value contains a newline") + +// validSSEFieldValue reports whether v is safe to emit as a single-line SSE +// field value (event name, comment, id). +// +// A newline in one of these fields terminates the field and lets the remainder +// be parsed as further SSE fields or a whole extra event. Where any part of the +// value is caller- or user-influenced, that is event forgery: an attacker can +// append "\n\nevent: ...\ndata: ..." and deliver arbitrary events to the client. +func validSSEFieldValue(v string) bool { + return !strings.ContainsAny(v, "\r\n") +} + +// writeSSEData writes a data payload, prefixing every line with "data: " as the +// SSE grammar requires. A bare multi-line payload would otherwise both corrupt +// the message and allow field injection. +func writeSSEData(w io.Writer, data []byte) error { + // Normalize CRLF and CR to LF so line splitting matches what an SSE parser + // on the client will do. + normalized := strings.ReplaceAll(string(data), "\r\n", "\n") + normalized = strings.ReplaceAll(normalized, "\r", "\n") + + for line := range strings.SplitSeq(normalized, "\n") { + if _, err := fmt.Fprintf(w, "data: %s\n", line); err != nil { + return err + } + } + + // Blank line terminates the event. + _, err := io.WriteString(w, "\n") + + return err +} + // Send sends an event to the stream. +// +// The event name must not contain a newline; data may span multiple lines and is +// encoded per the SSE grammar. func (s *sseStream) Send(event string, data []byte) error { + if event != "" && !validSSEFieldValue(event) { + return fmt.Errorf("%w: event name %q", errInvalidSSEField, event) + } + s.mu.Lock() defer s.mu.Unlock() @@ -95,7 +140,7 @@ func (s *sseStream) Send(event string, data []byte) error { } // Write data - if _, err := fmt.Fprintf(s.writer, "data: %s\n\n", string(data)); err != nil { + if err := writeSSEData(s.writer, data); err != nil { return err } @@ -171,7 +216,13 @@ func (s *sseStream) SetRetry(milliseconds int) error { } // SendComment sends a comment (keeps connection alive). +// +// The comment must not contain a newline; see validSSEFieldValue. func (s *sseStream) SendComment(comment string) error { + if !validSSEFieldValue(comment) { + return fmt.Errorf("%w: comment %q", errInvalidSSEField, comment) + } + s.mu.Lock() defer s.mu.Unlock() diff --git a/internal/router/streaming_websocket.go b/internal/router/streaming_websocket.go index 16d8d168..552aa8c8 100644 --- a/internal/router/streaming_websocket.go +++ b/internal/router/streaming_websocket.go @@ -4,28 +4,61 @@ import ( "context" "encoding/json" "errors" - "fmt" "net" "net/http" "sync" - "time" "github.com/gobwas/ws" "github.com/gobwas/ws/wsutil" + "github.com/google/uuid" ) // wsConnection implements Connection using gobwas/ws. type wsConnection struct { - id string - conn net.Conn - ctx context.Context //nolint:containedctx // context needed for WebSocket connection lifecycle and cancellation - cancel context.CancelFunc + id string + conn net.Conn + ctx context.Context //nolint:containedctx // context needed for WebSocket connection lifecycle and cancellation + cancel context.CancelFunc + + // readMu serializes Read: WebSocket framing is stateful, so concurrent + // readers would interleave partial frames. Held across the network read, so + // it must not be the same lock that Close and Write use. + readMu sync.Mutex + mu sync.Mutex closed bool + readLimit int64 remoteAddr string localAddr string } +// limitedConn caps how many bytes may be read from a connection for one +// message, without disturbing writes. +type limitedConn struct { + net.Conn + + remaining int64 +} + +func newLimitedConn(c net.Conn, limit int64) net.Conn { + return &limitedConn{Conn: c, remaining: limit} +} + +func (l *limitedConn) Read(p []byte) (int, error) { + if l.remaining <= 0 { + return 0, ErrMessageTooLarge + } + + if int64(len(p)) > l.remaining { + p = p[:l.remaining] + } + + n, err := l.Conn.Read(p) + l.remaining -= int64(n) + + return n, err +} + // newWSConnection creates a new WebSocket connection. func newWSConnection(id string, conn net.Conn, ctx context.Context) *wsConnection { connCtx, cancel := context.WithCancel(ctx) @@ -44,6 +77,7 @@ func newWSConnection(id string, conn net.Conn, ctx context.Context) *wsConnectio conn: conn, ctx: connCtx, cancel: cancel, + readLimit: DefaultMaxWebSocketMessageSize, remoteAddr: remoteAddr, localAddr: localAddr, } @@ -54,19 +88,50 @@ func (c *wsConnection) ID() string { return c.id } +// DefaultMaxWebSocketMessageSize caps a single inbound WebSocket message. +// Frame length is client-controlled, so an unbounded read lets one peer request +// an arbitrarily large allocation. +const DefaultMaxWebSocketMessageSize int64 = 1 << 20 // 1 MiB + +// ErrMessageTooLarge is returned when an inbound message exceeds the read limit. +var ErrMessageTooLarge = errors.New("websocket message exceeds read limit") + +// SetReadLimit overrides the maximum inbound message size for this connection. +// A value <= 0 restores the default. +func (c *wsConnection) SetReadLimit(limit int64) { + c.mu.Lock() + defer c.mu.Unlock() + + if limit <= 0 { + limit = DefaultMaxWebSocketMessageSize + } + + c.readLimit = limit +} + // Read reads a message from the WebSocket. +// +// Reads are serialized: the underlying framing is stateful, so two concurrent +// readers would interleave partial frames. func (c *wsConnection) Read() ([]byte, error) { - c.mu.Lock() + c.readMu.Lock() + defer c.readMu.Unlock() - if c.closed { - c.mu.Unlock() + c.mu.Lock() + closed := c.closed + limit := c.readLimit + c.mu.Unlock() + if closed { return nil, errors.New("connection closed") } - c.mu.Unlock() + if limit <= 0 { + limit = DefaultMaxWebSocketMessageSize + } - data, _, err := wsutil.ReadClientData(c.conn) + // Bound the read rather than trusting the advertised frame length. + data, _, err := wsutil.ReadClientData(newLimitedConn(c.conn, limit)) if err != nil { return nil, err } @@ -136,8 +201,18 @@ func (c *wsConnection) LocalAddr() string { return c.localAddr } -// upgradeToWebSocket upgrades an HTTP connection to WebSocket. -func upgradeToWebSocket(w http.ResponseWriter, r *http.Request) (net.Conn, error) { +// errOriginNotAllowed is returned when an upgrade request's Origin header is +// rejected. It is deliberately vague to the client; the detail goes to the log. +var errOriginNotAllowed = errors.New("origin not allowed") + +// upgradeToWebSocket upgrades an HTTP connection to WebSocket after validating +// the request Origin. See origin.go for why this check is mandatory rather than +// opt-in. +func upgradeToWebSocket(w http.ResponseWriter, r *http.Request, allowedOrigins []string) (net.Conn, error) { + if !requestOriginAllowed(r, allowedOrigins) { + return nil, errOriginNotAllowed + } + conn, _, _, err := ws.UpgradeHTTP(r, w) if err != nil { return nil, err @@ -147,6 +222,11 @@ func upgradeToWebSocket(w http.ResponseWriter, r *http.Request) (net.Conn, error } // generateConnectionID generates a unique connection ID. +// +// Uses a UUID rather than a timestamp: time.Now().UnixNano() collides for +// upgrades landing in the same clock tick, and two live connections sharing an +// ID means cross-talk wherever connections are tracked by ID. It is also +// trivially guessable. func generateConnectionID() string { - return fmt.Sprintf("ws_%d", time.Now().UnixNano()) + return "ws_" + uuid.NewString() } From 800a2dde3d02bc3b013822bca5e40029aa469659 Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Thu, 30 Jul 2026 10:52:09 -0500 Subject: [PATCH 6/7] feat(app): add body-size, WebSocket-origin and pprof-guard config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing capped request bodies. Binding decoded straight from r.Body, so a single unauthenticated request could drive allocation as large as the client chose to send. MaxRequestBodySize now defaults to 10 MiB and is enforced via MaxBytesReader before any handler, binder or middleware reads the body, with WithMaxBodySize to raise it on upload routes or drop the limit. WebSocketOrigins exposes the upgrade allow-list; unset means same-origin. pprof had no authorization seam. Once enabled the endpoints dump heap and goroutine state, let a caller pin a CPU for an arbitrary duration, and /cmdline discloses the process argv — which commonly carries credentials passed as flags. WithPprofGuard authorizes each request and 404s on denial so the endpoints stay undiscoverable; enabling pprof without a guard now logs a warning. Internal error exposure is wired to Environment, on everywhere but production. --- app.go | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++ app_impl.go | 32 ++++++++++++++++++++++++++++ router.go | 7 +++++++ 3 files changed, 99 insertions(+) diff --git a/app.go b/app.go index e46fa344..7fa06118 100644 --- a/app.go +++ b/app.go @@ -2,6 +2,7 @@ package forge import ( "context" + "net/http" "os" "time" ) @@ -85,12 +86,37 @@ type AppConfig struct { EnablePprof bool // Enable pprof profiling endpoints at /_/debug/pprof (default: false) PprofPrefix string // URL prefix for pprof endpoints (default: "/_/debug/pprof") + // PprofGuard authorizes access to the pprof endpoints. It is called before + // every pprof request; return false to deny (the caller gets a 404, so the + // endpoints are not discoverable). + // + // Strongly recommended whenever EnablePprof is on outside a local dev loop. + // The endpoints expose heap and goroutine contents, allow a caller to pin a + // CPU for an arbitrary duration, and /cmdline discloses the process argv — + // which commonly carries credentials passed as flags. + // + // Nil means no authorization check. + PprofGuard func(*http.Request) bool + ErrorHandler ErrorHandler // Server HTTPAddress string // Default: ":8080" HTTPTimeout time.Duration // Default: 30s + // MaxRequestBodySize caps request bodies in bytes. Bodies larger than this + // are rejected instead of being buffered, so a single request cannot drive + // unbounded allocation. Default: 10 MiB (router.DefaultMaxRequestBodySize). + // Negative disables the limit; override per route with WithMaxBodySize. + MaxRequestBodySize int64 + + // WebSocketOrigins is the Origin allow-list for WebSocket upgrades. Empty + // means same-origin only — browsers send cookies with upgrades but do not + // apply CORS to them, so an open upgrade endpoint is hijackable by any site + // the user visits. Entries may be "https://app.example.com", "app.example.com", + // "*.example.com", or "*" to allow any origin. + WebSocketOrigins []string + // Shutdown ShutdownTimeout time.Duration // Default: 30s ShutdownSignals []os.Signal // Default: SIGINT, SIGTERM @@ -378,6 +404,40 @@ func WithPprofPrefix(prefix string) AppOption { } } +// WithPprofGuard restricts access to the pprof endpoints. The guard runs before +// every pprof request; returning false yields a 404 so the endpoints stay +// undiscoverable. Implies WithPprof(). +// +// Use this whenever profiling is enabled on anything reachable beyond localhost: +// the endpoints dump heap and goroutine state, let a caller pin a CPU for an +// arbitrary duration, and /cmdline reveals the process argv. +// +// forge.WithPprofGuard(func(r *http.Request) bool { +// return subtle.ConstantTimeCompare( +// []byte(r.Header.Get("X-Debug-Token")), []byte(token)) == 1 +// }) +func WithPprofGuard(guard func(*http.Request) bool) AppOption { + return func(c *AppConfig) { + c.EnablePprof = true + c.PprofGuard = guard + } +} + +// WithMaxRequestBodySize caps request bodies app-wide, in bytes. Defaults to +// 10 MiB; pass a negative value to disable the limit. Override per route with +// WithMaxBodySize. +func WithMaxRequestBodySize(bytes int64) AppOption { + return func(c *AppConfig) { c.MaxRequestBodySize = bytes } +} + +// WithWebSocketOrigins sets the Origin allow-list for WebSocket upgrades. +// Without it only same-origin upgrades are accepted — see AppConfig.WebSocketOrigins. +func WithWebSocketOrigins(origins ...string) AppOption { + return func(c *AppConfig) { + c.WebSocketOrigins = append(c.WebSocketOrigins, origins...) + } +} + // NewApp creates a new Forge application. func NewApp(config AppConfig) App { return newApp(config) diff --git a/app_impl.go b/app_impl.go index 20627516..008c4526 100644 --- a/app_impl.go +++ b/app_impl.go @@ -262,6 +262,22 @@ func newApp(config AppConfig) *app { routerOpts = append(routerOpts, internalrouter.WithHTTPAddress(config.HTTPAddress)) } + // Internal error text is echoed to clients only outside production, where + // it aids debugging; in production a 500 carries no detail. Logs always have + // the full error either way. + shared.SetExposeInternalErrors(config.Environment != "production") + + // Cap request bodies. Zero leaves the router's own default in place. + if config.MaxRequestBodySize != 0 { + routerOpts = append(routerOpts, internalrouter.WithMaxRequestBodySize(config.MaxRequestBodySize)) + } + + // Restrict WebSocket upgrades to the configured origins. When unset the + // router enforces same-origin, which is the safe default. + if len(config.WebSocketOrigins) > 0 { + routerOpts = append(routerOpts, internalrouter.WithWebSocketOrigins(config.WebSocketOrigins...)) + } + // Enable panic recovery to prevent unhandled panics from crashing the server routerOpts = append(routerOpts, internalrouter.WithRecovery()) @@ -952,6 +968,14 @@ func (a *app) setupPprofEndpoints() error { // Single wildcard route handles everything: index page, specific // handlers, and named profiles (heap, goroutine, etc.). if err := a.router.Handle(prefix+"/*", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Authorize before doing anything else. 404 rather than 403 so the + // endpoints are not discoverable by an unauthorized caller. + if a.config.PprofGuard != nil && !a.config.PprofGuard(r) { + http.NotFound(w, r) + + return + } + // Strip prefix to get the sub-path (e.g. "/heap", "/profile", ""). sub := strings.TrimPrefix(r.URL.Path, prefix) @@ -975,8 +999,16 @@ func (a *app) setupPprofEndpoints() error { return err } + if a.config.PprofGuard == nil { + a.logger.Warn("pprof endpoints enabled without PprofGuard: heap, goroutine and cmdline data are unauthenticated", + F("prefix", prefix), + F("environment", a.config.Environment), + ) + } + a.logger.Info("pprof profiling endpoints enabled", F("prefix", prefix), + F("guarded", a.config.PprofGuard != nil), ) return nil diff --git a/router.go b/router.go index 4ab6d189..e59fc931 100644 --- a/router.go +++ b/router.go @@ -98,6 +98,13 @@ func WithTimeout(d time.Duration) RouteOption { return router.WithTimeout(d) } +// WithMaxBodySize caps this route's request body in bytes, overriding the +// app-wide MaxRequestBodySize. Raise it for upload endpoints; pass a negative +// value for no limit on this route. +func WithMaxBodySize(bytes int64) RouteOption { + return router.WithMaxBodySize(bytes) +} + func WithMetadata(key string, value any) RouteOption { return router.WithMetadata(key, value) } From 0c6ac3c49df67b953d823e857db0cd84581c6c71 Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Thu, 30 Jul 2026 12:18:20 -0500 Subject: [PATCH 7/7] chore: updated go mods --- examples/cors_preflight/go.mod | 2 +- examples/cors_preflight/go.sum | 4 ++-- examples/di-patterns/go.mod | 2 +- examples/di-patterns/go.sum | 4 ++-- examples/farp-auto-detection/go.mod | 2 +- examples/farp-auto-detection/go.sum | 4 ++-- examples/farp-discovery-example/go.mod | 2 +- examples/farp-discovery-example/go.sum | 4 ++-- examples/file_upload/go.mod | 2 +- examples/file_upload/go.sum | 4 ++-- examples/openapi-demo/go.mod | 2 +- examples/openapi-demo/go.sum | 4 ++-- examples/openapi_complete/go.mod | 2 +- examples/openapi_complete/go.sum | 4 ++-- examples/openapi_unified/go.mod | 2 +- examples/openapi_unified/go.sum | 4 ++-- examples/webtransport/go.mod | 2 +- examples/webtransport/go.sum | 4 ++-- extensions/auth/examples/auth_example/go.mod | 2 +- extensions/auth/examples/auth_example/go.sum | 4 ++-- extensions/cache/go.mod | 2 +- extensions/cache/go.sum | 4 ++-- extensions/consensus/go.mod | 2 +- extensions/consensus/go.sum | 4 ++-- extensions/cron/go.mod | 2 +- extensions/cron/go.sum | 4 ++-- extensions/database/go.mod | 2 +- extensions/database/go.sum | 4 ++-- extensions/discovery/go.mod | 2 +- extensions/discovery/go.sum | 4 ++-- extensions/events/examples/events-demo/go.mod | 2 +- extensions/events/examples/events-demo/go.sum | 4 ++-- extensions/events/go.mod | 2 +- extensions/events/go.sum | 4 ++-- extensions/features/go.mod | 2 +- extensions/features/go.sum | 4 ++-- extensions/grpc/examples/grpc-advanced/go.mod | 2 +- extensions/grpc/examples/grpc-advanced/go.sum | 4 ++-- extensions/grpc/go.mod | 2 +- extensions/grpc/go.sum | 4 ++-- extensions/hls/go.mod | 2 +- extensions/hls/go.sum | 4 ++-- extensions/kafka/go.mod | 2 +- extensions/kafka/go.sum | 4 ++-- extensions/mcp/examples/mcp-server/go.mod | 2 +- extensions/mcp/examples/mcp-server/go.sum | 4 ++-- extensions/mcp/go.mod | 2 +- extensions/mcp/go.sum | 4 ++-- extensions/mqtt/go.mod | 2 +- extensions/mqtt/go.sum | 4 ++-- extensions/orpc/examples/orpc_example/go.mod | 2 +- extensions/orpc/examples/orpc_example/go.sum | 4 ++-- extensions/orpc/go.mod | 2 +- extensions/orpc/go.sum | 4 ++-- extensions/queue/go.mod | 2 +- extensions/queue/go.sum | 4 ++-- extensions/search/go.mod | 2 +- extensions/search/go.sum | 4 ++-- extensions/security/examples/security-auto-middleware/go.mod | 2 +- extensions/security/examples/security-auto-middleware/go.sum | 4 ++-- extensions/security/go.mod | 2 +- extensions/security/go.sum | 4 ++-- extensions/storage/go.mod | 2 +- extensions/storage/go.sum | 4 ++-- extensions/streaming/go.mod | 2 +- extensions/streaming/go.sum | 4 ++-- extensions/webrtc/go.mod | 2 +- extensions/webrtc/go.sum | 4 ++-- go.sum | 2 -- 69 files changed, 102 insertions(+), 104 deletions(-) diff --git a/examples/cors_preflight/go.mod b/examples/cors_preflight/go.mod index 027251ff..394e25f9 100644 --- a/examples/cors_preflight/go.mod +++ b/examples/cors_preflight/go.mod @@ -63,7 +63,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect github.com/xraph/vessel v1.0.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect diff --git a/examples/cors_preflight/go.sum b/examples/cors_preflight/go.sum index 32bc6253..9af4450a 100644 --- a/examples/cors_preflight/go.sum +++ b/examples/cors_preflight/go.sum @@ -274,8 +274,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/examples/di-patterns/go.mod b/examples/di-patterns/go.mod index 84b74903..0e60a3bf 100644 --- a/examples/di-patterns/go.mod +++ b/examples/di-patterns/go.mod @@ -63,7 +63,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect github.com/xraph/vessel v1.0.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect diff --git a/examples/di-patterns/go.sum b/examples/di-patterns/go.sum index 32bc6253..9af4450a 100644 --- a/examples/di-patterns/go.sum +++ b/examples/di-patterns/go.sum @@ -274,8 +274,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/examples/farp-auto-detection/go.mod b/examples/farp-auto-detection/go.mod index 284b35d0..ec334b86 100644 --- a/examples/farp-auto-detection/go.mod +++ b/examples/farp-auto-detection/go.mod @@ -79,7 +79,7 @@ require ( github.com/xraph/confy v0.5.2 // indirect github.com/xraph/farp v1.3.0 // indirect github.com/xraph/farp/discovery v1.2.0 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect github.com/xraph/vessel v1.0.2 // indirect go.etcd.io/etcd/api/v3 v3.5.17 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect diff --git a/examples/farp-auto-detection/go.sum b/examples/farp-auto-detection/go.sum index fc1ba61d..ca36f668 100644 --- a/examples/farp-auto-detection/go.sum +++ b/examples/farp-auto-detection/go.sum @@ -296,8 +296,8 @@ github.com/xraph/farp v1.3.0 h1:9eH09TyRglpvyYL2Su4oxcrAaJm6D3Ey4w9vYDVO298= github.com/xraph/farp v1.3.0/go.mod h1:Nlli8WUsxvQL5wXiJqcAn6OsUHBzKJxrl9JLJ9J6Wqo= github.com/xraph/farp/discovery v1.2.0 h1:DGUZUGdYUhFxvAzTdPZc+11m1e95LzzEiyfS5Sh8xYA= github.com/xraph/farp/discovery v1.2.0/go.mod h1:Lx1zYvPRryyzUyVIPidl2XvspeRPRwHPNfPRQWUhRVg= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/examples/farp-discovery-example/go.mod b/examples/farp-discovery-example/go.mod index 585ed8e5..a48b37f5 100644 --- a/examples/farp-discovery-example/go.mod +++ b/examples/farp-discovery-example/go.mod @@ -79,7 +79,7 @@ require ( github.com/xraph/confy v0.5.2 // indirect github.com/xraph/farp v1.3.0 // indirect github.com/xraph/farp/discovery v1.2.0 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect github.com/xraph/vessel v1.0.2 // indirect go.etcd.io/etcd/api/v3 v3.5.17 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect diff --git a/examples/farp-discovery-example/go.sum b/examples/farp-discovery-example/go.sum index fc1ba61d..ca36f668 100644 --- a/examples/farp-discovery-example/go.sum +++ b/examples/farp-discovery-example/go.sum @@ -296,8 +296,8 @@ github.com/xraph/farp v1.3.0 h1:9eH09TyRglpvyYL2Su4oxcrAaJm6D3Ey4w9vYDVO298= github.com/xraph/farp v1.3.0/go.mod h1:Nlli8WUsxvQL5wXiJqcAn6OsUHBzKJxrl9JLJ9J6Wqo= github.com/xraph/farp/discovery v1.2.0 h1:DGUZUGdYUhFxvAzTdPZc+11m1e95LzzEiyfS5Sh8xYA= github.com/xraph/farp/discovery v1.2.0/go.mod h1:Lx1zYvPRryyzUyVIPidl2XvspeRPRwHPNfPRQWUhRVg= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/examples/file_upload/go.mod b/examples/file_upload/go.mod index ebde73c5..09bd6082 100644 --- a/examples/file_upload/go.mod +++ b/examples/file_upload/go.mod @@ -63,7 +63,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect github.com/xraph/vessel v1.0.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect diff --git a/examples/file_upload/go.sum b/examples/file_upload/go.sum index 32bc6253..9af4450a 100644 --- a/examples/file_upload/go.sum +++ b/examples/file_upload/go.sum @@ -274,8 +274,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/examples/openapi-demo/go.mod b/examples/openapi-demo/go.mod index 957c0cd6..959e0aa3 100644 --- a/examples/openapi-demo/go.mod +++ b/examples/openapi-demo/go.mod @@ -63,7 +63,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect github.com/xraph/vessel v1.0.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect diff --git a/examples/openapi-demo/go.sum b/examples/openapi-demo/go.sum index 32bc6253..9af4450a 100644 --- a/examples/openapi-demo/go.sum +++ b/examples/openapi-demo/go.sum @@ -274,8 +274,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/examples/openapi_complete/go.mod b/examples/openapi_complete/go.mod index 650847fd..af1fba9e 100644 --- a/examples/openapi_complete/go.mod +++ b/examples/openapi_complete/go.mod @@ -63,7 +63,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect github.com/xraph/vessel v1.0.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect diff --git a/examples/openapi_complete/go.sum b/examples/openapi_complete/go.sum index 32bc6253..9af4450a 100644 --- a/examples/openapi_complete/go.sum +++ b/examples/openapi_complete/go.sum @@ -274,8 +274,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/examples/openapi_unified/go.mod b/examples/openapi_unified/go.mod index 524abcc2..d94d571e 100644 --- a/examples/openapi_unified/go.mod +++ b/examples/openapi_unified/go.mod @@ -63,7 +63,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect github.com/xraph/vessel v1.0.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect diff --git a/examples/openapi_unified/go.sum b/examples/openapi_unified/go.sum index 32bc6253..9af4450a 100644 --- a/examples/openapi_unified/go.sum +++ b/examples/openapi_unified/go.sum @@ -274,8 +274,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/examples/webtransport/go.mod b/examples/webtransport/go.mod index 1bbf0979..3b86ffef 100644 --- a/examples/webtransport/go.mod +++ b/examples/webtransport/go.mod @@ -74,7 +74,7 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect github.com/xraph/forgeui v1.4.1 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect github.com/xraph/vessel v1.0.2 // indirect go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect diff --git a/examples/webtransport/go.sum b/examples/webtransport/go.sum index 41b2f1cf..21b9f9e3 100644 --- a/examples/webtransport/go.sum +++ b/examples/webtransport/go.sum @@ -288,8 +288,8 @@ github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= github.com/xraph/forgeui v1.4.1 h1:LHK1t/sZ+9zL+MNUZralO9/rc0f5UCa19dpbWTuRMNg= github.com/xraph/forgeui v1.4.1/go.mod h1:rH/+wb1tt2pXSHotWAvoP+Lt846xlIjuwPDSpS5K5mw= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= diff --git a/extensions/auth/examples/auth_example/go.mod b/extensions/auth/examples/auth_example/go.mod index 90229036..0365ea61 100644 --- a/extensions/auth/examples/auth_example/go.mod +++ b/extensions/auth/examples/auth_example/go.mod @@ -66,7 +66,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect github.com/xraph/vessel v1.0.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect diff --git a/extensions/auth/examples/auth_example/go.sum b/extensions/auth/examples/auth_example/go.sum index 84a47495..b756b1ec 100644 --- a/extensions/auth/examples/auth_example/go.sum +++ b/extensions/auth/examples/auth_example/go.sum @@ -283,8 +283,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/extensions/cache/go.mod b/extensions/cache/go.mod index 7d7b94f7..f63f2d3d 100644 --- a/extensions/cache/go.mod +++ b/extensions/cache/go.mod @@ -68,7 +68,7 @@ require ( github.com/spf13/pflag v1.0.9 // indirect github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect diff --git a/extensions/cache/go.sum b/extensions/cache/go.sum index 32bc6253..9af4450a 100644 --- a/extensions/cache/go.sum +++ b/extensions/cache/go.sum @@ -274,8 +274,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/extensions/consensus/go.mod b/extensions/consensus/go.mod index 9fb36d24..22f541a6 100644 --- a/extensions/consensus/go.mod +++ b/extensions/consensus/go.mod @@ -78,7 +78,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect diff --git a/extensions/consensus/go.sum b/extensions/consensus/go.sum index f48281e0..456cbce6 100644 --- a/extensions/consensus/go.sum +++ b/extensions/consensus/go.sum @@ -291,8 +291,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= diff --git a/extensions/cron/go.mod b/extensions/cron/go.mod index 5f71deb1..7defaab7 100644 --- a/extensions/cron/go.mod +++ b/extensions/cron/go.mod @@ -68,7 +68,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect diff --git a/extensions/cron/go.sum b/extensions/cron/go.sum index 38afff9f..372e9ab7 100644 --- a/extensions/cron/go.sum +++ b/extensions/cron/go.sum @@ -276,8 +276,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/extensions/database/go.mod b/extensions/database/go.mod index f8c5b9f6..312ceb9b 100644 --- a/extensions/database/go.mod +++ b/extensions/database/go.mod @@ -17,7 +17,7 @@ require ( github.com/uptrace/bun/dialect/pgdialect v1.2.15 github.com/uptrace/bun/dialect/sqlitedialect v1.2.15 github.com/xraph/forge v1.4.4 - github.com/xraph/go-utils v1.1.1 + github.com/xraph/go-utils v1.1.2 github.com/xraph/vessel v1.0.2 go.mongodb.org/mongo-driver v1.17.4 ) diff --git a/extensions/database/go.sum b/extensions/database/go.sum index 618f5229..e9870b6f 100644 --- a/extensions/database/go.sum +++ b/extensions/database/go.sum @@ -312,8 +312,8 @@ github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6 github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= diff --git a/extensions/discovery/go.mod b/extensions/discovery/go.mod index d84fe5f4..76054c96 100644 --- a/extensions/discovery/go.mod +++ b/extensions/discovery/go.mod @@ -82,7 +82,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect go.etcd.io/etcd/api/v3 v3.5.17 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/extensions/discovery/go.sum b/extensions/discovery/go.sum index fc1ba61d..ca36f668 100644 --- a/extensions/discovery/go.sum +++ b/extensions/discovery/go.sum @@ -296,8 +296,8 @@ github.com/xraph/farp v1.3.0 h1:9eH09TyRglpvyYL2Su4oxcrAaJm6D3Ey4w9vYDVO298= github.com/xraph/farp v1.3.0/go.mod h1:Nlli8WUsxvQL5wXiJqcAn6OsUHBzKJxrl9JLJ9J6Wqo= github.com/xraph/farp/discovery v1.2.0 h1:DGUZUGdYUhFxvAzTdPZc+11m1e95LzzEiyfS5Sh8xYA= github.com/xraph/farp/discovery v1.2.0/go.mod h1:Lx1zYvPRryyzUyVIPidl2XvspeRPRwHPNfPRQWUhRVg= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/extensions/events/examples/events-demo/go.mod b/extensions/events/examples/events-demo/go.mod index 1cc00c5e..0ae49366 100644 --- a/extensions/events/examples/events-demo/go.mod +++ b/extensions/events/examples/events-demo/go.mod @@ -72,7 +72,7 @@ require ( github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect github.com/xraph/vessel v1.0.2 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect go.mongodb.org/mongo-driver v1.17.4 // indirect diff --git a/extensions/events/examples/events-demo/go.sum b/extensions/events/examples/events-demo/go.sum index 81489525..eef32c51 100644 --- a/extensions/events/examples/events-demo/go.sum +++ b/extensions/events/examples/events-demo/go.sum @@ -288,8 +288,8 @@ github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= github.com/xraph/forge v1.4.4 h1:6zt75/XRxEDNPng7YOZSEJI0oKAuJpKrVLZjRRgvS70= github.com/xraph/forge v1.4.4/go.mod h1:Il5MLHm1npOC2UCZwDEd4E4p7lEDqlsg2pJ3W4nOQSo= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= diff --git a/extensions/events/go.mod b/extensions/events/go.mod index 0f5ccdaa..9a0d4ad3 100644 --- a/extensions/events/go.mod +++ b/extensions/events/go.mod @@ -11,7 +11,7 @@ require ( github.com/redis/go-redis/v9 v9.14.1 github.com/stretchr/testify v1.11.1 github.com/xraph/forge v1.4.4 - github.com/xraph/go-utils v1.1.1 + github.com/xraph/go-utils v1.1.2 github.com/xraph/vessel v1.0.2 go.mongodb.org/mongo-driver v1.17.4 ) diff --git a/extensions/events/go.sum b/extensions/events/go.sum index f69616b3..676197fe 100644 --- a/extensions/events/go.sum +++ b/extensions/events/go.sum @@ -294,8 +294,8 @@ github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6 github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= diff --git a/extensions/features/go.mod b/extensions/features/go.mod index 1350595c..0a5c38a2 100644 --- a/extensions/features/go.mod +++ b/extensions/features/go.mod @@ -84,7 +84,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect diff --git a/extensions/features/go.sum b/extensions/features/go.sum index ed5567b7..1057f069 100644 --- a/extensions/features/go.sum +++ b/extensions/features/go.sum @@ -320,8 +320,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/extensions/grpc/examples/grpc-advanced/go.mod b/extensions/grpc/examples/grpc-advanced/go.mod index 9bc6f83c..1ea9692f 100644 --- a/extensions/grpc/examples/grpc-advanced/go.mod +++ b/extensions/grpc/examples/grpc-advanced/go.mod @@ -61,7 +61,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect github.com/xraph/vessel v1.0.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect diff --git a/extensions/grpc/examples/grpc-advanced/go.sum b/extensions/grpc/examples/grpc-advanced/go.sum index 81590678..152884e3 100644 --- a/extensions/grpc/examples/grpc-advanced/go.sum +++ b/extensions/grpc/examples/grpc-advanced/go.sum @@ -264,8 +264,8 @@ github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= github.com/xraph/forge v1.4.4 h1:6zt75/XRxEDNPng7YOZSEJI0oKAuJpKrVLZjRRgvS70= github.com/xraph/forge v1.4.4/go.mod h1:Il5MLHm1npOC2UCZwDEd4E4p7lEDqlsg2pJ3W4nOQSo= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= diff --git a/extensions/grpc/go.mod b/extensions/grpc/go.mod index cd65feea..f2506402 100644 --- a/extensions/grpc/go.mod +++ b/extensions/grpc/go.mod @@ -67,7 +67,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect diff --git a/extensions/grpc/go.sum b/extensions/grpc/go.sum index f55ea723..55c6907a 100644 --- a/extensions/grpc/go.sum +++ b/extensions/grpc/go.sum @@ -278,8 +278,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= diff --git a/extensions/hls/go.mod b/extensions/hls/go.mod index f0b9d1ad..5e905396 100644 --- a/extensions/hls/go.mod +++ b/extensions/hls/go.mod @@ -91,7 +91,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect diff --git a/extensions/hls/go.sum b/extensions/hls/go.sum index 14a4569a..2d1231a8 100644 --- a/extensions/hls/go.sum +++ b/extensions/hls/go.sum @@ -312,8 +312,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/extensions/kafka/go.mod b/extensions/kafka/go.mod index 0ead8ca1..b6e9f0c8 100644 --- a/extensions/kafka/go.mod +++ b/extensions/kafka/go.mod @@ -81,7 +81,7 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect diff --git a/extensions/kafka/go.sum b/extensions/kafka/go.sum index 284528ae..9db7a63d 100644 --- a/extensions/kafka/go.sum +++ b/extensions/kafka/go.sum @@ -313,8 +313,8 @@ github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6 github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/extensions/mcp/examples/mcp-server/go.mod b/extensions/mcp/examples/mcp-server/go.mod index 7a820684..ae0933b3 100644 --- a/extensions/mcp/examples/mcp-server/go.mod +++ b/extensions/mcp/examples/mcp-server/go.mod @@ -59,7 +59,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect github.com/xraph/vessel v1.0.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect diff --git a/extensions/mcp/examples/mcp-server/go.sum b/extensions/mcp/examples/mcp-server/go.sum index 18b3b8fc..8a8e177b 100644 --- a/extensions/mcp/examples/mcp-server/go.sum +++ b/extensions/mcp/examples/mcp-server/go.sum @@ -258,8 +258,8 @@ github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= github.com/xraph/forge v1.4.4 h1:6zt75/XRxEDNPng7YOZSEJI0oKAuJpKrVLZjRRgvS70= github.com/xraph/forge v1.4.4/go.mod h1:Il5MLHm1npOC2UCZwDEd4E4p7lEDqlsg2pJ3W4nOQSo= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/extensions/mcp/go.mod b/extensions/mcp/go.mod index aa4be5e8..1c6bb1f5 100644 --- a/extensions/mcp/go.mod +++ b/extensions/mcp/go.mod @@ -5,7 +5,7 @@ go 1.25.7 require ( github.com/xraph/confy v0.5.2 github.com/xraph/forge v1.4.4 - github.com/xraph/go-utils v1.1.1 + github.com/xraph/go-utils v1.1.2 github.com/xraph/vessel v1.0.2 ) diff --git a/extensions/mcp/go.sum b/extensions/mcp/go.sum index 32bc6253..9af4450a 100644 --- a/extensions/mcp/go.sum +++ b/extensions/mcp/go.sum @@ -274,8 +274,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/extensions/mqtt/go.mod b/extensions/mqtt/go.mod index 2f58535e..e81606b3 100644 --- a/extensions/mqtt/go.mod +++ b/extensions/mqtt/go.mod @@ -66,7 +66,7 @@ require ( github.com/spf13/pflag v1.0.9 // indirect github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect diff --git a/extensions/mqtt/go.sum b/extensions/mqtt/go.sum index 6e6dfa11..c2492f80 100644 --- a/extensions/mqtt/go.sum +++ b/extensions/mqtt/go.sum @@ -278,8 +278,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/extensions/orpc/examples/orpc_example/go.mod b/extensions/orpc/examples/orpc_example/go.mod index a9cba372..e62f494a 100644 --- a/extensions/orpc/examples/orpc_example/go.mod +++ b/extensions/orpc/examples/orpc_example/go.mod @@ -59,7 +59,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect github.com/xraph/vessel v1.0.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect diff --git a/extensions/orpc/examples/orpc_example/go.sum b/extensions/orpc/examples/orpc_example/go.sum index b03554cc..dd9d9dbd 100644 --- a/extensions/orpc/examples/orpc_example/go.sum +++ b/extensions/orpc/examples/orpc_example/go.sum @@ -258,8 +258,8 @@ github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= github.com/xraph/forge v1.2.0 h1:+f5pAoWIa4vzFDD3j5b3qDk+X52/Hr1kyoV6CV8vx0g= github.com/xraph/forge v1.2.0/go.mod h1:n7mKRcLgNsGblsMDmpBrZNieMSe7dFmnGaA1O8sDZSw= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/extensions/orpc/go.mod b/extensions/orpc/go.mod index a7e2a059..cdd59dc7 100644 --- a/extensions/orpc/go.mod +++ b/extensions/orpc/go.mod @@ -64,7 +64,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect diff --git a/extensions/orpc/go.sum b/extensions/orpc/go.sum index 32bc6253..9af4450a 100644 --- a/extensions/orpc/go.sum +++ b/extensions/orpc/go.sum @@ -274,8 +274,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/extensions/queue/go.mod b/extensions/queue/go.mod index 25269f2c..07189e55 100644 --- a/extensions/queue/go.mod +++ b/extensions/queue/go.mod @@ -89,7 +89,7 @@ require ( github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect go.mongodb.org/mongo-driver v1.17.4 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/extensions/queue/go.sum b/extensions/queue/go.sum index 2e1bc0b2..ce4e3f8e 100644 --- a/extensions/queue/go.sum +++ b/extensions/queue/go.sum @@ -320,8 +320,8 @@ github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6 github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= diff --git a/extensions/search/go.mod b/extensions/search/go.mod index 75034a0e..77819dee 100644 --- a/extensions/search/go.mod +++ b/extensions/search/go.mod @@ -64,7 +64,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect diff --git a/extensions/search/go.sum b/extensions/search/go.sum index 32bc6253..9af4450a 100644 --- a/extensions/search/go.sum +++ b/extensions/search/go.sum @@ -274,8 +274,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/extensions/security/examples/security-auto-middleware/go.mod b/extensions/security/examples/security-auto-middleware/go.mod index 7617b21a..1940307b 100644 --- a/extensions/security/examples/security-auto-middleware/go.mod +++ b/extensions/security/examples/security-auto-middleware/go.mod @@ -60,7 +60,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect github.com/xraph/vessel v1.0.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect diff --git a/extensions/security/examples/security-auto-middleware/go.sum b/extensions/security/examples/security-auto-middleware/go.sum index b4d2c631..9a3c8812 100644 --- a/extensions/security/examples/security-auto-middleware/go.sum +++ b/extensions/security/examples/security-auto-middleware/go.sum @@ -260,8 +260,8 @@ github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= github.com/xraph/forge v1.4.4 h1:6zt75/XRxEDNPng7YOZSEJI0oKAuJpKrVLZjRRgvS70= github.com/xraph/forge v1.4.4/go.mod h1:Il5MLHm1npOC2UCZwDEd4E4p7lEDqlsg2pJ3W4nOQSo= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/extensions/security/go.mod b/extensions/security/go.mod index e17f21e2..43387308 100644 --- a/extensions/security/go.mod +++ b/extensions/security/go.mod @@ -5,7 +5,7 @@ go 1.25.7 require ( github.com/golang-jwt/jwt/v5 v5.3.0 github.com/xraph/forge v1.4.4 - github.com/xraph/go-utils v1.1.1 + github.com/xraph/go-utils v1.1.2 github.com/xraph/vessel v1.0.2 golang.org/x/crypto v0.51.0 ) diff --git a/extensions/security/go.sum b/extensions/security/go.sum index 6647711a..b7ce2a68 100644 --- a/extensions/security/go.sum +++ b/extensions/security/go.sum @@ -276,8 +276,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/extensions/storage/go.mod b/extensions/storage/go.mod index ecc8f61c..4bf79616 100644 --- a/extensions/storage/go.mod +++ b/extensions/storage/go.mod @@ -9,7 +9,7 @@ require ( github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.14 github.com/aws/aws-sdk-go-v2/service/s3 v1.88.6 github.com/xraph/forge v1.4.4 - github.com/xraph/go-utils v1.1.1 + github.com/xraph/go-utils v1.1.2 github.com/xraph/vessel v1.0.2 ) diff --git a/extensions/storage/go.sum b/extensions/storage/go.sum index 14a4569a..2d1231a8 100644 --- a/extensions/storage/go.sum +++ b/extensions/storage/go.sum @@ -312,8 +312,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/extensions/streaming/go.mod b/extensions/streaming/go.mod index ef608d51..a45862bc 100644 --- a/extensions/streaming/go.mod +++ b/extensions/streaming/go.mod @@ -70,7 +70,7 @@ require ( github.com/uptrace/bunrouter v1.0.23 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/extensions/streaming/go.sum b/extensions/streaming/go.sum index 41b2f1cf..21b9f9e3 100644 --- a/extensions/streaming/go.sum +++ b/extensions/streaming/go.sum @@ -288,8 +288,8 @@ github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= github.com/xraph/forgeui v1.4.1 h1:LHK1t/sZ+9zL+MNUZralO9/rc0f5UCa19dpbWTuRMNg= github.com/xraph/forgeui v1.4.1/go.mod h1:rH/+wb1tt2pXSHotWAvoP+Lt846xlIjuwPDSpS5K5mw= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= diff --git a/extensions/webrtc/go.mod b/extensions/webrtc/go.mod index 09d0b680..2b1a98a2 100644 --- a/extensions/webrtc/go.mod +++ b/extensions/webrtc/go.mod @@ -89,7 +89,7 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/xraph/confy v0.5.2 // indirect github.com/xraph/forgeui v1.4.1 // indirect - github.com/xraph/go-utils v1.1.1 // indirect + github.com/xraph/go-utils v1.1.2 // indirect github.com/xraph/vessel v1.0.2 // indirect go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect diff --git a/extensions/webrtc/go.sum b/extensions/webrtc/go.sum index 84be56b7..8a5055fa 100644 --- a/extensions/webrtc/go.sum +++ b/extensions/webrtc/go.sum @@ -337,8 +337,8 @@ github.com/xraph/confy v0.5.2 h1:YlZDDG2sZWyiSm4yjXTLQ7RFdy/3izg/plJh1g9guEA= github.com/xraph/confy v0.5.2/go.mod h1:FPupuOi04fSgh8pKb9BOTPxyzq9WhIncfY9aDH0bLO8= github.com/xraph/forgeui v1.4.1 h1:LHK1t/sZ+9zL+MNUZralO9/rc0f5UCa19dpbWTuRMNg= github.com/xraph/forgeui v1.4.1/go.mod h1:rH/+wb1tt2pXSHotWAvoP+Lt846xlIjuwPDSpS5K5mw= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= +github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= +github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo= github.com/xraph/vessel v1.0.2/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/go.sum b/go.sum index ab442eed..0872b885 100644 --- a/go.sum +++ b/go.sum @@ -306,8 +306,6 @@ github.com/xraph/farp/discovery v1.2.0 h1:DGUZUGdYUhFxvAzTdPZc+11m1e95LzzEiyfS5S github.com/xraph/farp/discovery v1.2.0/go.mod h1:Lx1zYvPRryyzUyVIPidl2XvspeRPRwHPNfPRQWUhRVg= github.com/xraph/forgeui v1.4.1 h1:LHK1t/sZ+9zL+MNUZralO9/rc0f5UCa19dpbWTuRMNg= github.com/xraph/forgeui v1.4.1/go.mod h1:rH/+wb1tt2pXSHotWAvoP+Lt846xlIjuwPDSpS5K5mw= -github.com/xraph/go-utils v1.1.1 h1:k5TOQ1qhXLdEX8W5F60mSuPcFOPc8sBsnhmjx/Kpr5g= -github.com/xraph/go-utils v1.1.1/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/go-utils v1.1.2 h1:h+gLUnSHbtfXPjFjUW2LaUBDi0inMG0pdx/lnvcez3E= github.com/xraph/go-utils v1.1.2/go.mod h1:tZN4SuGy9otCo6dETp7Cvkgyiy0BvaJaZbTBv4Euvo4= github.com/xraph/vessel v1.0.2 h1:IeNTwxiFgqH2vW9lh8PNXr1SeGdEc7cxQ6sH7jMokfo=