server.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  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. // index.md 缓存
  8. let indexCache = null;
  9. let indexCacheTime = 0;
  10. const CACHE_DURATION = 5000; // 5秒缓存
  11. // 路径安全验证函数 - 防止路径遍历攻击
  12. // 注意:此函数仅用于清理单个路径段(如分类名、文件名),不用于完整路径
  13. function sanitizePath(input) {
  14. if (!input || typeof input !== 'string') {
  15. return '';
  16. }
  17. // 安全清理策略:
  18. // 1. 移除路径遍历序列
  19. // 2. 移除开头的斜杠(防止绝对路径)
  20. // 3. 对于单个路径段,不应包含路径分隔符
  21. return input
  22. .replace(/\.\./g, '') // 移除 ..
  23. .replace(/^[\/\\]+/, '') // 移除开头的 / 和 \
  24. .replace(/[\/\\]/g, ''); // 移除路径分隔符(适用于单个段)
  25. }
  26. // 验证路径是否在允许的目录内
  27. function validatePath(fullPath, baseDir) {
  28. const resolvedPath = path.resolve(fullPath);
  29. const resolvedBaseDir = path.resolve(baseDir);
  30. return resolvedPath.startsWith(resolvedBaseDir);
  31. }
  32. // 中间件
  33. app.use(cors());
  34. app.use(express.json({ limit: '10mb' })); // 增加请求体大小限制到10MB
  35. app.use(express.urlencoded({ limit: '10mb', extended: true }));
  36. app.use(express.static('public'));
  37. // API: 获取图片资源
  38. app.get('/api/image/:category/*', async (req, res) => {
  39. try {
  40. // 对分类名进行处理(移除危险字符但保留中文)
  41. const category = req.params.category.replace(/\.\./g, '');
  42. const imagePath = req.params[0]; // 获取剩余路径部分,如 'assets/test.png'
  43. if (!category || !imagePath) {
  44. return res.status(400).json({ error: '无效的请求路径' });
  45. }
  46. const docsDir = path.join(__dirname, 'docs');
  47. const fullPath = path.join(docsDir, category, imagePath);
  48. // 验证路径安全性
  49. if (!validatePath(fullPath, docsDir)) {
  50. return res.status(403).json({ error: '拒绝访问' });
  51. }
  52. // 检查文件是否存在
  53. try {
  54. await fs.access(fullPath);
  55. } catch (err) {
  56. return res.status(404).json({ error: '图片不存在' });
  57. }
  58. // 发送图片文件
  59. res.sendFile(fullPath);
  60. } catch (error) {
  61. console.error('Error serving image:', error);
  62. res.status(500).json({ error: '无法加载图片' });
  63. }
  64. });
  65. // 获取 index.md 结构(带缓存)
  66. async function getIndexStructure() {
  67. const now = Date.now();
  68. if (indexCache && (now - indexCacheTime < CACHE_DURATION)) {
  69. return indexCache;
  70. }
  71. const indexPath = path.join(__dirname, 'index.md');
  72. const content = await fs.readFile(indexPath, 'utf-8');
  73. indexCache = parseIndexMd(content);
  74. indexCacheTime = now;
  75. return indexCache;
  76. }
  77. // API: 解析 index.md 结构
  78. app.get('/api/structure', async (req, res) => {
  79. try {
  80. const structure = await getIndexStructure();
  81. res.json(structure);
  82. } catch (error) {
  83. res.status(500).json({ error: '无法解析文档结构', details: error.message });
  84. }
  85. });
  86. // API: 获取指定分类的文档列表
  87. app.get('/api/category/:category', async (req, res) => {
  88. try {
  89. const category = sanitizePath(req.params.category);
  90. if (!category) {
  91. return res.status(400).json({ error: '无效的分类名称' });
  92. }
  93. const structure = await getIndexStructure();
  94. const categoryData = structure.find(cat => cat.name === category);
  95. if (!categoryData) {
  96. return res.status(404).json({ error: '分类不存在' });
  97. }
  98. res.json(categoryData);
  99. } catch (error) {
  100. res.status(500).json({ error: '无法获取分类信息', details: error.message });
  101. }
  102. });
  103. // API: 获取指定文档的内容
  104. app.get('/api/doc/:category/:docName', async (req, res) => {
  105. try {
  106. const category = sanitizePath(req.params.category);
  107. const docName = sanitizePath(req.params.docName);
  108. if (!category || !docName) {
  109. return res.status(400).json({ error: '无效的分类或文档名称' });
  110. }
  111. const docsDir = path.join(__dirname, 'docs');
  112. const docPath = path.join(docsDir, category, `${docName}.md`);
  113. // 验证路径是否在 docs 目录内
  114. if (!validatePath(docPath, docsDir)) {
  115. return res.status(403).json({ error: '拒绝访问:无效的路径' });
  116. }
  117. const content = await fs.readFile(docPath, 'utf-8');
  118. res.json({ content, category, docName });
  119. } catch (error) {
  120. res.status(500).json({ error: '无法读取文档', details: error.message });
  121. }
  122. });
  123. // API: 保存文档内容
  124. app.put('/api/doc/:category/:docName', async (req, res) => {
  125. try {
  126. const category = sanitizePath(req.params.category);
  127. const docName = sanitizePath(req.params.docName);
  128. const { content } = req.body;
  129. // 验证输入
  130. if (!category || !docName) {
  131. return res.status(400).json({ error: '无效的分类或文档名称' });
  132. }
  133. if (!content || typeof content !== 'string') {
  134. return res.status(400).json({ error: '无效的文档内容' });
  135. }
  136. const docsDir = path.join(__dirname, 'docs');
  137. const docPath = path.join(docsDir, category, `${docName}.md`);
  138. // 验证路径是否在 docs 目录内
  139. if (!validatePath(docPath, docsDir)) {
  140. return res.status(403).json({ error: '拒绝访问:无效的路径' });
  141. }
  142. // 检查文件是否存在
  143. try {
  144. await fs.access(docPath);
  145. } catch (error) {
  146. return res.status(404).json({ error: '文档不存在' });
  147. }
  148. // 保存文件
  149. await fs.writeFile(docPath, content, 'utf-8');
  150. res.json({
  151. success: true,
  152. message: '文档保存成功',
  153. category,
  154. docName
  155. });
  156. } catch (error) {
  157. console.error('Save document error:', error);
  158. res.status(500).json({ error: '无法保存文档', details: error.message });
  159. }
  160. });
  161. // API: 搜索文档
  162. app.get('/api/search/:category', async (req, res) => {
  163. try {
  164. const category = sanitizePath(req.params.category);
  165. const currentDoc = sanitizePath(req.query.currentDoc);
  166. const { q } = req.query;
  167. if (!category) {
  168. return res.status(400).json({ error: '无效的分类名称' });
  169. }
  170. if (!q || q.trim().length === 0) {
  171. return res.json({ currentDoc: [], otherDocs: [] });
  172. }
  173. const query = q.toLowerCase();
  174. const docsDir = path.join(__dirname, 'docs');
  175. const categoryPath = path.join(docsDir, category);
  176. // 验证路径是否在 docs 目录内
  177. if (!validatePath(categoryPath, docsDir)) {
  178. return res.status(403).json({ error: '拒绝访问:无效的路径' });
  179. }
  180. // 读取分类下的所有文档
  181. const files = await fs.readdir(categoryPath);
  182. const mdFiles = files.filter(file => file.endsWith('.md'));
  183. const currentDocResults = [];
  184. const otherDocsResults = [];
  185. // 搜索每个文档
  186. for (const file of mdFiles) {
  187. const docName = file.replace('.md', '');
  188. const filePath = path.join(categoryPath, file);
  189. const content = await fs.readFile(filePath, 'utf-8');
  190. // 搜索匹配的行
  191. const lines = content.split('\n');
  192. const matches = [];
  193. lines.forEach((line, index) => {
  194. const lowerLine = line.toLowerCase();
  195. if (lowerLine.includes(query)) {
  196. // 获取上下文(前后各50个字符)
  197. const startIndex = Math.max(0, lowerLine.indexOf(query) - 50);
  198. const endIndex = Math.min(line.length, lowerLine.indexOf(query) + query.length + 50);
  199. let snippet = line.substring(startIndex, endIndex);
  200. // 如果不是从头开始,添加省略号
  201. if (startIndex > 0) snippet = '...' + snippet;
  202. if (endIndex < line.length) snippet = snippet + '...';
  203. matches.push({
  204. line: index + 1,
  205. snippet: snippet,
  206. fullLine: line
  207. });
  208. }
  209. });
  210. if (matches.length > 0) {
  211. const result = {
  212. docName,
  213. matchCount: matches.length,
  214. matches: matches.slice(0, 5) // 最多返回5个匹配
  215. };
  216. // 区分当前文档和其他文档
  217. if (docName === currentDoc) {
  218. currentDocResults.push(result);
  219. } else {
  220. otherDocsResults.push(result);
  221. }
  222. }
  223. }
  224. res.json({
  225. query: q,
  226. currentDoc: currentDocResults,
  227. otherDocs: otherDocsResults
  228. });
  229. } catch (error) {
  230. res.status(500).json({ error: '搜索失败', details: error.message });
  231. }
  232. });
  233. // 解析 index.md 的函数
  234. function parseIndexMd(content) {
  235. const lines = content.split('\n');
  236. const structure = [];
  237. let currentCategory = null;
  238. for (const line of lines) {
  239. const trimmedLine = line.trim();
  240. // 匹配分类 [category]
  241. const categoryMatch = trimmedLine.match(/^\[(.+?)\]$/);
  242. if (categoryMatch) {
  243. currentCategory = {
  244. name: categoryMatch[1],
  245. docs: []
  246. };
  247. structure.push(currentCategory);
  248. continue;
  249. }
  250. // 匹配文档项 1: testa.md 或 3.1: testc1.md
  251. const docMatch = trimmedLine.match(/^([\d.]+):\s*(.+?)\.md$/);
  252. if (docMatch && currentCategory) {
  253. const [, number, docName] = docMatch;
  254. const level = (number.match(/\./g) || []).length;
  255. currentCategory.docs.push({
  256. number,
  257. name: docName,
  258. level,
  259. fullName: `${docName}.md`
  260. });
  261. }
  262. }
  263. return structure;
  264. }
  265. // 启动服务器
  266. app.listen(PORT, () => {
  267. console.log(`服务器运行在 http://localhost:${PORT}`);
  268. });