@floegence/flowersec-core 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/browser/controlplane.d.ts +1 -1
- package/dist/browser/controlplane.js +0 -1
- package/dist/browser/index.d.ts +3 -3
- package/dist/browser/index.js +2 -2
- package/dist/client-connect/transportSecurity.d.ts +1 -3
- package/dist/client-connect/transportSecurity.js +1 -5
- package/dist/controlplane/request.d.ts +1 -0
- package/dist/controlplane/request.js +77 -2
- package/dist/defaults.d.ts +1 -0
- package/dist/defaults.js +1 -0
- package/dist/endpoint/index.js +185 -33
- package/dist/facade.d.ts +1 -1
- package/dist/facade.js +1 -1
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- package/dist/proxy/appWindow.js +93 -7
- package/dist/proxy/bootstrap.d.ts +5 -24
- package/dist/proxy/bootstrap.js +1 -11
- package/dist/proxy/constants.d.ts +1 -0
- package/dist/proxy/constants.js +1 -0
- package/dist/proxy/controllerWindow.js +219 -78
- package/dist/proxy/integration.js +0 -6
- package/dist/proxy/portStream.d.ts +2 -1
- package/dist/proxy/portStream.js +173 -74
- package/dist/proxy/preset.d.ts +0 -1
- package/dist/proxy/preset.js +0 -9
- package/dist/proxy/runtimeScope.js +2 -7
- package/dist/proxy/server.d.ts +1 -0
- package/dist/proxy/server.js +466 -113
- package/dist/proxy/windowBridgeProtocol.d.ts +4 -3
- package/dist/proxy/windowBridgeProtocol.js +1 -1
- package/package.json +1 -1
- package/dist/proxy/profiles.d.ts +0 -18
- package/dist/proxy/profiles.js +0 -71
package/dist/proxy/server.js
CHANGED
|
@@ -3,41 +3,38 @@ import { DEFAULT_MAX_JSON_FRAME_BYTES, readJsonFrame, writeJsonFrame } from "../
|
|
|
3
3
|
import { RpcRouter, RpcServer } from "../rpc/server.js";
|
|
4
4
|
import { createByteReader } from "../streamio/index.js";
|
|
5
5
|
import { readU32be, u32be } from "../utils/bin.js";
|
|
6
|
-
import { DEFAULT_MAX_BODY_BYTES, DEFAULT_MAX_CHUNK_BYTES, DEFAULT_MAX_WS_FRAME_BYTES, PROXY_KIND_HTTP1, PROXY_KIND_WS, PROXY_PROTOCOL_VERSION, } from "./constants.js";
|
|
6
|
+
import { DEFAULT_MAX_BODY_BYTES, DEFAULT_MAX_CHUNK_BYTES, DEFAULT_MAX_CONCURRENT_STREAMS, DEFAULT_MAX_WS_FRAME_BYTES, PROXY_KIND_HTTP1, PROXY_KIND_WS, PROXY_PROTOCOL_VERSION, } from "./constants.js";
|
|
7
7
|
import { filterRequestHeaders, filterResponseHeaders, filterWsOpenHeaders, isSafeHeaderValue, isValidHeaderName, normalizeHeaderName } from "./headerPolicy.js";
|
|
8
8
|
export async function serveProxySession(session, options, signal) {
|
|
9
9
|
const compiled = compileOptions(options);
|
|
10
|
-
const
|
|
10
|
+
const activeProxy = new Set();
|
|
11
|
+
const activeRPC = new Set();
|
|
11
12
|
try {
|
|
12
13
|
while (!signal?.aborted) {
|
|
13
14
|
const accepted = await session.acceptStream(signal === undefined ? {} : { signal });
|
|
14
15
|
if (accepted.kind === "rpc") {
|
|
15
|
-
if (active.size >= compiled.maxConcurrentStreams) {
|
|
16
|
-
await accepted.stream.reset(new Error("proxy stream concurrency exhausted"));
|
|
17
|
-
continue;
|
|
18
|
-
}
|
|
19
16
|
const task = serveRPCStream(accepted.stream, options.rpcRouter ?? new RpcRouter(), options.rpcServerOptions, signal)
|
|
20
17
|
.catch(() => { })
|
|
21
|
-
.finally(() =>
|
|
22
|
-
|
|
18
|
+
.finally(() => activeRPC.delete(task));
|
|
19
|
+
activeRPC.add(task);
|
|
23
20
|
continue;
|
|
24
21
|
}
|
|
25
22
|
if (accepted.kind !== PROXY_KIND_HTTP1 && accepted.kind !== PROXY_KIND_WS) {
|
|
26
23
|
await accepted.stream.reset(new Error(`unsupported proxy stream kind ${accepted.kind}`));
|
|
27
24
|
continue;
|
|
28
25
|
}
|
|
29
|
-
if (
|
|
26
|
+
if (activeProxy.size >= compiled.maxConcurrentStreams) {
|
|
30
27
|
await accepted.stream.reset(new Error("proxy stream concurrency exhausted"));
|
|
31
28
|
continue;
|
|
32
29
|
}
|
|
33
30
|
const task = serveProxyStreamCompiled(accepted.kind, accepted.stream, compiled, signal)
|
|
34
31
|
.catch(() => { })
|
|
35
|
-
.finally(() =>
|
|
36
|
-
|
|
32
|
+
.finally(() => activeProxy.delete(task));
|
|
33
|
+
activeProxy.add(task);
|
|
37
34
|
}
|
|
38
35
|
}
|
|
39
36
|
finally {
|
|
40
|
-
await Promise.allSettled(
|
|
37
|
+
await Promise.allSettled([...activeProxy, ...activeRPC]);
|
|
41
38
|
}
|
|
42
39
|
}
|
|
43
40
|
async function serveRPCStream(stream, router, options, signal) {
|
|
@@ -50,7 +47,8 @@ async function serveRPCStream(stream, router, options, signal) {
|
|
|
50
47
|
await server.serve(signal);
|
|
51
48
|
}
|
|
52
49
|
export function serveProxyStream(kind, stream, options, signal) {
|
|
53
|
-
|
|
50
|
+
const compiled = compileOptions(options);
|
|
51
|
+
return serveProxyStreamCompiled(kind, stream, compiled, signal);
|
|
54
52
|
}
|
|
55
53
|
async function serveProxyStreamCompiled(kind, stream, options, signal) {
|
|
56
54
|
try {
|
|
@@ -67,78 +65,152 @@ async function serveProxyStreamCompiled(kind, stream, options, signal) {
|
|
|
67
65
|
}
|
|
68
66
|
}
|
|
69
67
|
async function serveHTTP(stream, options, signal) {
|
|
70
|
-
const reader = createByteReader(stream
|
|
68
|
+
const reader = createByteReader(stream);
|
|
71
69
|
let requestId = "unknown";
|
|
70
|
+
let responseStarted = false;
|
|
71
|
+
let streamReset = false;
|
|
72
|
+
let requestBody;
|
|
73
|
+
let responseBodyReader;
|
|
74
|
+
let controller;
|
|
75
|
+
let timer;
|
|
76
|
+
let removeAbortListener;
|
|
77
|
+
const resetStream = async (error) => {
|
|
78
|
+
if (streamReset)
|
|
79
|
+
return;
|
|
80
|
+
streamReset = true;
|
|
81
|
+
try {
|
|
82
|
+
await stream.reset(error);
|
|
83
|
+
}
|
|
84
|
+
catch { /* The peer may already have reset the stream. */ }
|
|
85
|
+
};
|
|
72
86
|
try {
|
|
73
87
|
const meta = assertHTTPRequestMeta(await readJsonFrame(reader, options.maxJsonFrameBytes));
|
|
74
88
|
requestId = meta.request_id;
|
|
75
89
|
const path = parsePath(meta.path);
|
|
76
|
-
const
|
|
90
|
+
const knownRequestLength = contentLength(meta.headers, "request");
|
|
91
|
+
if (knownRequestLength != null && knownRequestLength > options.maxBodyBytes) {
|
|
92
|
+
throw new ProxyServerError("request_body_too_large", "request body too large");
|
|
93
|
+
}
|
|
94
|
+
if (signal?.aborted)
|
|
95
|
+
throw new ProxyServerError("canceled", "proxy request canceled");
|
|
77
96
|
const headers = requestHeaders(meta.headers, options);
|
|
78
97
|
applyExternalOrigin(headers, meta.external_origin);
|
|
79
98
|
const target = new URL(options.upstream);
|
|
80
99
|
target.pathname = path.pathname;
|
|
81
100
|
target.search = path.search;
|
|
82
101
|
target.hash = "";
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
102
|
+
controller = new AbortController();
|
|
103
|
+
const abortUpstream = (error) => {
|
|
104
|
+
if (!controller.signal.aborted)
|
|
105
|
+
controller.abort(error);
|
|
106
|
+
requestBody?.abort(error);
|
|
107
|
+
if (responseStarted)
|
|
108
|
+
void resetStream(error);
|
|
109
|
+
};
|
|
110
|
+
const onAbort = () => abortUpstream(new ProxyServerError("canceled", "proxy request canceled"));
|
|
86
111
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
112
|
+
removeAbortListener = () => signal?.removeEventListener("abort", onAbort);
|
|
113
|
+
if (signal?.aborted)
|
|
114
|
+
onAbort();
|
|
115
|
+
const timeoutMs = resolveTimeout(meta.timeout_ms, options);
|
|
116
|
+
timer = timeoutMs > 0
|
|
117
|
+
? setTimeout(() => abortUpstream(new ProxyServerError("timeout", "upstream request timeout")), timeoutMs)
|
|
118
|
+
: undefined;
|
|
119
|
+
requestBody = createStreamingRequestBody(reader, options.maxChunkBytes, options.maxBodyBytes, abortUpstream);
|
|
120
|
+
const method = meta.method.trim().toUpperCase();
|
|
121
|
+
const requestInit = {
|
|
122
|
+
method,
|
|
123
|
+
headers,
|
|
124
|
+
redirect: "manual",
|
|
125
|
+
signal: controller.signal,
|
|
126
|
+
};
|
|
127
|
+
if (method === "GET" || method === "HEAD") {
|
|
128
|
+
requestBody.drain();
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
requestInit.body = requestBody.stream;
|
|
132
|
+
requestInit.duplex = "half";
|
|
133
|
+
}
|
|
134
|
+
if (controller.signal.aborted)
|
|
135
|
+
throw controller.signal.reason;
|
|
136
|
+
const response = await options.fetch(target, requestInit);
|
|
137
|
+
if (requestBody.error != null)
|
|
138
|
+
throw requestBody.error;
|
|
139
|
+
requestBody.drain();
|
|
140
|
+
const knownResponseLength = contentLength(response.headers, "response");
|
|
141
|
+
if (knownResponseLength != null && knownResponseLength > options.maxBodyBytes) {
|
|
142
|
+
await response.body?.cancel();
|
|
143
|
+
throw new ProxyServerError("response_body_too_large", "response body too large");
|
|
144
|
+
}
|
|
145
|
+
const responseHeaders = collectResponseHeaders(response.headers, options);
|
|
146
|
+
const responseMeta = {
|
|
147
|
+
v: PROXY_PROTOCOL_VERSION,
|
|
148
|
+
request_id: requestId,
|
|
149
|
+
ok: true,
|
|
150
|
+
status: response.status,
|
|
151
|
+
headers: responseHeaders,
|
|
152
|
+
};
|
|
153
|
+
await writeJsonFrame(stream, responseMeta);
|
|
154
|
+
responseStarted = true;
|
|
155
|
+
if (response.body == null) {
|
|
126
156
|
await stream.write(u32be(0));
|
|
157
|
+
const requestError = await requestBody.completion;
|
|
158
|
+
if (requestError != null)
|
|
159
|
+
throw requestError;
|
|
160
|
+
return;
|
|
127
161
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
162
|
+
responseBodyReader = response.body.getReader();
|
|
163
|
+
let total = 0;
|
|
164
|
+
while (true) {
|
|
165
|
+
const next = await Promise.race([
|
|
166
|
+
responseBodyReader.read(),
|
|
167
|
+
requestBody.failure.then((error) => { throw error; }),
|
|
168
|
+
]);
|
|
169
|
+
if (next.done)
|
|
170
|
+
break;
|
|
171
|
+
const bytes = next.value;
|
|
172
|
+
const nextTotal = total + bytes.length;
|
|
173
|
+
if (!Number.isSafeInteger(nextTotal) || nextTotal > options.maxBodyBytes) {
|
|
174
|
+
throw new ProxyServerError("response_body_too_large", "response body too large");
|
|
175
|
+
}
|
|
176
|
+
total = nextTotal;
|
|
177
|
+
for (let offset = 0; offset < bytes.length; offset += options.maxChunkBytes) {
|
|
178
|
+
const chunk = bytes.subarray(offset, Math.min(bytes.length, offset + options.maxChunkBytes));
|
|
179
|
+
await stream.write(u32be(chunk.length));
|
|
180
|
+
await stream.write(chunk);
|
|
181
|
+
}
|
|
132
182
|
}
|
|
183
|
+
await stream.write(u32be(0));
|
|
184
|
+
const requestError = await requestBody.completion;
|
|
185
|
+
if (requestError != null)
|
|
186
|
+
throw requestError;
|
|
133
187
|
}
|
|
134
188
|
catch (error) {
|
|
135
|
-
|
|
189
|
+
const effective = requestBody?.error ?? controller?.signal.reason ?? asError(error);
|
|
190
|
+
if (controller != null && !controller.signal.aborted)
|
|
191
|
+
controller.abort(effective);
|
|
192
|
+
if (responseStarted)
|
|
193
|
+
await resetStream(effective);
|
|
194
|
+
else
|
|
195
|
+
await writeHTTPError(stream, requestId, classifyHTTPError(effective));
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
if (timer != null)
|
|
199
|
+
clearTimeout(timer);
|
|
200
|
+
removeAbortListener?.();
|
|
201
|
+
requestBody?.stop();
|
|
202
|
+
try {
|
|
203
|
+
await responseBodyReader?.cancel();
|
|
204
|
+
}
|
|
205
|
+
catch { /* The upstream body may already be closed. */ }
|
|
136
206
|
}
|
|
137
207
|
}
|
|
138
208
|
async function serveWebSocket(stream, options, signal) {
|
|
139
209
|
const reader = createByteReader(stream, signal === undefined ? {} : { signal });
|
|
140
210
|
let connId = "unknown";
|
|
141
211
|
let raw;
|
|
212
|
+
let upstreamOpened = false;
|
|
213
|
+
let removePostOpenAbortListener;
|
|
142
214
|
try {
|
|
143
215
|
const meta = assertWSOpenMeta(await readJsonFrame(reader, options.maxJsonFrameBytes));
|
|
144
216
|
connId = meta.conn_id;
|
|
@@ -164,55 +236,137 @@ async function serveWebSocket(stream, options, signal) {
|
|
|
164
236
|
headers,
|
|
165
237
|
maxPayload: options.maxWsFrameBytes,
|
|
166
238
|
perMessageDeflate: false,
|
|
167
|
-
handshakeTimeout:
|
|
239
|
+
handshakeTimeout: resolveTimeout(undefined, options),
|
|
168
240
|
});
|
|
169
|
-
await waitForUpstreamOpen(raw, signal);
|
|
170
|
-
const response = {
|
|
171
|
-
v: PROXY_PROTOCOL_VERSION,
|
|
172
|
-
conn_id: connId,
|
|
173
|
-
ok: true,
|
|
174
|
-
protocol: String(raw.protocol ?? ""),
|
|
175
|
-
};
|
|
176
|
-
await writeJsonFrame(stream, response);
|
|
177
241
|
let writeChain = Promise.resolve();
|
|
242
|
+
let queuedWriteBytes = 0;
|
|
243
|
+
let terminalSettled = false;
|
|
244
|
+
let terminalError;
|
|
178
245
|
let terminalResolve;
|
|
179
246
|
const terminal = new Promise((resolve) => { terminalResolve = resolve; });
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
247
|
+
const settleTerminal = (error) => {
|
|
248
|
+
if (terminalSettled)
|
|
249
|
+
return;
|
|
250
|
+
terminalSettled = true;
|
|
251
|
+
terminalError = error;
|
|
252
|
+
terminalResolve(error);
|
|
253
|
+
};
|
|
254
|
+
const queueFrame = (op, payload, allowCloseFrame = false) => {
|
|
255
|
+
if (terminalSettled)
|
|
256
|
+
return Promise.reject(new Error("proxy WebSocket is closed"));
|
|
257
|
+
const frameBytes = payload.byteLength + 5;
|
|
258
|
+
const nextQueuedWriteBytes = queuedWriteBytes + frameBytes;
|
|
259
|
+
if (!Number.isSafeInteger(nextQueuedWriteBytes)
|
|
260
|
+
|| (!allowCloseFrame && nextQueuedWriteBytes > options.maxWsQueuedBytes)) {
|
|
261
|
+
const error = new ProxyServerError("resource_exhausted", "proxy WebSocket write queue exhausted");
|
|
262
|
+
settleTerminal(error);
|
|
263
|
+
try {
|
|
264
|
+
raw.close(1011, "proxy buffer limit exceeded");
|
|
265
|
+
}
|
|
266
|
+
catch { /* Best effort. */ }
|
|
267
|
+
return Promise.reject(error);
|
|
268
|
+
}
|
|
269
|
+
queuedWriteBytes = nextQueuedWriteBytes;
|
|
270
|
+
const write = writeChain
|
|
271
|
+
.then(() => {
|
|
272
|
+
if (terminalSettled)
|
|
273
|
+
throw terminalError ?? new Error("proxy WebSocket is closed");
|
|
274
|
+
return writeWSFrame(stream, op, payload, options.maxWsFrameBytes);
|
|
275
|
+
})
|
|
276
|
+
.finally(() => {
|
|
277
|
+
queuedWriteBytes = Math.max(0, queuedWriteBytes - frameBytes);
|
|
278
|
+
});
|
|
279
|
+
writeChain = write;
|
|
280
|
+
void write.catch((error) => settleTerminal(asError(error)));
|
|
281
|
+
return write;
|
|
282
|
+
};
|
|
283
|
+
const queueEventFrame = (op, payload) => {
|
|
284
|
+
void queueFrame(op, payload).catch(() => { });
|
|
184
285
|
};
|
|
185
|
-
raw.on("message", (data, isBinary) => writeFrame(isBinary ? 2 : 1, toBytes(data)));
|
|
186
|
-
raw.on("ping", (data) => writeFrame(9, toBytes(data)));
|
|
187
|
-
raw.on("pong", (data) => writeFrame(10, toBytes(data)));
|
|
188
286
|
raw.on("close", (code, reason) => {
|
|
287
|
+
if (terminalSettled)
|
|
288
|
+
return;
|
|
289
|
+
if (!upstreamOpened) {
|
|
290
|
+
settleTerminal(new Error("upstream WebSocket closed before open"));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
189
293
|
const reasonBytes = toBytes(reason);
|
|
190
294
|
const payload = new Uint8Array(2 + reasonBytes.length);
|
|
191
295
|
payload[0] = (code >>> 8) & 0xff;
|
|
192
296
|
payload[1] = code & 0xff;
|
|
193
297
|
payload.set(reasonBytes, 2);
|
|
194
|
-
|
|
195
|
-
terminalResolve();
|
|
298
|
+
void queueFrame(8, payload, true).then(() => settleTerminal(), (error) => settleTerminal(asError(error)));
|
|
196
299
|
});
|
|
197
|
-
raw.on("error", (error) =>
|
|
300
|
+
raw.on("error", (error) => settleTerminal(error));
|
|
301
|
+
await waitForUpstreamOpen(raw, signal);
|
|
302
|
+
upstreamOpened = true;
|
|
303
|
+
const response = {
|
|
304
|
+
v: PROXY_PROTOCOL_VERSION,
|
|
305
|
+
conn_id: connId,
|
|
306
|
+
ok: true,
|
|
307
|
+
protocol: String(raw.protocol ?? ""),
|
|
308
|
+
};
|
|
309
|
+
const openResponseWrite = writeJsonFrame(stream, response);
|
|
310
|
+
writeChain = openResponseWrite;
|
|
311
|
+
void openResponseWrite.catch((error) => settleTerminal(asError(error)));
|
|
312
|
+
raw.on("message", (data, isBinary) => queueEventFrame(isBinary ? 2 : 1, toBytes(data)));
|
|
313
|
+
raw.on("ping", (data) => queueEventFrame(9, toBytes(data)));
|
|
314
|
+
raw.on("pong", (data) => queueEventFrame(10, toBytes(data)));
|
|
315
|
+
const onPostOpenAbort = () => {
|
|
316
|
+
const error = asError(signal?.reason ?? new Error("proxy WebSocket aborted"));
|
|
317
|
+
settleTerminal(error);
|
|
318
|
+
try {
|
|
319
|
+
if (typeof raw.terminate === "function")
|
|
320
|
+
raw.terminate();
|
|
321
|
+
else
|
|
322
|
+
raw.close();
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
// The terminal error is already authoritative.
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
if (signal != null) {
|
|
329
|
+
signal.addEventListener("abort", onPostOpenAbort, { once: true });
|
|
330
|
+
removePostOpenAbortListener = () => signal.removeEventListener("abort", onPostOpenAbort);
|
|
331
|
+
if (signal.aborted)
|
|
332
|
+
onPostOpenAbort();
|
|
333
|
+
}
|
|
334
|
+
const openResponseResult = await Promise.race([
|
|
335
|
+
openResponseWrite.then(() => undefined),
|
|
336
|
+
terminal,
|
|
337
|
+
]);
|
|
338
|
+
if (openResponseResult != null)
|
|
339
|
+
throw openResponseResult;
|
|
340
|
+
try {
|
|
341
|
+
if (!terminalSettled && typeof raw.resume === "function")
|
|
342
|
+
raw.resume();
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
settleTerminal(asError(error));
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
198
348
|
const inbound = (async () => {
|
|
199
349
|
while (true) {
|
|
200
350
|
const frame = await readWSFrame(reader, options.maxWsFrameBytes);
|
|
201
351
|
switch (frame.op) {
|
|
202
352
|
case 1:
|
|
203
|
-
raw
|
|
353
|
+
await sendUpstreamFrame(raw, 1, frame.payload);
|
|
204
354
|
break;
|
|
205
355
|
case 2:
|
|
206
|
-
raw
|
|
356
|
+
await sendUpstreamFrame(raw, 2, frame.payload);
|
|
207
357
|
break;
|
|
208
|
-
case 8:
|
|
358
|
+
case 8: {
|
|
209
359
|
raw.close(frame.payload.length >= 2 ? (frame.payload[0] << 8) | frame.payload[1] : 1000, new TextDecoder().decode(frame.payload.subarray(2)));
|
|
360
|
+
const closeError = await terminal;
|
|
361
|
+
if (closeError != null)
|
|
362
|
+
throw closeError;
|
|
210
363
|
return;
|
|
364
|
+
}
|
|
211
365
|
case 9:
|
|
212
|
-
raw
|
|
366
|
+
await sendUpstreamFrame(raw, 9, frame.payload);
|
|
213
367
|
break;
|
|
214
368
|
case 10:
|
|
215
|
-
raw
|
|
369
|
+
await sendUpstreamFrame(raw, 10, frame.payload);
|
|
216
370
|
break;
|
|
217
371
|
default: throw new Error("invalid websocket frame operation");
|
|
218
372
|
}
|
|
@@ -224,10 +378,11 @@ async function serveWebSocket(stream, options, signal) {
|
|
|
224
378
|
await writeChain;
|
|
225
379
|
}
|
|
226
380
|
catch (error) {
|
|
227
|
-
if (
|
|
228
|
-
await writeWSOpenError(stream, connId, classifyWSError(error)
|
|
381
|
+
if (!upstreamOpened)
|
|
382
|
+
await writeWSOpenError(stream, connId, classifyWSError(error));
|
|
229
383
|
}
|
|
230
384
|
finally {
|
|
385
|
+
removePostOpenAbortListener?.();
|
|
231
386
|
try {
|
|
232
387
|
raw?.close();
|
|
233
388
|
}
|
|
@@ -247,6 +402,12 @@ function compileOptions(input) {
|
|
|
247
402
|
const maxChunkBytes = positive(input.maxChunkBytes, DEFAULT_MAX_CHUNK_BYTES, "maxChunkBytes");
|
|
248
403
|
const maxBodyBytes = positive(input.maxBodyBytes, DEFAULT_MAX_BODY_BYTES, "maxBodyBytes");
|
|
249
404
|
const maxWsFrameBytes = positive(input.maxWsFrameBytes, DEFAULT_MAX_WS_FRAME_BYTES, "maxWsFrameBytes");
|
|
405
|
+
const defaultMaxWsQueuedBytes = maxWsFrameBytes <= Number.MAX_SAFE_INTEGER - 5
|
|
406
|
+
? maxWsFrameBytes + 5
|
|
407
|
+
: maxWsFrameBytes;
|
|
408
|
+
const maxWsQueuedBytes = positive(input.maxWsQueuedBytes, defaultMaxWsQueuedBytes, "maxWsQueuedBytes");
|
|
409
|
+
if (maxWsQueuedBytes < 5)
|
|
410
|
+
throw new Error("maxWsQueuedBytes must be at least 5");
|
|
250
411
|
const defaultTimeoutMs = nonNegative(input.defaultTimeoutMs, 30_000, "defaultTimeoutMs");
|
|
251
412
|
const maxTimeoutMs = nonNegative(input.maxTimeoutMs, 300_000, "maxTimeoutMs");
|
|
252
413
|
if (maxTimeoutMs > 0 && defaultTimeoutMs > maxTimeoutMs)
|
|
@@ -258,9 +419,10 @@ function compileOptions(input) {
|
|
|
258
419
|
maxChunkBytes,
|
|
259
420
|
maxBodyBytes,
|
|
260
421
|
maxWsFrameBytes,
|
|
422
|
+
maxWsQueuedBytes,
|
|
261
423
|
defaultTimeoutMs,
|
|
262
424
|
maxTimeoutMs,
|
|
263
|
-
maxConcurrentStreams: positive(input.maxConcurrentStreams,
|
|
425
|
+
maxConcurrentStreams: positive(input.maxConcurrentStreams, DEFAULT_MAX_CONCURRENT_STREAMS, "maxConcurrentStreams"),
|
|
264
426
|
extraRequestHeaders: input.extraRequestHeaders ?? [],
|
|
265
427
|
extraResponseHeaders: input.extraResponseHeaders ?? [],
|
|
266
428
|
blockedResponseHeaders: normalizeNames(input.blockedResponseHeaders),
|
|
@@ -349,30 +511,137 @@ function applyExternalOrigin(headers, input) {
|
|
|
349
511
|
headers.set("x-forwarded-proto", new URL(external).protocol.slice(0, -1));
|
|
350
512
|
headers.set("host", new URL(external).host);
|
|
351
513
|
}
|
|
352
|
-
|
|
353
|
-
const chunks = [];
|
|
514
|
+
function createStreamingRequestBody(reader, maxChunkBytes, maxBodyBytes, onFailure) {
|
|
354
515
|
let total = 0;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
516
|
+
let finished = false;
|
|
517
|
+
let draining = false;
|
|
518
|
+
let readChain = Promise.resolve();
|
|
519
|
+
let error;
|
|
520
|
+
let streamController;
|
|
521
|
+
let resolveFailure;
|
|
522
|
+
const failure = new Promise((resolve) => { resolveFailure = resolve; });
|
|
523
|
+
let resolveCompletion;
|
|
524
|
+
const completion = new Promise((resolve) => { resolveCompletion = resolve; });
|
|
525
|
+
const fail = (cause) => {
|
|
526
|
+
if (error != null)
|
|
527
|
+
return error;
|
|
528
|
+
const raw = asError(cause);
|
|
529
|
+
error = raw instanceof ProxyServerError
|
|
530
|
+
? raw
|
|
531
|
+
: new ProxyServerError("request_body_invalid", raw.message);
|
|
532
|
+
finished = true;
|
|
533
|
+
try {
|
|
534
|
+
streamController?.error(error);
|
|
535
|
+
}
|
|
536
|
+
catch { /* The body consumer may already be gone. */ }
|
|
537
|
+
resolveFailure(error);
|
|
538
|
+
resolveCompletion(error);
|
|
539
|
+
onFailure(error);
|
|
540
|
+
return error;
|
|
541
|
+
};
|
|
542
|
+
const readChunkUnserialized = async () => {
|
|
543
|
+
try {
|
|
544
|
+
const length = readU32be(await reader.readExactly(4), 0);
|
|
545
|
+
if (length === 0) {
|
|
546
|
+
finished = true;
|
|
547
|
+
resolveCompletion(undefined);
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
if (length > maxChunkBytes) {
|
|
551
|
+
throw new ProxyServerError("request_body_too_large", "request chunk too large");
|
|
552
|
+
}
|
|
553
|
+
const nextTotal = total + length;
|
|
554
|
+
if (!Number.isSafeInteger(nextTotal) || nextTotal > maxBodyBytes) {
|
|
555
|
+
throw new ProxyServerError("request_body_too_large", "request body too large");
|
|
556
|
+
}
|
|
557
|
+
const chunk = await reader.readExactly(length);
|
|
558
|
+
total = nextTotal;
|
|
559
|
+
return chunk;
|
|
560
|
+
}
|
|
561
|
+
catch (cause) {
|
|
562
|
+
throw fail(cause);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
const readChunk = () => {
|
|
566
|
+
const next = readChain.then(readChunkUnserialized);
|
|
567
|
+
readChain = next.then(() => { }, () => { });
|
|
568
|
+
return next;
|
|
569
|
+
};
|
|
570
|
+
const drain = () => {
|
|
571
|
+
if (draining || finished)
|
|
572
|
+
return;
|
|
573
|
+
draining = true;
|
|
574
|
+
try {
|
|
575
|
+
streamController?.close();
|
|
576
|
+
}
|
|
577
|
+
catch { /* A pending pull will observe draining below. */ }
|
|
578
|
+
void (async () => {
|
|
579
|
+
while (!finished)
|
|
580
|
+
await readChunk();
|
|
581
|
+
})().catch(() => { });
|
|
582
|
+
};
|
|
583
|
+
const stream = new ReadableStream({
|
|
584
|
+
start(controller) { streamController = controller; },
|
|
585
|
+
async pull(controller) {
|
|
586
|
+
if (finished) {
|
|
587
|
+
if (error == null)
|
|
588
|
+
controller.close();
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const chunk = await readChunk();
|
|
592
|
+
if (draining)
|
|
593
|
+
return;
|
|
594
|
+
if (chunk == null)
|
|
595
|
+
controller.close();
|
|
596
|
+
else
|
|
597
|
+
controller.enqueue(chunk);
|
|
598
|
+
},
|
|
599
|
+
cancel() { drain(); },
|
|
600
|
+
}, { highWaterMark: 0 });
|
|
601
|
+
return {
|
|
602
|
+
stream,
|
|
603
|
+
failure,
|
|
604
|
+
completion,
|
|
605
|
+
get error() { return error; },
|
|
606
|
+
drain,
|
|
607
|
+
abort: (cause) => { if (!finished)
|
|
608
|
+
fail(cause); },
|
|
609
|
+
stop: () => {
|
|
610
|
+
if (finished)
|
|
611
|
+
return;
|
|
612
|
+
finished = true;
|
|
613
|
+
try {
|
|
614
|
+
streamController?.close();
|
|
615
|
+
}
|
|
616
|
+
catch { /* The fetch implementation may own the stream. */ }
|
|
617
|
+
resolveCompletion(undefined);
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
function contentLength(headers, side) {
|
|
622
|
+
const rawValues = headers instanceof Headers
|
|
623
|
+
? (headers.get("content-length") == null ? [] : [headers.get("content-length")])
|
|
624
|
+
: headers
|
|
625
|
+
.filter((header) => normalizeHeaderName(header.name) === "content-length")
|
|
626
|
+
.map((header) => header.value.trim());
|
|
627
|
+
if (rawValues.length === 0)
|
|
628
|
+
return undefined;
|
|
629
|
+
if (rawValues.length !== 1 || !/^(0|[1-9][0-9]*)$/.test(rawValues[0])) {
|
|
630
|
+
throw new ProxyServerError(side === "request" ? "invalid_request_meta" : "upstream_request_failed", `invalid ${side} content-length`);
|
|
365
631
|
}
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
body.set(chunk, offset);
|
|
370
|
-
offset += chunk.length;
|
|
632
|
+
const value = Number(rawValues[0]);
|
|
633
|
+
if (!Number.isSafeInteger(value)) {
|
|
634
|
+
throw new ProxyServerError(side === "request" ? "invalid_request_meta" : "upstream_request_failed", `invalid ${side} content-length`);
|
|
371
635
|
}
|
|
372
|
-
return
|
|
636
|
+
return value;
|
|
373
637
|
}
|
|
374
|
-
async function writeHTTPError(stream, requestId, code
|
|
375
|
-
const meta = {
|
|
638
|
+
async function writeHTTPError(stream, requestId, code) {
|
|
639
|
+
const meta = {
|
|
640
|
+
v: PROXY_PROTOCOL_VERSION,
|
|
641
|
+
request_id: requestId.trim() || "unknown",
|
|
642
|
+
ok: false,
|
|
643
|
+
error: { code, message: publicHTTPErrorMessage(code) },
|
|
644
|
+
};
|
|
376
645
|
try {
|
|
377
646
|
await writeJsonFrame(stream, meta);
|
|
378
647
|
await stream.write(u32be(0));
|
|
@@ -396,8 +665,13 @@ async function writeWSFrame(stream, op, payload, maxBytes) {
|
|
|
396
665
|
if (payload.length > 0)
|
|
397
666
|
await stream.write(payload);
|
|
398
667
|
}
|
|
399
|
-
async function writeWSOpenError(stream, connId, code
|
|
400
|
-
const response = {
|
|
668
|
+
async function writeWSOpenError(stream, connId, code) {
|
|
669
|
+
const response = {
|
|
670
|
+
v: PROXY_PROTOCOL_VERSION,
|
|
671
|
+
conn_id: connId.trim() || "unknown",
|
|
672
|
+
ok: false,
|
|
673
|
+
error: { code, message: publicWSErrorMessage(code) },
|
|
674
|
+
};
|
|
401
675
|
try {
|
|
402
676
|
await writeJsonFrame(stream, response);
|
|
403
677
|
}
|
|
@@ -405,13 +679,65 @@ async function writeWSOpenError(stream, connId, code, error) {
|
|
|
405
679
|
}
|
|
406
680
|
function waitForUpstreamOpen(websocket, signal) {
|
|
407
681
|
return new Promise((resolve, reject) => {
|
|
408
|
-
const cleanup = () => {
|
|
409
|
-
|
|
682
|
+
const cleanup = () => {
|
|
683
|
+
websocket.off("open", onOpen);
|
|
684
|
+
websocket.off("error", onError);
|
|
685
|
+
websocket.off("close", onClose);
|
|
686
|
+
signal?.removeEventListener("abort", onAbort);
|
|
687
|
+
};
|
|
688
|
+
const onOpen = () => {
|
|
689
|
+
try {
|
|
690
|
+
if (typeof websocket.pause === "function")
|
|
691
|
+
websocket.pause();
|
|
692
|
+
}
|
|
693
|
+
catch (error) {
|
|
694
|
+
cleanup();
|
|
695
|
+
reject(asError(error));
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
cleanup();
|
|
699
|
+
resolve();
|
|
700
|
+
};
|
|
410
701
|
const onError = (error) => { cleanup(); reject(error); };
|
|
702
|
+
const onClose = () => { cleanup(); reject(new Error("upstream WebSocket closed before open")); };
|
|
411
703
|
const onAbort = () => { cleanup(); reject(signal?.reason ?? new Error("aborted")); };
|
|
412
704
|
websocket.once("open", onOpen);
|
|
413
705
|
websocket.once("error", onError);
|
|
706
|
+
websocket.once("close", onClose);
|
|
414
707
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
708
|
+
if (signal?.aborted)
|
|
709
|
+
onAbort();
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
function sendUpstreamFrame(websocket, op, payload) {
|
|
713
|
+
return new Promise((resolve, reject) => {
|
|
714
|
+
const finish = (error) => {
|
|
715
|
+
if (error == null)
|
|
716
|
+
resolve();
|
|
717
|
+
else
|
|
718
|
+
reject(asError(error));
|
|
719
|
+
};
|
|
720
|
+
try {
|
|
721
|
+
switch (op) {
|
|
722
|
+
case 1:
|
|
723
|
+
websocket.send(payload, { binary: false }, finish);
|
|
724
|
+
return;
|
|
725
|
+
case 2:
|
|
726
|
+
websocket.send(payload, { binary: true }, finish);
|
|
727
|
+
return;
|
|
728
|
+
case 9:
|
|
729
|
+
websocket.ping(payload, undefined, finish);
|
|
730
|
+
return;
|
|
731
|
+
case 10:
|
|
732
|
+
websocket.pong(payload, undefined, finish);
|
|
733
|
+
return;
|
|
734
|
+
default:
|
|
735
|
+
reject(new Error("invalid upstream WebSocket operation"));
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
catch (error) {
|
|
739
|
+
reject(asError(error));
|
|
740
|
+
}
|
|
415
741
|
});
|
|
416
742
|
}
|
|
417
743
|
function resolveTimeout(input, options) {
|
|
@@ -458,6 +784,33 @@ function classifyWSError(error) {
|
|
|
458
784
|
return error.code;
|
|
459
785
|
return "upstream_ws_dial_failed";
|
|
460
786
|
}
|
|
787
|
+
function publicHTTPErrorMessage(code) {
|
|
788
|
+
switch (code) {
|
|
789
|
+
case "invalid_request_meta":
|
|
790
|
+
return "invalid proxy request";
|
|
791
|
+
case "request_body_too_large":
|
|
792
|
+
case "response_body_too_large":
|
|
793
|
+
return "proxy body limit exceeded";
|
|
794
|
+
case "resource_exhausted":
|
|
795
|
+
return "proxy resource limit exceeded";
|
|
796
|
+
case "timeout":
|
|
797
|
+
return "upstream request timed out";
|
|
798
|
+
case "canceled":
|
|
799
|
+
return "proxy request canceled";
|
|
800
|
+
default:
|
|
801
|
+
return "upstream request failed";
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
function publicWSErrorMessage(code) {
|
|
805
|
+
switch (code) {
|
|
806
|
+
case "invalid_ws_open_meta":
|
|
807
|
+
return "invalid proxy WebSocket request";
|
|
808
|
+
case "resource_exhausted":
|
|
809
|
+
return "proxy WebSocket resource limit exceeded";
|
|
810
|
+
default:
|
|
811
|
+
return "upstream WebSocket connection failed";
|
|
812
|
+
}
|
|
813
|
+
}
|
|
461
814
|
function toBytes(input) {
|
|
462
815
|
if (input instanceof Uint8Array)
|
|
463
816
|
return input;
|