Files
IOT_SIM/server/src/access.ts
T
xuyueandClaude Opus 5.5 697f31e9ce 部署:bubblewrap 沙箱、编译检查、访问口令、Docker Compose
- 学生程序每次运行放进 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>
2026-10-22 15:40:09 +08:00

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
}