a4362b842d
Build / build (push) Successful in 11m42s
Security: - rate limit /api/webhook routes per-IP and cap concurrent site syncs - global SSE connection cap (256) with new sse_gate - validate ?tail= and cap JSON log responses at 4 MiB - strip ANSI/CSI/OSC and control bytes from streamed log lines - redact webhook secret from request log middleware - scrub host details from /api/health for non-admin viewers - drop container_id from /api/system/stats/top for non-admins - generate webhook secrets via crypto/rand; require >=32 chars on insert - verify iid path consistency in streamContainerLogs - LimitReader on site webhook body; reject malformed non-empty bodies Concurrency / correctness: - stats collector: Stop() no longer hangs without Start(), semaphore acquired in parent loop so ctx cancellation short-circuits the queue, in-flight tick cancellable via shared base context, zero-ts guard - webhook handler: replace fire-and-forget goroutine with WaitGroup-tracked workers + Drain() wired into graceful shutdown - $derived(() => ...) mis-idiom fixed in ContainerStats / InstanceCard / ProjectCard (returned function instead of value) - SystemResourcesCard: rename `window` and `t` locals to avoid shadowing globalThis.window and the i18n `t` import Quality / performance: - replace O(n^2) insertion sort with sort.Slice in stats top - runMigrations only swallows duplicate-column / already-exists errors - PruneStatsSamplesBefore wrapped in a transaction - collapse N+1 in unusedImageStats / pruneImages to one ListAllInstances pass; surface DB errors instead of silently treating them as inactive - run Docker Info + DiskUsage in parallel via errgroup - container log SSE emits `: ping` heartbeat every 20 s - imageMatches case-insensitive on registry host (RFC behaviour) - log warning on invalid stage tag pattern instead of silent skip - reject malformed non-empty site webhook payloads Frontend / i18n: - shared formatBytes utility replaces three local copies - statsInterval store drives dynamic "no samples / collection disabled" copy across ContainerStats and SystemResourcesCard - top consumers row now shows owner_name (project/stage or site name) - drop seven `as any` casts on the Settings type; add cloudflare_api_token write-only field - move "Service status", "Docker daemon", "Docker unreachable", "Proxy unreachable", "reachable", and "Docker daemon is not reachable." strings into en/ru i18n bundles
41 lines
1.1 KiB
Go
41 lines
1.1 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"sync/atomic"
|
|
)
|
|
|
|
// maxConcurrentSSEStreams caps the global number of in-flight SSE
|
|
// connections. Each stream holds a goroutine, an event-bus subscription, and
|
|
// (for log streams) a Docker daemon TCP socket; a single tab opening
|
|
// thousands of EventSources would otherwise exhaust file descriptors.
|
|
const maxConcurrentSSEStreams = 256
|
|
|
|
// sseGate is a counting gate that limits concurrent SSE streams.
|
|
type sseGate struct {
|
|
cap int64
|
|
cur atomic.Int64
|
|
}
|
|
|
|
func newSSEGate(cap int) *sseGate { return &sseGate{cap: int64(cap)} }
|
|
|
|
// enter reserves a slot and returns a release func, or nil if the gate is full.
|
|
func (g *sseGate) enter() func() {
|
|
if g.cur.Add(1) > g.cap {
|
|
g.cur.Add(-1)
|
|
return nil
|
|
}
|
|
return func() { g.cur.Add(-1) }
|
|
}
|
|
|
|
// acquireSSESlot is a small helper used by every SSE handler to honour the
|
|
// global cap. Returns false (and writes a 503) if the cap is reached.
|
|
func acquireSSESlot(w http.ResponseWriter, gate *sseGate) (release func(), ok bool) {
|
|
release = gate.enter()
|
|
if release == nil {
|
|
respondError(w, http.StatusServiceUnavailable, "stream limit reached")
|
|
return nil, false
|
|
}
|
|
return release, true
|
|
}
|