@floegence/flowersec-core 0.25.0 → 0.27.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 +2 -2
- package/dist/browser/connect.d.ts +2 -1
- package/dist/browser/index.d.ts +3 -3
- package/dist/browser/index.js +2 -2
- package/dist/client-connect/connectCore.d.ts +1 -1
- package/dist/client-connect/transportSecurity.d.ts +1 -3
- package/dist/client-connect/transportSecurity.js +0 -4
- package/dist/connect/artifact.d.ts +0 -1
- package/dist/connect/artifact.js +0 -4
- package/dist/connect/{internalNormalize.d.ts → resolveArtifact.d.ts} +8 -8
- package/dist/connect/resolveArtifact.js +87 -0
- 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/e2ee/record.js +8 -4
- package/dist/e2ee/secureChannel.d.ts +1 -0
- package/dist/e2ee/secureChannel.js +18 -4
- package/dist/facade.d.ts +1 -4
- package/dist/facade.js +5 -11
- package/dist/node/connect.d.ts +0 -2
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- 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/integration.js +0 -6
- 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 +0 -1
- package/dist/proxy/server.js +257 -145
- package/dist/tunnel-client/connect.d.ts +1 -1
- package/dist/tunnel-client/connect.js +1 -11
- package/package.json +4 -6
- package/dist/browser/controlplane.d.ts +0 -15
- package/dist/browser/controlplane.js +0 -65
- package/dist/connect/internalNormalize.js +0 -173
- package/dist/proxy/profiles.d.ts +0 -18
- package/dist/proxy/profiles.js +0 -71
package/dist/proxy/bootstrap.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { connectBrowser
|
|
1
|
+
import { connectBrowser } from "../browser/connect.js";
|
|
2
2
|
import { registerProxyIntegration, } from "./integration.js";
|
|
3
3
|
import { registerProxyControllerWindow } from "./controllerWindow.js";
|
|
4
4
|
import { extractProxyRuntimeScopeV1, resolvePresetInputFromScope, resolveRuntimeLimitsFromScope, resolveRuntimePresetLimits, } from "./runtimeScope.js";
|
|
@@ -23,12 +23,10 @@ function scopeRuntimeToIntegrationOptions(scope, opts) {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
async function connectProxyBrowserClient(client, opts) {
|
|
26
|
-
const compat = opts;
|
|
27
26
|
const integrationInput = {
|
|
28
27
|
client,
|
|
29
28
|
serviceWorker: opts.serviceWorker,
|
|
30
29
|
...(opts.preset === undefined ? {} : { preset: opts.preset }),
|
|
31
|
-
...(compat.profile === undefined ? {} : { profile: compat.profile }),
|
|
32
30
|
...(opts.runtimeGlobalKey === undefined ? {} : { runtimeGlobalKey: opts.runtimeGlobalKey }),
|
|
33
31
|
...(opts.runtime === undefined ? {} : { runtime: opts.runtime }),
|
|
34
32
|
...(opts.plugins === undefined ? {} : { plugins: opts.plugins }),
|
|
@@ -101,20 +99,12 @@ function connectProxyControllerClient(client, opts) {
|
|
|
101
99
|
},
|
|
102
100
|
};
|
|
103
101
|
}
|
|
104
|
-
export async function connectTunnelProxyBrowser(grant, opts) {
|
|
105
|
-
const client = await connectTunnelBrowser(grant, opts.connect ?? {});
|
|
106
|
-
return await connectProxyBrowserClient(client, opts);
|
|
107
|
-
}
|
|
108
102
|
export async function connectArtifactProxyBrowser(artifact, opts = {}) {
|
|
109
103
|
const scope = extractProxyRuntimeScopeV1(artifact, "service_worker");
|
|
110
104
|
const client = await connectBrowser(artifact, opts.connect ?? {});
|
|
111
105
|
const nextOpts = scopeRuntimeToIntegrationOptions(scope, opts);
|
|
112
106
|
return await connectProxyBrowserClient(client, nextOpts);
|
|
113
107
|
}
|
|
114
|
-
export async function connectTunnelProxyControllerBrowser(grant, opts) {
|
|
115
|
-
const client = await connectTunnelBrowser(grant, opts.connect ?? {});
|
|
116
|
-
return connectProxyControllerClient(client, opts);
|
|
117
|
-
}
|
|
118
108
|
export async function connectArtifactProxyControllerBrowser(artifact, opts = {}) {
|
|
119
109
|
const scope = extractProxyRuntimeScopeV1(artifact, "controller_bridge");
|
|
120
110
|
const client = await connectBrowser(artifact, opts.connect ?? {});
|
|
@@ -4,3 +4,4 @@ export declare const PROXY_KIND_WS: "flowersec-proxy/ws";
|
|
|
4
4
|
export declare const DEFAULT_MAX_CHUNK_BYTES: number;
|
|
5
5
|
export declare const DEFAULT_MAX_BODY_BYTES: number;
|
|
6
6
|
export declare const DEFAULT_MAX_WS_FRAME_BYTES: number;
|
|
7
|
+
export declare const DEFAULT_MAX_CONCURRENT_STREAMS: 64;
|
package/dist/proxy/constants.js
CHANGED
|
@@ -5,3 +5,4 @@ export const PROXY_KIND_WS = "flowersec-proxy/ws";
|
|
|
5
5
|
export const DEFAULT_MAX_CHUNK_BYTES = SDK_DEFAULTS.proxy.maxChunkBytes;
|
|
6
6
|
export const DEFAULT_MAX_BODY_BYTES = SDK_DEFAULTS.proxy.maxBodyBytes;
|
|
7
7
|
export const DEFAULT_MAX_WS_FRAME_BYTES = SDK_DEFAULTS.proxy.maxWsFrameBytes;
|
|
8
|
+
export const DEFAULT_MAX_CONCURRENT_STREAMS = SDK_DEFAULTS.proxy.maxConcurrentStreams;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { profileToPresetManifest } from "./profiles.js";
|
|
2
1
|
import { resolveProxyPreset } from "./preset.js";
|
|
3
2
|
import { registerServiceWorkerAndEnsureControl } from "./registerServiceWorker.js";
|
|
4
3
|
import { createProxyRuntime, ensureServiceWorkerRuntimeRegistered } from "./runtime.js";
|
|
@@ -165,13 +164,8 @@ function shouldIgnoreMismatch(plugins, ctx) {
|
|
|
165
164
|
return false;
|
|
166
165
|
}
|
|
167
166
|
function resolveIntegrationPreset(opts) {
|
|
168
|
-
if (opts.preset !== undefined && opts.profile !== undefined) {
|
|
169
|
-
throw new Error("preset and deprecated profile cannot be used together");
|
|
170
|
-
}
|
|
171
167
|
if (opts.preset !== undefined)
|
|
172
168
|
return resolveProxyPreset(opts.preset);
|
|
173
|
-
if (opts.profile !== undefined)
|
|
174
|
-
return resolveProxyPreset(profileToPresetManifest(opts.profile));
|
|
175
169
|
return resolveProxyPreset();
|
|
176
170
|
}
|
|
177
171
|
function buildRuntimeOptions(preset, runtime) {
|
package/dist/proxy/preset.d.ts
CHANGED
|
@@ -26,5 +26,4 @@ export type ResolvedProxyPreset = Readonly<{
|
|
|
26
26
|
export declare const DEFAULT_PROXY_PRESET_MANIFEST: ProxyPresetManifest;
|
|
27
27
|
export type ProxyPresetInput = ProxyPresetManifest | Partial<ProxyPresetLimits>;
|
|
28
28
|
export declare function assertProxyPresetManifest(value: unknown): ProxyPresetManifest;
|
|
29
|
-
export declare function resolveNamedProxyPreset(name: string): ProxyPresetManifest;
|
|
30
29
|
export declare function resolveProxyPreset(input?: ProxyPresetInput): ResolvedProxyPreset;
|
package/dist/proxy/preset.js
CHANGED
|
@@ -75,15 +75,6 @@ export function assertProxyPresetManifest(value) {
|
|
|
75
75
|
limits: assertLimits(value.limits),
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
|
-
export function resolveNamedProxyPreset(name) {
|
|
79
|
-
switch (String(name ?? "").trim()) {
|
|
80
|
-
case "":
|
|
81
|
-
case "default":
|
|
82
|
-
return DEFAULT_PROXY_PRESET_MANIFEST;
|
|
83
|
-
default:
|
|
84
|
-
throw new Error(`unknown proxy preset: ${name}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
78
|
function toResolved(manifest) {
|
|
88
79
|
return Object.freeze({
|
|
89
80
|
v: 1,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertProxyPresetManifest,
|
|
1
|
+
import { assertProxyPresetManifest, } from "./preset.js";
|
|
2
2
|
const RUNTIME_SCOPE_NAME = "proxy.runtime";
|
|
3
3
|
const PRESET_ID_RE = /^[a-z][a-z0-9._-]{0,63}$/;
|
|
4
4
|
const MAX_RUNTIME_PAYLOAD_BYTES = 8 * 1024;
|
|
@@ -203,12 +203,7 @@ export function resolvePresetInputFromScope(scope, presetOverride) {
|
|
|
203
203
|
return presetOverride;
|
|
204
204
|
if (scope.preset.snapshot)
|
|
205
205
|
return scope.preset.snapshot;
|
|
206
|
-
|
|
207
|
-
return resolveNamedProxyPreset(scope.preset.presetId);
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
return undefined;
|
|
211
|
-
}
|
|
206
|
+
throw new Error("proxy.runtime preset snapshot is required");
|
|
212
207
|
}
|
|
213
208
|
export function resolveRuntimePresetLimits(scope) {
|
|
214
209
|
if (scope.limits === undefined)
|
package/dist/proxy/server.d.ts
CHANGED
package/dist/proxy/server.js
CHANGED
|
@@ -3,42 +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
|
|
11
|
-
const
|
|
10
|
+
const activeProxy = new Set();
|
|
11
|
+
const activeRPC = new Set();
|
|
12
12
|
try {
|
|
13
13
|
while (!signal?.aborted) {
|
|
14
14
|
const accepted = await session.acceptStream(signal === undefined ? {} : { signal });
|
|
15
15
|
if (accepted.kind === "rpc") {
|
|
16
|
-
if (active.size >= compiled.maxConcurrentStreams) {
|
|
17
|
-
await accepted.stream.reset(new Error("proxy stream concurrency exhausted"));
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
16
|
const task = serveRPCStream(accepted.stream, options.rpcRouter ?? new RpcRouter(), options.rpcServerOptions, signal)
|
|
21
17
|
.catch(() => { })
|
|
22
|
-
.finally(() =>
|
|
23
|
-
|
|
18
|
+
.finally(() => activeRPC.delete(task));
|
|
19
|
+
activeRPC.add(task);
|
|
24
20
|
continue;
|
|
25
21
|
}
|
|
26
22
|
if (accepted.kind !== PROXY_KIND_HTTP1 && accepted.kind !== PROXY_KIND_WS) {
|
|
27
23
|
await accepted.stream.reset(new Error(`unsupported proxy stream kind ${accepted.kind}`));
|
|
28
24
|
continue;
|
|
29
25
|
}
|
|
30
|
-
if (
|
|
26
|
+
if (activeProxy.size >= compiled.maxConcurrentStreams) {
|
|
31
27
|
await accepted.stream.reset(new Error("proxy stream concurrency exhausted"));
|
|
32
28
|
continue;
|
|
33
29
|
}
|
|
34
|
-
const task = serveProxyStreamCompiled(accepted.kind, accepted.stream, compiled,
|
|
30
|
+
const task = serveProxyStreamCompiled(accepted.kind, accepted.stream, compiled, signal)
|
|
35
31
|
.catch(() => { })
|
|
36
|
-
.finally(() =>
|
|
37
|
-
|
|
32
|
+
.finally(() => activeProxy.delete(task));
|
|
33
|
+
activeProxy.add(task);
|
|
38
34
|
}
|
|
39
35
|
}
|
|
40
36
|
finally {
|
|
41
|
-
await Promise.allSettled(
|
|
37
|
+
await Promise.allSettled([...activeProxy, ...activeRPC]);
|
|
42
38
|
}
|
|
43
39
|
}
|
|
44
40
|
async function serveRPCStream(stream, router, options, signal) {
|
|
@@ -52,12 +48,12 @@ async function serveRPCStream(stream, router, options, signal) {
|
|
|
52
48
|
}
|
|
53
49
|
export function serveProxyStream(kind, stream, options, signal) {
|
|
54
50
|
const compiled = compileOptions(options);
|
|
55
|
-
return serveProxyStreamCompiled(kind, stream, compiled,
|
|
51
|
+
return serveProxyStreamCompiled(kind, stream, compiled, signal);
|
|
56
52
|
}
|
|
57
|
-
async function serveProxyStreamCompiled(kind, stream, options,
|
|
53
|
+
async function serveProxyStreamCompiled(kind, stream, options, signal) {
|
|
58
54
|
try {
|
|
59
55
|
if (kind === PROXY_KIND_HTTP1)
|
|
60
|
-
await serveHTTP(stream, options,
|
|
56
|
+
await serveHTTP(stream, options, signal);
|
|
61
57
|
else
|
|
62
58
|
await serveWebSocket(stream, options, signal);
|
|
63
59
|
}
|
|
@@ -68,94 +64,145 @@ async function serveProxyStreamCompiled(kind, stream, options, requestBodyBudget
|
|
|
68
64
|
catch { /* The peer may already have reset the stream. */ }
|
|
69
65
|
}
|
|
70
66
|
}
|
|
71
|
-
async function serveHTTP(stream, options,
|
|
72
|
-
const reader = createByteReader(stream
|
|
67
|
+
async function serveHTTP(stream, options, signal) {
|
|
68
|
+
const reader = createByteReader(stream);
|
|
73
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
|
+
};
|
|
74
86
|
try {
|
|
75
87
|
const meta = assertHTTPRequestMeta(await readJsonFrame(reader, options.maxJsonFrameBytes));
|
|
76
88
|
requestId = meta.request_id;
|
|
77
89
|
const path = parsePath(meta.path);
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (signal
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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");
|
|
96
|
+
const headers = requestHeaders(meta.headers, options);
|
|
97
|
+
applyExternalOrigin(headers, meta.external_origin);
|
|
98
|
+
const target = new URL(options.upstream);
|
|
99
|
+
target.pathname = path.pathname;
|
|
100
|
+
target.search = path.search;
|
|
101
|
+
target.hash = "";
|
|
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"));
|
|
111
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
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) {
|
|
156
|
+
await stream.write(u32be(0));
|
|
157
|
+
const requestError = await requestBody.completion;
|
|
158
|
+
if (requestError != null)
|
|
159
|
+
throw requestError;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
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");
|
|
146
175
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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);
|
|
151
181
|
}
|
|
152
182
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
183
|
+
await stream.write(u32be(0));
|
|
184
|
+
const requestError = await requestBody.completion;
|
|
185
|
+
if (requestError != null)
|
|
186
|
+
throw requestError;
|
|
156
187
|
}
|
|
157
188
|
catch (error) {
|
|
158
|
-
|
|
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. */ }
|
|
159
206
|
}
|
|
160
207
|
}
|
|
161
208
|
async function serveWebSocket(stream, options, signal) {
|
|
@@ -354,7 +401,6 @@ function compileOptions(input) {
|
|
|
354
401
|
const maxJsonFrameBytes = positive(input.maxJsonFrameBytes, DEFAULT_MAX_JSON_FRAME_BYTES, "maxJsonFrameBytes");
|
|
355
402
|
const maxChunkBytes = positive(input.maxChunkBytes, DEFAULT_MAX_CHUNK_BYTES, "maxChunkBytes");
|
|
356
403
|
const maxBodyBytes = positive(input.maxBodyBytes, DEFAULT_MAX_BODY_BYTES, "maxBodyBytes");
|
|
357
|
-
const maxBufferedRequestBodyBytes = positive(input.maxBufferedRequestBodyBytes, maxBodyBytes, "maxBufferedRequestBodyBytes");
|
|
358
404
|
const maxWsFrameBytes = positive(input.maxWsFrameBytes, DEFAULT_MAX_WS_FRAME_BYTES, "maxWsFrameBytes");
|
|
359
405
|
const defaultMaxWsQueuedBytes = maxWsFrameBytes <= Number.MAX_SAFE_INTEGER - 5
|
|
360
406
|
? maxWsFrameBytes + 5
|
|
@@ -372,12 +418,11 @@ function compileOptions(input) {
|
|
|
372
418
|
maxJsonFrameBytes,
|
|
373
419
|
maxChunkBytes,
|
|
374
420
|
maxBodyBytes,
|
|
375
|
-
maxBufferedRequestBodyBytes,
|
|
376
421
|
maxWsFrameBytes,
|
|
377
422
|
maxWsQueuedBytes,
|
|
378
423
|
defaultTimeoutMs,
|
|
379
424
|
maxTimeoutMs,
|
|
380
|
-
maxConcurrentStreams: positive(input.maxConcurrentStreams,
|
|
425
|
+
maxConcurrentStreams: positive(input.maxConcurrentStreams, DEFAULT_MAX_CONCURRENT_STREAMS, "maxConcurrentStreams"),
|
|
381
426
|
extraRequestHeaders: input.extraRequestHeaders ?? [],
|
|
382
427
|
extraResponseHeaders: input.extraResponseHeaders ?? [],
|
|
383
428
|
blockedResponseHeaders: normalizeNames(input.blockedResponseHeaders),
|
|
@@ -466,62 +511,129 @@ function applyExternalOrigin(headers, input) {
|
|
|
466
511
|
headers.set("x-forwarded-proto", new URL(external).protocol.slice(0, -1));
|
|
467
512
|
headers.set("host", new URL(external).host);
|
|
468
513
|
}
|
|
469
|
-
function
|
|
470
|
-
let usedBytes = 0;
|
|
471
|
-
return {
|
|
472
|
-
reserve: (bytes) => {
|
|
473
|
-
const next = usedBytes + bytes;
|
|
474
|
-
if (!Number.isSafeInteger(next) || next > maxBytes) {
|
|
475
|
-
throw new ProxyServerError("resource_exhausted", "proxy request body buffer exhausted");
|
|
476
|
-
}
|
|
477
|
-
usedBytes = next;
|
|
478
|
-
},
|
|
479
|
-
release: (bytes) => {
|
|
480
|
-
usedBytes = Math.max(0, usedBytes - bytes);
|
|
481
|
-
},
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
async function readBody(reader, maxChunkBytes, maxBodyBytes, budget) {
|
|
485
|
-
const chunks = [];
|
|
514
|
+
function createStreamingRequestBody(reader, maxChunkBytes, maxBodyBytes, onFailure) {
|
|
486
515
|
let total = 0;
|
|
487
|
-
let
|
|
488
|
-
|
|
489
|
-
|
|
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 {
|
|
490
544
|
const length = readU32be(await reader.readExactly(4), 0);
|
|
491
|
-
if (length === 0)
|
|
492
|
-
|
|
493
|
-
|
|
545
|
+
if (length === 0) {
|
|
546
|
+
finished = true;
|
|
547
|
+
resolveCompletion(undefined);
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
if (length > maxChunkBytes) {
|
|
494
551
|
throw new ProxyServerError("request_body_too_large", "request chunk too large");
|
|
495
|
-
|
|
496
|
-
|
|
552
|
+
}
|
|
553
|
+
const nextTotal = total + length;
|
|
554
|
+
if (!Number.isSafeInteger(nextTotal) || nextTotal > maxBodyBytes) {
|
|
497
555
|
throw new ProxyServerError("request_body_too_large", "request body too large");
|
|
498
556
|
}
|
|
499
|
-
|
|
500
|
-
total =
|
|
501
|
-
|
|
557
|
+
const chunk = await reader.readExactly(length);
|
|
558
|
+
total = nextTotal;
|
|
559
|
+
return chunk;
|
|
502
560
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
for (const chunk of chunks) {
|
|
506
|
-
body.set(chunk, offset);
|
|
507
|
-
offset += chunk.length;
|
|
561
|
+
catch (cause) {
|
|
562
|
+
throw fail(cause);
|
|
508
563
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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`);
|
|
520
631
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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`);
|
|
524
635
|
}
|
|
636
|
+
return value;
|
|
525
637
|
}
|
|
526
638
|
async function writeHTTPError(stream, requestId, code) {
|
|
527
639
|
const meta = {
|
|
@@ -6,4 +6,4 @@ export type TunnelConnectOptions = ConnectOptionsBase & Readonly<{
|
|
|
6
6
|
/** Optional caller-provided endpoint instance ID (base64url). */
|
|
7
7
|
endpointInstanceId?: string;
|
|
8
8
|
}>;
|
|
9
|
-
export declare function connectTunnel(
|
|
9
|
+
export declare function connectTunnel(input: unknown, opts: TunnelConnectOptions): Promise<ClientInternal>;
|
|
@@ -11,18 +11,8 @@ function isRecord(v) {
|
|
|
11
11
|
function hasOwn(o, key) {
|
|
12
12
|
return Object.prototype.hasOwnProperty.call(o, key);
|
|
13
13
|
}
|
|
14
|
-
function unwrapGrant(v) {
|
|
15
|
-
if (!isRecord(v))
|
|
16
|
-
return v;
|
|
17
|
-
if (hasOwn(v, "grant_client"))
|
|
18
|
-
return v["grant_client"];
|
|
19
|
-
if (hasOwn(v, "grant_server"))
|
|
20
|
-
return v["grant_server"];
|
|
21
|
-
return v;
|
|
22
|
-
}
|
|
23
14
|
// connectTunnel attaches to a tunnel and returns an RPC-ready session.
|
|
24
|
-
export async function connectTunnel(
|
|
25
|
-
const input = unwrapGrant(grant);
|
|
15
|
+
export async function connectTunnel(input, opts) {
|
|
26
16
|
if (input == null) {
|
|
27
17
|
throw new FlowersecError({ stage: "validate", code: "missing_grant", path: "tunnel", message: "missing grant" });
|
|
28
18
|
}
|