web-font/backend/server/server.ts
崮生(子虚) e0f34c36b6 修复上传功能和服务器健壮性问题
- 修复 multipart 解析器:endDelimiter 提前检查导致循环退出
- 修复 Node.js v24 兼容:改为先读取 body 再构造 Request
- 修复 HTTP keep-alive 死锁:按 Content-Length 精确读取 body
- 修复服务器崩溃:connectionHandle 加 try-catch,单连接错误不影响全局
- 修复 URL 写死:webfont.shenzilong.com 改为 localhost
- 复制按钮加"已复制/复制失败"反馈提示
- TEMP_MAX_FILES 改为环境变量可配置
- dev 环境默认启用临时上传和管理员上传
- CSS 代码默认展示,使用技巧不再折叠

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 17:11:55 +08:00

233 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { cMiddleware, cRequest, cResponse, type cNext } from "./req_res";
// 配置
// 路由器类
export class cRouter {
private middleware: cMiddleware[] = [];
use(middleware: cMiddleware) {
this.middleware.push(middleware);
return this;
}
async handle(req: cRequest, res: cResponse) {
let index = -1;
const next = async (req: cRequest, res: cResponse) => {
index += 1;
// console.log(`开始执行第 ${index} ${this.middleware[index]?.name} 中间件`);
const r = (await this.middleware[index]?.(req, res, next)) ?? { req, res };
// console.log(`执行完毕第 ${index} 中间件`);
return r;
};
return next(req, res);
}
}
// 实现一个简化的 HTTP 服务器
export class SimpleHttpServer {
private router: cRouter = new cRouter();
constructor(options: { port: number; hostname?: string }) {
let release_name = global.tjs ? "tjs" : globalThis?.process?.release?.name;
console.log("[release.name]", release_name);
if (global.tjs) {
this.tjsServer(options);
return this;
}
if (release_name === "llrt" || release_name == "node") {
import("./tcp_server").then((m) => {
const server = m.createTcpServer((socket) => {
connectionHandle(socket, (req, res) => this.router.handle(req, res));
});
server.listen(options.port, options.hostname, () => {
console.log(`Server is listening on port ${options.port}`);
});
});
}
}
private async tjsServer(options: { port: number; hostname?: string }) {
const listener = (await global.tjs.listen(
"tcp",
options.hostname ?? "::",
options.port,
{},
)) as tjs.Listener;
console.log(`Server is listening on port ${options.port}`);
for await (const connection of listener) {
connectionHandle(connection, this.router.handle.bind(this.router));
}
}
use(...middlewares: cMiddleware[]) {
middlewares.forEach((middleware) => this.router.use(middleware));
return this;
}
}
const decoder = new TextDecoder("utf-8");
const encoder = new TextEncoder();
// 请求头终止符
const target = encoder.encode("\r\n\r\n");
async function connectionHandle(
connection: {
readable: ReadableStream<Uint8Array>;
writable: WritableStream<Uint8Array>;
close: () => void;
},
handle: cNext,
) {
try {
const { header, body } = await createStreamAfterTarget(connection.readable, target);
if (!header) {
return;
}
const httpHeaderText = decoder.decode(header);
const httpHeader = parseHttpRequest(httpHeaderText);
const hasBody = httpHeader.method !== "GET" && httpHeader.method !== "HEAD";
/** 根据 Content-Length 读取指定长度的 body避免在 keep-alive 连接上无限等待 */
let bodyArrayBuffer: ArrayBuffer | undefined;
if (hasBody && body) {
const contentLength = parseInt(httpHeader.headers["Content-Length"] ?? "0", 10);
if (contentLength > 0) {
const chunks: Uint8Array[] = [];
let received = 0;
for await (const chunk of body) {
chunks.push(chunk);
received += chunk.length;
if (received >= contentLength) break;
}
const merged = new Uint8Array(received);
let offset = 0;
for (const chunk of chunks) {
merged.set(chunk, offset);
offset += chunk.length;
}
bodyArrayBuffer = merged.buffer.slice(merged.byteOffset, merged.byteOffset + merged.byteLength);
} else if (httpHeader.headers["Transfer-Encoding"] === "chunked") {
/** chunked transfer encoding 暂不支持,跳过 body */
}
}
const rawReq = new Request("http://" + httpHeader.headers["Host"] + httpHeader.url, {
method: httpHeader.method,
headers: httpHeader.headers,
});
/** 将 body 数据挂到 request 对象上,供中间件直接读取 */
(rawReq as Request & { _bodyBuffer?: ArrayBuffer })._bodyBuffer = bodyArrayBuffer;
const rawRes = new Response();
const { req, res } = await handle(rawReq, rawRes);
const resWriter = connection.writable.getWriter();
let headerText: string[] = [];
res.headers.forEach((value, key) => {
headerText.push(`${key}: ${value}`);
});
const resHeaertText = `HTTP/1.1 ${res.status} OK\r\n${headerText.join("\r\n")}\r\n\r\n`;
await resWriter.write(encoder.encode(resHeaertText));
if (res.body) {
// node 运行时
// 释放写入器的锁定
resWriter.releaseLock();
// https://github.com/saghul/txiki.js/issues/646
await res.body?.pipeTo(connection.writable);
} else {
// @ts-expect-error
if (res._bodyInit) {
// tjs 运行时
// @ts-expect-error
await resWriter.write(res._bodyInit);
} else {
// llrt 运行时
const buffer = new Uint8Array(await (await res.blob()).arrayBuffer());
await resWriter.write(buffer);
}
}
if (!resWriter.closed) {
await resWriter.close();
}
connection.close();
} catch (err) {
console.log("[connectionHandle error]", err);
try { connection.close(); } catch { /* ignore */ }
}
}
function parseHttpRequest(requestText: string) {
const lines = requestText.trim().split("\n");
if (lines.length === 0) {
throw new Error("Invalid HTTP request");
}
// 解析请求行
const [method, url, httpVersion] = lines[0].split(" ");
// 解析头部
const headers: Record<string, string> = {};
for (let i = 1; i < lines.length; i++) {
const line = lines[i];
if (line === "") break; // 空行表示头部结束
const [key, ...valueParts] = line.split(":");
const value = valueParts.join(":").trim();
headers[key.trim()] = value;
}
return {
method,
url,
httpVersion,
headers,
};
}
async function createStreamAfterTarget(
originalStream: ReadableStream<Uint8Array>,
target: Uint8Array,
) {
const reader = originalStream.getReader();
let buffer = new Uint8Array();
// Function to check if target is found in the buffer
function containsTarget(buffer: Uint8Array, target: Uint8Array): number {
for (let i = 0; i <= buffer.length - target.length; i++) {
if (buffer.slice(i, i + target.length).every((value, index) => value === target[index])) {
return i;
}
}
return -1;
}
let controller = null as unknown as ReadableStreamDefaultController<Uint8Array>;
while (true) {
const { done, value } = await reader.read();
if (done) {
controller.close();
break; // Stream ended
}
if (controller) {
controller.enqueue(value);
continue;
}
// Append the new chunk to the buffer
const newBuffer = new Uint8Array(buffer.length + value.length);
newBuffer.set(buffer);
newBuffer.set(value, buffer.length);
buffer = newBuffer;
// Check if the target is found in the buffer
const targetIndex = containsTarget(buffer, target);
if (targetIndex !== -1) {
// Found the target data, return the remaining buffer after the target data
const start = targetIndex + target.length;
const header = buffer.slice(0, start);
const remainingData = buffer.slice(start);
const body = new ReadableStream<Uint8Array>({
start(c) {
controller = c;
controller.enqueue(remainingData);
},
});
// Create a new stream from the remaining data
return {
header,
body,
};
}
}
return { header: null, body: new ReadableStream<Uint8Array>() }; // Return an empty stream if the target is not found
}