Heavy security and file splitting
CI / Go Tests (push) Successful in 15s
CI / Build (push) Successful in 9s
Format / gofmt (push) Successful in 7s
Release Artifacts / Validate release tag (push) Successful in 2s
Release Artifacts / Build and release executables (push) Successful in 15s
Release Artifacts / Build and release Docker image (push) Successful in 28s
CI / Go Tests (push) Successful in 15s
CI / Build (push) Successful in 9s
Format / gofmt (push) Successful in 7s
Release Artifacts / Validate release tag (push) Successful in 2s
Release Artifacts / Build and release executables (push) Successful in 15s
Release Artifacts / Build and release Docker image (push) Successful in 28s
This commit is contained in:
@@ -18,6 +18,15 @@ type visitor struct {
|
||||
lastSeen time.Time
|
||||
}
|
||||
|
||||
// rateLimiterMaxVisitors caps the per-limiter visitor map (token buckets keyed by client IP).
|
||||
// Prevents unbounded memory growth under many distinct clients (or DoS) while prune
|
||||
// still removes stale (>ttl) entries on every Allow. 1024 is generous for the intended
|
||||
// LAN/minimal-public use; scans are O(cap) worst case.
|
||||
const (
|
||||
rateLimiterMaxVisitors = 1024
|
||||
rateLimiterPruneTTL = 10 * time.Minute
|
||||
)
|
||||
|
||||
type RateLimiter struct {
|
||||
mu sync.Mutex
|
||||
visitors map[string]*visitor
|
||||
@@ -31,7 +40,7 @@ func NewRateLimiter(requestsPerMinute int, burst int) *RateLimiter {
|
||||
visitors: map[string]*visitor{},
|
||||
rate: rate.Limit(float64(requestsPerMinute) / 60.0),
|
||||
burst: burst,
|
||||
ttl: 10 * time.Minute,
|
||||
ttl: rateLimiterPruneTTL,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +62,13 @@ func (r *RateLimiter) Allow(ip string) bool {
|
||||
limiter: rate.NewLimiter(r.rate, r.burst),
|
||||
}
|
||||
r.visitors[ip] = v
|
||||
if len(r.visitors) > rateLimiterMaxVisitors {
|
||||
// evict arbitrary entry (prune already removed stales); keeps map bounded
|
||||
for k := range r.visitors {
|
||||
delete(r.visitors, k)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v.lastSeen = now
|
||||
@@ -235,3 +251,25 @@ func writeJSON(w http.ResponseWriter, status int, payload any) {
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(payload)
|
||||
}
|
||||
|
||||
// SecurityHeadersMiddleware sets safe default security headers on all responses.
|
||||
// HSTS is conditional on the hstsEnabled toggle (default true in config for public proxy+TLS use;
|
||||
// false allows pure local HTTP as noted in README LAN Deployment Notes).
|
||||
// Other headers always-on per minimal security defaults.
|
||||
// CSP uses 'unsafe-inline' for style-src (and data: for media) due to current Vite/Svelte
|
||||
// build output; this is an acknowledged defense-in-depth tradeoff for the minimal trusted UI
|
||||
// (script-src remains strict 'self'; no user-controlled content in styles).
|
||||
func SecurityHeadersMiddleware(hstsEnabled bool, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h := w.Header()
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
h.Set("X-Frame-Options", "DENY")
|
||||
if hstsEnabled {
|
||||
h.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
||||
}
|
||||
h.Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; media-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self';")
|
||||
h.Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user