| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513 |
- const express = require('express');
- const path = require('path');
- const fs = require('fs').promises;
- const cors = require('cors');
- const session = require('express-session');
- const app = express();
- const PORT = 3000;
- // 硬编码的密码
- const PASSWORD = 'cjy@0526';
- // 目录常量
- const DIRS = {
- public: path.join(__dirname, 'public'),
- docs: path.join(__dirname, 'docs'),
- index: path.join(__dirname, 'index.md')
- };
- // index.md 缓存
- let indexCache = null;
- let indexCacheTime = 0;
- const CACHE_DURATION = 5000; // 5秒缓存
- // 路径安全验证函数 - 防止路径遍历攻击
- function sanitizePath(input) {
- if (!input || typeof input !== 'string') {
- return '';
- }
- // 安全清理策略:
- // 1. 移除路径遍历序列
- // 2. 移除开头的斜杠(防止绝对路径)
- return input
- .replace(/\.\./g, '') // 移除 ..
- .replace(/^[\/\\]+/, ''); // 移除开头的 / 和 \
- }
- // 验证路径是否在允许的目录内
- function validatePath(fullPath, baseDir) {
- const resolvedPath = path.resolve(fullPath);
- const resolvedBaseDir = path.resolve(baseDir);
- return resolvedPath.startsWith(resolvedBaseDir);
- }
- // 检查是否已认证
- function isAuthenticated(req) {
- return req.session && req.session.isAuthenticated === true;
- }
- // 统一的错误响应助手
- const ErrorResponse = {
- badRequest: (res, message, details = null) => {
- const response = { error: message };
- if (details) response.details = details;
- return res.status(400).json(response);
- },
- unauthorized: (res, message = '未授权') => {
- return res.status(401).json({ error: message });
- },
- forbidden: (res, message = '拒绝访问') => {
- return res.status(403).json({ error: message });
- },
- notFound: (res, message, details = null) => {
- const response = { error: message };
- if (details) response.details = details;
- return res.status(404).json(response);
- },
- serverError: (res, message, details = null) => {
- const response = { error: message };
- if (details) response.details = details;
- return res.status(500).json(response);
- }
- };
- // 中间件
- // CORS 配置 - 允许前端携带凭证(cookies)
- app.use(cors({
- origin: true, // 允许所有来源(生产环境建议指定具体域名)
- credentials: true // 允许携带凭证
- }));
- app.use(express.json({ limit: '10mb' })); // 增加请求体大小限制到10MB
- app.use(express.urlencoded({ limit: '10mb', extended: true }));
- // Session 中间件
- app.use(session({
- secret: 'cjydocs-secret-key-2024', // 会话密钥
- resave: false,
- saveUninitialized: false,
- cookie: {
- maxAge: 7 * 24 * 60 * 60 * 1000, // 7天
- httpOnly: true,
- secure: false, // 如果使用 HTTPS,设置为 true
- sameSite: 'lax', // 允许跨站点请求携带 cookie
- path: '/' // Cookie 路径
- },
- name: 'cjydocs.sid', // Session cookie 名称
- rolling: true // 每次请求时重置过期时间
- }));
- // 登录验证中间件
- function requireAuth(req, res, next) {
- if (isAuthenticated(req)) {
- return next();
- }
- // 如果是 API 请求,返回 401
- if (req.path.startsWith('/api')) {
- return ErrorResponse.unauthorized(res, '未登录,请先登录');
- }
- // 如果是页面请求,重定向到登录页
- res.redirect('/login.html');
- }
- // 根路径重定向处理
- app.get('/', (req, res) => {
- if (isAuthenticated(req)) {
- res.sendFile(path.join(DIRS.public, 'index.html'), (err) => {
- if (err && !res.headersSent) {
- console.error('发送首页文件失败:', err);
- ErrorResponse.serverError(res, '无法加载首页', err.message);
- }
- });
- } else {
- res.redirect('/login.html');
- }
- });
- // reader.html 需要登录
- app.get('/reader.html', (req, res) => {
- if (isAuthenticated(req)) {
- res.sendFile(path.join(DIRS.public, 'reader.html'));
- } else {
- res.redirect('/login.html');
- }
- });
- // 静态文件服务(CSS、JS、图片等)
- app.use('/css', express.static(path.join(DIRS.public, 'css')));
- app.use('/js', express.static(path.join(DIRS.public, 'js')));
- // login.html 不需要登录
- app.get('/login.html', (req, res) => {
- // 如果已登录,重定向到首页
- if (isAuthenticated(req)) {
- res.redirect('/');
- } else {
- res.sendFile(path.join(DIRS.public, 'login.html'));
- }
- });
- // API: 登录
- app.post('/api/login', (req, res) => {
- const { password } = req.body;
- if (!password || typeof password !== 'string') {
- return ErrorResponse.badRequest(res, '请输入密码');
- }
- if (password === PASSWORD) {
- req.session.isAuthenticated = true;
- // 保存 session 后再响应
- req.session.save((err) => {
- if (err) {
- console.error('Session保存失败:', err);
- return ErrorResponse.serverError(res, '登录失败,请重试');
- }
- res.json({ success: true, message: '登录成功' });
- });
- } else {
- return ErrorResponse.unauthorized(res, '密码错误');
- }
- });
- // API: 登出
- app.post('/api/logout', (req, res) => {
- req.session.destroy((err) => {
- if (err) {
- return ErrorResponse.serverError(res, '登出失败');
- }
- res.json({ success: true, message: '登出成功' });
- });
- });
- // API: 检查登录状态
- app.get('/api/check-auth', (req, res) => {
- res.json({ authenticated: isAuthenticated(req) });
- });
- // API: 获取图片资源(需要登录)
- app.get('/api/image/:category/*', requireAuth, async (req, res) => {
- try {
- const category = sanitizePath(req.params.category);
- const imagePath = req.params[0]; // 获取剩余路径部分,如 'assets/test.png'
- if (!category || !imagePath) {
- return ErrorResponse.badRequest(res, '无效的请求路径');
- }
- const fullPath = path.join(DIRS.docs, category, imagePath);
- // 验证路径安全性
- if (!validatePath(fullPath, DIRS.docs)) {
- return ErrorResponse.forbidden(res, '拒绝访问');
- }
- // 检查文件是否存在
- try {
- await fs.access(fullPath);
- } catch (err) {
- return ErrorResponse.notFound(res, '图片不存在');
- }
- // 发送图片文件
- res.sendFile(fullPath);
- } catch (error) {
- console.error('Error serving image:', error);
- return ErrorResponse.serverError(res, '无法加载图片');
- }
- });
- // 获取 index.md 结构(带缓存)
- async function getIndexStructure() {
- const now = Date.now();
- if (indexCache && (now - indexCacheTime < CACHE_DURATION)) {
- return indexCache;
- }
- const content = await fs.readFile(DIRS.index, 'utf-8');
- indexCache = parseIndexMd(content);
- indexCacheTime = now;
- return indexCache;
- }
- // API: 解析 index.md 结构(需要登录)
- app.get('/api/structure', requireAuth, async (req, res) => {
- try {
- const structure = await getIndexStructure();
- res.json(structure);
- } catch (error) {
- return ErrorResponse.serverError(res, '无法解析文档结构', error.message);
- }
- });
- // API: 获取指定分类的文档列表(需要登录)
- app.get('/api/category/:category', requireAuth, async (req, res) => {
- try {
- const category = sanitizePath(req.params.category);
- if (!category) {
- return ErrorResponse.badRequest(res, '无效的分类名称');
- }
- const structure = await getIndexStructure();
- const categoryData = structure.find(cat => cat.name === category);
- if (!categoryData) {
- return ErrorResponse.notFound(res, '分类不存在');
- }
- res.json(categoryData);
- } catch (error) {
- return ErrorResponse.serverError(res, '无法获取分类信息', error.message);
- }
- });
- // API: 获取指定文档的内容(需要登录)
- // 修改为支持通配符路径参数,以正确处理子目录
- app.get('/api/doc/:category/*', requireAuth, async (req, res) => {
- try {
- const category = sanitizePath(req.params.category);
- const docName = req.params[0]; // 获取剩余的路径部分,支持子目录
- if (!category || !docName) {
- return ErrorResponse.badRequest(res, '无效的分类或文档名称');
- }
- // 直接使用 docName,它可能包含子目录路径
- const docPath = path.join(DIRS.docs, category, `${docName}.md`);
- // 验证路径是否在 docs 目录内
- if (!validatePath(docPath, DIRS.docs)) {
- return ErrorResponse.forbidden(res, '拒绝访问:无效的路径');
- }
- const content = await fs.readFile(docPath, 'utf-8');
- res.json({ content, category, docName });
- } catch (error) {
- return ErrorResponse.serverError(res, '无法读取文档', error.message);
- }
- });
- // API: 保存文档内容(需要登录)
- // 修改为支持通配符路径参数,以正确处理子目录
- app.put('/api/doc/:category/*', requireAuth, async (req, res) => {
- try {
- const category = sanitizePath(req.params.category);
- const docName = req.params[0]; // 获取剩余的路径部分,支持子目录
- const { content } = req.body;
- // 验证输入
- if (!category || !docName) {
- return ErrorResponse.badRequest(res, '无效的分类或文档名称');
- }
- if (!content || typeof content !== 'string') {
- return ErrorResponse.badRequest(res, '无效的文档内容');
- }
- // 直接使用 docName,它可能包含子目录路径
- const docPath = path.join(DIRS.docs, category, `${docName}.md`);
- // 验证路径是否在 docs 目录内
- if (!validatePath(docPath, DIRS.docs)) {
- return ErrorResponse.forbidden(res, '拒绝访问:无效的路径');
- }
- // 检查文件是否存在
- try {
- await fs.access(docPath);
- } catch (error) {
- return ErrorResponse.notFound(res, '文档不存在');
- }
- // 保存文件
- await fs.writeFile(docPath, content, 'utf-8');
- res.json({
- success: true,
- message: '文档保存成功',
- category,
- docName
- });
- } catch (error) {
- console.error('Save document error:', error);
- return ErrorResponse.serverError(res, '无法保存文档', error.message);
- }
- });
- // API: 搜索文档(需要登录)
- app.get('/api/search/:category', requireAuth, async (req, res) => {
- try {
- const category = sanitizePath(req.params.category);
- const currentDoc = req.query.currentDoc || '';
- const { q } = req.query;
- if (!category) {
- return ErrorResponse.badRequest(res, '无效的分类名称');
- }
- if (!q || q.trim().length === 0) {
- return res.json({ currentDoc: [], otherDocs: [] });
- }
- const query = q.toLowerCase();
- const categoryPath = path.join(DIRS.docs, category);
- // 验证路径是否在 docs 目录内
- if (!validatePath(categoryPath, DIRS.docs)) {
- return ErrorResponse.forbidden(res, '拒绝访问:无效的路径');
- }
- // 从 index.md 获取当前分类下应该搜索的文档列表
- const structure = await getIndexStructure();
- const categoryData = structure.find(cat => cat.name === category);
- if (!categoryData) {
- return ErrorResponse.notFound(res, '分类不存在');
- }
- // 并行读取所有文档
- const MAX_MATCHES_PER_DOC = 5;
- const decodedCurrentDoc = decodeURIComponent(currentDoc).replace(/\\/g, '/');
- const fileReadPromises = categoryData.docs.map(async (doc) => {
- const docName = doc.name;
- const filePath = path.join(categoryPath, `${docName}.md`);
- try {
- const content = await fs.readFile(filePath, 'utf-8');
- return { docName, content, exists: true };
- } catch (err) {
- console.warn(`文档不存在,跳过搜索: ${filePath}`);
- return { docName, exists: false };
- }
- });
- const fileContents = await Promise.all(fileReadPromises);
- const currentDocResults = [];
- const otherDocsResults = [];
- // 搜索每个文档
- for (const { docName, content, exists } of fileContents) {
- if (!exists) continue;
- const lines = content.split('\n');
- const matches = [];
- // 搜索匹配的行,达到上限后提前退出
- for (let index = 0; index < lines.length && matches.length < MAX_MATCHES_PER_DOC; index++) {
- const line = lines[index];
- const lowerLine = line.toLowerCase();
- if (lowerLine.includes(query)) {
- const startIndex = Math.max(0, lowerLine.indexOf(query) - 50);
- const endIndex = Math.min(line.length, lowerLine.indexOf(query) + query.length + 50);
- let snippet = line.substring(startIndex, endIndex);
- if (startIndex > 0) snippet = '...' + snippet;
- if (endIndex < line.length) snippet = snippet + '...';
- matches.push({
- line: index + 1,
- snippet,
- fullLine: line
- });
- }
- }
- if (matches.length > 0) {
- const result = {
- docName,
- matchCount: matches.length,
- matches
- };
- // 区分当前文档和其他文档
- if (docName === decodedCurrentDoc) {
- currentDocResults.push(result);
- } else {
- otherDocsResults.push(result);
- }
- }
- }
- res.json({
- query: q,
- currentDoc: currentDocResults,
- otherDocs: otherDocsResults
- });
- } catch (error) {
- return ErrorResponse.serverError(res, '搜索失败', error.message);
- }
- });
- // 解析 index.md 的函数
- function parseIndexMd(content) {
- const lines = content.split('\n');
- const structure = [];
- let currentCategory = null;
- for (const line of lines) {
- const trimmedLine = line.trim();
- // 匹配分类 [category]
- const categoryMatch = trimmedLine.match(/^\[(.+?)\]$/);
- if (categoryMatch) {
- currentCategory = {
- name: categoryMatch[1],
- docs: []
- };
- structure.push(currentCategory);
- continue;
- }
- // 匹配文档项 1: testa.md 或 3.1: testc1.md
- const docMatch = trimmedLine.match(/^([\d.]+):\s*(.+?)\.md$/);
- if (docMatch && currentCategory) {
- const [, number, docName] = docMatch;
- const level = (number.match(/\./g) || []).length;
- currentCategory.docs.push({
- number,
- name: docName,
- level,
- fullName: `${docName}.md`
- });
- }
- }
- return structure;
- }
- // 全局错误处理中间件
- app.use((err, req, res, next) => {
- console.error('全局错误捕获:', err);
- res.status(err.status || 500).json({
- error: '服务器内部错误',
- message: err.message,
- path: req.path
- });
- });
- // 404 处理
- app.use((req, res) => {
- res.status(404).json({
- error: '页面不存在',
- path: req.path
- });
- });
- // 启动服务器
- app.listen(PORT, () => {
- console.log(`服务器运行在 http://localhost:${PORT}`);
- });
|