zero-http 0.2.0 → 0.2.1
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 +314 -115
- package/documentation/full-server.js +82 -1
- package/documentation/public/data/api.json +154 -33
- package/documentation/public/data/examples.json +35 -11
- package/documentation/public/data/options.json +14 -8
- package/documentation/public/index.html +109 -17
- package/documentation/public/scripts/data-sections.js +4 -4
- package/documentation/public/scripts/helpers.js +4 -4
- package/documentation/public/scripts/playground.js +201 -0
- package/documentation/public/scripts/ui.js +6 -6
- package/documentation/public/scripts/uploads.js +9 -9
- package/documentation/public/styles.css +12 -12
- package/documentation/public/vendor/icons/compress.svg +27 -0
- package/documentation/public/vendor/icons/https.svg +23 -0
- package/documentation/public/vendor/icons/router.svg +29 -0
- package/documentation/public/vendor/icons/sse.svg +25 -0
- package/documentation/public/vendor/icons/websocket.svg +21 -0
- package/index.js +21 -4
- package/lib/app.js +145 -15
- package/lib/body/json.js +3 -0
- package/lib/body/multipart.js +2 -0
- package/lib/body/raw.js +3 -0
- package/lib/body/text.js +3 -0
- package/lib/body/urlencoded.js +3 -0
- package/lib/{fetch.js → fetch/index.js} +30 -1
- package/lib/http/index.js +9 -0
- package/lib/{request.js → http/request.js} +7 -1
- package/lib/{response.js → http/response.js} +70 -1
- package/lib/middleware/compress.js +194 -0
- package/lib/middleware/index.js +12 -0
- package/lib/router/index.js +278 -0
- package/lib/sse/index.js +8 -0
- package/lib/sse/stream.js +322 -0
- package/lib/ws/connection.js +440 -0
- package/lib/ws/handshake.js +122 -0
- package/lib/ws/index.js +12 -0
- package/package.json +1 -1
- package/lib/router.js +0 -87
- /package/lib/{cors.js → middleware/cors.js} +0 -0
- /package/lib/{logger.js → middleware/logger.js} +0 -0
- /package/lib/{rateLimit.js → middleware/rateLimit.js} +0 -0
- /package/lib/{static.js → middleware/static.js} +0 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module ws/connection
|
|
3
|
+
* @description Full-featured WebSocket connection wrapper over a raw TCP socket.
|
|
4
|
+
* Implements RFC 6455 framing for text, binary, ping, pong, and close.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Auto-incrementing connection ID counter. */
|
|
8
|
+
let _wsIdCounter = 0;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* WebSocket ready-state constants (mirrors the browser WebSocket API).
|
|
12
|
+
* @enum {number}
|
|
13
|
+
*/
|
|
14
|
+
const WS_READY_STATE = {
|
|
15
|
+
CONNECTING: 0,
|
|
16
|
+
OPEN: 1,
|
|
17
|
+
CLOSING: 2,
|
|
18
|
+
CLOSED: 3,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Full-featured WebSocket connection wrapper over a raw TCP socket.
|
|
23
|
+
* Implements RFC 6455 framing for text, binary, ping, pong, and close.
|
|
24
|
+
*
|
|
25
|
+
* @class
|
|
26
|
+
*
|
|
27
|
+
* Properties:
|
|
28
|
+
* - `id` {string} Unique connection identifier.
|
|
29
|
+
* - `readyState` {number} Current state (0-3, see WS_READY_STATE).
|
|
30
|
+
* - `protocol` {string} Negotiated sub-protocol (or '').
|
|
31
|
+
* - `extensions` {string} Requested extensions header (or '').
|
|
32
|
+
* - `headers` {object} Request headers from the upgrade.
|
|
33
|
+
* - `ip` {string|null} Remote IP address.
|
|
34
|
+
* - `query` {object} Parsed query-string params from the upgrade URL.
|
|
35
|
+
* - `url` {string} Full upgrade request URL.
|
|
36
|
+
* - `bufferedAmount`{number} Bytes waiting to be flushed to the network.
|
|
37
|
+
* - `maxPayload` {number} Maximum accepted incoming payload (bytes).
|
|
38
|
+
*
|
|
39
|
+
* Events (via `.on()`):
|
|
40
|
+
* - `'message'` (data: string|Buffer) — Text or binary message received.
|
|
41
|
+
* - `'close'` (code?: number, reason?: string) — Connection closed.
|
|
42
|
+
* - `'error'` (err: Error) — Socket error.
|
|
43
|
+
* - `'pong'` (payload: Buffer) — Pong frame received.
|
|
44
|
+
* - `'ping'` (payload: Buffer) — Ping frame received (auto-ponged).
|
|
45
|
+
* - `'drain'` — Socket write buffer drained.
|
|
46
|
+
*/
|
|
47
|
+
class WebSocketConnection
|
|
48
|
+
{
|
|
49
|
+
/**
|
|
50
|
+
* @param {import('net').Socket} socket - The upgraded TCP socket.
|
|
51
|
+
* @param {object} [meta]
|
|
52
|
+
* @param {number} [meta.maxPayload=1048576]
|
|
53
|
+
* @param {number} [meta.pingInterval=30000]
|
|
54
|
+
* @param {string} [meta.protocol]
|
|
55
|
+
* @param {string} [meta.extensions]
|
|
56
|
+
* @param {object} [meta.headers]
|
|
57
|
+
* @param {string} [meta.ip]
|
|
58
|
+
* @param {object} [meta.query]
|
|
59
|
+
* @param {string} [meta.url]
|
|
60
|
+
* @param {boolean} [meta.secure=false]
|
|
61
|
+
*/
|
|
62
|
+
constructor(socket, meta = {})
|
|
63
|
+
{
|
|
64
|
+
this._socket = socket;
|
|
65
|
+
this._buffer = Buffer.alloc(0);
|
|
66
|
+
|
|
67
|
+
/** Unique connection identifier. */
|
|
68
|
+
this.id = 'ws_' + (++_wsIdCounter) + '_' + Date.now().toString(36);
|
|
69
|
+
|
|
70
|
+
/** Current ready state. */
|
|
71
|
+
this.readyState = WS_READY_STATE.OPEN;
|
|
72
|
+
|
|
73
|
+
/** Negotiated sub-protocol. */
|
|
74
|
+
this.protocol = meta.protocol || '';
|
|
75
|
+
|
|
76
|
+
/** Requested extensions. */
|
|
77
|
+
this.extensions = meta.extensions || '';
|
|
78
|
+
|
|
79
|
+
/** Request headers from the upgrade. */
|
|
80
|
+
this.headers = meta.headers || {};
|
|
81
|
+
|
|
82
|
+
/** Remote IP address. */
|
|
83
|
+
this.ip = meta.ip || (socket.remoteAddress || null);
|
|
84
|
+
|
|
85
|
+
/** Parsed query params from the upgrade URL. */
|
|
86
|
+
this.query = meta.query || {};
|
|
87
|
+
|
|
88
|
+
/** Full upgrade URL. */
|
|
89
|
+
this.url = meta.url || '';
|
|
90
|
+
|
|
91
|
+
/** `true` when the underlying connection is over TLS (WSS). */
|
|
92
|
+
this.secure = !!meta.secure;
|
|
93
|
+
|
|
94
|
+
/** Maximum incoming frame payload in bytes (default 1 MB). */
|
|
95
|
+
this.maxPayload = meta.maxPayload || 1048576;
|
|
96
|
+
|
|
97
|
+
/** Timestamp (ms) when the connection was established. */
|
|
98
|
+
this.connectedAt = Date.now();
|
|
99
|
+
|
|
100
|
+
/** Arbitrary user-data store. Attach anything you need. */
|
|
101
|
+
this.data = {};
|
|
102
|
+
|
|
103
|
+
/** @type {Object<string, Function[]>} */
|
|
104
|
+
this._listeners = {};
|
|
105
|
+
|
|
106
|
+
/** @private */
|
|
107
|
+
this._pingTimer = null;
|
|
108
|
+
|
|
109
|
+
// Set up auto-ping keep-alive
|
|
110
|
+
const pingInterval = meta.pingInterval !== undefined ? meta.pingInterval : 30000;
|
|
111
|
+
if (pingInterval > 0)
|
|
112
|
+
{
|
|
113
|
+
this._pingTimer = setInterval(() => this.ping(), pingInterval);
|
|
114
|
+
if (this._pingTimer.unref) this._pingTimer.unref();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
socket.on('data', (chunk) => this._onData(chunk));
|
|
118
|
+
socket.on('close', () =>
|
|
119
|
+
{
|
|
120
|
+
if (this.readyState !== WS_READY_STATE.CLOSED)
|
|
121
|
+
{
|
|
122
|
+
this.readyState = WS_READY_STATE.CLOSED;
|
|
123
|
+
this._clearPing();
|
|
124
|
+
this._emit('close', 1006, '');
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
socket.on('error', (err) => this._emit('error', err));
|
|
128
|
+
socket.on('drain', () => this._emit('drain'));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// -- Event Emitter ---------------------------------
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Register an event listener.
|
|
135
|
+
* @param {'message'|'close'|'error'|'pong'|'ping'|'drain'} event
|
|
136
|
+
* @param {Function} fn
|
|
137
|
+
* @returns {WebSocketConnection} this
|
|
138
|
+
*/
|
|
139
|
+
on(event, fn)
|
|
140
|
+
{
|
|
141
|
+
if (!this._listeners[event]) this._listeners[event] = [];
|
|
142
|
+
this._listeners[event].push(fn);
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Register a one-time event listener.
|
|
148
|
+
* @param {'message'|'close'|'error'|'pong'|'ping'|'drain'} event
|
|
149
|
+
* @param {Function} fn
|
|
150
|
+
* @returns {WebSocketConnection} this
|
|
151
|
+
*/
|
|
152
|
+
once(event, fn)
|
|
153
|
+
{
|
|
154
|
+
const wrapper = (...args) => { this.off(event, wrapper); fn(...args); };
|
|
155
|
+
wrapper._original = fn;
|
|
156
|
+
return this.on(event, wrapper);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Remove a specific event listener.
|
|
161
|
+
* @param {string} event
|
|
162
|
+
* @param {Function} fn
|
|
163
|
+
* @returns {WebSocketConnection} this
|
|
164
|
+
*/
|
|
165
|
+
off(event, fn)
|
|
166
|
+
{
|
|
167
|
+
const list = this._listeners[event];
|
|
168
|
+
if (!list) return this;
|
|
169
|
+
this._listeners[event] = list.filter(f => f !== fn && f._original !== fn);
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Remove all listeners for an event, or all events if none specified.
|
|
175
|
+
* @param {string} [event]
|
|
176
|
+
* @returns {WebSocketConnection} this
|
|
177
|
+
*/
|
|
178
|
+
removeAllListeners(event)
|
|
179
|
+
{
|
|
180
|
+
if (event) delete this._listeners[event];
|
|
181
|
+
else this._listeners = {};
|
|
182
|
+
return this;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Count listeners for a given event.
|
|
187
|
+
* @param {string} event
|
|
188
|
+
* @returns {number}
|
|
189
|
+
*/
|
|
190
|
+
listenerCount(event)
|
|
191
|
+
{
|
|
192
|
+
return (this._listeners[event] || []).length;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** @private */
|
|
196
|
+
_emit(event, ...args)
|
|
197
|
+
{
|
|
198
|
+
const fns = this._listeners[event];
|
|
199
|
+
if (fns) fns.slice().forEach(fn => { try { fn(...args); } catch (e) { } });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// -- Sending ---------------------------------------
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Send a text or binary message.
|
|
206
|
+
* @param {string|Buffer} data - Payload.
|
|
207
|
+
* @param {object} [opts]
|
|
208
|
+
* @param {boolean} [opts.binary] - Force binary frame (opcode 0x02).
|
|
209
|
+
* @param {Function} [opts.callback] - Called after the data is flushed.
|
|
210
|
+
* @returns {boolean} `false` if the socket buffer is full (backpressure).
|
|
211
|
+
*/
|
|
212
|
+
send(data, opts)
|
|
213
|
+
{
|
|
214
|
+
if (this.readyState !== WS_READY_STATE.OPEN) return false;
|
|
215
|
+
const cb = opts && opts.callback;
|
|
216
|
+
const forceBinary = opts && opts.binary;
|
|
217
|
+
const isBinary = forceBinary || Buffer.isBuffer(data);
|
|
218
|
+
const opcode = isBinary ? 0x02 : 0x01;
|
|
219
|
+
const payload = isBinary ? (Buffer.isBuffer(data) ? data : Buffer.from(data)) : Buffer.from(String(data), 'utf8');
|
|
220
|
+
const frame = this._buildFrame(opcode, payload);
|
|
221
|
+
try { return this._socket.write(frame, cb); } catch (e) { return false; }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Send a JSON-serialised message (sets text frame).
|
|
226
|
+
* @param {*} obj - Value to serialise.
|
|
227
|
+
* @param {Function} [cb] - Called after the data is flushed.
|
|
228
|
+
* @returns {boolean}
|
|
229
|
+
*/
|
|
230
|
+
sendJSON(obj, cb)
|
|
231
|
+
{
|
|
232
|
+
return this.send(JSON.stringify(obj), { callback: cb });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Send a ping frame.
|
|
237
|
+
* @param {string|Buffer} [payload] - Optional payload (max 125 bytes).
|
|
238
|
+
* @param {Function} [cb] - Called after the frame is flushed.
|
|
239
|
+
* @returns {boolean}
|
|
240
|
+
*/
|
|
241
|
+
ping(payload, cb)
|
|
242
|
+
{
|
|
243
|
+
if (this.readyState !== WS_READY_STATE.OPEN) return false;
|
|
244
|
+
const data = payload ? (Buffer.isBuffer(payload) ? payload : Buffer.from(String(payload))) : Buffer.alloc(0);
|
|
245
|
+
try { return this._socket.write(this._buildFrame(0x09, data), cb); } catch (e) { return false; }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Send a pong frame.
|
|
250
|
+
* @param {string|Buffer} [payload] - Optional payload.
|
|
251
|
+
* @param {Function} [cb] - Called after the frame is flushed.
|
|
252
|
+
* @returns {boolean}
|
|
253
|
+
*/
|
|
254
|
+
pong(payload, cb)
|
|
255
|
+
{
|
|
256
|
+
if (this.readyState !== WS_READY_STATE.OPEN) return false;
|
|
257
|
+
const data = payload ? (Buffer.isBuffer(payload) ? payload : Buffer.from(String(payload))) : Buffer.alloc(0);
|
|
258
|
+
try { return this._socket.write(this._buildFrame(0x0A, data), cb); } catch (e) { return false; }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Close the WebSocket connection.
|
|
263
|
+
* @param {number} [code=1000] - Close status code.
|
|
264
|
+
* @param {string} [reason] - Close reason string.
|
|
265
|
+
*/
|
|
266
|
+
close(code, reason)
|
|
267
|
+
{
|
|
268
|
+
if (this.readyState === WS_READY_STATE.CLOSED || this.readyState === WS_READY_STATE.CLOSING) return;
|
|
269
|
+
this.readyState = WS_READY_STATE.CLOSING;
|
|
270
|
+
this._clearPing();
|
|
271
|
+
const statusCode = code || 1000;
|
|
272
|
+
const reasonBuf = reason ? Buffer.from(String(reason), 'utf8') : Buffer.alloc(0);
|
|
273
|
+
const payload = Buffer.alloc(2 + reasonBuf.length);
|
|
274
|
+
payload.writeUInt16BE(statusCode, 0);
|
|
275
|
+
reasonBuf.copy(payload, 2);
|
|
276
|
+
try
|
|
277
|
+
{
|
|
278
|
+
this._socket.write(this._buildFrame(0x08, payload));
|
|
279
|
+
this._socket.end();
|
|
280
|
+
}
|
|
281
|
+
catch (e) { }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Forcefully destroy the underlying socket without a close frame.
|
|
286
|
+
*/
|
|
287
|
+
terminate()
|
|
288
|
+
{
|
|
289
|
+
this.readyState = WS_READY_STATE.CLOSED;
|
|
290
|
+
this._clearPing();
|
|
291
|
+
try { this._socket.destroy(); } catch (e) { }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// -- Computed Properties ---------------------------
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Bytes waiting in the send buffer.
|
|
298
|
+
* @type {number}
|
|
299
|
+
*/
|
|
300
|
+
get bufferedAmount()
|
|
301
|
+
{
|
|
302
|
+
return this._socket ? (this._socket.writableLength || 0) : 0;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* How long this connection has been alive (ms).
|
|
307
|
+
* @type {number}
|
|
308
|
+
*/
|
|
309
|
+
get uptime()
|
|
310
|
+
{
|
|
311
|
+
return Date.now() - this.connectedAt;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// -- Internals -------------------------------------
|
|
315
|
+
|
|
316
|
+
/** @private */
|
|
317
|
+
_clearPing()
|
|
318
|
+
{
|
|
319
|
+
if (this._pingTimer) { clearInterval(this._pingTimer); this._pingTimer = null; }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** @private Build a WebSocket frame. */
|
|
323
|
+
_buildFrame(opcode, payload)
|
|
324
|
+
{
|
|
325
|
+
const len = payload.length;
|
|
326
|
+
let header;
|
|
327
|
+
if (len < 126)
|
|
328
|
+
{
|
|
329
|
+
header = Buffer.alloc(2);
|
|
330
|
+
header[0] = 0x80 | opcode; // FIN + opcode
|
|
331
|
+
header[1] = len;
|
|
332
|
+
}
|
|
333
|
+
else if (len < 65536)
|
|
334
|
+
{
|
|
335
|
+
header = Buffer.alloc(4);
|
|
336
|
+
header[0] = 0x80 | opcode;
|
|
337
|
+
header[1] = 126;
|
|
338
|
+
header.writeUInt16BE(len, 2);
|
|
339
|
+
}
|
|
340
|
+
else
|
|
341
|
+
{
|
|
342
|
+
header = Buffer.alloc(10);
|
|
343
|
+
header[0] = 0x80 | opcode;
|
|
344
|
+
header[1] = 127;
|
|
345
|
+
header.writeUInt32BE(0, 2);
|
|
346
|
+
header.writeUInt32BE(len, 6);
|
|
347
|
+
}
|
|
348
|
+
return Buffer.concat([header, payload]);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** @private Parse incoming WebSocket frames. */
|
|
352
|
+
_onData(chunk)
|
|
353
|
+
{
|
|
354
|
+
this._buffer = Buffer.concat([this._buffer, chunk]);
|
|
355
|
+
|
|
356
|
+
while (this._buffer.length >= 2)
|
|
357
|
+
{
|
|
358
|
+
const firstByte = this._buffer[0];
|
|
359
|
+
const secondByte = this._buffer[1];
|
|
360
|
+
const opcode = firstByte & 0x0F;
|
|
361
|
+
const masked = (secondByte & 0x80) !== 0;
|
|
362
|
+
let payloadLen = secondByte & 0x7F;
|
|
363
|
+
let offset = 2;
|
|
364
|
+
|
|
365
|
+
if (payloadLen === 126)
|
|
366
|
+
{
|
|
367
|
+
if (this._buffer.length < 4) return;
|
|
368
|
+
payloadLen = this._buffer.readUInt16BE(2);
|
|
369
|
+
offset = 4;
|
|
370
|
+
}
|
|
371
|
+
else if (payloadLen === 127)
|
|
372
|
+
{
|
|
373
|
+
if (this._buffer.length < 10) return;
|
|
374
|
+
payloadLen = this._buffer.readUInt32BE(6);
|
|
375
|
+
offset = 10;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Enforce max payload
|
|
379
|
+
if (payloadLen > this.maxPayload)
|
|
380
|
+
{
|
|
381
|
+
this.close(1009, 'Message too big');
|
|
382
|
+
this._buffer = Buffer.alloc(0);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const maskSize = masked ? 4 : 0;
|
|
387
|
+
const totalLen = offset + maskSize + payloadLen;
|
|
388
|
+
if (this._buffer.length < totalLen) return;
|
|
389
|
+
|
|
390
|
+
let payload = this._buffer.slice(offset + maskSize, totalLen);
|
|
391
|
+
if (masked)
|
|
392
|
+
{
|
|
393
|
+
const mask = this._buffer.slice(offset, offset + 4);
|
|
394
|
+
payload = Buffer.alloc(payloadLen);
|
|
395
|
+
for (let i = 0; i < payloadLen; i++)
|
|
396
|
+
{
|
|
397
|
+
payload[i] = this._buffer[offset + maskSize + i] ^ mask[i & 3];
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
this._buffer = this._buffer.slice(totalLen);
|
|
402
|
+
|
|
403
|
+
switch (opcode)
|
|
404
|
+
{
|
|
405
|
+
case 0x01: // text
|
|
406
|
+
this._emit('message', payload.toString('utf8'));
|
|
407
|
+
break;
|
|
408
|
+
case 0x02: // binary
|
|
409
|
+
this._emit('message', payload);
|
|
410
|
+
break;
|
|
411
|
+
case 0x08: // close
|
|
412
|
+
{
|
|
413
|
+
const closeCode = payload.length >= 2 ? payload.readUInt16BE(0) : 1005;
|
|
414
|
+
const closeReason = payload.length > 2 ? payload.slice(2).toString('utf8') : '';
|
|
415
|
+
this.readyState = WS_READY_STATE.CLOSED;
|
|
416
|
+
this._clearPing();
|
|
417
|
+
try { this._socket.write(this._buildFrame(0x08, payload)); } catch (e) { }
|
|
418
|
+
this._socket.end();
|
|
419
|
+
this._emit('close', closeCode, closeReason);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
case 0x09: // ping
|
|
423
|
+
this._emit('ping', payload);
|
|
424
|
+
try { this._socket.write(this._buildFrame(0x0A, payload)); } catch (e) { }
|
|
425
|
+
break;
|
|
426
|
+
case 0x0A: // pong
|
|
427
|
+
this._emit('pong', payload);
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Ready-state constants exposed on the class for convenience. */
|
|
435
|
+
WebSocketConnection.CONNECTING = WS_READY_STATE.CONNECTING;
|
|
436
|
+
WebSocketConnection.OPEN = WS_READY_STATE.OPEN;
|
|
437
|
+
WebSocketConnection.CLOSING = WS_READY_STATE.CLOSING;
|
|
438
|
+
WebSocketConnection.CLOSED = WS_READY_STATE.CLOSED;
|
|
439
|
+
|
|
440
|
+
module.exports = WebSocketConnection;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module ws/handshake
|
|
3
|
+
* @description WebSocket upgrade handshake logic (RFC 6455).
|
|
4
|
+
* Handles the HTTP → WS upgrade, verifyClient, sub-protocol
|
|
5
|
+
* negotiation, query parsing, and instantiation of a
|
|
6
|
+
* {@link WebSocketConnection}.
|
|
7
|
+
*/
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const WebSocketConnection = require('./connection');
|
|
10
|
+
|
|
11
|
+
/** RFC 6455 magic GUID used in the Sec-WebSocket-Accept hash. */
|
|
12
|
+
const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Handle an HTTP upgrade request and turn it into a WebSocket connection.
|
|
16
|
+
*
|
|
17
|
+
* @param {import('http').IncomingMessage} req - The upgrade request.
|
|
18
|
+
* @param {import('net').Socket} socket - The underlying TCP socket.
|
|
19
|
+
* @param {Buffer} head - First packet of the upgraded stream.
|
|
20
|
+
* @param {Map<string, { handler: Function, opts: object }>} wsHandlers - Registered WS path→handler map.
|
|
21
|
+
*/
|
|
22
|
+
function handleUpgrade(req, socket, head, wsHandlers)
|
|
23
|
+
{
|
|
24
|
+
// Guard against socket errors (e.g. ECONNRESET from restricted webviews)
|
|
25
|
+
socket.on('error', () => {});
|
|
26
|
+
|
|
27
|
+
const urlPath = req.url.split('?')[0];
|
|
28
|
+
const entry = wsHandlers.get(urlPath);
|
|
29
|
+
|
|
30
|
+
if (!entry)
|
|
31
|
+
{
|
|
32
|
+
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
|
|
33
|
+
socket.destroy();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { handler, opts } = entry;
|
|
38
|
+
|
|
39
|
+
// -- Optional client verification ------------------
|
|
40
|
+
if (typeof opts.verifyClient === 'function')
|
|
41
|
+
{
|
|
42
|
+
try
|
|
43
|
+
{
|
|
44
|
+
if (!opts.verifyClient(req))
|
|
45
|
+
{
|
|
46
|
+
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
|
47
|
+
socket.destroy();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (e)
|
|
52
|
+
{
|
|
53
|
+
socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
|
|
54
|
+
socket.destroy();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// -- Validate key ----------------------------------
|
|
60
|
+
const key = req.headers['sec-websocket-key'];
|
|
61
|
+
if (!key)
|
|
62
|
+
{
|
|
63
|
+
socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
64
|
+
socket.destroy();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// -- Negotiate sub-protocol ------------------------
|
|
69
|
+
const clientProtocols = req.headers['sec-websocket-protocol'];
|
|
70
|
+
const extensions = req.headers['sec-websocket-extensions'] || '';
|
|
71
|
+
|
|
72
|
+
// -- Perform the WebSocket handshake (RFC 6455) ----
|
|
73
|
+
const accept = crypto.createHash('sha1').update(key + WS_MAGIC).digest('base64');
|
|
74
|
+
|
|
75
|
+
let handshake =
|
|
76
|
+
'HTTP/1.1 101 Switching Protocols\r\n' +
|
|
77
|
+
'Upgrade: websocket\r\n' +
|
|
78
|
+
'Connection: Upgrade\r\n' +
|
|
79
|
+
'Sec-WebSocket-Accept: ' + accept + '\r\n';
|
|
80
|
+
|
|
81
|
+
// Echo first requested protocol if any
|
|
82
|
+
const protocol = clientProtocols ? clientProtocols.split(',')[0].trim() : '';
|
|
83
|
+
if (protocol) handshake += 'Sec-WebSocket-Protocol: ' + protocol + '\r\n';
|
|
84
|
+
|
|
85
|
+
handshake += '\r\n';
|
|
86
|
+
socket.write(handshake);
|
|
87
|
+
|
|
88
|
+
// -- Parse query string ----------------------------
|
|
89
|
+
const qIdx = req.url.indexOf('?');
|
|
90
|
+
const query = {};
|
|
91
|
+
if (qIdx !== -1)
|
|
92
|
+
{
|
|
93
|
+
for (const [k, v] of new URLSearchParams(req.url.slice(qIdx + 1)))
|
|
94
|
+
{
|
|
95
|
+
query[k] = v;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// -- Create the connection wrapper -----------------
|
|
100
|
+
const ws = new WebSocketConnection(socket, {
|
|
101
|
+
maxPayload: opts.maxPayload,
|
|
102
|
+
pingInterval: opts.pingInterval,
|
|
103
|
+
protocol,
|
|
104
|
+
extensions,
|
|
105
|
+
headers: req.headers,
|
|
106
|
+
ip: req.socket ? req.socket.remoteAddress : null,
|
|
107
|
+
query,
|
|
108
|
+
url: req.url,
|
|
109
|
+
secure: !!(req.socket && req.socket.encrypted),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
try
|
|
113
|
+
{
|
|
114
|
+
handler(ws, req);
|
|
115
|
+
}
|
|
116
|
+
catch (err)
|
|
117
|
+
{
|
|
118
|
+
ws.close(1011, 'Internal error');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = handleUpgrade;
|
package/lib/ws/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module ws
|
|
3
|
+
* @description WebSocket support for zero-http.
|
|
4
|
+
* Exports the connection class and upgrade handler.
|
|
5
|
+
*/
|
|
6
|
+
const WebSocketConnection = require('./connection');
|
|
7
|
+
const handleUpgrade = require('./handshake');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
WebSocketConnection,
|
|
11
|
+
handleUpgrade,
|
|
12
|
+
};
|
package/package.json
CHANGED
package/lib/router.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module router
|
|
3
|
-
* @description Simple pattern-matching router with named parameters,
|
|
4
|
-
* wildcard catch-alls, and sequential handler chains.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Convert a route path pattern into a RegExp and extract named parameter keys.
|
|
9
|
-
* Supports `:param` segments and trailing `*` wildcards.
|
|
10
|
-
*
|
|
11
|
-
* @param {string} path - Route pattern (e.g. '/users/:id', '/api/*').
|
|
12
|
-
* @returns {{ regex: RegExp, keys: string[] }} Compiled regex and ordered parameter names.
|
|
13
|
-
*/
|
|
14
|
-
function pathToRegex(path)
|
|
15
|
-
{
|
|
16
|
-
// Wildcard catch-all: /api/*
|
|
17
|
-
if (path.endsWith('*'))
|
|
18
|
-
{
|
|
19
|
-
const prefix = path.slice(0, -1); // e.g. "/api/"
|
|
20
|
-
const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
21
|
-
return { regex: new RegExp('^' + escaped + '(.*)$'), keys: ['0'] };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const parts = path.split('/').filter(Boolean);
|
|
25
|
-
const keys = [];
|
|
26
|
-
const pattern = parts.map(p =>
|
|
27
|
-
{
|
|
28
|
-
if (p.startsWith(':')) { keys.push(p.slice(1)); return '([^/]+)'; }
|
|
29
|
-
return p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
30
|
-
}).join('/');
|
|
31
|
-
return { regex: new RegExp('^/' + pattern + '/?$'), keys };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
class Router
|
|
35
|
-
{
|
|
36
|
-
/** Create a new Router with an empty route table. */
|
|
37
|
-
constructor() { this.routes = []; }
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Register a route.
|
|
41
|
-
*
|
|
42
|
-
* @param {string} method - HTTP method (e.g. 'GET') or 'ALL' to match any.
|
|
43
|
-
* @param {string} path - Route pattern.
|
|
44
|
-
* @param {Function[]} handlers - One or more handler functions `(req, res, next) => void`.
|
|
45
|
-
*/
|
|
46
|
-
add(method, path, handlers)
|
|
47
|
-
{
|
|
48
|
-
const { regex, keys } = pathToRegex(path);
|
|
49
|
-
this.routes.push({ method: method.toUpperCase(), path, regex, keys, handlers });
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Match an incoming request against the route table and execute the first
|
|
54
|
-
* matching handler chain. Sends a 404 JSON response when no route matches.
|
|
55
|
-
*
|
|
56
|
-
* @param {import('./request')} req - Wrapped request.
|
|
57
|
-
* @param {import('./response')} res - Wrapped response.
|
|
58
|
-
*/
|
|
59
|
-
handle(req, res)
|
|
60
|
-
{
|
|
61
|
-
const method = req.method.toUpperCase();
|
|
62
|
-
const url = req.url.split('?')[0];
|
|
63
|
-
for (const r of this.routes)
|
|
64
|
-
{
|
|
65
|
-
// ALL matches any method
|
|
66
|
-
if (r.method !== 'ALL' && r.method !== method) continue;
|
|
67
|
-
const m = url.match(r.regex);
|
|
68
|
-
if (!m) continue;
|
|
69
|
-
req.params = {};
|
|
70
|
-
r.keys.forEach((k, i) => req.params[k] = decodeURIComponent(m[i + 1] || ''));
|
|
71
|
-
// run handlers sequentially
|
|
72
|
-
let idx = 0;
|
|
73
|
-
const next = () =>
|
|
74
|
-
{
|
|
75
|
-
if (idx < r.handlers.length)
|
|
76
|
-
{
|
|
77
|
-
const h = r.handlers[idx++];
|
|
78
|
-
return h(req, res, next);
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
return next();
|
|
82
|
-
}
|
|
83
|
-
res.status(404).json({ error: 'Not Found' });
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
module.exports = Router;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|