updates to api protections
This commit is contained in:
@@ -117,17 +117,18 @@ PUBLIC_DATA_CACHE_STALE_MS=86400000
|
||||
PUBLIC_DATA_PREWARM_INTERVAL_MS=300000
|
||||
PUBLIC_DATA_COLD_TIMEOUT_MS=8000
|
||||
VITE_STATIC_DATA=false
|
||||
VITE_SITE_GATE=false
|
||||
VITE_SITE_GATE=true
|
||||
API_RATE_LIMIT_WINDOW_MS=60000
|
||||
API_RATE_LIMIT_MAX=120
|
||||
SITE_SESSION_SECRET=long-random-shared-secret
|
||||
SITE_SESSION_TTL_SECONDS=43200
|
||||
```
|
||||
|
||||
HTML responses set a signed, HttpOnly site-session cookie. `/api/*` and `/data/*`
|
||||
requests must present that cookie and same-origin browser request metadata, so the
|
||||
data is served to active site sessions instead of as an open public API. All PM2
|
||||
web instances must share the same `SITE_SESSION_SECRET`.
|
||||
Successful Turnstile verification sets signed, HttpOnly Turnstile and site-session
|
||||
cookies. `/api/*` and `/data/*` requests must present those cookies plus
|
||||
same-origin browser request metadata, so the data is served to verified active
|
||||
site sessions instead of as an open public API. All PM2 web instances must share
|
||||
the same `SITE_SESSION_SECRET`.
|
||||
|
||||
On startup, the web server preloads the critical public snapshots before
|
||||
signalling PM2 `ready`: team leaderboard, player leaderboard, home teams, and
|
||||
|
||||
+1
-1
@@ -64,5 +64,5 @@ DISCORD_INCLUDE_PATCH=true
|
||||
# TURNSTILE_SECRET_KEY is the server-only secret used to call the Siteverify endpoint.
|
||||
VITE_TURNSTILE_SITE_KEY=
|
||||
TURNSTILE_SECRET_KEY=
|
||||
VITE_SITE_GATE=false
|
||||
VITE_SITE_GATE=true
|
||||
VITE_STATIC_DATA=false
|
||||
|
||||
@@ -59,7 +59,10 @@ const liveRefreshMs = 15000
|
||||
const siteVersion = import.meta.env.VITE_SITE_VERSION || '1.0.0'
|
||||
|
||||
const turnstileSiteKey = import.meta.env.VITE_TURNSTILE_SITE_KEY || ''
|
||||
const siteGateEnabled = String(import.meta.env.VITE_SITE_GATE || 'false').toLowerCase() === 'true'
|
||||
const siteGateSetting = import.meta.env.VITE_SITE_GATE
|
||||
const siteGateEnabled = siteGateSetting == null
|
||||
? Boolean(turnstileSiteKey)
|
||||
: String(siteGateSetting).toLowerCase() === 'true'
|
||||
const staticDataBase = (import.meta.env.VITE_STATIC_DATA_BASE || '/data').replace(/\/+$/, '')
|
||||
const staticDataEnabled = String(import.meta.env.VITE_STATIC_DATA || 'false').toLowerCase() === 'true'
|
||||
const missingStaticDataPaths = new Set()
|
||||
|
||||
+21
-3
@@ -1281,8 +1281,19 @@ function isProtectedDataPath(req) {
|
||||
}
|
||||
}
|
||||
|
||||
function isSiteSessionBootstrapPath(req) {
|
||||
if (!req.url) return false
|
||||
try {
|
||||
const pathname = new URL(req.url, `http://${req.headers.host || 'localhost'}`).pathname
|
||||
return pathname === '/api/turnstile/verify' || pathname === '/api/turnstile/session'
|
||||
} catch {
|
||||
return req.url === '/api/turnstile/verify' || req.url === '/api/turnstile/session'
|
||||
}
|
||||
}
|
||||
|
||||
function requireSiteSession(req, res) {
|
||||
if (!isProtectedDataPath(req)) return true
|
||||
if (isSiteSessionBootstrapPath(req)) return true
|
||||
|
||||
if (!SITE_SESSION_HMAC_KEY) {
|
||||
sendJson(res, 503, { error: 'Site session signing is not configured' })
|
||||
@@ -1299,6 +1310,11 @@ function requireSiteSession(req, res) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!isTurnstileSessionVerified(req)) {
|
||||
sendJson(res, 403, { error: 'Turnstile session required' })
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -2469,12 +2485,10 @@ function htmlWithSeo(req, data) {
|
||||
|
||||
function sendHtml(req, res, data, status = 200) {
|
||||
const html = htmlWithSeo(req, data)
|
||||
const siteSessionCookie = buildSiteSessionCookie(req)
|
||||
send(res, status, html, {
|
||||
...securityHeaders(req, { html: true }),
|
||||
'content-type': mimeTypes['.html'],
|
||||
'cache-control': 'no-cache',
|
||||
...(siteSessionCookie ? { 'set-cookie': siteSessionCookie } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2978,7 +2992,11 @@ const server = http.createServer((req, res) => {
|
||||
sendJson(res, 403, { error: 'Turnstile verification failed', detail: verification.error })
|
||||
return
|
||||
}
|
||||
const headers = TURNSTILE_SECRET_KEY ? { 'set-cookie': buildTurnstileSessionCookie(req) } : {}
|
||||
const cookies = [
|
||||
buildTurnstileSessionCookie(req),
|
||||
buildSiteSessionCookie(req),
|
||||
].filter(Boolean)
|
||||
const headers = cookies.length ? { 'set-cookie': cookies } : {}
|
||||
send(res, 200, JSON.stringify({ success: true, ttl: TURNSTILE_SESSION_TTL_SECONDS }), {
|
||||
...jsonHeaders,
|
||||
...headers,
|
||||
|
||||
Reference in New Issue
Block a user