- 学生程序每次运行放进 bubblewrap:断网,只读挂载系统文件、运行时和自己的工程目录, 看不到其他进程和服务器环境变量;限制堆内存 128 MB、CPU 时间 120 秒、运行 60 分钟, nice 10,全站同时运行上限 150;启动时自检沙箱,不可用时拒绝运行 - 编译检查(SIM001):网络、反射、启动进程、调用本机代码、加载程序集、dynamic 给出中文报错 - 访问口令:cookie 保存口令哈希,按真实 IP 限制输错次数;前端口令页 - 服务端配置集中到 config.ts,可用环境变量覆盖;Bun 直接提供打包好的网页 - Dockerfile(多阶段,服务端打包成单文件)、compose.yaml(Caddy 只收 HTTP,接入 NPM 的 npm_proxy 网络)、deploy/deploy.sh(本机构建后经 ssh 传到服务器)和部署说明 - 测试:沙箱隔离、内存上限、CPU 时间上限、编译检查 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
59 lines
1.9 KiB
TypeScript
59 lines
1.9 KiB
TypeScript
// 访问口令:账号系统(第 2 阶段)做好之前,用一个统一口令挡住外人。
|
|
// 输对口令后发一个 cookie,值是口令的哈希;老师换口令后,旧 cookie 全部失效。
|
|
import type { Context } from 'hono'
|
|
import { setCookie } from 'hono/cookie'
|
|
import { config } from './config'
|
|
|
|
const COOKIE = 'iot_access'
|
|
// 全班通常共用学校的一个出口 IP,上限不能太小,否则几个同学输错就把全班挡在外面
|
|
const MAX_FAILURES = 30
|
|
const FAILURE_WINDOW_MS = 10 * 60_000
|
|
|
|
const token = config.accessPassword
|
|
? new Bun.CryptoHasher('sha256').update(`iot-sim-access:${config.accessPassword}`).digest('hex')
|
|
: ''
|
|
|
|
export const accessRequired = token !== ''
|
|
|
|
/** 请求是否带着有效的口令 cookie(没设口令时总是放行) */
|
|
export function hasAccess(cookieHeader: string | null) {
|
|
if (!accessRequired) return true
|
|
const value = cookieHeader
|
|
?.split(';')
|
|
.map((part) => part.trim().split('='))
|
|
.find(([name]) => name === COOKIE)?.[1]
|
|
return value === token
|
|
}
|
|
|
|
/** 每个 IP 一段时间内的失败次数,防止暴力猜口令 */
|
|
const failures = new Map<string, { count: number; since: number }>()
|
|
|
|
export function tooManyFailures(ip: string) {
|
|
const entry = failures.get(ip)
|
|
if (!entry) return false
|
|
if (Date.now() - entry.since > FAILURE_WINDOW_MS) {
|
|
failures.delete(ip)
|
|
return false
|
|
}
|
|
return entry.count >= MAX_FAILURES
|
|
}
|
|
|
|
/** 检查口令;对了就设置 cookie */
|
|
export function login(c: Context, password: string, ip: string) {
|
|
if (password !== config.accessPassword) {
|
|
const entry = failures.get(ip) ?? { count: 0, since: Date.now() }
|
|
entry.count++
|
|
failures.set(ip, entry)
|
|
return false
|
|
}
|
|
failures.delete(ip)
|
|
setCookie(c, COOKIE, token, {
|
|
httpOnly: true,
|
|
sameSite: 'Lax',
|
|
secure: new URL(c.req.url).protocol === 'https:' || c.req.header('x-forwarded-proto') === 'https',
|
|
path: '/',
|
|
maxAge: 180 * 24 * 3600,
|
|
})
|
|
return true
|
|
}
|