Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ee91c98
windows: keep the dashboard on loopback and refuse unauthenticated pr…
claude Nov 5, 2026
edabbff
api: fix a 1-in-4096 flake in TestOAuthClientAuthAndPKCE
claude Nov 5, 2026
8dbf715
web: run the lib unit tests in CI
claude Nov 5, 2026
e993cee
imageupdate: stop a nil-response panic when the authenticated registr…
claude Nov 5, 2026
3c34fb1
webauthn: check BeginLogin errors in the password+passkey login flow
claude Nov 5, 2026
011d61f
windows: do not forward the wake secret header to apps
claude Nov 5, 2026
28fff6e
web: fix accessibility violations found by an axe audit
claude Nov 5, 2026
c59f310
sdk: fix the sandbox README link to the API docs
claude Nov 5, 2026
6f5999a
deploy: document optional server settings in aven.env.example
claude Nov 5, 2026
1416051
deploy: fix a data race when starting a PITR restore
claude Nov 5, 2026
1cbfe6a
Remove dead code reported by staticcheck
claude Nov 5, 2026
71732ba
web: meet AA contrast in the light theme
claude Nov 5, 2026
56982d0
aven-server: keep one panicking background loop from taking the serve…
claude Nov 5, 2026
b05ace8
agent: turn a panic in one operation into that operation's error
claude Nov 5, 2026
e30eb08
aven-server: gofmt
claude Nov 5, 2026
2ab9e12
tcp proxy: restrict clients with an IP allowlist
claude Nov 5, 2026
7ee9cc7
web: edit the TCP proxy client allowlist
claude Nov 5, 2026
21b789d
docs: describe TCP proxy client allowlists
claude Nov 5, 2026
16a9b4a
tests: check the results four tests computed and ignored
claude Nov 5, 2026
80f8953
wake handlers: replace the deprecated ReverseProxy.Director with Rewrite
claude Nov 5, 2026
35279b1
auth: cap login sessions at an absolute lifetime (default 90 days)
claude Nov 5, 2026
26881e4
web: faint text meets AA contrast in the dark theme
claude Nov 5, 2026
76045d8
Make staticcheck clean on every GOOS
claude Nov 5, 2026
6cff52e
ci: run gofmt, staticcheck and the race detector
claude Nov 5, 2026
5ba9463
api: gofmt auth.go
claude Nov 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ jobs:
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- name: gofmt
run: test -z "$(gofmt -l cmd internal | tee /dev/stderr)"
- run: go vet ./...
# staticcheck caught two shadowed-err bugs (a nil-response panic, an unchecked WebAuthn error); keep it clean.
- name: staticcheck
run: go run honnef.co/go/tools/cmd/staticcheck@2026.1 ./...
- run: go test ./...
- run: make server agents cli
- name: Cross-build Windows CLI
Expand All @@ -25,6 +30,18 @@ jobs:
GOOS=windows GOARCH=arm64 CGO_ENABLED=0 go build -o /dev/null ./cmd/aven
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o /dev/null ./cmd/aven-server

# The race detector found a real data race (PITR restore status) that the plain run missed.
# It is about ten times slower, so it runs as its own job next to the others.
go-race:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
# internal/api alone takes about ten minutes under -race, past go test's default timeout.
- run: go test -race -timeout 30m ./...

web:
runs-on: ubuntu-latest
defaults:
Expand All @@ -40,6 +57,7 @@ jobs:
- run: npm ci
- run: npm run typecheck
- run: npm run i18n:check
- run: npm test
- run: npm run build

sdk:
Expand Down
2 changes: 1 addition & 1 deletion README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ Aven은 Keel이라는 이전 프로젝트를 잇습니다. 이 공개 저장소

