@floegence/flowersec-core 2.5.3 → 3.0.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 +46 -18
- package/dist/browser/connectSessionV3.d.ts +13 -0
- package/dist/browser/connectSessionV3.js +75 -0
- package/dist/browser/index.d.ts +3 -2
- package/dist/browser/index.js +2 -1
- package/dist/browser/v2.d.ts +3 -0
- package/dist/browser/v2.js +2 -0
- package/dist/facade.d.ts +15 -8
- package/dist/facade.js +5 -4
- package/dist/framing/jsonframe.d.ts +1 -1
- package/dist/framing/jsonframe.js +3 -1
- package/dist/interop/proxyServerPeer.js +1 -1
- package/dist/interop/serverParityPeer.js +8 -1
- package/dist/node/acceptor.d.ts +10 -0
- package/dist/node/acceptor.js +53 -7
- package/dist/node/acceptorV3.d.ts +57 -0
- package/dist/node/acceptorV3.js +227 -0
- package/dist/node/connectSessionV3.d.ts +21 -0
- package/dist/node/connectSessionV3.js +73 -0
- package/dist/node/index.d.ts +15 -10
- package/dist/node/index.js +6 -5
- package/dist/node/nativeTransportAddon.d.ts +39 -1
- package/dist/node/nativeTransportAddon.js +29 -5
- package/dist/node/rawQuicAdapterV3.d.ts +10 -0
- package/dist/node/rawQuicAdapterV3.js +76 -0
- package/dist/node/rawQuicServerV3.d.ts +24 -0
- package/dist/node/rawQuicServerV3.js +34 -0
- package/dist/node/runtimeAuthorizationV3.d.ts +29 -0
- package/dist/node/runtimeAuthorizationV3.js +126 -0
- package/dist/node/tunnelRuntimeV3.d.ts +57 -0
- package/dist/node/tunnelRuntimeV3.js +857 -0
- package/dist/node/v2.d.ts +11 -0
- package/dist/node/v2.js +6 -0
- package/dist/node/webSocketServerV3.d.ts +27 -0
- package/dist/node/webSocketServerV3.js +219 -0
- package/dist/proxy/runtime.js +39 -6
- package/dist/proxy/serviceWorker.d.ts +2 -0
- package/dist/proxy/serviceWorker.js +63 -7
- package/dist/proxy/windowBridge.d.ts +1 -0
- package/dist/proxy/windowBridge.js +93 -33
- package/dist/proxy/wsPatch.d.ts +1 -0
- package/dist/proxy/wsPatch.js +39 -6
- package/dist/public/streamHandlers.d.ts +3 -0
- package/dist/public/streamHandlers.js +6 -1
- package/dist/rpc/client.js +9 -7
- package/dist/rpc/server.js +44 -20
- package/dist/rpc/validate.d.ts +1 -0
- package/dist/rpc/validate.js +18 -6
- package/dist/v2/index.d.ts +14 -0
- package/dist/v2/index.js +7 -0
- package/dist/v2/protocol.js +23 -32
- package/dist/v2/publicSession.js +4 -0
- package/dist/v2/session.d.ts +5 -0
- package/dist/v2/session.js +94 -35
- package/dist/v3/admissionError.d.ts +4 -0
- package/dist/v3/admissionError.js +8 -0
- package/dist/v3/artifact.d.ts +145 -0
- package/dist/v3/artifact.js +1162 -0
- package/dist/v3/artifactLease.d.ts +17 -0
- package/dist/v3/artifactLease.js +109 -0
- package/dist/v3/browserRuntime.d.ts +37 -0
- package/dist/v3/browserRuntime.js +197 -0
- package/dist/v3/browserSessionRuntime.d.ts +2 -0
- package/dist/v3/browserSessionRuntime.js +21 -0
- package/dist/v3/capability.d.ts +32 -0
- package/dist/v3/capability.js +345 -0
- package/dist/v3/carrier.d.ts +84 -0
- package/dist/v3/carrier.js +411 -0
- package/dist/v3/connectionController.d.ts +87 -0
- package/dist/v3/connectionController.js +805 -0
- package/dist/v3/contract.d.ts +65 -0
- package/dist/v3/contract.js +3 -0
- package/dist/v3/controller.d.ts +43 -0
- package/dist/v3/controller.js +303 -0
- package/dist/v3/handshake.d.ts +81 -0
- package/dist/v3/handshake.js +471 -0
- package/dist/v3/jcs.d.ts +6 -0
- package/dist/v3/jcs.js +69 -0
- package/dist/v3/jsonPreflight.d.ts +1 -0
- package/dist/v3/jsonPreflight.js +251 -0
- package/dist/v3/nodeRuntime.d.ts +40 -0
- package/dist/v3/nodeRuntime.js +315 -0
- package/dist/v3/nodeSessionRuntime.d.ts +2 -0
- package/dist/v3/nodeSessionRuntime.js +13 -0
- package/dist/v3/protocol.d.ts +99 -0
- package/dist/v3/protocol.js +773 -0
- package/dist/v3/publicApi.d.ts +12 -0
- package/dist/v3/publicApi.js +46 -0
- package/dist/v3/publicSession.d.ts +1 -0
- package/dist/v3/publicSession.js +281 -0
- package/dist/v3/retryDisposition.d.ts +16 -0
- package/dist/v3/retryDisposition.js +30 -0
- package/dist/v3/runtimeAdapters.d.ts +9 -0
- package/dist/v3/runtimeAdapters.js +109 -0
- package/dist/v3/security.d.ts +34 -0
- package/dist/v3/security.js +100 -0
- package/dist/v3/serverAdmission.d.ts +38 -0
- package/dist/v3/serverAdmission.js +223 -0
- package/dist/v3/session.d.ts +318 -0
- package/dist/v3/session.js +2289 -0
- package/dist/v3/sessionConnector.d.ts +37 -0
- package/dist/v3/sessionConnector.js +440 -0
- package/dist/v3/streamLifetimeLedger.d.ts +31 -0
- package/dist/v3/streamLifetimeLedger.js +131 -0
- package/dist/v3/streamMetadata.d.ts +1 -0
- package/dist/v3/streamMetadata.js +7 -0
- package/dist/v3/transportConstants.d.ts +56 -0
- package/dist/v3/transportConstants.js +59 -0
- package/dist/v3/unreliableMessage.d.ts +44 -0
- package/dist/v3/unreliableMessage.js +284 -0
- package/dist/v3/webSocketCarrier.d.ts +31 -0
- package/dist/v3/webSocketCarrier.js +370 -0
- package/dist/v3/webTransportCarrier.d.ts +27 -0
- package/dist/v3/webTransportCarrier.js +534 -0
- package/dist/yamux/stream.d.ts +3 -0
- package/dist/yamux/stream.js +17 -4
- package/package.json +4 -4
- package/sbom/cyclonedx.json +6 -6
- package/sbom/spdx.json +11 -11
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "../v2/index.js";
|
|
2
|
+
export { connect, createConnectionController } from "./connectSession.js";
|
|
3
|
+
export type { ConnectionControllerOptions, SessionOptions, SessionTLSOptions, } from "./connectSession.js";
|
|
4
|
+
export { AcceptedSession, Acceptor, HandlerRegistrationError, RPCHandlers, SessionHandlers, createAcceptor, } from "./acceptor.js";
|
|
5
|
+
export type { AcceptorListener, AcceptorOptions, AuthorizationDecision, RPCHandler, RPCHandlerResult, NotificationHandler, SessionHandlerOptions, } from "./acceptor.js";
|
|
6
|
+
export { TunnelRuntime, createTunnelRuntime } from "./tunnelRuntime.js";
|
|
7
|
+
export type { TunnelAuthorizationDecision, TunnelRuntimeListener, TunnelRuntimeOptions, } from "./tunnelRuntime.js";
|
|
8
|
+
export { AuthorizationRecord, AuthorizationResponse, TunnelAuthorizationResponse, ControlPlaneError, EndpointSet, IssuedArtifact, Issuer, RuntimeAuthorizationRequest, authorizeRuntime, authorizeTunnelRuntime, createEndpointSet, parseAuthorizationRecord, parseRuntimeAuthorizationRequest, rejectRuntime, rejectTunnelRuntime, } from "./controlplane.js";
|
|
9
|
+
export type { ArtifactMetadata, ControlPlaneErrorCode, DirectIssueOptions, IssuedTunnelPair, Scope, SessionOptions as ControlPlaneSessionOptions, TunnelIssueOptions, } from "./controlplane.js";
|
|
10
|
+
export { ProxyServer, ProxyServerError } from "./proxyServer.js";
|
|
11
|
+
export type { ProxyServerOptions, StreamHandlerRegistrar } from "./proxyServer.js";
|
package/dist/node/v2.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from "../v2/index.js";
|
|
2
|
+
export { connect, createConnectionController } from "./connectSession.js";
|
|
3
|
+
export { AcceptedSession, Acceptor, HandlerRegistrationError, RPCHandlers, SessionHandlers, createAcceptor, } from "./acceptor.js";
|
|
4
|
+
export { TunnelRuntime, createTunnelRuntime } from "./tunnelRuntime.js";
|
|
5
|
+
export { AuthorizationRecord, AuthorizationResponse, TunnelAuthorizationResponse, ControlPlaneError, EndpointSet, IssuedArtifact, Issuer, RuntimeAuthorizationRequest, authorizeRuntime, authorizeTunnelRuntime, createEndpointSet, parseAuthorizationRecord, parseRuntimeAuthorizationRequest, rejectRuntime, rejectTunnelRuntime, } from "./controlplane.js";
|
|
6
|
+
export { ProxyServer, ProxyServerError } from "./proxyServer.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CarrierSessionV3 } from "../v3/carrier.js";
|
|
2
|
+
import type { PathKind } from "../v3/contract.js";
|
|
3
|
+
export type NodeWebSocketListenerOptionsV3 = Readonly<{
|
|
4
|
+
host: string;
|
|
5
|
+
port: number;
|
|
6
|
+
path: PathKind;
|
|
7
|
+
tls: Readonly<{
|
|
8
|
+
certificate: string;
|
|
9
|
+
privateKey: string;
|
|
10
|
+
}>;
|
|
11
|
+
allowedOrigins: readonly string[];
|
|
12
|
+
inboundBidirectionalStreamCapacity: number;
|
|
13
|
+
maxPendingSessions?: number;
|
|
14
|
+
pendingSessionTimeoutMs?: number;
|
|
15
|
+
cleanupTimeoutMs?: number;
|
|
16
|
+
}>;
|
|
17
|
+
export type NodeWebSocketListenerV3 = Readonly<{
|
|
18
|
+
address(): Readonly<{
|
|
19
|
+
host: string;
|
|
20
|
+
port: number;
|
|
21
|
+
}>;
|
|
22
|
+
accept(options?: Readonly<{
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
}>): Promise<CarrierSessionV3>;
|
|
25
|
+
close(): Promise<void>;
|
|
26
|
+
}>;
|
|
27
|
+
export declare function startNodeWebSocketListenerV3(options: NodeWebSocketListenerOptionsV3): Promise<NodeWebSocketListenerV3>;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { createServer as createHTTPSServer } from "node:https";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { constants } from "node:crypto";
|
|
4
|
+
import { createServerWebSocketCarrierSessionV3 } from "../v3/webSocketCarrier.js";
|
|
5
|
+
import { WebSocketBinaryTransport } from "../ws-client/binaryTransport.js";
|
|
6
|
+
import { defaultWsMaxPayload } from "./wsDefaults.js";
|
|
7
|
+
import { FLOWERSEC_V3_PATHS, websocketSubprotocolForPathV3, } from "../v3/transportConstants.js";
|
|
8
|
+
export async function startNodeWebSocketListenerV3(options) {
|
|
9
|
+
validateOptions(options);
|
|
10
|
+
const maxPendingSessions = options.maxPendingSessions ?? 1_024;
|
|
11
|
+
const pendingSessionTimeoutMs = options.pendingSessionTimeoutMs ?? 10_000;
|
|
12
|
+
const cleanupTimeoutMs = options.cleanupTimeoutMs ?? 2_000;
|
|
13
|
+
const protocol = websocketSubprotocolForPathV3(options.path);
|
|
14
|
+
const endpoint = FLOWERSEC_V3_PATHS.websocket[options.path];
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const wsModule = require("ws");
|
|
17
|
+
const WebSocketServer = wsModule.WebSocketServer;
|
|
18
|
+
const server = createHTTPSServer({
|
|
19
|
+
key: options.tls.privateKey,
|
|
20
|
+
cert: options.tls.certificate,
|
|
21
|
+
minVersion: "TLSv1.3",
|
|
22
|
+
maxVersion: "TLSv1.3",
|
|
23
|
+
secureOptions: constants.SSL_OP_NO_TICKET,
|
|
24
|
+
});
|
|
25
|
+
const sessions = new SessionQueueV3(maxPendingSessions, pendingSessionTimeoutMs);
|
|
26
|
+
const sockets = new Set();
|
|
27
|
+
const networkSockets = new Set();
|
|
28
|
+
const wss = new WebSocketServer({
|
|
29
|
+
noServer: true,
|
|
30
|
+
perMessageDeflate: false,
|
|
31
|
+
maxPayload: defaultWsMaxPayload({}),
|
|
32
|
+
handleProtocols(protocols) { return protocols.size === 1 && protocols.has(protocol) ? protocol : false; },
|
|
33
|
+
});
|
|
34
|
+
server.on("connection", (socket) => {
|
|
35
|
+
networkSockets.add(socket);
|
|
36
|
+
socket.once("close", () => networkSockets.delete(socket));
|
|
37
|
+
});
|
|
38
|
+
server.on("upgrade", (request, socket, head) => {
|
|
39
|
+
const origin = request.headers.origin;
|
|
40
|
+
const requested = request.headers["sec-websocket-protocol"];
|
|
41
|
+
if (request.url !== endpoint || typeof origin !== "string" ||
|
|
42
|
+
!options.allowedOrigins.includes(origin) || requested !== protocol) {
|
|
43
|
+
socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
|
|
44
|
+
socket.destroy();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
wss.handleUpgrade(request, socket, head, (webSocket) => wss.emit("connection", webSocket));
|
|
48
|
+
});
|
|
49
|
+
wss.on("connection", (socket) => {
|
|
50
|
+
sockets.add(socket);
|
|
51
|
+
const session = createServerWebSocketCarrierSessionV3(new WebSocketBinaryTransport(socket), {
|
|
52
|
+
path: options.path,
|
|
53
|
+
inboundBidirectionalStreamCapacity: options.inboundBidirectionalStreamCapacity,
|
|
54
|
+
});
|
|
55
|
+
socket.once("close", () => {
|
|
56
|
+
sockets.delete(socket);
|
|
57
|
+
sessions.remove(session);
|
|
58
|
+
});
|
|
59
|
+
sessions.push(session);
|
|
60
|
+
});
|
|
61
|
+
await new Promise((resolve, reject) => {
|
|
62
|
+
server.once("error", reject);
|
|
63
|
+
server.listen(options.port, options.host, resolve);
|
|
64
|
+
});
|
|
65
|
+
let closePromise;
|
|
66
|
+
return Object.freeze({
|
|
67
|
+
address() {
|
|
68
|
+
const address = server.address();
|
|
69
|
+
if (address === null || typeof address === "string")
|
|
70
|
+
throw new Error("Node WebSocket v3 listener is not listening");
|
|
71
|
+
return Object.freeze({ host: address.address, port: address.port });
|
|
72
|
+
},
|
|
73
|
+
async accept(acceptOptions = {}) { return await sessions.shift(acceptOptions.signal); },
|
|
74
|
+
async close() {
|
|
75
|
+
closePromise ??= closeListener();
|
|
76
|
+
await closePromise;
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
async function closeListener() {
|
|
80
|
+
sessions.close();
|
|
81
|
+
for (const socket of sockets)
|
|
82
|
+
socket.close();
|
|
83
|
+
const graceful = Promise.all([
|
|
84
|
+
new Promise((resolve, reject) => {
|
|
85
|
+
wss.close((error) => error === undefined ? resolve() : reject(error));
|
|
86
|
+
}),
|
|
87
|
+
new Promise((resolve, reject) => {
|
|
88
|
+
server.close((error) => error === undefined ? resolve() : reject(error));
|
|
89
|
+
}),
|
|
90
|
+
]);
|
|
91
|
+
let timer;
|
|
92
|
+
let timedOut;
|
|
93
|
+
try {
|
|
94
|
+
timedOut = await Promise.race([
|
|
95
|
+
graceful.then(() => false),
|
|
96
|
+
new Promise((resolve) => {
|
|
97
|
+
timer = setTimeout(() => resolve(true), cleanupTimeoutMs);
|
|
98
|
+
timer.unref?.();
|
|
99
|
+
}),
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
if (timer !== undefined)
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
}
|
|
106
|
+
if (!timedOut)
|
|
107
|
+
return;
|
|
108
|
+
for (const socket of sockets)
|
|
109
|
+
socket.terminate();
|
|
110
|
+
for (const socket of networkSockets)
|
|
111
|
+
socket.destroy();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function validateOptions(options) {
|
|
115
|
+
if (!Number.isInteger(options.port) || options.port < 0 || options.port > 65_535 ||
|
|
116
|
+
options.allowedOrigins.length === 0 || new Set(options.allowedOrigins).size !== options.allowedOrigins.length ||
|
|
117
|
+
options.allowedOrigins.some((origin) => !validOrigin(origin)) ||
|
|
118
|
+
!Number.isInteger(options.inboundBidirectionalStreamCapacity) ||
|
|
119
|
+
options.inboundBidirectionalStreamCapacity < 3 || options.inboundBidirectionalStreamCapacity > 130 ||
|
|
120
|
+
(options.maxPendingSessions !== undefined && (!Number.isSafeInteger(options.maxPendingSessions) || options.maxPendingSessions < 1 || options.maxPendingSessions > 1_024)) ||
|
|
121
|
+
(options.pendingSessionTimeoutMs !== undefined && (!Number.isSafeInteger(options.pendingSessionTimeoutMs) || options.pendingSessionTimeoutMs < 1 || options.pendingSessionTimeoutMs > 600_000)) ||
|
|
122
|
+
(options.cleanupTimeoutMs !== undefined && (!Number.isSafeInteger(options.cleanupTimeoutMs) || options.cleanupTimeoutMs < 1 || options.cleanupTimeoutMs > 600_000)) ||
|
|
123
|
+
options.tls.certificate.length === 0 || options.tls.privateKey.length === 0) {
|
|
124
|
+
throw new TypeError("invalid Node WebSocket v3 listener options");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function validOrigin(raw) {
|
|
128
|
+
try {
|
|
129
|
+
const value = new URL(raw);
|
|
130
|
+
return (value.protocol === "https:" || value.protocol === "http:") && value.origin === raw &&
|
|
131
|
+
value.username === "" && value.password === "";
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
class SessionQueueV3 {
|
|
138
|
+
maximum;
|
|
139
|
+
timeoutMs;
|
|
140
|
+
values = [];
|
|
141
|
+
waiters = new Set();
|
|
142
|
+
closed = false;
|
|
143
|
+
constructor(maximum, timeoutMs) {
|
|
144
|
+
this.maximum = maximum;
|
|
145
|
+
this.timeoutMs = timeoutMs;
|
|
146
|
+
}
|
|
147
|
+
push(value) {
|
|
148
|
+
if (this.closed) {
|
|
149
|
+
value.abort({ code: 1, reason: "WebSocket v3 listener closed" });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const waiter = this.waiters.values().next().value;
|
|
153
|
+
if (waiter !== undefined) {
|
|
154
|
+
this.waiters.delete(waiter);
|
|
155
|
+
waiter.resolve(value);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (this.values.length >= this.maximum) {
|
|
159
|
+
value.abort({ code: 1, reason: "WebSocket v3 pending capacity exceeded" });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const entry = {
|
|
163
|
+
value,
|
|
164
|
+
timer: setTimeout(() => {
|
|
165
|
+
if (this.remove(value)) {
|
|
166
|
+
value.abort({ code: 1, reason: "WebSocket v3 pending admission timed out" });
|
|
167
|
+
}
|
|
168
|
+
}, this.timeoutMs),
|
|
169
|
+
};
|
|
170
|
+
this.values.push(entry);
|
|
171
|
+
}
|
|
172
|
+
remove(value) {
|
|
173
|
+
const index = this.values.findIndex((entry) => entry.value === value);
|
|
174
|
+
if (index < 0)
|
|
175
|
+
return false;
|
|
176
|
+
const [entry] = this.values.splice(index, 1);
|
|
177
|
+
if (entry !== undefined)
|
|
178
|
+
clearTimeout(entry.timer);
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
async shift(signal) {
|
|
182
|
+
if (signal?.aborted === true)
|
|
183
|
+
throw signal.reason ?? new Error("accept canceled");
|
|
184
|
+
const entry = this.values.shift();
|
|
185
|
+
if (entry !== undefined) {
|
|
186
|
+
clearTimeout(entry.timer);
|
|
187
|
+
return entry.value;
|
|
188
|
+
}
|
|
189
|
+
if (this.closed)
|
|
190
|
+
throw new Error("WebSocket v3 listener is closed");
|
|
191
|
+
return await new Promise((resolve, reject) => {
|
|
192
|
+
const waiter = {
|
|
193
|
+
resolve: (session) => { cleanup(); resolve(session); },
|
|
194
|
+
reject: (error) => { cleanup(); reject(error); },
|
|
195
|
+
};
|
|
196
|
+
const abort = () => {
|
|
197
|
+
this.waiters.delete(waiter);
|
|
198
|
+
reject(signal?.reason ?? new Error("accept canceled"));
|
|
199
|
+
};
|
|
200
|
+
const cleanup = () => signal?.removeEventListener("abort", abort);
|
|
201
|
+
this.waiters.add(waiter);
|
|
202
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
close() {
|
|
206
|
+
if (this.closed)
|
|
207
|
+
return;
|
|
208
|
+
this.closed = true;
|
|
209
|
+
const error = new Error("WebSocket v3 listener is closed");
|
|
210
|
+
for (const waiter of this.waiters)
|
|
211
|
+
waiter.reject(error);
|
|
212
|
+
this.waiters.clear();
|
|
213
|
+
for (const entry of this.values) {
|
|
214
|
+
clearTimeout(entry.timer);
|
|
215
|
+
entry.value.abort({ code: 1, reason: "WebSocket v3 listener closed" });
|
|
216
|
+
}
|
|
217
|
+
this.values.length = 0;
|
|
218
|
+
}
|
|
219
|
+
}
|
package/dist/proxy/runtime.js
CHANGED
|
@@ -45,11 +45,34 @@ function normalizeTimeout(input) {
|
|
|
45
45
|
}
|
|
46
46
|
return input;
|
|
47
47
|
}
|
|
48
|
+
class InvalidProxyPathError extends TypeError {
|
|
49
|
+
}
|
|
48
50
|
function normalizePath(input) {
|
|
49
|
-
if (input !== input.trim() || !input.startsWith("/") || input.startsWith("//") || /[\u0000-\u0020]/u.test(input) || input.includes("://")) {
|
|
50
|
-
throw new
|
|
51
|
+
if (input !== input.trim() || !input.startsWith("/") || input.startsWith("//") || /[\u0000-\u0020]/u.test(input) || input.includes("://") || input.includes("#")) {
|
|
52
|
+
throw new InvalidProxyPathError("proxy path must be an origin-relative path");
|
|
51
53
|
}
|
|
52
|
-
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = new URL(input, "https://flowersec.invalid/");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new InvalidProxyPathError("proxy path must be an origin-relative path");
|
|
60
|
+
}
|
|
61
|
+
const pathname = normalizePercentEscapes(parsed.pathname, true).replace(/\/{2,}/gu, "/");
|
|
62
|
+
const search = normalizePercentEscapes(parsed.search, false);
|
|
63
|
+
return pathname + search;
|
|
64
|
+
}
|
|
65
|
+
function normalizePercentEscapes(input, rejectEncodedSeparators) {
|
|
66
|
+
if (/%(?![0-9a-f]{2})/iu.test(input))
|
|
67
|
+
throw new InvalidProxyPathError("proxy path contains invalid percent encoding");
|
|
68
|
+
return input.replace(/%([0-9a-f]{2})/giu, (_match, hex) => {
|
|
69
|
+
const value = Number.parseInt(hex, 16);
|
|
70
|
+
if (rejectEncodedSeparators && (value === 0x2f || value === 0x5c)) {
|
|
71
|
+
throw new InvalidProxyPathError("proxy path contains an encoded separator");
|
|
72
|
+
}
|
|
73
|
+
const character = String.fromCharCode(value);
|
|
74
|
+
return /[A-Za-z0-9\-._~]/u.test(character) ? character : `%${hex.toUpperCase()}`;
|
|
75
|
+
});
|
|
53
76
|
}
|
|
54
77
|
function pathName(path) {
|
|
55
78
|
const query = path.indexOf("?");
|
|
@@ -176,11 +199,19 @@ class StreamAdmission {
|
|
|
176
199
|
return Promise.reject(new SessionError("resource_exhausted"));
|
|
177
200
|
}
|
|
178
201
|
return new Promise((resolve, reject) => {
|
|
202
|
+
let cleaned = false;
|
|
203
|
+
const cleanup = () => {
|
|
204
|
+
if (cleaned)
|
|
205
|
+
return;
|
|
206
|
+
cleaned = true;
|
|
207
|
+
signal?.removeEventListener("abort", onAbort);
|
|
208
|
+
};
|
|
179
209
|
const entry = {
|
|
180
210
|
bytes,
|
|
181
211
|
...(signal === undefined ? {} : { signal }),
|
|
182
|
-
|
|
183
|
-
|
|
212
|
+
cleanup,
|
|
213
|
+
resolve: (release) => { cleanup(); resolve(release); },
|
|
214
|
+
reject: (error) => { cleanup(); reject(error); },
|
|
184
215
|
};
|
|
185
216
|
const onAbort = () => {
|
|
186
217
|
const index = this.queue.indexOf(entry);
|
|
@@ -188,7 +219,7 @@ class StreamAdmission {
|
|
|
188
219
|
return;
|
|
189
220
|
this.queue.splice(index, 1);
|
|
190
221
|
this.queuedBytes -= bytes;
|
|
191
|
-
reject(new SessionError("canceled"));
|
|
222
|
+
entry.reject(new SessionError("canceled"));
|
|
192
223
|
};
|
|
193
224
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
194
225
|
this.queue.push(entry);
|
|
@@ -251,6 +282,8 @@ async function* readChunks(reader, maxChunkBytes, maxBodyBytes) {
|
|
|
251
282
|
function publicFailure(error) {
|
|
252
283
|
if (error instanceof ProxyPolicyError)
|
|
253
284
|
return { status: 403, code: "policy_denied", message: "proxy request denied" };
|
|
285
|
+
if (error instanceof InvalidProxyPathError)
|
|
286
|
+
return { status: 400, code: "invalid_request", message: "invalid proxy request" };
|
|
254
287
|
if (error instanceof SessionError && (error.code === "resource_exhausted" || error.code === "closed" || error.code === "going_away")) {
|
|
255
288
|
return { status: 503, code: error.code, message: "proxy service unavailable" };
|
|
256
289
|
}
|
|
@@ -15,6 +15,8 @@ export type ProxyServiceWorkerScriptOptions = Readonly<{
|
|
|
15
15
|
sameOriginOnly?: boolean;
|
|
16
16
|
maxRequestBodyBytes?: number;
|
|
17
17
|
maxInjectHTMLBytes?: number;
|
|
18
|
+
responseMetadataTimeoutMs?: number;
|
|
19
|
+
responseBodyInactivityTimeoutMs?: number;
|
|
18
20
|
passthrough?: ProxyServiceWorkerPassthroughOptions;
|
|
19
21
|
proxyPathPrefix?: string;
|
|
20
22
|
stripProxyPathPrefix?: boolean;
|
|
@@ -17,6 +17,12 @@ function bounded(name, value, fallback, maximum) {
|
|
|
17
17
|
throw new TypeError(`${name} is invalid`);
|
|
18
18
|
return result;
|
|
19
19
|
}
|
|
20
|
+
function optionalBounded(name, value, maximum) {
|
|
21
|
+
const result = value ?? 0;
|
|
22
|
+
if (!Number.isSafeInteger(result) || result < 0 || result > maximum)
|
|
23
|
+
throw new TypeError(`${name} is invalid`);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
20
26
|
function token(name, value, fallback = "") {
|
|
21
27
|
const result = value ?? fallback;
|
|
22
28
|
if (result !== result.trim() || /[\u0000-\u0020\u007f]/u.test(result) || result.length > 512)
|
|
@@ -42,6 +48,8 @@ function normalizeOptions(options) {
|
|
|
42
48
|
sameOriginOnly: options.sameOriginOnly ?? true,
|
|
43
49
|
maxRequestBodyBytes: bounded("maxRequestBodyBytes", options.maxRequestBodyBytes, 64 * 1024 * 1024, 256 * 1024 * 1024),
|
|
44
50
|
maxInjectHTMLBytes: bounded("maxInjectHTMLBytes", options.maxInjectHTMLBytes, 8 * 1024 * 1024, 32 * 1024 * 1024),
|
|
51
|
+
responseMetadataTimeoutMs: bounded("responseMetadataTimeoutMs", options.responseMetadataTimeoutMs, 10_000, 300_000),
|
|
52
|
+
responseBodyInactivityTimeoutMs: optionalBounded("responseBodyInactivityTimeoutMs", options.responseBodyInactivityTimeoutMs, 300_000),
|
|
45
53
|
passthroughPaths: strings("passthrough.paths", options.passthrough?.paths),
|
|
46
54
|
passthroughPrefixes: strings("passthrough.prefixes", options.passthrough?.prefixes),
|
|
47
55
|
proxyPathPrefix,
|
|
@@ -127,6 +135,8 @@ function serviceWorkerMain(config) {
|
|
|
127
135
|
return new Response("proxy runtime unavailable", { status: 503 });
|
|
128
136
|
}
|
|
129
137
|
let body;
|
|
138
|
+
if (request.signal.aborted)
|
|
139
|
+
return new Response("proxy request canceled", { status: 499 });
|
|
130
140
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
131
141
|
const requestBody = await request.clone().arrayBuffer();
|
|
132
142
|
if (requestBody.byteLength > config.maxRequestBodyBytes)
|
|
@@ -138,16 +148,54 @@ function serviceWorkerMain(config) {
|
|
|
138
148
|
let metadata = null;
|
|
139
149
|
let controller = null;
|
|
140
150
|
let finished = false;
|
|
141
|
-
|
|
151
|
+
let remoteAbortSent = false;
|
|
152
|
+
let bodyInactivityTimer;
|
|
153
|
+
const abortRemote = () => {
|
|
154
|
+
if (remoteAbortSent)
|
|
155
|
+
return;
|
|
156
|
+
remoteAbortSent = true;
|
|
157
|
+
try {
|
|
158
|
+
channel.port1.postMessage({ type: "flowersec-proxy:abort" });
|
|
159
|
+
}
|
|
160
|
+
catch { /* Port already failed. */ }
|
|
161
|
+
};
|
|
162
|
+
const cleanup = () => {
|
|
163
|
+
clearTimeout(metadataTimer);
|
|
164
|
+
clearTimeout(bodyInactivityTimer);
|
|
165
|
+
clearInterval(runtimeWatchdog);
|
|
166
|
+
request.signal.removeEventListener("abort", requestAborted);
|
|
167
|
+
channel.port1.close();
|
|
168
|
+
};
|
|
169
|
+
const finishError = (status, message, cancelRemote = false) => {
|
|
142
170
|
if (finished)
|
|
143
171
|
return;
|
|
144
172
|
finished = true;
|
|
173
|
+
if (cancelRemote)
|
|
174
|
+
abortRemote();
|
|
145
175
|
if (controller !== null)
|
|
146
176
|
controller.error(new Error(message));
|
|
147
177
|
else
|
|
148
178
|
resolve(new Response(message, { status }));
|
|
149
|
-
|
|
179
|
+
cleanup();
|
|
180
|
+
};
|
|
181
|
+
const requestAborted = () => finishError(499, "proxy request canceled", true);
|
|
182
|
+
const armBodyInactivityTimer = () => {
|
|
183
|
+
clearTimeout(bodyInactivityTimer);
|
|
184
|
+
if (config.responseBodyInactivityTimeoutMs === 0)
|
|
185
|
+
return;
|
|
186
|
+
bodyInactivityTimer = setTimeout(() => finishError(504, "proxy response body timed out", true), config.responseBodyInactivityTimeoutMs);
|
|
150
187
|
};
|
|
188
|
+
const metadataTimer = setTimeout(() => finishError(504, "proxy response timed out", true), config.responseMetadataTimeoutMs);
|
|
189
|
+
const runtimeWatchdog = setInterval(() => {
|
|
190
|
+
void worker.clients.get(target.id).then((current) => {
|
|
191
|
+
if (current !== null || finished)
|
|
192
|
+
return;
|
|
193
|
+
if (config.windowTarget === "registered_runtime")
|
|
194
|
+
runtimeClientId = "";
|
|
195
|
+
finishError(503, "proxy runtime unavailable", true);
|
|
196
|
+
}).catch(() => finishError(503, "proxy runtime unavailable", true));
|
|
197
|
+
}, 250);
|
|
198
|
+
request.signal.addEventListener("abort", requestAborted, { once: true });
|
|
151
199
|
channel.port1.onmessage = (message) => {
|
|
152
200
|
const value = message.data;
|
|
153
201
|
if (value === null || typeof value !== "object" || finished)
|
|
@@ -162,13 +210,21 @@ function serviceWorkerMain(config) {
|
|
|
162
210
|
headers.append(entry.name, entry.value);
|
|
163
211
|
}
|
|
164
212
|
metadata = { status: value.status, headers };
|
|
213
|
+
clearTimeout(metadataTimer);
|
|
214
|
+
armBodyInactivityTimer();
|
|
165
215
|
const stream = new ReadableStream({
|
|
166
216
|
start(valueController) {
|
|
167
217
|
controller = valueController;
|
|
168
218
|
channel.port1.postMessage({ type: "flowersec-proxy:response_credit" });
|
|
169
219
|
},
|
|
170
220
|
pull() { channel.port1.postMessage({ type: "flowersec-proxy:response_credit" }); },
|
|
171
|
-
cancel() {
|
|
221
|
+
cancel() {
|
|
222
|
+
if (finished)
|
|
223
|
+
return;
|
|
224
|
+
finished = true;
|
|
225
|
+
abortRemote();
|
|
226
|
+
cleanup();
|
|
227
|
+
},
|
|
172
228
|
});
|
|
173
229
|
resolve(new Response(stream, { status: metadata.status, headers: metadata.headers }));
|
|
174
230
|
return;
|
|
@@ -177,19 +233,20 @@ function serviceWorkerMain(config) {
|
|
|
177
233
|
if (controller === null || !(value.data instanceof ArrayBuffer))
|
|
178
234
|
return finishError(502, "invalid proxy response");
|
|
179
235
|
controller.enqueue(new Uint8Array(value.data));
|
|
236
|
+
armBodyInactivityTimer();
|
|
180
237
|
return;
|
|
181
238
|
}
|
|
182
239
|
if (value.type === "flowersec-proxy:response_end") {
|
|
183
240
|
finished = true;
|
|
184
241
|
controller?.close();
|
|
185
|
-
|
|
242
|
+
cleanup();
|
|
186
243
|
return;
|
|
187
244
|
}
|
|
188
245
|
if (value.type === "flowersec-proxy:response_error") {
|
|
189
246
|
finishError(Number.isInteger(value.status) ? value.status : 502, typeof value.message === "string" ? value.message : "proxy request failed");
|
|
190
247
|
}
|
|
191
248
|
};
|
|
192
|
-
channel.port1.onmessageerror = () => finishError(502, "proxy request failed");
|
|
249
|
+
channel.port1.onmessageerror = () => finishError(502, "proxy request failed", true);
|
|
193
250
|
const headers = Array.from(request.headers.entries()).map(([name, value]) => ({ name, value }));
|
|
194
251
|
try {
|
|
195
252
|
target.postMessage({
|
|
@@ -207,8 +264,7 @@ function serviceWorkerMain(config) {
|
|
|
207
264
|
catch {
|
|
208
265
|
if (config.windowTarget === "registered_runtime")
|
|
209
266
|
runtimeClientId = "";
|
|
210
|
-
|
|
211
|
-
resolve(new Response("proxy runtime unavailable", { status: 503 }));
|
|
267
|
+
finishError(503, "proxy runtime unavailable");
|
|
212
268
|
}
|
|
213
269
|
});
|
|
214
270
|
const injection = config.injectHTML;
|