websocket-peer.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import { createHash } from "node:crypto";
  2. import { EventEmitter, once } from "node:events";
  3. import { createServer } from "node:net";
  4. import type { Server, Socket } from "node:net";
  5. type Frame = Readonly<{
  6. opcode: number;
  7. payload: Buffer<ArrayBufferLike>;
  8. }>;
  9. function encodeFrame(opcode: number, payload: Buffer<ArrayBufferLike>): Buffer<ArrayBufferLike> {
  10. const length = payload.length;
  11. if (length < 126) {
  12. return Buffer.concat([Buffer.from([0x80 | opcode, length]), payload]);
  13. }
  14. if (length <= 65_535) {
  15. const header = Buffer.alloc(4);
  16. header[0] = 0x80 | opcode;
  17. header[1] = 126;
  18. header.writeUInt16BE(length, 2);
  19. return Buffer.concat([header, payload]);
  20. }
  21. const header = Buffer.alloc(10);
  22. header[0] = 0x80 | opcode;
  23. header[1] = 127;
  24. header.writeBigUInt64BE(BigInt(length), 2);
  25. return Buffer.concat([header, payload]);
  26. }
  27. function decodeFrame(
  28. buffer: Buffer<ArrayBufferLike>,
  29. ): { frame?: Frame; remaining: Buffer<ArrayBufferLike> } {
  30. if (buffer.length < 2) {
  31. return { remaining: buffer };
  32. }
  33. const first = buffer[0];
  34. const second = buffer[1];
  35. if (first === undefined || second === undefined) {
  36. return { remaining: buffer };
  37. }
  38. const opcode = first & 0x0f;
  39. const masked = (second & 0x80) !== 0;
  40. let payloadLength = second & 0x7f;
  41. let offset = 2;
  42. if (payloadLength === 126) {
  43. if (buffer.length < 4) {
  44. return { remaining: buffer };
  45. }
  46. payloadLength = buffer.readUInt16BE(2);
  47. offset = 4;
  48. } else if (payloadLength === 127) {
  49. if (buffer.length < 10) {
  50. return { remaining: buffer };
  51. }
  52. const largeLength = buffer.readBigUInt64BE(2);
  53. if (largeLength > BigInt(Number.MAX_SAFE_INTEGER)) {
  54. throw new Error("test frame is too large");
  55. }
  56. payloadLength = Number(largeLength);
  57. offset = 10;
  58. }
  59. const maskLength = masked ? 4 : 0;
  60. if (buffer.length < offset + maskLength + payloadLength) {
  61. return { remaining: buffer };
  62. }
  63. const mask = masked ? buffer.subarray(offset, offset + 4) : undefined;
  64. offset += maskLength;
  65. const payload = Buffer.from(buffer.subarray(offset, offset + payloadLength));
  66. if (mask !== undefined) {
  67. for (let index = 0; index < payload.length; index += 1) {
  68. const maskByte = mask[index % 4];
  69. const payloadByte = payload[index];
  70. if (maskByte !== undefined && payloadByte !== undefined) {
  71. payload[index] = payloadByte ^ maskByte;
  72. }
  73. }
  74. }
  75. return {
  76. frame: { opcode, payload },
  77. remaining: buffer.subarray(offset + payloadLength),
  78. };
  79. }
  80. export class TestWebSocketConnection extends EventEmitter {
  81. readonly socket: Socket;
  82. readonly textFrames: string[] = [];
  83. abortOnCloseFrame = false;
  84. private frameBuffer: Buffer<ArrayBufferLike> = Buffer.alloc(0);
  85. constructor(socket: Socket, initialFrameBytes: Buffer<ArrayBufferLike>) {
  86. super();
  87. this.socket = socket;
  88. socket.on("data", (chunk: Buffer) => this.consume(chunk));
  89. socket.on("close", () => this.emit("socketClose"));
  90. socket.on("error", (error: Error) => this.emit("socketError", error));
  91. if (initialFrameBytes.length > 0) {
  92. this.consume(initialFrameBytes);
  93. }
  94. }
  95. private consume(chunk: Buffer<ArrayBufferLike>): void {
  96. this.frameBuffer = Buffer.concat([this.frameBuffer, chunk]);
  97. while (true) {
  98. const decoded = decodeFrame(this.frameBuffer);
  99. if (decoded.frame === undefined) {
  100. return;
  101. }
  102. this.frameBuffer = decoded.remaining;
  103. if (decoded.frame.opcode === 0x1) {
  104. const text = decoded.frame.payload.toString("utf8");
  105. this.textFrames.push(text);
  106. this.emit("text", text);
  107. } else if (decoded.frame.opcode === 0x8) {
  108. this.emit("closeFrame");
  109. if (this.abortOnCloseFrame) {
  110. this.socket.destroy(new Error("test peer aborted during local shutdown"));
  111. } else {
  112. this.socket.write(encodeFrame(0x8, decoded.frame.payload));
  113. this.socket.end();
  114. }
  115. } else if (decoded.frame.opcode === 0x9) {
  116. this.socket.write(encodeFrame(0xa, decoded.frame.payload));
  117. }
  118. }
  119. }
  120. sendText(value: string): void {
  121. this.socket.write(encodeFrame(0x1, Buffer.from(value)));
  122. }
  123. sendBinary(value: Buffer<ArrayBufferLike>): void {
  124. this.socket.write(encodeFrame(0x2, value));
  125. }
  126. close(code = 1000): void {
  127. const payload = Buffer.alloc(2);
  128. payload.writeUInt16BE(code);
  129. this.socket.write(encodeFrame(0x8, payload));
  130. this.socket.end();
  131. }
  132. }
  133. export class TestWebSocketPeer extends EventEmitter {
  134. readonly server: Server;
  135. readonly connections: TestWebSocketConnection[] = [];
  136. readonly completeHandshake: boolean;
  137. readonly handshakeDelayMs: number;
  138. private constructor(options: { completeHandshake: boolean; handshakeDelayMs: number }) {
  139. super();
  140. this.completeHandshake = options.completeHandshake;
  141. this.handshakeDelayMs = options.handshakeDelayMs;
  142. this.server = createServer((socket) => this.acceptSocket(socket));
  143. }
  144. static async start(
  145. options: { completeHandshake?: boolean; handshakeDelayMs?: number } = {},
  146. ): Promise<TestWebSocketPeer> {
  147. const peer = new TestWebSocketPeer({
  148. completeHandshake: options.completeHandshake ?? true,
  149. handshakeDelayMs: options.handshakeDelayMs ?? 0,
  150. });
  151. peer.server.listen(0, "127.0.0.1");
  152. await once(peer.server, "listening");
  153. return peer;
  154. }
  155. get port(): number {
  156. const address = this.server.address();
  157. if (address === null || typeof address === "string") {
  158. throw new Error("test server is not listening on TCP");
  159. }
  160. return address.port;
  161. }
  162. private acceptSocket(socket: Socket): void {
  163. let handshakeBuffer = Buffer.alloc(0);
  164. const onHandshakeData = (chunk: Buffer): void => {
  165. handshakeBuffer = Buffer.concat([handshakeBuffer, chunk]);
  166. const boundary = handshakeBuffer.indexOf("\r\n\r\n");
  167. if (boundary === -1 || !this.completeHandshake) {
  168. return;
  169. }
  170. socket.removeListener("data", onHandshakeData);
  171. const request = handshakeBuffer.subarray(0, boundary + 4).toString("utf8");
  172. const remaining = handshakeBuffer.subarray(boundary + 4);
  173. const key = /^Sec-WebSocket-Key:\s*(.+)$/im.exec(request)?.[1]?.trim();
  174. if (key === undefined) {
  175. socket.destroy(new Error("missing WebSocket key"));
  176. return;
  177. }
  178. const accept = createHash("sha1")
  179. .update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
  180. .digest("base64");
  181. setTimeout(() => {
  182. socket.write(
  183. "HTTP/1.1 101 Switching Protocols\r\n" +
  184. "Upgrade: websocket\r\n" +
  185. "Connection: Upgrade\r\n" +
  186. `Sec-WebSocket-Accept: ${accept}\r\n\r\n`,
  187. );
  188. const connection = new TestWebSocketConnection(socket, remaining);
  189. this.connections.push(connection);
  190. this.emit("connection", connection);
  191. }, this.handshakeDelayMs);
  192. };
  193. socket.on("data", onHandshakeData);
  194. socket.on("error", () => undefined);
  195. }
  196. async nextConnection(): Promise<TestWebSocketConnection> {
  197. const existing = this.connections[0];
  198. if (existing !== undefined) {
  199. return existing;
  200. }
  201. const [connection] = await once(this, "connection");
  202. return connection as TestWebSocketConnection;
  203. }
  204. async stop(): Promise<void> {
  205. for (const connection of this.connections) {
  206. connection.socket.destroy();
  207. }
  208. this.server.close();
  209. await once(this.server, "close");
  210. }
  211. }