**네트워크**
- 관리형 Caddy 프록시로 생성 도메인과 커스텀 도메인에 TLS 자동 발급. 경로 규칙, 리디렉션, basic auth, 와일드카드 도메인.
- IPv6 우선: 도메인별 공개 IPv6 주소(선택), 아웃바운드 IPv6 제어, 데이터베이스 같은 TCP 서비스를 위한 TCP 프록시.
- IPv6 우선: 도메인별 공개 IPv6 주소(선택), 아웃바운드 IPv6 제어, 데이터베이스 같은 TCP 서비스를 위한 TCP 프록시(프록시별 접속 허용 IP 지정 가능).
- 서비스 사이 사설 DNS 이름. 암호화된 mesh(Tailscale 또는 WireGuard 주소)로 서버를 넘어서도 이어집니다.
- 엣지 캐싱, WAF 규칙, Cloudflare Tunnel 진입.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ AI coding agents were used substantially in development. Changes went through hu

**Networking**
- Generated and custom domains with automatic TLS through a managed Caddy proxy, path rules, redirects, basic auth and wildcard domains.
- IPv6 first: optional per-domain public IPv6 addresses, outbound IPv6 control and TCP proxies for databases and other raw TCP services.
- IPv6 first: optional per-domain public IPv6 addresses, outbound IPv6 control and TCP proxies for databases and other raw TCP services, with an optional client IP allowlist per proxy.
- Private DNS names between services, also across servers over an encrypted mesh (Tailscale or WireGuard addresses).
- Edge caching, WAF rules and Cloudflare Tunnel ingress.

Expand Down
53 changes: 53 additions & 0 deletions cmd/aven-server/loops.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package main

import (
"context"
"log/slog"
"runtime/debug"
"sync"
"time"
)

// Restart delays for a background loop that panicked: the first restart waits loopRestartMin, each
// further one doubles up to loopRestartMax, so a loop that panics on every tick cannot spin.
var (
loopRestartMin = 30 * time.Second
loopRestartMax = 5 * time.Minute
)

// goLoop runs fn as a tracked background task. A panic in fn is logged with its stack and fn is started
// again after a delay, so one faulty loop (an image-update check, a metrics collector) cannot take down
// the API, the proxy sync and every other loop with it. fn returning normally ends the task.
func goLoop(bg *sync.WaitGroup, ctx context.Context, log *slog.Logger, name string, fn func()) {
bg.Go(func() { runRecovering(ctx, log, name, fn) })
}

func runRecovering(ctx context.Context, log *slog.Logger, name string, fn func()) {
delay := loopRestartMin
for {
if !runOnce(log, name, fn) || ctx.Err() != nil {
return
}
log.Error("background task restarts after a panic", "task", name, "in", delay)
t := time.NewTimer(delay)
select {
case <-ctx.Done():
t.Stop()
return
case <-t.C:
}
delay = min(delay*2, loopRestartMax)
}
}

// runOnce calls fn and reports whether it panicked.
func runOnce(log *slog.Logger, name string, fn func()) (panicked bool) {
defer func() {
if v := recover(); v != nil {
panicked = true
log.Error("background task panicked", "task", name, "panic", v, "stack", string(debug.Stack()))
}
}()
fn()
return false
}
54 changes: 54 additions & 0 deletions cmd/aven-server/loops_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package main

import (
"context"
"io"
"log/slog"
"sync"
"sync/atomic"
"testing"
"time"
)

func withRestartDelays(t *testing.T, lo, hi time.Duration) {
t.Helper()
oldLo, oldHi := loopRestartMin, loopRestartMax
loopRestartMin, loopRestartMax = lo, hi
t.Cleanup(func() { loopRestartMin, loopRestartMax = oldLo, oldHi })
}

func TestGoLoopRestartsAfterPanic(t *testing.T) {
withRestartDelays(t, time.Millisecond, 4*time.Millisecond)
log := slog.New(slog.NewTextHandler(io.Discard, nil))
var runs atomic.Int32
var bg sync.WaitGroup
goLoop(&bg, context.Background(), log, "test", func() {
if runs.Add(1) < 3 {
panic("boom")
}
})
bg.Wait()
if got := runs.Load(); got != 3 {
t.Fatalf("runs = %d, want 3 (two panics, then a normal return)", got)
}
}

