Implements D-025 (revised 2026-10-15) and D-043. Setup gate (D-025): - Remove the bind refusal (CheckSetupBind/ErrNonLoopbackSetup/server.IsLoopback); Serve binds any address, so the app can listen on 0.0.0.0:8080 behind Zoraxy. - The gate is now per request and strict (httpx.SetupRequestAllowed): a loopback TCP peer AND no X-Forwarded-For — the direct SSH tunnel. Anything else is 404 before dispatch; an unparseable peer fails closed (AC-7/AC-10). The raw XFF is read before ClientIP SETS it (A6). Proxy scheme (D-043): - httpx.RequestIsHTTPS resolves the scheme per request: direct TLS, or a trusted proxy's X-Forwarded-Proto. The door carries it on the request context (httpx.WithSecure) so the session cookie, its __Host- prefix, the pre-session and ceremony cookies, and HSTS all agree (AC-13). - Over plain http (the tunnel) cookies stay bare and non-Secure, which fixes the bootstrap from an https:// config.example.yaml; Config.IsSecure() is removed as now dead. Tests: rewrite the setup-gate tests for AC-7/AC-10, add AC-13, and unit-test SetupRequestAllowed / RequestIsHTTPS.
61 lines
2.3 KiB
Go
61 lines
2.3 KiB
Go
// Package httpx holds the door's HTTP helpers: the session cookie, the client
|
|
// IP resolution (trusted proxies, A6), the security headers, log redaction, and
|
|
// the CSRF token (SPEC-003 §6/§8).
|
|
//
|
|
// It imports only net/http + the standard library — the door owns HTTP; the
|
|
// pure routing model (internal/core/routing) must never import this.
|
|
package httpx
|
|
|
|
import "net/http"
|
|
|
|
// SessionCookieName is the base name of the session cookie (SPEC-003 §6). The
|
|
// value is an opaque random id; the server-side session row is authoritative.
|
|
const SessionCookieName = "myday_session"
|
|
|
|
// HostPrefix is the `__Host-` cookie-name prefix (L1s). A browser only accepts
|
|
// a `__Host-` cookie when it is Secure · Path=/ · has no Domain attribute, so
|
|
// the cookie is pinned to the exact origin — a sibling subdomain cannot shadow
|
|
// it (a cookie-fixation / session-shadowing DoS).
|
|
const HostPrefix = "__Host-"
|
|
|
|
// SessionCookieNameFor returns the session cookie's name for the transport:
|
|
// `__Host-myday_session` over https (Secure is set — required by the prefix)
|
|
// and the bare name otherwise, because a browser REJECTS a `__Host-` cookie
|
|
// sent without Secure over plain http (the loopback bootstrap / human test).
|
|
//
|
|
// The `secure` decision is made PER REQUEST by the door (D-043): TLS, or a
|
|
// trusted proxy's `X-Forwarded-Proto: https`. Over the plain-http bootstrap it
|
|
// is false, so the bare name is used.
|
|
func SessionCookieNameFor(secure bool) string {
|
|
if secure {
|
|
return HostPrefix + SessionCookieName
|
|
}
|
|
return SessionCookieName
|
|
}
|
|
|
|
// SetSessionCookie writes the hardened session cookie (SPEC-003 §6):
|
|
// HttpOnly · Secure · SameSite=Lax · Path=/. The lifetime is server-side, so
|
|
// no Max-Age is set (a browser-session cookie).
|
|
func SetSessionCookie(w http.ResponseWriter, name, value string, secure bool) {
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: name,
|
|
Value: value,
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
Secure: secure,
|
|
SameSite: http.SameSiteLaxMode,
|
|
})
|
|
}
|
|
|
|
// ClearSessionCookie expires the session cookie (logout, or an invalid session).
|
|
func ClearSessionCookie(w http.ResponseWriter, name string, secure bool) {
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: name,
|
|
Value: "",
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
Secure: secure,
|
|
SameSite: http.SameSiteLaxMode,
|
|
MaxAge: -1,
|
|
})
|
|
}
|