/** * 访问令牌。 * * Patchbay 默认只监听 127.0.0.1,远程访问走 SSH 隧道。即便如此仍然要认证—— * 同一台机器上跑着别的服务(比如容器里的进程)时,「只监听本机」并不等于「只有你能访问」。 * * 令牌可以用环境变量 PATCHBAY_TOKEN 指定;没指定就在首次启动时生成一个, * 存到配置文件旁边的 .patchbay-token 里,并打印在控制台上。 */ import { chmod, readFile, writeFile } from 'node:fs/promises' import { dirname, join } from 'node:path' import type { Env } from './env.ts' export async function resolveToken(env: Env): Promise<{ token: string; source: string }> { const fromEnv = Bun.env.PATCHBAY_TOKEN if (fromEnv) return { token: fromEnv, source: '环境变量 PATCHBAY_TOKEN' } const path = join(dirname(env.configPath), '.patchbay-token') try { const saved = (await readFile(path, 'utf8')).trim() if (saved) return { token: saved, source: path } } catch { // 文件不存在,往下走去生成一个 } const token = crypto.randomUUID().replaceAll('-', '') await writeFile(path, `${token}\n`, 'utf8') // 令牌等同于密码,不该让同机器上的其他用户读到。Windows 上 chmod 基本无效,失败就算了。 await chmod(path, 0o600).catch(() => {}) return { token, source: `${path}(新生成)` } } /** * 常数时间比较,避免通过响应耗时逐字节猜出令牌。 * * 长度不同直接返回 false 会泄漏长度,但令牌长度本来就是固定且公开的,无所谓。 */ export function tokenMatches(expected: string, given: string | null): boolean { if (!given || given.length !== expected.length) return false let diff = 0 for (let i = 0; i < expected.length; i += 1) { diff |= expected.charCodeAt(i) ^ given.charCodeAt(i) } return diff === 0 } /** 从请求里取令牌,支持 Authorization 头和查询参数(后者给 WebSocket 用)。 */ export function tokenFromRequest(req: Request): string | null { const header = req.headers.get('Authorization') if (header?.startsWith('Bearer ')) return header.slice(7) return new URL(req.url).searchParams.get('token') }