func TestGoLoopStopsWaitingOnCancel(t *testing.T) {
withRestartDelays(t, time.Hour, time.Hour)
log := slog.New(slog.NewTextHandler(io.Discard, nil))
ctx, cancel := context.WithCancel(context.Background())
var runs atomic.Int32
var bg sync.WaitGroup
goLoop(&bg, ctx, log, "test", func() {
runs.Add(1)
panic("boom")
})
time.Sleep(20 * time.Millisecond)
cancel()
if !waitTimeout(&bg, 5*time.Second) {
t.Fatal("the task kept waiting for its restart after cancellation")
}
if got := runs.Load(); got != 1 {
t.Fatalf("runs = %d, want 1", got)
}
}
118 changes: 94 additions & 24 deletions cmd/aven-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,11 @@ func main() {
os.Exit(1)
}
defer closeLog()
// The dashboard listener speaks plain HTTP, so it stays on loopback by default on every platform.
// On Windows, Docker Desktop forwards host.docker.internal to the host's loopback, so containers still reach it.
defaultListen := "[::1]:7070"
if runtime.GOOS == "windows" {
defaultListen = "0.0.0.0:7070"
defaultListen = "127.0.0.1:7070"
}
listen := flag.String("listen", env("LISTEN", defaultListen), "API/UI listen address")
dataDir := flag.String("data", env("DATA", compat.DefaultDataDir()), "data directory (AVEN_DATA)")
Expand Down Expand Up @@ -142,9 +144,47 @@ func checkEnv(listen string) error {
if _, err := parsePrefixes(compat.Env("TRUSTED_PROXIES")); err != nil {
return fmt.Errorf("%s: %w", compat.Describe("TRUSTED_PROXIES"), err)
}
if _, err := sessionMaxAge(); err != nil {
return fmt.Errorf("%s: %w", compat.Describe("SESSION_MAX_AGE"), err)
}
if _, err := tcpAllowDefault(); err != nil {
return fmt.Errorf("%s: %w", compat.Describe("TCP_PORT_ALLOW_CIDRS"), err)
}
return nil
}

// sessionMaxAge is AVEN_SESSION_MAX_AGE: the absolute lifetime of a login session, as a Go duration
// ("2160h") or a number of days ("90d"); "off" removes the cap. Unset uses api.DefaultSessionMaxAge.
func sessionMaxAge() (time.Duration, error) {
v := strings.TrimSpace(compat.Env("SESSION_MAX_AGE"))
switch {
case v == "":
return 0, nil
case strings.EqualFold(v, "off"):
return -1, nil
}
if days, ok := strings.CutSuffix(v, "d"); ok {
n, err := strconv.Atoi(days)
if err != nil || n < 1 {
return 0, fmt.Errorf("%q is not a number of days (e.g. 90d)", v)
}
return time.Duration(n) * 24 * time.Hour, nil
}
d, err := time.ParseDuration(v)
if err != nil || d < time.Hour {
return 0, fmt.Errorf("%q: use a duration of at least 1h (e.g. 2160h), a number of days (90d) or off", v)
}
return d, nil
}

// tcpAllowDefault is AVEN_TCP_PORT_ALLOW_CIDRS: the client allowlist of TCP proxies without their own
// (IPv4 addresses or CIDRs separated by commas or spaces).
func tcpAllowDefault() ([]string, error) {
return node.NormalizeTCPAllowCIDRs(strings.FieldsFunc(compat.Env("TCP_PORT_ALLOW_CIDRS"), func(r rune) bool {
return r == ',' || r == ' ' || r == '\t' || r == '\n'
}))
}

