@flexent/ws-proxy 2.12.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 ADDED
@@ -0,0 +1,25 @@
1
+ # @flexent/ws-proxy
2
+
3
+ Transparent WebSocket upgrade relay for Node.js HTTP servers.
4
+
5
+ ## Usage
6
+
7
+ Subclass `WsProxy` and implement outbound connection handling:
8
+
9
+ ```ts
10
+ import { Duplex } from 'node:stream';
11
+ import { WsProxy, WsProxyContext } from '@flexent/ws-proxy';
12
+
13
+ class MyProxy extends WsProxy {
14
+
15
+ protected override async processConnection(_ctx: WsProxyContext): Promise<void> {}
16
+
17
+ protected override async connectOutbound(_ctx: WsProxyContext): Promise<Duplex> {
18
+ // Return a TCP socket or other Duplex connected to the upstream server.
19
+ throw new Error('Not implemented');
20
+ }
21
+
22
+ }
23
+ ```
24
+
25
+ Call `onUpgrade` from your HTTP server's `upgrade` event to relay the WebSocket handshake and stream data bidirectionally.
@@ -0,0 +1,16 @@
1
+ import { IncomingMessage } from 'node:http';
2
+ import { Duplex } from 'node:stream';
3
+ import { OutboundUpgradeOptions, WsProxyContext } from './types.js';
4
+ export declare abstract class WsProxy {
5
+ onUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer): void;
6
+ protected abstract processConnection(ctx: WsProxyContext): Promise<void>;
7
+ protected abstract connectOutbound(ctx: WsProxyContext): Promise<Duplex>;
8
+ protected buildOutboundUpgrade(ctx: WsProxyContext): string;
9
+ protected onConnected(_ctx: WsProxyContext, _outbound: Duplex): void;
10
+ protected onError(error: unknown, ctx: WsProxyContext): void;
11
+ protected encodeForwardedUpgrade(request: IncomingMessage, options: OutboundUpgradeOptions): string;
12
+ private relay;
13
+ private runRelay;
14
+ private assertWebSocketUpgrade;
15
+ private reject;
16
+ }
@@ -0,0 +1,134 @@
1
+ import { STATUS_CODES } from 'node:http';
2
+ import { WsProxyError } from './errors.js';
3
+ import { pipeBidirectional } from './util/pipe.js';
4
+ const FORWARDED_HEADER_NAMES = [
5
+ 'connection',
6
+ 'upgrade',
7
+ 'sec-websocket-key',
8
+ 'sec-websocket-version',
9
+ 'sec-websocket-protocol',
10
+ 'sec-websocket-extensions',
11
+ 'origin',
12
+ 'pragma',
13
+ 'cache-control',
14
+ ];
15
+ export class WsProxy {
16
+ onUpgrade(request, socket, head) {
17
+ const ctx = { request, socket, head };
18
+ socket.pause();
19
+ try {
20
+ this.assertWebSocketUpgrade(ctx);
21
+ }
22
+ catch (error) {
23
+ this.onError(error, ctx);
24
+ return;
25
+ }
26
+ this.relay(ctx);
27
+ }
28
+ buildOutboundUpgrade(ctx) {
29
+ return this.encodeForwardedUpgrade(ctx.request, {
30
+ path: ctx.request.url ?? '/',
31
+ });
32
+ }
33
+ onConnected(_ctx, _outbound) { }
34
+ onError(error, ctx) {
35
+ const status = error instanceof WsProxyError ? error.status : 502;
36
+ const message = error instanceof Error ? error.message : 'WebSocket proxy failed';
37
+ ctx.socket.resume();
38
+ this.reject(ctx.socket, status, message);
39
+ }
40
+ encodeForwardedUpgrade(request, options) {
41
+ const omitted = new Set((options.omitHeaders ?? []).map(name => name.toLowerCase()));
42
+ const headers = {};
43
+ for (const name of FORWARDED_HEADER_NAMES) {
44
+ if (omitted.has(name)) {
45
+ continue;
46
+ }
47
+ const value = request.headers[name];
48
+ if (value == null) {
49
+ continue;
50
+ }
51
+ headers[canonicalHeaderName(name)] = value;
52
+ }
53
+ if (options.host) {
54
+ headers.Host = options.host;
55
+ }
56
+ else if (request.headers.host) {
57
+ headers.Host = request.headers.host;
58
+ }
59
+ for (const [name, value] of Object.entries(options.headers ?? {})) {
60
+ if (value == null) {
61
+ delete headers[canonicalHeaderName(name)];
62
+ continue;
63
+ }
64
+ headers[canonicalHeaderName(name)] = value;
65
+ }
66
+ const lines = [`GET ${options.path} HTTP/${request.httpVersion}`];
67
+ for (const [name, value] of Object.entries(headers)) {
68
+ if (Array.isArray(value)) {
69
+ for (const item of value) {
70
+ lines.push(`${name}: ${item}`);
71
+ }
72
+ }
73
+ else {
74
+ lines.push(`${name}: ${value}`);
75
+ }
76
+ }
77
+ return `${lines.join('\r\n')}\r\n\r\n`;
78
+ }
79
+ relay(ctx) {
80
+ this.runRelay(ctx).catch(error => {
81
+ this.onError(error, ctx);
82
+ });
83
+ }
84
+ async runRelay(ctx) {
85
+ await this.processConnection(ctx);
86
+ const outbound = await this.connectOutbound(ctx);
87
+ outbound.write(this.buildOutboundUpgrade(ctx));
88
+ ctx.socket.resume();
89
+ pipeBidirectional(ctx.socket, outbound);
90
+ if (ctx.head.length > 0) {
91
+ queueMicrotask(() => outbound.write(ctx.head));
92
+ }
93
+ this.onConnected(ctx, outbound);
94
+ }
95
+ assertWebSocketUpgrade(ctx) {
96
+ if (ctx.request.method !== 'GET') {
97
+ throw new WsProxyError(400, 'Not a WebSocket upgrade request');
98
+ }
99
+ if (ctx.request.headers.upgrade?.toLowerCase() !== 'websocket') {
100
+ throw new WsProxyError(400, 'Not a WebSocket upgrade request');
101
+ }
102
+ const connection = ctx.request.headers.connection?.toLowerCase() ?? '';
103
+ if (!connection.split(',').some(part => part.trim() === 'upgrade')) {
104
+ throw new WsProxyError(400, 'Not a WebSocket upgrade request');
105
+ }
106
+ const key = ctx.request.headers['sec-websocket-key'];
107
+ if (!key || Array.isArray(key)) {
108
+ throw new WsProxyError(400, 'Missing Sec-WebSocket-Key');
109
+ }
110
+ }
111
+ reject(socket, status, message) {
112
+ if (socket.destroyed || socket.writableEnded) {
113
+ return;
114
+ }
115
+ const statusText = STATUS_CODES[status] ?? 'Error';
116
+ const body = message ?? statusText;
117
+ const payload = [
118
+ `HTTP/1.1 ${status} ${statusText}`,
119
+ 'Connection: close',
120
+ 'Content-Type: text/plain; charset=utf-8',
121
+ `Content-Length: ${Buffer.byteLength(body)}`,
122
+ '',
123
+ body,
124
+ ].join('\r\n');
125
+ socket.end(payload);
126
+ }
127
+ }
128
+ function canonicalHeaderName(name) {
129
+ return name
130
+ .split('-')
131
+ .map(part => part ? `${part[0].toUpperCase()}${part.slice(1).toLowerCase()}` : part)
132
+ .join('-');
133
+ }
134
+ //# sourceMappingURL=WsProxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WsProxy.js","sourceRoot":"","sources":["../../src/main/WsProxy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export declare class WsProxyError extends Error {
2
+ readonly status: number;
3
+ name: string;
4
+ constructor(status: number, message: string);
5
+ }
@@ -0,0 +1,8 @@
1
+ export class WsProxyError extends Error {
2
+ constructor(status, message) {
3
+ super(message);
4
+ this.status = status;
5
+ this.name = 'WsProxyError';
6
+ }
7
+ }
8
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/main/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,YAAa,SAAQ,KAAK;IAInC,YACa,MAAc,EACvB,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAC;QAHN,WAAM,GAAN,MAAM,CAAQ;QAHlB,SAAI,GAAG,cAAc,CAAC;IAO/B,CAAC;CAEJ"}
@@ -0,0 +1,3 @@
1
+ export * from './errors.js';
2
+ export * from './types.js';
3
+ export * from './WsProxy.js';
@@ -0,0 +1,4 @@
1
+ export * from './errors.js';
2
+ export * from './types.js';
3
+ export * from './WsProxy.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/main/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { IncomingMessage } from 'node:http';
2
+ import { Duplex } from 'node:stream';
3
+ export interface WsProxyContext {
4
+ readonly request: IncomingMessage;
5
+ readonly socket: Duplex;
6
+ readonly head: Buffer;
7
+ }
8
+ export interface OutboundUpgradeOptions {
9
+ path: string;
10
+ host?: string;
11
+ headers?: Record<string, string | string[]>;
12
+ omitHeaders?: string[];
13
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/main/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ import { Duplex } from 'node:stream';
2
+ export declare function pipeBidirectional(left: Duplex, right: Duplex): void;
@@ -0,0 +1,22 @@
1
+ export function pipeBidirectional(left, right) {
2
+ const onLeftError = (error) => destroyQuietly(right, error);
3
+ const onRightError = (error) => destroyQuietly(left, error);
4
+ const onClose = () => {
5
+ left.off('error', onLeftError);
6
+ right.off('error', onRightError);
7
+ left.off('close', onClose);
8
+ right.off('close', onClose);
9
+ };
10
+ left.on('error', onLeftError);
11
+ right.on('error', onRightError);
12
+ left.on('close', onClose);
13
+ right.on('close', onClose);
14
+ left.pipe(right, { end: false });
15
+ right.pipe(left, { end: false });
16
+ }
17
+ function destroyQuietly(socket, error) {
18
+ if (!socket.destroyed) {
19
+ socket.destroy(error);
20
+ }
21
+ }
22
+ //# sourceMappingURL=pipe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipe.js","sourceRoot":"","sources":["../../../src/main/util/pipe.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,KAAa;IACzD,MAAM,WAAW,GAAG,CAAC,KAAY,EAAE,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACnE,MAAM,YAAY,GAAG,CAAC,KAAY,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACnE,MAAM,OAAO,GAAG,GAAG,EAAE;QACjB,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;QAC/B,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;QACjC,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC3B,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAChC,CAAC,CAAC;IACF,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;IAC9B,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IAChC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC1B,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC3B,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IACjC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;AACrC,CAAC;AAED,SAAS,cAAc,CAAC,MAAc,EAAE,KAAa;IACjD,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;QACpB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;AACL,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ import assert from 'node:assert/strict';
2
+ import { PassThrough } from 'node:stream';
3
+ import { WsProxy } from '../main/WsProxy.js';
4
+ describe('@flexent/ws-proxy', () => {
5
+ it('rejects non-WebSocket upgrade requests', async () => {
6
+ const proxy = new TestProxy();
7
+ const socket = new PassThrough();
8
+ const chunks = [];
9
+ socket.on('data', chunk => chunks.push(chunk));
10
+ proxy.onUpgrade({
11
+ method: 'GET',
12
+ headers: { connection: 'keep-alive' },
13
+ }, socket, Buffer.alloc(0));
14
+ await new Promise(resolve => socket.on('end', () => resolve()));
15
+ assert.match(Buffer.concat(chunks).toString('utf8'), /400/);
16
+ });
17
+ it('encodes forwarded upgrade requests', () => {
18
+ const proxy = new TestProxy();
19
+ const payload = proxy.publicEncode({
20
+ method: 'GET',
21
+ httpVersion: '1.1',
22
+ url: '/lease?sessionId=foo',
23
+ headers: {
24
+ host: 'browser.example',
25
+ upgrade: 'websocket',
26
+ connection: 'Upgrade',
27
+ 'sec-websocket-key': 'dGhlIHNhbXBsZSBub25jZQ==',
28
+ 'sec-websocket-version': '13',
29
+ },
30
+ }, {
31
+ path: '/lease?t=token',
32
+ host: 'host.example',
33
+ });
34
+ assert.match(payload, /^GET \/lease\?t=token HTTP\/1\.1\r\n/);
35
+ assert.match(payload, /Host: host\.example\r\n/i);
36
+ assert.match(payload, /Sec-Websocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n/i);
37
+ });
38
+ });
39
+ class TestProxy extends WsProxy {
40
+ async processConnection() { }
41
+ async connectOutbound() {
42
+ return new PassThrough();
43
+ }
44
+ publicEncode(request, options) {
45
+ return this.encodeForwardedUpgrade(request, options);
46
+ }
47
+ }
48
+ //# sourceMappingURL=ws-proxy.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-proxy.test.js","sourceRoot":"","sources":["../../src/test/ws-proxy.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AAExC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAE7C,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IAE/B,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;QACpD,MAAM,KAAK,GAAG,IAAI,SAAS,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;QACjC,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAC/C,KAAK,CAAC,SAAS,CAAC;YACZ,MAAM,EAAE,KAAK;YACb,OAAO,EAAE,EAAE,UAAU,EAAE,YAAY,EAAE;SACrB,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/C,MAAM,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QACtE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC1C,MAAM,KAAK,GAAG,IAAI,SAAS,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAG,KAAK,CAAC,YAAY,CAAC;YAC/B,MAAM,EAAE,KAAK;YACb,WAAW,EAAE,KAAK;YAClB,GAAG,EAAE,sBAAsB;YAC3B,OAAO,EAAE;gBACL,IAAI,EAAE,iBAAiB;gBACvB,OAAO,EAAE,WAAW;gBACpB,UAAU,EAAE,SAAS;gBACrB,mBAAmB,EAAE,0BAA0B;gBAC/C,uBAAuB,EAAE,IAAI;aAChC;SACe,EAAE;YAClB,IAAI,EAAE,gBAAgB;YACtB,IAAI,EAAE,cAAc;SACvB,CAAC,CAAC;QACH,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,sCAAsC,CAAC,CAAC;QAC9D,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,0BAA0B,CAAC,CAAC;QAClD,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,kDAAkD,CAAC,CAAC;IAC9E,CAAC,CAAC,CAAC;AAEP,CAAC,CAAC,CAAC;AAEH,MAAM,SAAU,SAAQ,OAAO;IAER,KAAK,CAAC,iBAAiB,KAAmB,CAAC;IAE3C,KAAK,CAAC,eAAe;QACpC,OAAO,IAAI,WAAW,EAAE,CAAC;IAC7B,CAAC;IAED,YAAY,CAAC,OAAwB,EAAE,OAAwC;QAC3E,OAAO,IAAI,CAAC,sBAAsB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACzD,CAAC;CAEJ"}
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@flexent/ws-proxy",
3
+ "version": "2.12.0",
4
+ "description": "Transparent WebSocket upgrade relay for Node.js HTTP servers",
5
+ "author": "Boris Okunskiy",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": "./out/main/index.js"
11
+ },
12
+ "files": [
13
+ "out"
14
+ ],
15
+ "scripts": {
16
+ "test": "NODE_ENV=test mocha"
17
+ }
18
+ }