部署: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>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-22 15:40:09 +08:00
1 parent 081ac6f924
commit 697f31e9ce
25 files changed
+869 -52

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
<script setup lang="ts">
// 访问口令页:输对后刷新页面进入平台(口令由服务器记在 cookie 里,之后不用再输)。
import { ref } from 'vue'
import { NButton, NConfigProvider, NInput, zhCN } from 'naive-ui'
const password = ref('')
const error = ref('')
const busy = ref(false)
async function submit() {
if (!password.value || busy.value) return
busy.value = true
error.value = ''
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: password.value }),
})
if (response.ok) {
location.reload()
return
}
error.value = ((await response.json().catch(() => ({}))) as { message?: string }).message ?? '登录失败'
} catch {
error.value = '连不上服务器,请稍后再试'
} finally {
busy.value = false
}
}
</script>
<template>
<NConfigProvider :locale="zhCN">
<div class="gate">
<form class="card" @submit.prevent="submit">
<h1>物联网编程仿真平台</h1>
<p>请输入老师给的访问口令</p>
<NInput v-model:value="password" type="password" show-password-on="click" placeholder="访问口令" autofocus />
<div v-if="error" class="error">{{ error }}</div>
<NButton type="primary" attr-type="submit" block :loading="busy" :disabled="!password">进入</NButton>
</form>
</div>
</NConfigProvider>
</template>
<style scoped>
.gate {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 16px;
box-sizing: border-box;
background: #f3f4f6;
}
.card {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
max-width: 320px;
padding: 28px 24px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
h1 {
margin: 0;
font-size: 18px;
}
p {
margin: 0;
color: #6b7280;
font-size: 13px;
}
.error {
color: #dc2626;
font-size: 13px;
}
</style>
+7 -1
View File
@@ -1,5 +1,11 @@
import { createApp } from 'vue'
import AccessGate from './AccessGate.vue'
import App from './App.vue'
import './winforms/winforms.css'
createApp(App).mount('#app')
// 服务器设置了访问口令而浏览器还没输过时,先显示口令页
const auth = await fetch('/api/auth')
.then((response) => response.json() as Promise<{ ok: boolean }>)
.catch(() => ({ ok: true }))
createApp(auth.ok ? App : AccessGate).mount('#app')