|
1 |
| -import * as net from 'net'; |
2 | 1 | import * as crypto from 'crypto';
|
| 2 | +import * as stream from 'stream'; |
3 | 3 | import {WsCloseFrame, WsFrameDecoder, WsFrameHeader, WsFrameOpcode, WsPingFrame, WsPongFrame} from '../codec';
|
4 | 4 | import {utf8Size} from '../../../../util/strings/utf8';
|
5 |
| -import {FanOut} from 'thingies/es2020/fanout'; |
| 5 | +import {listToUint8} from '../../../../util/buffers/concat'; |
6 | 6 | import type {WsFrameEncoder} from '../codec/WsFrameEncoder';
|
7 | 7 |
|
| 8 | +export type WsServerConnectionSocket = stream.Duplex; |
| 9 | + |
8 | 10 | export class WsServerConnection {
|
9 | 11 | public closed: boolean = false;
|
10 | 12 | public maxIncomingMessage: number = 2 * 1024 * 1024;
|
11 | 13 | public maxBackpressure: number = 2 * 1024 * 1024;
|
12 | 14 |
|
13 |
| - /** |
14 |
| - * If this is not null, then the connection is receiving a stream: a sequence |
15 |
| - * of fragment frames. |
16 |
| - */ |
17 |
| - protected stream: FanOut<Uint8Array> | null = null; |
18 |
| - |
19 | 15 | public readonly defaultOnPing = (data: Uint8Array | null): void => {
|
20 | 16 | this.sendPong(data);
|
21 | 17 | };
|
22 | 18 |
|
| 19 | + private _fragments: Uint8Array[] = []; |
| 20 | + private _fragmentsSize: number = 0; |
| 21 | + public readonly defaultOnFragment = (isLast: boolean, data: Uint8Array, isUtf8: boolean): void => { |
| 22 | + const fragments = this._fragments; |
| 23 | + this._fragmentsSize += data.length; |
| 24 | + if (this._fragmentsSize > this.maxIncomingMessage) { |
| 25 | + this.onClose(1009, 'TOO_LARGE'); |
| 26 | + return; |
| 27 | + } |
| 28 | + fragments.push(data); |
| 29 | + if (!isLast) return; |
| 30 | + this._fragments = []; |
| 31 | + this._fragmentsSize = 0; |
| 32 | + const message = listToUint8(fragments); |
| 33 | + this.onmessage(message, isUtf8); |
| 34 | + }; |
| 35 | + |
23 | 36 | public onmessage: (data: Uint8Array, isUtf8: boolean) => void = () => {};
|
| 37 | + public onfragment: (isLast: boolean, data: Uint8Array, isUtf8: boolean) => void = this.defaultOnFragment; |
24 | 38 | public onping: (data: Uint8Array | null) => void = this.defaultOnPing;
|
25 | 39 | public onpong: (data: Uint8Array | null) => void = () => {};
|
26 | 40 | public onclose: (code: number, reason: string) => void = () => {};
|
27 | 41 |
|
28 |
| - constructor(protected readonly encoder: WsFrameEncoder, public readonly socket: net.Socket) { |
| 42 | + constructor(protected readonly encoder: WsFrameEncoder, public readonly socket: WsServerConnectionSocket) { |
29 | 43 | const decoder = new WsFrameDecoder();
|
30 |
| - let currentFrame: WsFrameHeader | null = null; |
| 44 | + let currentFrameHeader: WsFrameHeader | null = null; |
| 45 | + let fragmentStartFrameHeader: WsFrameHeader | null = null; |
31 | 46 | const handleData = (data: Uint8Array): void => {
|
32 | 47 | try {
|
33 | 48 | decoder.push(data);
|
34 |
| - if (currentFrame) { |
35 |
| - const length = currentFrame.length; |
36 |
| - if (length <= decoder.reader.size()) { |
37 |
| - const buf = new Uint8Array(length); |
38 |
| - decoder.copyFrameData(currentFrame, buf, 0); |
39 |
| - const isText = currentFrame.opcode === WsFrameOpcode.TEXT; |
40 |
| - currentFrame = null; |
41 |
| - this.onmessage(buf, isText); |
| 49 | + main: while (true) { |
| 50 | + if (currentFrameHeader instanceof WsFrameHeader) { |
| 51 | + const length = currentFrameHeader.length; |
| 52 | + if (length > this.maxIncomingMessage) { |
| 53 | + this.onClose(1009, 'TOO_LARGE'); |
| 54 | + return; |
| 55 | + } |
| 56 | + if (length <= decoder.reader.size()) { |
| 57 | + const buf = new Uint8Array(length); |
| 58 | + decoder.copyFrameData(currentFrameHeader, buf, 0); |
| 59 | + if (fragmentStartFrameHeader instanceof WsFrameHeader) { |
| 60 | + const isText = fragmentStartFrameHeader.opcode === WsFrameOpcode.TEXT; |
| 61 | + const isLast = currentFrameHeader.fin === 1; |
| 62 | + currentFrameHeader = null; |
| 63 | + if (isLast) fragmentStartFrameHeader = null; |
| 64 | + this.onfragment(isLast, buf, isText); |
| 65 | + } else { |
| 66 | + const isText = currentFrameHeader.opcode === WsFrameOpcode.TEXT; |
| 67 | + currentFrameHeader = null; |
| 68 | + this.onmessage(buf, isText); |
| 69 | + } |
| 70 | + } else break; |
42 | 71 | }
|
43 |
| - } |
44 |
| - while (true) { |
45 | 72 | const frame = decoder.readFrameHeader();
|
46 | 73 | if (!frame) break;
|
47 |
| - else if (frame instanceof WsPingFrame) this.onping(frame.data); |
48 |
| - else if (frame instanceof WsPongFrame) this.onpong(frame.data); |
49 |
| - else if (frame instanceof WsCloseFrame) this.onClose(frame.code, frame.reason); |
50 |
| - else if (frame instanceof WsFrameHeader) { |
51 |
| - if (this.stream) { |
| 74 | + if (frame instanceof WsPingFrame) { |
| 75 | + this.onping(frame.data); |
| 76 | + continue main; |
| 77 | + } |
| 78 | + if (frame instanceof WsPongFrame) { |
| 79 | + this.onpong(frame.data); |
| 80 | + continue main; |
| 81 | + } |
| 82 | + if (frame instanceof WsCloseFrame) { |
| 83 | + decoder.readCloseFrameData(frame); |
| 84 | + this.onClose(frame.code, frame.reason); |
| 85 | + continue main; |
| 86 | + } |
| 87 | + if (frame instanceof WsFrameHeader) { |
| 88 | + if (fragmentStartFrameHeader) { |
52 | 89 | if (frame.opcode !== WsFrameOpcode.CONTINUE) {
|
53 | 90 | this.onClose(1002, 'DATA');
|
54 | 91 | return;
|
55 | 92 | }
|
56 |
| - throw new Error('streaming not implemented'); |
| 93 | + currentFrameHeader = frame; |
57 | 94 | }
|
58 |
| - const length = frame.length; |
59 |
| - if (length > this.maxIncomingMessage) { |
60 |
| - this.onClose(1009, 'TOO_LARGE'); |
61 |
| - return; |
62 |
| - } |
63 |
| - if (length <= decoder.reader.size()) { |
64 |
| - const buf = new Uint8Array(length); |
65 |
| - decoder.copyFrameData(frame, buf, 0); |
66 |
| - const isText = frame.opcode === WsFrameOpcode.TEXT; |
67 |
| - this.onmessage(buf, isText); |
68 |
| - } else { |
69 |
| - currentFrame = frame; |
| 95 | + if (frame.fin === 0) { |
| 96 | + fragmentStartFrameHeader = frame; |
| 97 | + currentFrameHeader = frame; |
| 98 | + continue main; |
70 | 99 | }
|
| 100 | + currentFrameHeader = frame; |
| 101 | + continue main; |
71 | 102 | }
|
72 | 103 | }
|
73 | 104 | } catch (error) {
|
|
0 commit comments