http.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import { IncomingMessage, ServerResponse } from 'node:http';
  2. type JsonValue = Record<string, unknown> | Array<unknown>;
  3. export function sendJson(response: ServerResponse, statusCode: number, payload: JsonValue): void {
  4. const body = JSON.stringify(payload);
  5. response.writeHead(statusCode, {
  6. 'Access-Control-Allow-Origin': '*',
  7. 'Access-Control-Allow-Headers': 'Content-Type',
  8. 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
  9. 'Content-Type': 'application/json; charset=utf-8',
  10. 'Content-Length': Buffer.byteLength(body),
  11. });
  12. response.end(body);
  13. }
  14. export function readBody(request: IncomingMessage): Promise<Record<string, unknown>> {
  15. return new Promise((resolve) => {
  16. let body = '';
  17. request.on('data', (chunk) => {
  18. body += chunk;
  19. });
  20. request.on('end', () => {
  21. if (!body) {
  22. resolve({});
  23. return;
  24. }
  25. try {
  26. resolve(JSON.parse(body));
  27. } catch {
  28. resolve({});
  29. }
  30. });
  31. });
  32. }
  33. /**
  34. * 从 multipart/form-data 请求中读取上传文件
  35. * 简易实现,支持单文件上传
  36. */
  37. export function readMultipartFile(request: IncomingMessage): Promise<{ fileName: string; buffer: Buffer }> {
  38. return new Promise((resolve, reject) => {
  39. const contentType = request.headers['content-type'] || '';
  40. const chunks: Buffer[] = [];
  41. request.on('data', (chunk: Buffer) => {
  42. chunks.push(chunk);
  43. });
  44. request.on('end', () => {
  45. const rawBuffer = Buffer.concat(chunks);
  46. if (contentType.includes('multipart/form-data')) {
  47. try {
  48. const result = parseMultipart(rawBuffer, contentType);
  49. resolve(result);
  50. } catch (error) {
  51. reject(error);
  52. }
  53. } else {
  54. // Treat as raw file upload with filename from header
  55. const disposition = request.headers['content-disposition'] || '';
  56. const fileNameMatch = disposition.match(/filename="?([^";\n]+)"?/);
  57. const fileName = fileNameMatch ? fileNameMatch[1] : 'upload.bin';
  58. resolve({ fileName, buffer: rawBuffer });
  59. }
  60. });
  61. request.on('error', reject);
  62. });
  63. }
  64. function parseMultipart(buffer: Buffer, contentType: string): { fileName: string; buffer: Buffer } {
  65. // 提取 boundary(可能带引号)
  66. const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/);
  67. if (!boundaryMatch) {
  68. throw new Error('Missing multipart boundary');
  69. }
  70. const boundary = `--${boundaryMatch[1]}`;
  71. const boundaryBytes = Buffer.from(boundary, 'ascii');
  72. // 在 buffer 中按 boundary 定位各 part
  73. let pos = bufferIndexOf(buffer, boundaryBytes, 0);
  74. if (pos === -1) {
  75. throw new Error('No file found in multipart data');
  76. }
  77. // 跳过第一个 boundary + CRLF
  78. pos += boundaryBytes.length + 2;
  79. while (pos < buffer.length) {
  80. // 找到 header/body 分隔的 \r\n\r\n
  81. const headerEndMarker = Buffer.from('\r\n\r\n', 'ascii');
  82. const headerEnd = bufferIndexOf(buffer, headerEndMarker, pos);
  83. if (headerEnd === -1) break;
  84. const headersStr = buffer.subarray(pos, headerEnd).toString('utf-8');
  85. const bodyStart = headerEnd + 4;
  86. // 找下一个 boundary
  87. const nextBoundary = bufferIndexOf(buffer, boundaryBytes, bodyStart);
  88. // body 结束于下一个 boundary 前的 \r\n
  89. const bodyEnd = nextBoundary === -1 ? buffer.length : nextBoundary - 2;
  90. // 检查是否有 filename
  91. const fileNameMatch = headersStr.match(/filename="?([^";\r\n]+)"?/);
  92. if (fileNameMatch) {
  93. const fileName = decodeFileName(fileNameMatch[1].trim());
  94. const fileBuffer = buffer.subarray(bodyStart, bodyEnd);
  95. return { fileName, buffer: fileBuffer };
  96. }
  97. // 跳到下一个 part
  98. if (nextBoundary === -1) break;
  99. pos = nextBoundary + boundaryBytes.length;
  100. // 跳过 CRLF 或 -- (结束标记)
  101. if (buffer[pos] === 0x2d && buffer[pos + 1] === 0x2d) break; // '--' = end
  102. pos += 2; // skip \r\n
  103. }
  104. throw new Error('No file found in multipart data');
  105. }
  106. function bufferIndexOf(buf: Buffer, search: Buffer, fromIndex: number): number {
  107. for (let i = fromIndex; i <= buf.length - search.length; i++) {
  108. let found = true;
  109. for (let j = 0; j < search.length; j++) {
  110. if (buf[i + j] !== search[j]) {
  111. found = false;
  112. break;
  113. }
  114. }
  115. if (found) return i;
  116. }
  117. return -1;
  118. }
  119. function decodeFileName(name: string): string {
  120. try {
  121. return decodeURIComponent(name);
  122. } catch {
  123. return name;
  124. }
  125. }
  126. /**
  127. * 从 URL 中提取路径参数
  128. * 例如: extractPathParam('/api/tasks/abc123', '/api/tasks/') => 'abc123'
  129. */
  130. export function extractPathParam(pathname: string, prefix: string): string {
  131. if (!pathname.startsWith(prefix)) return '';
  132. const rest = pathname.substring(prefix.length);
  133. const slashIndex = rest.indexOf('/');
  134. return slashIndex === -1 ? rest : rest.substring(0, slashIndex);
  135. }
  136. /**
  137. * 提取路径尾部
  138. * 例如: extractPathTail('/api/tasks/abc123/export', '/api/tasks/') => 'abc123/export'
  139. */
  140. export function extractPathTail(pathname: string, prefix: string): string {
  141. if (!pathname.startsWith(prefix)) return '';
  142. return pathname.substring(prefix.length);
  143. }