server.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. const express = require('express');
  2. const path = require('path');
  3. const fs = require('fs').promises;
  4. const cors = require('cors');
  5. const app = express();
  6. const PORT = 3000;
  7. // 路径安全验证函数 - 防止路径遍历攻击
  8. function sanitizePath(input) {
  9. if (!input || typeof input !== 'string') {
  10. return '';
  11. }
  12. // 移除 ../ 和 ..\ 防止路径遍历
  13. // 移除路径分隔符 / 和 \
  14. // 保留中文等 Unicode 字符
  15. return input
  16. .replace(/\.\./g, '') // 移除 ..
  17. .replace(/^[\/\\]+/, '') // 移除开头的 / 和 \
  18. .replace(/[\/\\]/g, ''); // 移除所有 / 和 \
  19. }
  20. // 验证路径是否在允许的目录内
  21. function validatePath(fullPath, baseDir) {
  22. const resolvedPath = path.resolve(fullPath);
  23. const resolvedBaseDir = path.resolve(baseDir);
  24. return resolvedPath.startsWith(resolvedBaseDir);
  25. }
  26. // 中间件
  27. app.use(cors());
  28. app.use(express.json());
  29. app.use(express.static('public'));
  30. // API: 获取 index.md 内容
  31. app.get('/api/index', async (req, res) => {
  32. try {
  33. const indexPath = path.join(__dirname, 'docs', 'index.md');
  34. const content = await fs.readFile(indexPath, 'utf-8');
  35. res.json({ content });
  36. } catch (error) {
  37. res.status(500).json({ error: '无法读取 index.md', details: error.message });
  38. }
  39. });
  40. // API: 解析 index.md 结构
  41. app.get('/api/structure', async (req, res) => {
  42. try {
  43. const indexPath = path.join(__dirname, 'docs', 'index.md');
  44. const content = await fs.readFile(indexPath, 'utf-8');
  45. // 解析结构
  46. const structure = parseIndexMd(content);
  47. res.json(structure);
  48. } catch (error) {
  49. res.status(500).json({ error: '无法解析文档结构', details: error.message });
  50. }
  51. });
  52. // API: 获取指定分类的文档列表
  53. app.get('/api/category/:category', async (req, res) => {
  54. try {
  55. const category = sanitizePath(req.params.category);
  56. if (!category) {
  57. return res.status(400).json({ error: '无效的分类名称' });
  58. }
  59. const indexPath = path.join(__dirname, 'docs', 'index.md');
  60. const content = await fs.readFile(indexPath, 'utf-8');
  61. // 解析并获取指定分类的文档
  62. const structure = parseIndexMd(content);
  63. const categoryData = structure.find(cat => cat.name === category);
  64. if (!categoryData) {
  65. return res.status(404).json({ error: '分类不存在' });
  66. }
  67. res.json(categoryData);
  68. } catch (error) {
  69. res.status(500).json({ error: '无法获取分类信息', details: error.message });
  70. }
  71. });
  72. // API: 获取指定文档的内容
  73. app.get('/api/doc/:category/:docName', async (req, res) => {
  74. try {
  75. const category = sanitizePath(req.params.category);
  76. const docName = sanitizePath(req.params.docName);
  77. if (!category || !docName) {
  78. return res.status(400).json({ error: '无效的分类或文档名称' });
  79. }
  80. const docsDir = path.join(__dirname, 'docs');
  81. const docPath = path.join(docsDir, category, `${docName}.md`);
  82. // 验证路径是否在 docs 目录内
  83. if (!validatePath(docPath, docsDir)) {
  84. return res.status(403).json({ error: '拒绝访问:无效的路径' });
  85. }
  86. const content = await fs.readFile(docPath, 'utf-8');
  87. res.json({ content, category, docName });
  88. } catch (error) {
  89. res.status(500).json({ error: '无法读取文档', details: error.message });
  90. }
  91. });
  92. // API: 搜索文档
  93. app.get('/api/search/:category', async (req, res) => {
  94. try {
  95. const category = sanitizePath(req.params.category);
  96. const currentDoc = sanitizePath(req.query.currentDoc);
  97. const { q } = req.query;
  98. if (!category) {
  99. return res.status(400).json({ error: '无效的分类名称' });
  100. }
  101. if (!q || q.trim().length === 0) {
  102. return res.json({ currentDoc: [], otherDocs: [] });
  103. }
  104. const query = q.toLowerCase();
  105. const docsDir = path.join(__dirname, 'docs');
  106. const categoryPath = path.join(docsDir, category);
  107. // 验证路径是否在 docs 目录内
  108. if (!validatePath(categoryPath, docsDir)) {
  109. return res.status(403).json({ error: '拒绝访问:无效的路径' });
  110. }
  111. // 读取分类下的所有文档
  112. const files = await fs.readdir(categoryPath);
  113. const mdFiles = files.filter(file => file.endsWith('.md'));
  114. const currentDocResults = [];
  115. const otherDocsResults = [];
  116. // 搜索每个文档
  117. for (const file of mdFiles) {
  118. const docName = file.replace('.md', '');
  119. const filePath = path.join(categoryPath, file);
  120. const content = await fs.readFile(filePath, 'utf-8');
  121. // 搜索匹配的行
  122. const lines = content.split('\n');
  123. const matches = [];
  124. lines.forEach((line, index) => {
  125. const lowerLine = line.toLowerCase();
  126. if (lowerLine.includes(query)) {
  127. // 获取上下文(前后各50个字符)
  128. const startIndex = Math.max(0, lowerLine.indexOf(query) - 50);
  129. const endIndex = Math.min(line.length, lowerLine.indexOf(query) + query.length + 50);
  130. let snippet = line.substring(startIndex, endIndex);
  131. // 如果不是从头开始,添加省略号
  132. if (startIndex > 0) snippet = '...' + snippet;
  133. if (endIndex < line.length) snippet = snippet + '...';
  134. matches.push({
  135. line: index + 1,
  136. snippet: snippet,
  137. fullLine: line
  138. });
  139. }
  140. });
  141. if (matches.length > 0) {
  142. const result = {
  143. docName,
  144. matchCount: matches.length,
  145. matches: matches.slice(0, 5) // 最多返回5个匹配
  146. };
  147. // 区分当前文档和其他文档
  148. if (docName === currentDoc) {
  149. currentDocResults.push(result);
  150. } else {
  151. otherDocsResults.push(result);
  152. }
  153. }
  154. }
  155. res.json({
  156. query: q,
  157. currentDoc: currentDocResults,
  158. otherDocs: otherDocsResults
  159. });
  160. } catch (error) {
  161. res.status(500).json({ error: '搜索失败', details: error.message });
  162. }
  163. });
  164. // 解析 index.md 的函数
  165. function parseIndexMd(content) {
  166. const lines = content.split('\n');
  167. const structure = [];
  168. let currentCategory = null;
  169. for (const line of lines) {
  170. const trimmedLine = line.trim();
  171. // 匹配分类 [category]
  172. const categoryMatch = trimmedLine.match(/^\[(.+?)\]$/);
  173. if (categoryMatch) {
  174. currentCategory = {
  175. name: categoryMatch[1],
  176. docs: []
  177. };
  178. structure.push(currentCategory);
  179. continue;
  180. }
  181. // 匹配文档项 1: testa.md 或 3.1: testc1.md
  182. const docMatch = trimmedLine.match(/^([\d.]+):\s*(.+?)\.md$/);
  183. if (docMatch && currentCategory) {
  184. const [, number, docName] = docMatch;
  185. const level = (number.match(/\./g) || []).length;
  186. currentCategory.docs.push({
  187. number,
  188. name: docName,
  189. level,
  190. fullName: `${docName}.md`
  191. });
  192. }
  193. }
  194. return structure;
  195. }
  196. // 启动服务器
  197. app.listen(PORT, () => {
  198. console.log(`服务器运行在 http://localhost:${PORT}`);
  199. });