func run(log *slog.Logger, listen, dataDir string) (err error) {
sigCtx, stopSignals := serverSignalContext()
defer stopSignals()
Expand Down Expand Up @@ -303,6 +343,7 @@ func runWithContext(sigCtx context.Context, stopSignals context.CancelFunc, log
RailpackFrontend: env("RAILPACK_FRONTEND", "ghcr.io/railwayapp/railpack-frontend:v0.42.0"),
TCPPortMin: envInt("TCP_PORT_MIN", 20000),
TCPPortMax: envInt("TCP_PORT_MAX", 29999),
TCPAllowDefault: tcpAllowDefaultOrNil(),
PrivateAddress: strings.TrimSpace(compat.Env("PRIVATE_ADDRESS")),
Mesh: env("MESH", "auto"),
MeshPort: envInt("MESH_PORT", 7443),
Expand Down Expand Up @@ -381,16 +422,16 @@ func runWithContext(sigCtx context.Context, stopSignals context.CancelFunc, log
} else if mgr.LocalMesh != nil {
log.Info("mesh gateway enabled", "listen", mgr.LocalMesh.Listen)
}
bg.Go(func() { mgr.ReconcileLoop(ctx, 30*time.Second) })
bg.Go(func() { mgr.EgressLoop(ctx, 30*time.Second) }) // 아웃바운드 IPv6 차단 규칙(outbound_ipv6=false)을 컨테이너 주소에 맞춘다
bg.Go(func() { mgr.DomainIPv6Loop(ctx, time.Minute) }) // 도메인별 공개 IPv6(노드 풀이 있을 때만): 시작할 때, 도메인 변경 때, 60초마다
goLoop(&bg, ctx, log, "reconcile", func() { mgr.ReconcileLoop(ctx, 30*time.Second) })
goLoop(&bg, ctx, log, "egress", func() { mgr.EgressLoop(ctx, 30*time.Second) }) // 아웃바운드 IPv6 차단 규칙(outbound_ipv6=false)을 컨테이너 주소에 맞춘다
goLoop(&bg, ctx, log, "domain-ipv6", func() { mgr.DomainIPv6Loop(ctx, time.Minute) }) // 도메인별 공개 IPv6(노드 풀이 있을 때만): 시작할 때, 도메인 변경 때, 60초마다
// DNS 조회/연결(flow) 로그: 환경 bridge의 패킷 헤더를 관찰한다(CAP_NET_RAW 필요, AVEN_NETWORK_LOGS=off로 끈다).
if runtime.GOOS != "windows" && !strings.EqualFold(strings.TrimSpace(env("NETWORK_LOGS", "on")), "off") {
mgr.StartLocalNetworkLogs(ctx)
}
bg.Go(func() { mgr.WatchCrashes(ctx) })
bg.Go(func() { mgr.CronLoop(ctx) })
bg.Go(func() { mgr.ImageUpdateLoop(ctx) })
goLoop(&bg, ctx, log, "watch-crashes", func() { mgr.WatchCrashes(ctx) })
goLoop(&bg, ctx, log, "cron", func() { mgr.CronLoop(ctx) })
goLoop(&bg, ctx, log, "image-updates", func() { mgr.ImageUpdateLoop(ctx) })
// wake handler: keel-proxy와 같이 쓰는 볼륨의 unix socket에서만 listen한다(외부에 열지 않음).
if wl, err := listenWake(ctx, docker, caddy); err != nil {
log.Warn("wake handler disabled (sleeping services cannot wake)", "err", err)
Expand All @@ -401,15 +442,16 @@ func runWithContext(sigCtx context.Context, stopSignals context.CancelFunc, log
}
wakeHandler := mgr.WakeHandler()
if wakeSecret != "" {
wakeHandler = requireWakeSecret(wakeSecret, wakeHandler)
// The wake handler proxies the request on to the app, so drop the secret once it is checked.
wakeHandler = requireHeaderSecret(wakeSecretHeader, wakeSecret, stripHeader(wakeSecretHeader, wakeHandler))
}
ws := &http.Server{Handler: wakeHandler, ReadHeaderTimeout: 10 * time.Second, BaseContext: baseCtx}
servers = append(servers, ws)
go ws.Serve(wl)
log.Info("wake handler listening", "addr", wl.Addr().String())
}
bg.Go(func() { mgr.SleepLoop(ctx) }) // 로컬(wake handler가 있을 때)과 원격 노드의 sleeping 판정
bg.Go(func() { caddy.FollowAccessLogs(ctx, mgr.HTTPLogs.Add) })
goLoop(&bg, ctx, log, "sleep", func() { mgr.SleepLoop(ctx) }) // 로컬(wake handler가 있을 때)과 원격 노드의 sleeping 판정
goLoop(&bg, ctx, log, "access-logs", func() { caddy.FollowAccessLogs(ctx, mgr.HTTPLogs.Add) })
collector := &metrics.Collector{Store: st, Docker: docker, Log: log, DataDir: dataDir, Events: bus, RemoteNodes: func(ctx context.Context) []metrics.NodeSource {
var out []metrics.NodeSource
for _, id := range mgr.OnlineRemoteNodeIDs(ctx) {
Expand All @@ -427,14 +469,14 @@ func runWithContext(sigCtx context.Context, stopSignals context.CancelFunc, log
}
return out
}}
bg.Go(func() { collector.Run(ctx) })
goLoop(&bg, ctx, log, "metrics", func() { collector.Run(ctx) })
// 지표 알림 규칙(30초마다)과 시간별 사용량 집계(매시, 놓친 시간은 보관 중인 지표로 채운다).
sources := &alerts.Sources{Store: st, HTTP: mgr.HTTPLogs.Window, Restarts: mgr.InstanceRestarts, VolumeSize: collector.VolumeSize}
bg.Go(func() { (&alerts.Evaluator{Store: st, Events: bus, Log: log, Value: sources.Value}).Run(ctx) })
goLoop(&bg, ctx, log, "alerts", func() { (&alerts.Evaluator{Store: st, Events: bus, Log: log, Value: sources.Value}).Run(ctx) })
// 수평 자동 확장(30초마다, 최근 2분 지표 평균으로 HPA 방식 계산).
bg.Go(func() { mgr.AutoscaleLoop(ctx) })
bg.Go(func() { (&usage.Roller{Store: st, Events: bus, Log: log, Instances: mgr}).Run(ctx) })
bg.Go(func() {
goLoop(&bg, ctx, log, "autoscale", func() { mgr.AutoscaleLoop(ctx) })
goLoop(&bg, ctx, log, "usage", func() { (&usage.Roller{Store: st, Events: bus, Log: log, Instances: mgr}).Run(ctx) })
goLoop(&bg, ctx, log, "prune", func() {
t := time.NewTicker(time.Hour)
defer t.Stop()
for {
Expand All @@ -456,18 +498,19 @@ func runWithContext(sigCtx context.Context, stopSignals context.CancelFunc, log
Store: st, Agents: hub, Deploy: mgr, Events: bus, Metrics: collector, AdminPassword: password, APIToken: compat.Env("API_TOKEN"),
AllowFileRepo: compat.Env("ALLOW_FILE_REPO") != "",
SessionKey: key, SecureCookie: compat.Env("INSECURE_COOKIE") == "",
Web: webui.FS(), Version: version, NodeName: hostname, Log: log,
SessionMaxAge: mustSessionMaxAge(),
Web: webui.FS(), Version: version, NodeName: hostname, Log: log,
GitHubSecret: compat.Env("GITHUB_WEBHOOK_SECRET"), GitHubAPIBase: env("GITHUB_API_URL", "https://api.github.com"),
GitHubClientID: compat.Env("GITHUB_CLIENT_ID"), GitHubClientSecret: compat.Env("GITHUB_CLIENT_SECRET"), PublicURL: compat.Env("PUBLIC_URL"),
AgentBinDir: compat.Env("AGENT_BIN_DIR"),
DisableSelfService: runtime.GOOS == "windows",
Notifier: notifier, Pricing: pricing,
Traces: traceReceiver,
}
bg.Go(func() { tracing.RetentionLoop(ctx, st, traceRetention(log), log) })
goLoop(&bg, ctx, log, "trace-retention", func() { tracing.RetentionLoop(ctx, st, traceRetention(log), log) })
trusted, _ := parsePrefixes(compat.Env("TRUSTED_PROXIES")) // checked by checkEnv
srv.TrustedProxies = trusted
bg.Go(func() { srv.RunHA(ctx) }) // 데이터베이스 HA 클러스터 감시와 자동 failover(10초마다)
goLoop(&bg, ctx, log, "ha", func() { srv.RunHA(ctx) }) // 데이터베이스 HA 클러스터 감시와 자동 failover(10초마다)
// 공개 도메인 상태 확인(DNS 분류, Cloudflare/터널 감지, 소유 확인, 인증서): 60초마다, 검증된 도메인은 최대 10분까지 간격을 늘린다.
domainChecker := &netcheck.Checker{Store: st, Targets: srv.NetCheckTargets, CheckCerts: true, Log: log}
// 확인 전 신규 도메인이 검증되면 라우트를 다시 보내 ACME 보류(on-demand)를 풀고 관리 정책으로 옮긴다(원격 노드 포함).
Expand All @@ -480,7 +523,7 @@ func runWithContext(sigCtx context.Context, stopSignals context.CancelFunc, log
}()
}
srv.SetNetCheck(domainChecker)
bg.Go(func() { domainChecker.Run(ctx) })
goLoop(&bg, ctx, log, "domain-check", func() { domainChecker.Run(ctx) })
// 옛 Bearer 비밀키 agent는 기본 허용(첫 인증서 연결 때 자동 업그레이드).
srv.CA = agentCA
srv.DisableLegacyAgents = compat.Env("AGENT_LEGACY_BEARER") == "off"
Expand All @@ -499,9 +542,9 @@ func runWithContext(sigCtx context.Context, stopSignals context.CancelFunc, log
}
}
handler := srv.Handler()
bg.Go(func() { srv.SandboxLoop(ctx) })
goLoop(&bg, ctx, log, "sandboxes", func() { srv.SandboxLoop(ctx) })
if runtime.GOOS != "windows" {
bg.Go(func() { srv.SelfUpdateLoop(ctx) }) // aven-server(self 서비스) 저장소의 새 커밋 확인(30분마다)
goLoop(&bg, ctx, log, "self-update", func() { srv.SelfUpdateLoop(ctx) }) // aven-server(self 서비스) 저장소의 새 커밋 확인(30분마다)
}
hs := &http.Server{Handler: handler, ReadHeaderTimeout: 10 * time.Second, BaseContext: baseCtx}
// OTLP 수신기(OTLP/HTTP + h2c OTLP/gRPC). 컨테이너가 host-gateway로 닿아야 하므로 기본은 모든 주소다.
Expand Down Expand Up @@ -563,7 +606,10 @@ func runWithContext(sigCtx context.Context, stopSignals context.CancelFunc, log
if err != nil {
return fmt.Errorf("self route listener: %w", err)
}
selfServer := &http.Server{Handler: api.ViaGateway(selfSecret, handler), ReadHeaderTimeout: 10 * time.Second, BaseContext: baseCtx}
// Unlike the Linux unix socket, this TCP port can be reached from the network, so a request
// without the proxy's secret header is refused instead of being served as an untrusted client.
selfHandler := requireHeaderSecret(api.GatewayHeader, selfSecret, api.ViaGateway(selfSecret, handler))
selfServer := &http.Server{Handler: selfHandler, ReadHeaderTimeout: 10 * time.Second, BaseContext: baseCtx}
servers = append(servers, selfServer)
go selfServer.Serve(selfListener)
log.Info("aven-server listening for the proxy (own public host)", "host", selfHost, "addr", proxy.NativeSelfListen)
Expand Down Expand Up @@ -805,13 +851,37 @@ func listenWake(ctx context.Context, docker *node.Docker, caddy *proxy.Caddy) (n
return l, nil
}

func requireWakeSecret(secret string, next http.Handler) http.Handler {
// wakeSecretHeader carries the wake handler secret from Caddy when the wake handler listens on TCP (Windows).
const wakeSecretHeader = "X-Aven-Wake-Secret"

// requireHeaderSecret refuses requests whose header does not carry secret. An empty secret refuses everything.
func requireHeaderSecret(header, secret string, next http.Handler) http.Handler {
want := []byte(secret)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if subtle.ConstantTimeCompare([]byte(r.Header.Get("X-Aven-Wake-Secret")), want) != 1 {
if secret == "" || subtle.ConstantTimeCompare([]byte(r.Header.Get(header)), want) != 1 {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}

// stripHeader removes header from the request before next sees it.
func stripHeader(header string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Header.Del(header)
next.ServeHTTP(w, r)
})
}

// tcpAllowDefaultOrNil is tcpAllowDefault after checkEnv has validated it.
func tcpAllowDefaultOrNil() []string {
l, _ := tcpAllowDefault()
return l
}

// mustSessionMaxAge is sessionMaxAge after checkEnv has validated it.
func mustSessionMaxAge() time.Duration {
d, _ := sessionMaxAge()
return d
}
Loading
Loading