mirror of
https://github.com/2234839/web-font.git
synced 2026-05-05 18:38:12 +08:00
- 修复 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>
233 lines
7.5 KiB
TypeScript
233 lines
7.5 KiB
TypeScript
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
|
||
}
|