const express = require('express'); const path = require('path'); const fs = require('fs').promises; const cors = require('cors'); const app = express(); const PORT = 3000; // index.md 缓存 let indexCache = null; let indexCacheTime = 0; const CACHE_DURATION = 5000; // 5秒缓存 // 路径安全验证函数 - 防止路径遍历攻击 function sanitizePath(input) { if (!input || typeof input !== 'string') { return ''; } // 移除 ../ 和 ..\ 防止路径遍历 // 移除路径分隔符 / 和 \ // 保留中文等 Unicode 字符 return input .replace(/\.\./g, '') // 移除 .. .replace(/^[\/\\]+/, '') // 移除开头的 / 和 \ .replace(/[\/\\]/g, ''); // 移除所有 / 和 \ } // 验证路径是否在允许的目录内 function validatePath(fullPath, baseDir) { const resolvedPath = path.resolve(fullPath); const resolvedBaseDir = path.resolve(baseDir); return resolvedPath.startsWith(resolvedBaseDir); } // 中间件 app.use(cors()); app.use(express.json({ limit: '10mb' })); // 增加请求体大小限制到10MB app.use(express.urlencoded({ limit: '10mb', extended: true })); app.use(express.static('public')); // 获取 index.md 结构(带缓存) async function getIndexStructure() { const now = Date.now(); if (indexCache && (now - indexCacheTime < CACHE_DURATION)) { return indexCache; } const indexPath = path.join(__dirname, 'index.md'); const content = await fs.readFile(indexPath, 'utf-8'); indexCache = parseIndexMd(content); indexCacheTime = now; return indexCache; } // API: 解析 index.md 结构 app.get('/api/structure', async (req, res) => { try { const structure = await getIndexStructure(); res.json(structure); } catch (error) { res.status(500).json({ error: '无法解析文档结构', details: error.message }); } }); // API: 获取指定分类的文档列表 app.get('/api/category/:category', async (req, res) => { try { const category = sanitizePath(req.params.category); if (!category) { return res.status(400).json({ error: '无效的分类名称' }); } const structure = await getIndexStructure(); const categoryData = structure.find(cat => cat.name === category); if (!categoryData) { return res.status(404).json({ error: '分类不存在' }); } res.json(categoryData); } catch (error) { res.status(500).json({ error: '无法获取分类信息', details: error.message }); } }); // API: 获取指定文档的内容 app.get('/api/doc/:category/:docName', async (req, res) => { try { const category = sanitizePath(req.params.category); const docName = sanitizePath(req.params.docName); if (!category || !docName) { return res.status(400).json({ error: '无效的分类或文档名称' }); } const docsDir = path.join(__dirname, 'docs'); const docPath = path.join(docsDir, category, `${docName}.md`); // 验证路径是否在 docs 目录内 if (!validatePath(docPath, docsDir)) { return res.status(403).json({ error: '拒绝访问:无效的路径' }); } const content = await fs.readFile(docPath, 'utf-8'); res.json({ content, category, docName }); } catch (error) { res.status(500).json({ error: '无法读取文档', details: error.message }); } }); // API: 保存文档内容 app.put('/api/doc/:category/:docName', async (req, res) => { try { const category = sanitizePath(req.params.category); const docName = sanitizePath(req.params.docName); const { content } = req.body; // 验证输入 if (!category || !docName) { return res.status(400).json({ error: '无效的分类或文档名称' }); } if (!content || typeof content !== 'string') { return res.status(400).json({ error: '无效的文档内容' }); } const docsDir = path.join(__dirname, 'docs'); const docPath = path.join(docsDir, category, `${docName}.md`); // 验证路径是否在 docs 目录内 if (!validatePath(docPath, docsDir)) { return res.status(403).json({ error: '拒绝访问:无效的路径' }); } // 检查文件是否存在 try { await fs.access(docPath); } catch (error) { return res.status(404).json({ error: '文档不存在' }); } // 保存文件 await fs.writeFile(docPath, content, 'utf-8'); res.json({ success: true, message: '文档保存成功', category, docName }); } catch (error) { console.error('Save document error:', error); res.status(500).json({ error: '无法保存文档', details: error.message }); } }); // API: 搜索文档 app.get('/api/search/:category', async (req, res) => { try { const category = sanitizePath(req.params.category); const currentDoc = sanitizePath(req.query.currentDoc); const { q } = req.query; if (!category) { return res.status(400).json({ error: '无效的分类名称' }); } if (!q || q.trim().length === 0) { return res.json({ currentDoc: [], otherDocs: [] }); } const query = q.toLowerCase(); const docsDir = path.join(__dirname, 'docs'); const categoryPath = path.join(docsDir, category); // 验证路径是否在 docs 目录内 if (!validatePath(categoryPath, docsDir)) { return res.status(403).json({ error: '拒绝访问:无效的路径' }); } // 读取分类下的所有文档 const files = await fs.readdir(categoryPath); const mdFiles = files.filter(file => file.endsWith('.md')); const currentDocResults = []; const otherDocsResults = []; // 搜索每个文档 for (const file of mdFiles) { const docName = file.replace('.md', ''); const filePath = path.join(categoryPath, file); const content = await fs.readFile(filePath, 'utf-8'); // 搜索匹配的行 const lines = content.split('\n'); const matches = []; lines.forEach((line, index) => { const lowerLine = line.toLowerCase(); if (lowerLine.includes(query)) { // 获取上下文(前后各50个字符) 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: snippet, fullLine: line }); } }); if (matches.length > 0) { const result = { docName, matchCount: matches.length, matches: matches.slice(0, 5) // 最多返回5个匹配 }; // 区分当前文档和其他文档 if (docName === currentDoc) { currentDocResults.push(result); } else { otherDocsResults.push(result); } } } res.json({ query: q, currentDoc: currentDocResults, otherDocs: otherDocsResults }); } catch (error) { res.status(500).json({ error: '搜索失败', details: 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.listen(PORT, () => { console.log(`服务器运行在 http://localhost:${PORT}`); });