update osm

This commit is contained in:
2026-05-16 09:02:39 +01:00
parent 878e2a6a47
commit f36bdf3738
2 changed files with 191 additions and 107 deletions
+80 -11
View File
@@ -536,6 +536,69 @@ function readJsonBody(req) {
})
}
const TURNSTILE_SESSION_COOKIE = 'tssbot_turnstile'
const TURNSTILE_SESSION_TTL_SECONDS = 30 * 60
const TURNSTILE_SESSION_HMAC_KEY = crypto
.createHash('sha256')
.update(`tssbot-turnstile-session|${TURNSTILE_SECRET_KEY}`)
.digest()
function signTurnstileSession(expiresAt) {
return crypto
.createHmac('sha256', TURNSTILE_SESSION_HMAC_KEY)
.update(String(expiresAt))
.digest('base64url')
}
function buildTurnstileSessionCookie(req) {
const expiresAt = Math.floor(Date.now() / 1000) + TURNSTILE_SESSION_TTL_SECONDS
const signature = signTurnstileSession(expiresAt)
const value = `${expiresAt}.${signature}`
const proto = req.headers['x-forwarded-proto'] || (req.socket.encrypted ? 'https' : 'http')
const isHttps = String(proto).split(',')[0].trim() === 'https'
const parts = [
`${TURNSTILE_SESSION_COOKIE}=${value}`,
'Path=/',
`Max-Age=${TURNSTILE_SESSION_TTL_SECONDS}`,
'HttpOnly',
'SameSite=Strict',
]
if (isHttps) parts.push('Secure')
return parts.join('; ')
}
function parseRequestCookies(req) {
const header = req.headers.cookie || ''
const out = {}
for (const part of header.split(/;\s*/)) {
if (!part) continue
const eq = part.indexOf('=')
if (eq < 1) continue
out[part.slice(0, eq)] = part.slice(eq + 1)
}
return out
}
function isTurnstileSessionVerified(req) {
if (!TURNSTILE_SECRET_KEY) return true
const cookies = parseRequestCookies(req)
const cookie = cookies[TURNSTILE_SESSION_COOKIE]
if (!cookie) return false
const dot = cookie.indexOf('.')
if (dot < 1) return false
const expiresAtStr = cookie.slice(0, dot)
const signature = cookie.slice(dot + 1)
const expiresAt = Number(expiresAtStr)
if (!Number.isFinite(expiresAt) || expiresAt < Math.floor(Date.now() / 1000)) return false
const expected = signTurnstileSession(expiresAt)
if (signature.length !== expected.length) return false
try {
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
} catch {
return false
}
}
function callTurnstileSiteverify(token, remoteIp, idempotencyKey) {
return new Promise((resolve) => {
const params = new URLSearchParams()
@@ -1446,16 +1509,13 @@ const server = http.createServer((req, res) => {
return
}
if (!isTurnstileSessionVerified(req)) {
sendJson(res, 403, { error: 'Turnstile session required', detail: 'Solve the site challenge first' })
return
}
readJsonBody(req)
.then(async (payload) => {
const verification = await verifyTurnstileToken(payload.turnstile_token, clientIp(req), {
expectedAction: 'analytics-delete',
expectedHostname: expectedTurnstileHostname(),
})
if (!verification.success) {
sendJson(res, 403, { error: 'Turnstile verification required', detail: verification.error })
return
}
.then((payload) => {
const result = deleteViewerData(payload)
sendJson(res, 200, result)
})
@@ -1477,19 +1537,28 @@ const server = http.createServer((req, res) => {
readJsonBody(req)
.then(async (payload) => {
const verification = await verifyTurnstileToken(payload.token, clientIp(req), {
expectedAction: typeof payload.action === 'string' ? payload.action : undefined,
expectedAction: 'site-gate',
expectedHostname: expectedTurnstileHostname(),
})
if (!verification.success) {
sendJson(res, 403, { error: 'Turnstile verification failed', detail: verification.error })
return
}
sendJson(res, 200, { success: true })
const headers = TURNSTILE_SECRET_KEY ? { 'set-cookie': buildTurnstileSessionCookie(req) } : {}
send(res, 200, JSON.stringify({ success: true, ttl: TURNSTILE_SESSION_TTL_SECONDS }), {
...jsonHeaders,
...headers,
})
})
.catch((error) => sendJson(res, 400, { error: error.message }))
return
}
if (req.method === 'GET' && req.url === '/api/turnstile/session') {
sendJson(res, 200, { verified: isTurnstileSessionVerified(req) })
return
}
if (req.method === 'OPTIONS' && req.url.startsWith('/api/')) {
sendJson(res, 403, { error: 'CORS requests are not allowed' })
return