server.js 改进:
- 使用标准 URL API 代替简单的 split('?')[0]
- 添加路径遍历攻击防护(path.normalize + 路径检查)
- 改进错误处理,不暴露内部错误详情
- 添加 try-catch 保护 URL 解析
view-doc.html 改进:
- markdown-it 配置 html: false 防止 XSS 攻击
- 添加文档路径安全验证(检查 .. / \ 字符)
- 使用并行请求代替串行(Promise.all)
- 使用 textContent 设置标题防止 XSS
- 优化错误提示信息
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
196 lines
7.2 KiB
JavaScript
196 lines
7.2 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* AI 智能抽背助手 - 原型系统本地预览服务器
|
|
*
|
|
* 使用方法:
|
|
* node server.js
|
|
*
|
|
* 功能:
|
|
* - 启动本地 HTTP 服务器
|
|
* - 自动打开浏览器访问原型系统
|
|
* - 支持热重载(修改文件后自动刷新)
|
|
*
|
|
* @author generate-prototype-grid skill
|
|
* @date 2026-03-28
|
|
*/
|
|
|
|
const http = require('http');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { exec } = require('child_process');
|
|
const { URL } = require('url');
|
|
|
|
const PORT = 8080;
|
|
const PROTOTYPE_DIR = __dirname;
|
|
const PARENT_DIR = path.dirname(__dirname);
|
|
|
|
// MIME 类型映射
|
|
const MIME_TYPES = {
|
|
'.html': 'text/html; charset=utf-8',
|
|
'.css': 'text/css; charset=utf-8',
|
|
'.js': 'application/javascript; charset=utf-8',
|
|
'.json': 'application/json; charset=utf-8',
|
|
'.png': 'image/png',
|
|
'.jpg': 'image/jpeg',
|
|
'.jpeg': 'image/jpeg',
|
|
'.gif': 'image/gif',
|
|
'.svg': 'image/svg+xml',
|
|
'.ico': 'image/x-icon',
|
|
'.woff': 'font/woff',
|
|
'.woff2': 'font/woff2',
|
|
'.ttf': 'font/ttf',
|
|
'.md': 'text/markdown; charset=utf-8',
|
|
};
|
|
|
|
// 创建服务器
|
|
const server = http.createServer((req, res) => {
|
|
// 使用 URL 模块安全解析 URL
|
|
let urlPath;
|
|
try {
|
|
const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
urlPath = parsedUrl.pathname || '/';
|
|
} catch {
|
|
urlPath = '/';
|
|
}
|
|
|
|
// 处理根路径
|
|
if (urlPath === '/') {
|
|
urlPath = '/index.html';
|
|
}
|
|
|
|
// 安全验证:防止路径遍历攻击
|
|
// 规范化路径并检查是否在允许的目录内
|
|
const isPlanningArtifacts = urlPath.startsWith("/planning-artifacts/");
|
|
const baseDir = isPlanningArtifacts ? PARENT_DIR : PROTOTYPE_DIR;
|
|
const relativePath = isPlanningArtifacts ? urlPath : urlPath;
|
|
|
|
const normalizedPath = path.normalize(path.join(baseDir, relativePath));
|
|
if (!normalizedPath.startsWith(baseDir)) {
|
|
// 路径遍历攻击检测
|
|
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
res.end('<!DOCTYPE html><html><head><meta charset="UTF-8"><title>400 - 错误请求</title></head><body><h1>400 - 错误请求</h1><p>无效的路径</p></body></html>');
|
|
return;
|
|
}
|
|
|
|
const filePath = normalizedPath;
|
|
const extname = path.extname(filePath).toLowerCase();
|
|
|
|
// 设置响应头
|
|
const contentType = MIME_TYPES[extname] || 'application/octet-stream';
|
|
|
|
// 读取并返回文件
|
|
fs.readFile(filePath, (err, content) => {
|
|
if (err) {
|
|
if (err.code === 'ENOENT') {
|
|
// 文件不存在
|
|
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
res.end(`
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>404 - 文件未找到</title>
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
height: 100vh;
|
|
margin: 0;
|
|
background: #f5f5f5;
|
|
}
|
|
.error-box {
|
|
text-align: center;
|
|
background: white;
|
|
padding: 40px;
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
}
|
|
h1 { color: #1890FF; margin: 0 0 16px 0; }
|
|
p { color: #666; margin: 0 0 24px 0; }
|
|
a {
|
|
color: #1890FF;
|
|
text-decoration: none;
|
|
padding: 8px 16px;
|
|
border: 1px solid #1890FF;
|
|
border-radius: 6px;
|
|
display: inline-block;
|
|
}
|
|
a:hover { background: #1890FF; color: white; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="error-box">
|
|
<h1>404</h1>
|
|
<p>文件未找到</p>
|
|
<a href="/">返回首页</a>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`);
|
|
} else {
|
|
// 服务器错误 - 不暴露内部错误详情
|
|
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
res.end('<!DOCTYPE html><html><head><meta charset="UTF-8"><title>500 - 服务器错误</title></head><body><h1>500 - 服务器错误</h1><p>服务器内部错误,请稍后重试</p></body></html>');
|
|
}
|
|
} else {
|
|
// 成功返回文件
|
|
res.writeHead(200, { 'Content-Type': contentType });
|
|
res.end(content);
|
|
}
|
|
});
|
|
});
|
|
|
|
// 启动服务器
|
|
server.listen(PORT, () => {
|
|
console.log('='.repeat(60));
|
|
console.log(' AI 智能抽背助手 - 原型系统本地预览服务器');
|
|
console.log('='.repeat(60));
|
|
console.log('');
|
|
console.log(` 服务器地址: http://localhost:${PORT}`);
|
|
console.log('');
|
|
console.log(' 可用页面:');
|
|
console.log(` - 统一入口: http://localhost:${PORT}/index.html`);
|
|
console.log(` - 平铺展示: http://localhost:${PORT}/admin-index.html`);
|
|
console.log(` - 文档中心: http://localhost:${PORT}/docs-index.html`);
|
|
console.log('');
|
|
console.log(' 后台管理页面:');
|
|
console.log(` - 数据概览: http://localhost:${PORT}/pages/admin/dashboard.html`);
|
|
console.log(` - 抽背记录查询: http://localhost:${PORT}/pages/admin/quiz-records.html`);
|
|
console.log(` - 学生管理: http://localhost:${PORT}/pages/admin/student-management.html`);
|
|
console.log(` - 知识点管理: http://localhost:${PORT}/pages/admin/knowledge-management.html`);
|
|
console.log(` - 学习任务管理: http://localhost:${PORT}/pages/admin/task-management.html`);
|
|
console.log(` - 系统设置: http://localhost:${PORT}/pages/admin/settings.html`);
|
|
console.log('');
|
|
console.log(' 按 Ctrl+C 停止服务器');
|
|
console.log('='.repeat(60));
|
|
console.log('');
|
|
|
|
// 自动打开浏览器
|
|
const openCommand = process.platform === 'win32' ? 'start' :
|
|
process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
|
|
exec(`${openCommand} http://localhost:${PORT}`, (error) => {
|
|
if (error) {
|
|
console.log(' 提示: 无法自动打开浏览器,请手动访问上述地址');
|
|
} else {
|
|
console.log(' 已自动打开浏览器');
|
|
}
|
|
console.log('');
|
|
});
|
|
});
|
|
|
|
// 优雅退出
|
|
process.on('SIGINT', () => {
|
|
console.log('');
|
|
console.log('正在停止服务器...');
|
|
server.close(() => {
|
|
console.log('服务器已停止');
|
|
process.exit(0);
|
|
});
|
|
});
|
|
|
|
module.exports = server;
|