const express = require('express'); const path = require('path'); const fs = require('fs').promises; const cors = require('cors'); const app = express(); const PORT = 3000; // 路径安全验证函数 - 防止路径遍历攻击 function sanitizePath(input) { if (!input || typeof input !== 'string') { return ''; } // 移除 ../ 和 ..\ 防止路径遍历 // 移除开头的 / 和 \ // 只保留字母、数字、连字符、下划线和点号 return input .replace(/\.\./g, '') .replace(/^[\/\\]+/, '') .replace(/[\/\\]/g, '') .replace(/[^a-zA-Z0-9\-_.]/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()); app.use(express.static('public')); // API: 获取 index.md 内容 app.get('/api/index', async (req, res) => { try { const indexPath = path.join(__dirname, 'docs', 'index.md'); const content = await fs.readFile(indexPath, 'utf-8'); res.json({ content }); } catch (error) { res.status(500).json({ error: '无法读取 index.md', details: error.message }); } }); // API: 解析 index.md 结构 app.get('/api/structure', async (req, res) => { try { const indexPath = path.join(__dirname, 'docs', 'index.md'); const content = await fs.readFile(indexPath, 'utf-8'); // 解析结构 const structure = parseIndexMd(content); 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 indexPath = path.join(__dirname, 'docs', 'index.md'); const content = await fs.readFile(indexPath, 'utf-8'); // 解析并获取指定分类的文档 const structure = parseIndexMd(content); 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.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}`); });