Download server/auth.js from Cnass/Claude: direct link, hf CLI and curl.
- Browser
- Download file 6.84 kB
-
https://huggingface.co/spaces/Cnass/Claude/resolve/main/server/auth.js
- Command line
-
hf download hf://spaces/Cnass/Claude/server/auth.js
-
curl -L -o auth.js https://huggingface.co/spaces/Cnass/Claude/resolve/main/server/auth.js
6.84 kB
| const fs = require('fs'); | |
| const path = require('path'); | |
| const crypto = require('crypto'); | |
| const APP_PASSWORD = process.env.APP_PASSWORD; | |
| const COOKIE_NAME = 'cc_session'; | |
| const COOKIE_DAYS = 30; | |
| const COOKIE_MAX_AGE_S = 60 * 60 * 24 * COOKIE_DAYS; | |
| const STATE_DIR = path.join(process.env.HOME || '/home/node', '.cc-web'); | |
| const SECRET_FILE = path.join(STATE_DIR, 'session-secret'); | |
| const AUTH_FILE = path.join(STATE_DIR, 'auth.json'); | |
| // Without a SESSION_SECRET secret one is generated once and kept in HOME, so | |
| // a restart (or a Space waking up) no longer logs every device out. | |
| function loadSecret() { | |
| if (process.env.SESSION_SECRET) return process.env.SESSION_SECRET; | |
| try { | |
| const saved = fs.readFileSync(SECRET_FILE, 'utf8').trim(); | |
| if (saved.length >= 32) return saved; | |
| } catch { | |
| // ilk calisma | |
| } | |
| const secret = crypto.randomBytes(32).toString('hex'); | |
| try { | |
| fs.mkdirSync(STATE_DIR, { recursive: true }); | |
| fs.writeFileSync(SECRET_FILE, `${secret}\n`, { mode: 0o600 }); | |
| } catch (err) { | |
| console.warn(`[auth] UYARI: oturum anahtari kaydedilemedi (${err.message}); yeniden baslatmada girisler sifirlanir.`); | |
| } | |
| return secret; | |
| } | |
| const SESSION_SECRET = loadSecret(); | |
| // "Log out everywhere" bumps the epoch: every cookie signed before is void. | |
| function loadEpoch() { | |
| try { | |
| const saved = JSON.parse(fs.readFileSync(AUTH_FILE, 'utf8')); | |
| return Number.isInteger(saved.epoch) && saved.epoch >= 0 ? saved.epoch : 0; | |
| } catch { | |
| return 0; | |
| } | |
| } | |
| let epoch = loadEpoch(); | |
| function hmac(value) { | |
| return crypto.createHmac('sha256', SESSION_SECRET).update(value).digest('hex'); | |
| } | |
| // v2.<issued at, seconds>.<epoch>.<signature> | |
| function makeToken(now = Date.now()) { | |
| const value = `v2.${Math.floor(now / 1000)}.${epoch}`; | |
| return `${value}.${hmac(value)}`; | |
| } | |
| function verifyCookieValue(signed, now = Date.now()) { | |
| if (typeof signed !== 'string' || !signed) return false; | |
| const idx = signed.lastIndexOf('.'); | |
| if (idx < 0) return false; | |
| const value = signed.slice(0, idx); | |
| const sig = signed.slice(idx + 1); | |
| if (!/^[0-9a-f]{64}$/.test(sig)) return false; | |
| const sigBuf = Buffer.from(sig, 'hex'); | |
| const expBuf = Buffer.from(hmac(value), 'hex'); | |
| if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) return false; | |
| const parts = value.split('.'); | |
| if (parts.length !== 3 || parts[0] !== 'v2') return false; | |
| const issued = Number(parts[1]); | |
| const tokenEpoch = Number(parts[2]); | |
| if (!Number.isFinite(issued) || tokenEpoch !== epoch) return false; | |
| const age = now / 1000 - issued; | |
| return age > -300 && age < COOKIE_MAX_AGE_S; | |
| } | |
| function parseCookies(header) { | |
| const out = {}; | |
| (header || '').split(';').forEach((part) => { | |
| const idx = part.indexOf('='); | |
| if (idx < 0) return; | |
| const k = part.slice(0, idx).trim(); | |
| const v = part.slice(idx + 1).trim(); | |
| if (!k) return; | |
| try { | |
| out[k] = decodeURIComponent(v); | |
| } catch { | |
| out[k] = v; | |
| } | |
| }); | |
| return out; | |
| } | |
| function checkPassword(candidate) { | |
| if (!APP_PASSWORD) return false; | |
| // Hashing first makes the comparison constant-time whatever the lengths. | |
| const a = crypto.createHash('sha256').update(String(candidate ?? '')).digest(); | |
| const b = crypto.createHash('sha256').update(APP_PASSWORD).digest(); | |
| return crypto.timingSafeEqual(a, b); | |
| } | |
| function isAuthenticated(req) { | |
| const cookies = parseCookies(req.headers.cookie); | |
| return verifyCookieValue(cookies[COOKIE_NAME]); | |
| } | |
| function setAuthCookie(res) { | |
| res.setHeader('Set-Cookie', `${COOKIE_NAME}=${makeToken()}; HttpOnly; Secure; Path=/; SameSite=Strict; Max-Age=${COOKIE_MAX_AGE_S}`); | |
| } | |
| function clearAuthCookie(res) { | |
| res.setHeader('Set-Cookie', `${COOKIE_NAME}=; HttpOnly; Secure; Path=/; SameSite=Strict; Max-Age=0`); | |
| } | |
| // Every device has to log in again. | |
| function logoutEverywhere() { | |
| epoch += 1; | |
| try { | |
| fs.mkdirSync(STATE_DIR, { recursive: true }); | |
| fs.writeFileSync(AUTH_FILE, JSON.stringify({ epoch, changedAt: Date.now() }), { mode: 0o600 }); | |
| } catch (err) { | |
| console.error('[auth] cikis kaydedilemedi:', err.message); | |
| } | |
| } | |
| function requireAuth(req, res, next) { | |
| if (isAuthenticated(req)) return next(); | |
| res.status(401).json({ error: 'unauthorized' }); | |
| } | |
| // ---- login rate limiting ---- | |
| // Per client address: 5 wrong passwords are free, then each one doubles a | |
| // lockout (30 s, 1 min, 2 min ... at most 30 min). A global cap slows down | |
| // guessing spread over many addresses. | |
| const FREE_FAILURES = 5; | |
| const BASE_LOCK_MS = 30 * 1000; | |
| const MAX_LOCK_MS = 30 * 60 * 1000; | |
| const FORGET_MS = 6 * 60 * 60 * 1000; | |
| const GLOBAL_WINDOW_MS = 60 * 1000; | |
| const GLOBAL_MAX_FAILURES = 40; | |
| const failures = new Map(); | |
| let globalFailures = []; | |
| const PRIVATE_IP = /^(?:10\.|127\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.|169\.254\.|::1$|f[cd][0-9a-f]{2}:|fe80:)/i; | |
| // HF's proxy appends the real address to X-Forwarded-For; a value the client | |
| // sent itself stays in front of it, so the right-most public entry is used. | |
| function clientIp(req) { | |
| const forwarded = String(req.headers['x-forwarded-for'] || '') | |
| .split(',') | |
| .map((s) => s.trim().replace(/^::ffff:/, '')) | |
| .filter(Boolean); | |
| for (let i = forwarded.length - 1; i >= 0; i -= 1) { | |
| if (!PRIVATE_IP.test(forwarded[i])) return forwarded[i]; | |
| } | |
| return String((req.socket && req.socket.remoteAddress) || 'unknown').replace(/^::ffff:/, ''); | |
| } | |
| function prune(now) { | |
| for (const [ip, entry] of failures) { | |
| if (now - entry.last > FORGET_MS) failures.delete(ip); | |
| } | |
| globalFailures = globalFailures.filter((t) => now - t < GLOBAL_WINDOW_MS); | |
| } | |
| // Milliseconds until this address may try again (0: now). | |
| function loginWait(req, now = Date.now()) { | |
| prune(now); | |
| const entry = failures.get(clientIp(req)); | |
| const own = entry && entry.until > now ? entry.until - now : 0; | |
| const global = globalFailures.length >= GLOBAL_MAX_FAILURES ? GLOBAL_WINDOW_MS - (now - globalFailures[0]) : 0; | |
| return Math.max(own, global, 0); | |
| } | |
| function recordFailure(req, now = Date.now()) { | |
| const ip = clientIp(req); | |
| const entry = failures.get(ip) || { count: 0, until: 0, last: 0 }; | |
| entry.count += 1; | |
| entry.last = now; | |
| if (entry.count > FREE_FAILURES) { | |
| entry.until = now + Math.min(MAX_LOCK_MS, BASE_LOCK_MS * 2 ** (entry.count - FREE_FAILURES - 1)); | |
| } | |
| failures.set(ip, entry); | |
| globalFailures.push(now); | |
| if (entry.count === FREE_FAILURES + 1) console.warn(`[auth] ${ip} adresinden cok sayida hatali giris; gecici olarak engellendi.`); | |
| } | |
| function recordSuccess(req) { | |
| failures.delete(clientIp(req)); | |
| } | |
| module.exports = { | |
| APP_PASSWORD, | |
| checkPassword, | |
| isAuthenticated, | |
| setAuthCookie, | |
| clearAuthCookie, | |
| logoutEverywhere, | |
| requireAuth, | |
| parseCookies, | |
| verifyCookieValue, | |
| makeToken, | |
| loginWait, | |
| recordFailure, | |
| recordSuccess, | |
| clientIp, | |
| COOKIE_NAME, | |
| }; | |