部署: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:
1 parent
081ac6f924
commit
697f31e9ce
25 files changed
+869
-52
No files matched your search
@@ -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
@@ -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')
|
||||
Reference in new issue
Block a user