lupine.api 1.1.58 → 1.1.59
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 +3 -3
- package/admin/admin-about.tsx +12 -16
- package/admin/admin-config.tsx +47 -44
- package/admin/admin-css.tsx +3 -3
- package/admin/admin-db.tsx +75 -75
- package/admin/admin-frame-helper.tsx +364 -364
- package/admin/admin-frame.tsx +164 -164
- package/admin/admin-index.tsx +65 -65
- package/admin/admin-login.tsx +111 -111
- package/admin/admin-menu-edit.tsx +637 -637
- package/admin/admin-menu-list.tsx +87 -87
- package/admin/admin-page-edit.tsx +564 -564
- package/admin/admin-page-list.tsx +83 -83
- package/admin/admin-performance.tsx +28 -28
- package/admin/admin-release.tsx +427 -426
- package/admin/admin-resources.tsx +382 -382
- package/admin/admin-shell.tsx +89 -89
- package/admin/admin-table-data.tsx +146 -146
- package/admin/admin-table-list.tsx +230 -230
- package/admin/admin-test-animations.tsx +395 -395
- package/admin/admin-test-component.tsx +823 -808
- package/admin/admin-test-edit.tsx +319 -319
- package/admin/admin-test-themes.tsx +56 -56
- package/admin/admin-tokens.tsx +338 -338
- package/admin/design/admin-design.tsx +174 -174
- package/admin/design/block-grid.tsx +36 -36
- package/admin/design/block-grid1.tsx +21 -21
- package/admin/design/block-paragraph.tsx +19 -19
- package/admin/design/block-title.tsx +19 -19
- package/admin/design/design-block-box.tsx +140 -140
- package/admin/design/drag-data.tsx +24 -24
- package/admin/index.ts +9 -9
- package/admin/package.json +15 -15
- package/admin/tsconfig.json +127 -127
- package/dev/copy-folder.js +32 -32
- package/dev/cp-index-html.js +69 -69
- package/dev/file-utils.js +12 -12
- package/dev/index.js +18 -19
- package/dev/package.json +12 -12
- package/dev/plugin-ifelse.js +168 -168
- package/dev/plugin-ifelse.test.js +37 -37
- package/dev/run-cmd.js +14 -14
- package/dev/send-request.js +12 -12
- package/package.json +55 -55
- package/src/admin-api/admin-api-helper.ts +210 -205
- package/src/admin-api/admin-api.ts +65 -65
- package/src/admin-api/admin-auth.ts +152 -146
- package/src/admin-api/admin-config.ts +94 -84
- package/src/admin-api/admin-csv.ts +94 -94
- package/src/admin-api/admin-db.ts +269 -269
- package/src/admin-api/admin-menu.ts +135 -135
- package/src/admin-api/admin-page.ts +135 -135
- package/src/admin-api/admin-performance.ts +128 -128
- package/src/admin-api/admin-release.ts +703 -700
- package/src/admin-api/admin-resources.ts +318 -318
- package/src/admin-api/admin-token-helper.ts +82 -79
- package/src/admin-api/admin-tokens.ts +90 -90
- package/src/admin-api/index.ts +2 -2
- package/src/admin-api/web-config-api.ts +19 -19
- package/src/api/api-cache.ts +103 -103
- package/src/api/api-helper.ts +44 -44
- package/src/api/api-module.ts +67 -60
- package/src/api/api-router.ts +177 -177
- package/src/api/api-shared-storage.ts +64 -64
- package/src/api/async-storage.ts +5 -5
- package/src/api/debug-service.ts +56 -56
- package/src/api/encode-html.ts +27 -27
- package/src/api/handle-status.ts +75 -75
- package/src/api/index.ts +15 -16
- package/src/api/mini-web-socket.ts +270 -270
- package/src/api/server-content-type.ts +82 -82
- package/src/api/server-render.ts +235 -215
- package/src/api/shell-service.ts +74 -74
- package/src/api/simple-storage.ts +80 -80
- package/src/api/static-server.ts +128 -125
- package/src/api/to-client-delivery.ts +26 -26
- package/src/app/app-cache.ts +55 -55
- package/src/app/app-helper.ts +62 -62
- package/src/app/app-message.ts +109 -109
- package/src/app/app-shared-storage.ts +363 -363
- package/src/app/app-start.ts +136 -136
- package/src/app/cleanup-exit.ts +16 -16
- package/src/app/host-to-path.ts +38 -38
- package/src/app/index.ts +11 -11
- package/src/app/process-dev-requests.ts +130 -130
- package/src/app/web-listener.ts +294 -294
- package/src/app/web-processor.ts +47 -42
- package/src/app/web-server.ts +100 -100
- package/src/common-js/web-env.js +104 -104
- package/src/index.ts +7 -7
- package/src/lang/api-lang-en.ts +26 -26
- package/src/lang/api-lang-zh-cn.ts +27 -27
- package/src/lang/index.ts +2 -2
- package/src/lang/lang-helper.ts +76 -76
- package/src/lang/lang-props.ts +6 -6
- package/src/lib/db/db-helper.ts +23 -23
- package/src/lib/db/db-mysql.ts +249 -250
- package/src/lib/db/db-sqlite.ts +101 -101
- package/src/lib/db/db.spec.ts +28 -28
- package/src/lib/db/db.ts +325 -325
- package/src/lib/db/index.ts +5 -5
- package/src/lib/index.ts +3 -3
- package/src/lib/logger.spec.ts +214 -214
- package/src/lib/logger.ts +281 -281
- package/src/lib/runtime-require.ts +37 -37
- package/src/lib/utils/cookie-util.ts +34 -34
- package/src/lib/utils/crypto.ts +58 -58
- package/src/lib/utils/date-utils.ts +317 -317
- package/src/lib/utils/deep-merge.ts +37 -37
- package/src/lib/utils/delay.ts +12 -12
- package/src/lib/utils/file-setting.ts +55 -55
- package/src/lib/utils/format-bytes.ts +11 -11
- package/src/lib/utils/fs-utils.ts +158 -158
- package/src/lib/utils/get-env.ts +27 -27
- package/src/lib/utils/index.ts +12 -12
- package/src/lib/utils/is-type.ts +48 -48
- package/src/lib/utils/load-env.ts +14 -14
- package/src/lib/utils/pad.ts +6 -6
- package/src/models/api-base.ts +5 -5
- package/src/models/api-module-props.ts +10 -11
- package/src/models/api-router-props.ts +26 -26
- package/src/models/app-cache-props.ts +33 -33
- package/src/models/app-data-props.ts +10 -10
- package/src/models/app-helper-props.ts +6 -6
- package/src/models/app-shared-storage-props.ts +38 -38
- package/src/models/app-start-props.ts +18 -18
- package/src/models/async-storage-props.ts +13 -13
- package/src/models/db-config.ts +30 -30
- package/src/models/host-to-path-props.ts +12 -12
- package/src/models/index.ts +16 -16
- package/src/models/json-object.ts +8 -8
- package/src/models/locals-props.ts +36 -36
- package/src/models/logger-props.ts +84 -84
- package/src/models/simple-storage-props.ts +13 -14
- package/src/models/to-client-delivery-props.ts +6 -6
- package/tsconfig.json +115 -115
- package/dev/plugin-gen-versions.js +0 -20
|
@@ -1,270 +1,270 @@
|
|
|
1
|
-
import { IncomingMessage } from 'http';
|
|
2
|
-
import { Duplex } from 'stream';
|
|
3
|
-
import crypto from 'crypto';
|
|
4
|
-
|
|
5
|
-
// https://github.com/ErickWendel/websockets-with-nodejs-from-scratch/blob/main/nodejs-raw-websocket/server.mjs
|
|
6
|
-
// https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers
|
|
7
|
-
// https://github.com/websockets/ws/blob/d343a0cf7bba29a4e14217cb010446bec8fdf444/lib/receiver.js
|
|
8
|
-
|
|
9
|
-
// This is a specification specific to WebSocket protocol
|
|
10
|
-
const WEBSOCKET_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
11
|
-
|
|
12
|
-
const MASK_KEY_BYTES_LENGTH = 4;
|
|
13
|
-
// opcodes
|
|
14
|
-
const OPCODE_CONTINUATION = 0x0;
|
|
15
|
-
const OPCODE_TEXT = 0x1;
|
|
16
|
-
const OPCODE_BINARY = 0x2;
|
|
17
|
-
const OPCODE_CLOSE = 0x8;
|
|
18
|
-
const OPCODE_PING = 0x9;
|
|
19
|
-
const OPCODE_PONG = 0xa;
|
|
20
|
-
|
|
21
|
-
// This is only used in debug mode (no clusters)
|
|
22
|
-
export type MiniWebSocketMsgProps = (msg: string, socket: Duplex) => void;
|
|
23
|
-
export type MiniWebSocketCloseProps = (socket: Duplex, status: string) => void;
|
|
24
|
-
export class MiniWebSocket {
|
|
25
|
-
clientRefreshFlag = Date.now();
|
|
26
|
-
clients = new Set<Duplex>();
|
|
27
|
-
private _onMessage: MiniWebSocketMsgProps;
|
|
28
|
-
private _onClose?: MiniWebSocketCloseProps;
|
|
29
|
-
|
|
30
|
-
constructor(onMessage: MiniWebSocketMsgProps, onClose?: MiniWebSocketCloseProps) {
|
|
31
|
-
this._onMessage = onMessage;
|
|
32
|
-
this._onClose = onClose;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
public handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer) {
|
|
36
|
-
const key = req.headers['sec-websocket-key'];
|
|
37
|
-
const upgrade = (req.headers.upgrade || '').toString().toLowerCase();
|
|
38
|
-
const connection = (req.headers.connection || '').toString().toLowerCase();
|
|
39
|
-
|
|
40
|
-
if (!key || upgrade !== 'websocket' || !connection.includes('upgrade')) {
|
|
41
|
-
socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
42
|
-
socket.destroy();
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const cleanup = (socket: Duplex, status: string) => {
|
|
47
|
-
if (this.clients.has(socket)) this.clients.delete(socket);
|
|
48
|
-
if (!socket.destroyed) socket.destroy();
|
|
49
|
-
if (this._onClose) this._onClose(socket, status);
|
|
50
|
-
};
|
|
51
|
-
this.clients.add(socket);
|
|
52
|
-
socket.on('close', () => cleanup(socket, 'close'));
|
|
53
|
-
socket.on('error', () => cleanup(socket, 'error'));
|
|
54
|
-
socket.on('end', () => cleanup(socket, 'end'));
|
|
55
|
-
socket.write(this.handleHeaders(key));
|
|
56
|
-
|
|
57
|
-
// buffer for cumulative data, parse frame; first append head (possible residual data)
|
|
58
|
-
let buffer = head && head.length ? Buffer.from(head) : Buffer.alloc(0);
|
|
59
|
-
const onData = (chunk: Buffer) => {
|
|
60
|
-
buffer = Buffer.concat([buffer, chunk]);
|
|
61
|
-
// try to parse as many complete frames as possible
|
|
62
|
-
parseFrames();
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const parseFrames = () => {
|
|
66
|
-
while (true) {
|
|
67
|
-
if (buffer.length < 2) return;
|
|
68
|
-
|
|
69
|
-
const firstByte = buffer[0];
|
|
70
|
-
const secondByte = buffer[1];
|
|
71
|
-
|
|
72
|
-
const fin = (firstByte & 0x80) !== 0; // FIN bit
|
|
73
|
-
const rsv = firstByte & 0x70; // must be 0
|
|
74
|
-
const opcode = firstByte & 0x0f;
|
|
75
|
-
|
|
76
|
-
const masked = (secondByte & 0x80) !== 0; // must be 1
|
|
77
|
-
let payloadLen = secondByte & 0x7f;
|
|
78
|
-
|
|
79
|
-
let offset = 2;
|
|
80
|
-
|
|
81
|
-
// illegal case: RSV set bits; or client not mask
|
|
82
|
-
if (rsv !== 0 || !masked) {
|
|
83
|
-
this.sendClose(socket, 1002); // protocol error
|
|
84
|
-
cleanup(socket, 'protocol error');
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// only accept single frame (FIN=1) simple message, ignore fragmented/continuation frame
|
|
89
|
-
if (!fin && opcode !== OPCODE_CONTINUATION) {
|
|
90
|
-
this.sendClose(socket, 1003); // unsupported data
|
|
91
|
-
cleanup(socket, 'unsupported opcode');
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (payloadLen === 126) {
|
|
96
|
-
if (buffer.length < offset + 2) return;
|
|
97
|
-
payloadLen = buffer.readUInt16BE(offset);
|
|
98
|
-
offset += 2;
|
|
99
|
-
} else if (payloadLen === 127) {
|
|
100
|
-
// read 64-bit length: we only take low 32 bits (enough for your scene)
|
|
101
|
-
if (buffer.length < offset + 8) return;
|
|
102
|
-
// ignore high 32
|
|
103
|
-
payloadLen = buffer.readUInt32BE(offset + 4);
|
|
104
|
-
offset += 8;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const needed = offset + MASK_KEY_BYTES_LENGTH + payloadLen;
|
|
108
|
-
if (buffer.length < needed) return;
|
|
109
|
-
|
|
110
|
-
const maskKey = buffer.slice(offset, offset + MASK_KEY_BYTES_LENGTH);
|
|
111
|
-
offset += MASK_KEY_BYTES_LENGTH;
|
|
112
|
-
|
|
113
|
-
let payload = buffer.slice(offset, offset + payloadLen);
|
|
114
|
-
// unmask
|
|
115
|
-
for (let i = 0; i < payload.length; i++) {
|
|
116
|
-
payload[i] ^= maskKey[i % MASK_KEY_BYTES_LENGTH];
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// consume this frame
|
|
120
|
-
buffer = buffer.slice(needed);
|
|
121
|
-
|
|
122
|
-
switch (opcode) {
|
|
123
|
-
case OPCODE_TEXT: {
|
|
124
|
-
const text = payload.toString('utf8');
|
|
125
|
-
try {
|
|
126
|
-
this._onMessage(text, socket);
|
|
127
|
-
} catch {
|
|
128
|
-
// ignore user callback error
|
|
129
|
-
}
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
case OPCODE_BINARY: {
|
|
133
|
-
// don't support binary, close
|
|
134
|
-
this.sendClose(socket, 1003); // unsupported data
|
|
135
|
-
cleanup(socket, 'unsupported binary');
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
case OPCODE_PING: {
|
|
139
|
-
// reply Pong, with same payload
|
|
140
|
-
this.safeWrite(socket, this.encodeFrame(payload, OPCODE_PONG));
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
case OPCODE_PONG: {
|
|
144
|
-
// ignore
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
case OPCODE_CLOSE: {
|
|
148
|
-
// parse status code
|
|
149
|
-
let code = 1000;
|
|
150
|
-
if (payload.length >= 2) {
|
|
151
|
-
code = payload.readUInt16BE(0);
|
|
152
|
-
}
|
|
153
|
-
// reply Close, then end
|
|
154
|
-
this.sendClose(socket, code);
|
|
155
|
-
setTimeout(() => {
|
|
156
|
-
try {
|
|
157
|
-
socket.end();
|
|
158
|
-
} catch {}
|
|
159
|
-
cleanup(socket, 'close');
|
|
160
|
-
}, 20);
|
|
161
|
-
return; // end loop
|
|
162
|
-
}
|
|
163
|
-
case OPCODE_CONTINUATION: {
|
|
164
|
-
// don't support continuation, close
|
|
165
|
-
this.sendClose(socket, 1003);
|
|
166
|
-
cleanup(socket, 'unsupported continuation');
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
default: {
|
|
170
|
-
// unknown opcode
|
|
171
|
-
this.sendClose(socket, 1002);
|
|
172
|
-
cleanup(socket, 'unknown opcode');
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
|
-
socket.on('data', onData);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
private handleHeaders(key: string) {
|
|
182
|
-
const acceptKey = crypto
|
|
183
|
-
.createHash('sha1')
|
|
184
|
-
.update(key + WEBSOCKET_GUID)
|
|
185
|
-
.digest('base64');
|
|
186
|
-
const headers = [
|
|
187
|
-
'HTTP/1.1 101 Switching Protocols',
|
|
188
|
-
// 'Content-Type: text/html',
|
|
189
|
-
'Upgrade: websocket',
|
|
190
|
-
'Connection: Upgrade',
|
|
191
|
-
`Sec-WebSocket-Accept: ${acceptKey}`,
|
|
192
|
-
'\r\n',
|
|
193
|
-
];
|
|
194
|
-
return headers.join('\r\n');
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
public broadcast(msg: string) {
|
|
198
|
-
const payload = Buffer.from(msg);
|
|
199
|
-
const frame = this.encodeFrame(payload, OPCODE_TEXT);
|
|
200
|
-
for (const socket of this.clients) {
|
|
201
|
-
this.safeWrite(socket, frame);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
public sendMessage(socket: Duplex, msg: string) {
|
|
205
|
-
const payload = Buffer.from(msg);
|
|
206
|
-
this.safeWrite(socket, this.encodeFrame(payload, OPCODE_TEXT));
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
encodeFrame(payload: Buffer, opcode: number) {
|
|
210
|
-
const len = payload.length;
|
|
211
|
-
|
|
212
|
-
// 0x80 === 128 in binary
|
|
213
|
-
// FIN=1 + opcode
|
|
214
|
-
if (len <= 125) {
|
|
215
|
-
const header = Buffer.from([0x80 | opcode, len]);
|
|
216
|
-
return Buffer.concat([header, payload]);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (len < 0x10000) {
|
|
220
|
-
// alloc 4 bytes
|
|
221
|
-
// [0] - 128 + 1 - 10000001 fin + opcode
|
|
222
|
-
// [1] - 126 + 0 - payload length marker + mask indicator
|
|
223
|
-
// [2] 0 - content length
|
|
224
|
-
// [3] 113 - content length
|
|
225
|
-
// [ 4 - ..] - the message itself
|
|
226
|
-
const header = Buffer.allocUnsafe(4);
|
|
227
|
-
header[0] = 0x80 | opcode;
|
|
228
|
-
header[1] = 126;
|
|
229
|
-
header.writeUInt16BE(len, 2);
|
|
230
|
-
return Buffer.concat([header, payload]);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// 64-bit length:write high 32 bits as 0, low 32 bits as len (enough for your scene)
|
|
234
|
-
const header = Buffer.allocUnsafe(10);
|
|
235
|
-
header[0] = 0x80 | opcode;
|
|
236
|
-
header[1] = 127;
|
|
237
|
-
header.writeUInt32BE(0, 2); // high 32
|
|
238
|
-
header.writeUInt32BE(len, 6); // low 32
|
|
239
|
-
return Buffer.concat([header, payload]);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
sendClose(socket: Duplex, code = 1000) {
|
|
243
|
-
const payload = Buffer.allocUnsafe(2);
|
|
244
|
-
payload.writeUInt16BE(code, 0);
|
|
245
|
-
this.safeWrite(socket, this.encodeFrame(payload, OPCODE_CLOSE));
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
safeWrite(socket: Duplex, data: Buffer) {
|
|
249
|
-
try {
|
|
250
|
-
if (!socket.destroyed) socket.write(data);
|
|
251
|
-
} catch {
|
|
252
|
-
// ignore
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
close(socket: Duplex, code = 1000) {
|
|
257
|
-
this.sendClose(socket, code);
|
|
258
|
-
setTimeout(() => {
|
|
259
|
-
try {
|
|
260
|
-
socket.end();
|
|
261
|
-
} catch {}
|
|
262
|
-
}, 20);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
closeAll(code = 1000) {
|
|
266
|
-
for (const s of this.clients) {
|
|
267
|
-
this.close(s, code);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
1
|
+
import { IncomingMessage } from 'http';
|
|
2
|
+
import { Duplex } from 'stream';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
|
|
5
|
+
// https://github.com/ErickWendel/websockets-with-nodejs-from-scratch/blob/main/nodejs-raw-websocket/server.mjs
|
|
6
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers
|
|
7
|
+
// https://github.com/websockets/ws/blob/d343a0cf7bba29a4e14217cb010446bec8fdf444/lib/receiver.js
|
|
8
|
+
|
|
9
|
+
// This is a specification specific to WebSocket protocol
|
|
10
|
+
const WEBSOCKET_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
11
|
+
|
|
12
|
+
const MASK_KEY_BYTES_LENGTH = 4;
|
|
13
|
+
// opcodes
|
|
14
|
+
const OPCODE_CONTINUATION = 0x0;
|
|
15
|
+
const OPCODE_TEXT = 0x1;
|
|
16
|
+
const OPCODE_BINARY = 0x2;
|
|
17
|
+
const OPCODE_CLOSE = 0x8;
|
|
18
|
+
const OPCODE_PING = 0x9;
|
|
19
|
+
const OPCODE_PONG = 0xa;
|
|
20
|
+
|
|
21
|
+
// This is only used in debug mode (no clusters)
|
|
22
|
+
export type MiniWebSocketMsgProps = (msg: string, socket: Duplex) => void;
|
|
23
|
+
export type MiniWebSocketCloseProps = (socket: Duplex, status: string) => void;
|
|
24
|
+
export class MiniWebSocket {
|
|
25
|
+
clientRefreshFlag = Date.now();
|
|
26
|
+
clients = new Set<Duplex>();
|
|
27
|
+
private _onMessage: MiniWebSocketMsgProps;
|
|
28
|
+
private _onClose?: MiniWebSocketCloseProps;
|
|
29
|
+
|
|
30
|
+
constructor(onMessage: MiniWebSocketMsgProps, onClose?: MiniWebSocketCloseProps) {
|
|
31
|
+
this._onMessage = onMessage;
|
|
32
|
+
this._onClose = onClose;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer) {
|
|
36
|
+
const key = req.headers['sec-websocket-key'];
|
|
37
|
+
const upgrade = (req.headers.upgrade || '').toString().toLowerCase();
|
|
38
|
+
const connection = (req.headers.connection || '').toString().toLowerCase();
|
|
39
|
+
|
|
40
|
+
if (!key || upgrade !== 'websocket' || !connection.includes('upgrade')) {
|
|
41
|
+
socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
42
|
+
socket.destroy();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const cleanup = (socket: Duplex, status: string) => {
|
|
47
|
+
if (this.clients.has(socket)) this.clients.delete(socket);
|
|
48
|
+
if (!socket.destroyed) socket.destroy();
|
|
49
|
+
if (this._onClose) this._onClose(socket, status);
|
|
50
|
+
};
|
|
51
|
+
this.clients.add(socket);
|
|
52
|
+
socket.on('close', () => cleanup(socket, 'close'));
|
|
53
|
+
socket.on('error', () => cleanup(socket, 'error'));
|
|
54
|
+
socket.on('end', () => cleanup(socket, 'end'));
|
|
55
|
+
socket.write(this.handleHeaders(key));
|
|
56
|
+
|
|
57
|
+
// buffer for cumulative data, parse frame; first append head (possible residual data)
|
|
58
|
+
let buffer = head && head.length ? Buffer.from(head) : Buffer.alloc(0);
|
|
59
|
+
const onData = (chunk: Buffer) => {
|
|
60
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
61
|
+
// try to parse as many complete frames as possible
|
|
62
|
+
parseFrames();
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const parseFrames = () => {
|
|
66
|
+
while (true) {
|
|
67
|
+
if (buffer.length < 2) return;
|
|
68
|
+
|
|
69
|
+
const firstByte = buffer[0];
|
|
70
|
+
const secondByte = buffer[1];
|
|
71
|
+
|
|
72
|
+
const fin = (firstByte & 0x80) !== 0; // FIN bit
|
|
73
|
+
const rsv = firstByte & 0x70; // must be 0
|
|
74
|
+
const opcode = firstByte & 0x0f;
|
|
75
|
+
|
|
76
|
+
const masked = (secondByte & 0x80) !== 0; // must be 1
|
|
77
|
+
let payloadLen = secondByte & 0x7f;
|
|
78
|
+
|
|
79
|
+
let offset = 2;
|
|
80
|
+
|
|
81
|
+
// illegal case: RSV set bits; or client not mask
|
|
82
|
+
if (rsv !== 0 || !masked) {
|
|
83
|
+
this.sendClose(socket, 1002); // protocol error
|
|
84
|
+
cleanup(socket, 'protocol error');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// only accept single frame (FIN=1) simple message, ignore fragmented/continuation frame
|
|
89
|
+
if (!fin && opcode !== OPCODE_CONTINUATION) {
|
|
90
|
+
this.sendClose(socket, 1003); // unsupported data
|
|
91
|
+
cleanup(socket, 'unsupported opcode');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (payloadLen === 126) {
|
|
96
|
+
if (buffer.length < offset + 2) return;
|
|
97
|
+
payloadLen = buffer.readUInt16BE(offset);
|
|
98
|
+
offset += 2;
|
|
99
|
+
} else if (payloadLen === 127) {
|
|
100
|
+
// read 64-bit length: we only take low 32 bits (enough for your scene)
|
|
101
|
+
if (buffer.length < offset + 8) return;
|
|
102
|
+
// ignore high 32
|
|
103
|
+
payloadLen = buffer.readUInt32BE(offset + 4);
|
|
104
|
+
offset += 8;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const needed = offset + MASK_KEY_BYTES_LENGTH + payloadLen;
|
|
108
|
+
if (buffer.length < needed) return;
|
|
109
|
+
|
|
110
|
+
const maskKey = buffer.slice(offset, offset + MASK_KEY_BYTES_LENGTH);
|
|
111
|
+
offset += MASK_KEY_BYTES_LENGTH;
|
|
112
|
+
|
|
113
|
+
let payload = buffer.slice(offset, offset + payloadLen);
|
|
114
|
+
// unmask
|
|
115
|
+
for (let i = 0; i < payload.length; i++) {
|
|
116
|
+
payload[i] ^= maskKey[i % MASK_KEY_BYTES_LENGTH];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// consume this frame
|
|
120
|
+
buffer = buffer.slice(needed);
|
|
121
|
+
|
|
122
|
+
switch (opcode) {
|
|
123
|
+
case OPCODE_TEXT: {
|
|
124
|
+
const text = payload.toString('utf8');
|
|
125
|
+
try {
|
|
126
|
+
this._onMessage(text, socket);
|
|
127
|
+
} catch {
|
|
128
|
+
// ignore user callback error
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case OPCODE_BINARY: {
|
|
133
|
+
// don't support binary, close
|
|
134
|
+
this.sendClose(socket, 1003); // unsupported data
|
|
135
|
+
cleanup(socket, 'unsupported binary');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
case OPCODE_PING: {
|
|
139
|
+
// reply Pong, with same payload
|
|
140
|
+
this.safeWrite(socket, this.encodeFrame(payload, OPCODE_PONG));
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case OPCODE_PONG: {
|
|
144
|
+
// ignore
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case OPCODE_CLOSE: {
|
|
148
|
+
// parse status code
|
|
149
|
+
let code = 1000;
|
|
150
|
+
if (payload.length >= 2) {
|
|
151
|
+
code = payload.readUInt16BE(0);
|
|
152
|
+
}
|
|
153
|
+
// reply Close, then end
|
|
154
|
+
this.sendClose(socket, code);
|
|
155
|
+
setTimeout(() => {
|
|
156
|
+
try {
|
|
157
|
+
socket.end();
|
|
158
|
+
} catch {}
|
|
159
|
+
cleanup(socket, 'close');
|
|
160
|
+
}, 20);
|
|
161
|
+
return; // end loop
|
|
162
|
+
}
|
|
163
|
+
case OPCODE_CONTINUATION: {
|
|
164
|
+
// don't support continuation, close
|
|
165
|
+
this.sendClose(socket, 1003);
|
|
166
|
+
cleanup(socket, 'unsupported continuation');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
default: {
|
|
170
|
+
// unknown opcode
|
|
171
|
+
this.sendClose(socket, 1002);
|
|
172
|
+
cleanup(socket, 'unknown opcode');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
socket.on('data', onData);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private handleHeaders(key: string) {
|
|
182
|
+
const acceptKey = crypto
|
|
183
|
+
.createHash('sha1')
|
|
184
|
+
.update(key + WEBSOCKET_GUID)
|
|
185
|
+
.digest('base64');
|
|
186
|
+
const headers = [
|
|
187
|
+
'HTTP/1.1 101 Switching Protocols',
|
|
188
|
+
// 'Content-Type: text/html',
|
|
189
|
+
'Upgrade: websocket',
|
|
190
|
+
'Connection: Upgrade',
|
|
191
|
+
`Sec-WebSocket-Accept: ${acceptKey}`,
|
|
192
|
+
'\r\n',
|
|
193
|
+
];
|
|
194
|
+
return headers.join('\r\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
public broadcast(msg: string) {
|
|
198
|
+
const payload = Buffer.from(msg);
|
|
199
|
+
const frame = this.encodeFrame(payload, OPCODE_TEXT);
|
|
200
|
+
for (const socket of this.clients) {
|
|
201
|
+
this.safeWrite(socket, frame);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
public sendMessage(socket: Duplex, msg: string) {
|
|
205
|
+
const payload = Buffer.from(msg);
|
|
206
|
+
this.safeWrite(socket, this.encodeFrame(payload, OPCODE_TEXT));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
encodeFrame(payload: Buffer, opcode: number) {
|
|
210
|
+
const len = payload.length;
|
|
211
|
+
|
|
212
|
+
// 0x80 === 128 in binary
|
|
213
|
+
// FIN=1 + opcode
|
|
214
|
+
if (len <= 125) {
|
|
215
|
+
const header = Buffer.from([0x80 | opcode, len]);
|
|
216
|
+
return Buffer.concat([header, payload]);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (len < 0x10000) {
|
|
220
|
+
// alloc 4 bytes
|
|
221
|
+
// [0] - 128 + 1 - 10000001 fin + opcode
|
|
222
|
+
// [1] - 126 + 0 - payload length marker + mask indicator
|
|
223
|
+
// [2] 0 - content length
|
|
224
|
+
// [3] 113 - content length
|
|
225
|
+
// [ 4 - ..] - the message itself
|
|
226
|
+
const header = Buffer.allocUnsafe(4);
|
|
227
|
+
header[0] = 0x80 | opcode;
|
|
228
|
+
header[1] = 126;
|
|
229
|
+
header.writeUInt16BE(len, 2);
|
|
230
|
+
return Buffer.concat([header, payload]);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 64-bit length:write high 32 bits as 0, low 32 bits as len (enough for your scene)
|
|
234
|
+
const header = Buffer.allocUnsafe(10);
|
|
235
|
+
header[0] = 0x80 | opcode;
|
|
236
|
+
header[1] = 127;
|
|
237
|
+
header.writeUInt32BE(0, 2); // high 32
|
|
238
|
+
header.writeUInt32BE(len, 6); // low 32
|
|
239
|
+
return Buffer.concat([header, payload]);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
sendClose(socket: Duplex, code = 1000) {
|
|
243
|
+
const payload = Buffer.allocUnsafe(2);
|
|
244
|
+
payload.writeUInt16BE(code, 0);
|
|
245
|
+
this.safeWrite(socket, this.encodeFrame(payload, OPCODE_CLOSE));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
safeWrite(socket: Duplex, data: Buffer) {
|
|
249
|
+
try {
|
|
250
|
+
if (!socket.destroyed) socket.write(data);
|
|
251
|
+
} catch {
|
|
252
|
+
// ignore
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
close(socket: Duplex, code = 1000) {
|
|
257
|
+
this.sendClose(socket, code);
|
|
258
|
+
setTimeout(() => {
|
|
259
|
+
try {
|
|
260
|
+
socket.end();
|
|
261
|
+
} catch {}
|
|
262
|
+
}, 20);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
closeAll(code = 1000) {
|
|
266
|
+
for (const s of this.clients) {
|
|
267
|
+
this.close(s, code);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|