Repository navigation
Expand file tree
/
Copy pathnode-http.ts
More file actions
76 lines (70 loc) · 2.79 KB
/
Copy pathnode-http.ts
File metadata and controls
76 lines (70 loc) · 2.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import http, { type IncomingMessage, type ServerResponse } from 'node:http';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import { TLSSocket } from 'node:tls';
import { createDaemonProxy, type DaemonProxy, type DaemonProxyOptions } from './daemon-proxy.ts';
export function createDaemonProxyServer(options: DaemonProxyOptions): http.Server {
return http.createServer(createDaemonProxyRequestListener(createDaemonProxy(options)));
}
/** Serves a proxy from any `node:http` or `node:https` server; upload tickets follow its scheme. */
export function createDaemonProxyRequestListener(proxy: DaemonProxy): http.RequestListener {
return (req, res) => {
void serveProxyRequest(proxy, req, res).catch((error: unknown) => {
if (!res.destroyed) res.destroy(error instanceof Error ? error : undefined);
});
};
}
async function serveProxyRequest(
proxy: DaemonProxy,
req: IncomingMessage,
res: ServerResponse,
): Promise<void> {
const request = toWebRequest(req, res);
if (!request) {
res.statusCode = 400;
res.end();
return;
}
const response = await proxy.handle(request);
res.statusCode = response.status;
for (const [name, value] of response.headers) res.setHeader(name, value);
if (!response.body) {
res.end();
return;
}
await pipeline(Readable.fromWeb(response.body as Parameters<typeof Readable.fromWeb>[0]), res);
}
/** `null` when the Host header and path do not form a URL. */
function toWebRequest(req: IncomingMessage, res: ServerResponse): Request | null {
const scheme = req.socket instanceof TLSSocket ? 'https' : 'http';
const url = URL.parse(req.url ?? '/', `${scheme}://${req.headers.host ?? '127.0.0.1'}`);
if (!url) return null;
const method = req.method ?? 'GET';
const hasBody = method !== 'GET' && method !== 'HEAD';
return new Request(url, {
method,
headers: toWebHeaders(req),
signal: clientGoneSignal(req, res),
...(hasBody
? { body: Readable.toWeb(req) as ReadableStream<Uint8Array>, duplex: 'half' as const }
: {}),
});
}
function toWebHeaders(req: IncomingMessage): Headers {
const headers = new Headers();
for (const [name, value] of Object.entries(req.headers)) {
if (value === undefined) continue;
for (const item of Array.isArray(value) ? value : [value]) headers.append(name, item);
}
return headers;
}
function clientGoneSignal(req: IncomingMessage, res: ServerResponse): AbortSignal {
const clientGone = new AbortController();
const abortIfResponseIncomplete = () => {
if (res.writableFinished || clientGone.signal.aborted) return;
clientGone.abort(new Error('Proxy client disconnected before the response ended'));
};
req.on('aborted', abortIfResponseIncomplete);
res.on('close', abortIfResponseIncomplete);
return clientGone.signal;
}