ws.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. 'use strict';
  2. /**
  3. * 零依赖的 WebSocket 客户端(RFC6455 子集,够 CDP 用)。
  4. *
  5. * 为什么不用 Node 内置的全局 WebSocket:
  6. * Chromium 的 DevTools WebSocket 端点会拒绝带 Origin 头的握手。
  7. * 浏览器环境自动带 Origin,undici 的实现行为不确定。
  8. * 这里用 http.request 手动升级,完全掌控请求头 —— 不发 Origin,握手必定通过。
  9. *
  10. * 支持:文本/二进制帧、分片续帧、ping/pong、64 位长度、关闭帧。
  11. * 客户端发帧一律 mask(协议要求)。
  12. */
  13. const http = require('http');
  14. const crypto = require('crypto');
  15. const { EventEmitter } = require('events');
  16. const OP = { CONT: 0x0, TEXT: 0x1, BIN: 0x2, CLOSE: 0x8, PING: 0x9, PONG: 0xa };
  17. function encodeFrame(payload, opcode = OP.TEXT) {
  18. const len = payload.length;
  19. let head;
  20. if (len < 126) {
  21. head = Buffer.alloc(2);
  22. head[1] = len;
  23. } else if (len < 65536) {
  24. head = Buffer.alloc(4);
  25. head[1] = 126;
  26. head.writeUInt16BE(len, 2);
  27. } else {
  28. head = Buffer.alloc(10);
  29. head[1] = 127;
  30. head.writeUInt32BE(Math.floor(len / 4294967296), 2);
  31. head.writeUInt32BE(len >>> 0, 6);
  32. }
  33. head[0] = 0x80 | opcode; // FIN + opcode
  34. head[1] |= 0x80; // MASK
  35. const mask = crypto.randomBytes(4);
  36. const out = Buffer.alloc(head.length + 4 + len);
  37. head.copy(out, 0);
  38. mask.copy(out, head.length);
  39. const off = head.length + 4;
  40. for (let i = 0; i < len; i++) out[off + i] = payload[i] ^ mask[i & 3];
  41. return out;
  42. }
  43. class RawWebSocket extends EventEmitter {
  44. constructor(url) {
  45. super();
  46. this.url = new URL(url);
  47. this.sock = null;
  48. this.buf = Buffer.alloc(0);
  49. this.frag = []; // 分片累积
  50. this.fragOp = null;
  51. this.readyState = 0; // 0 connecting 1 open 2 closing 3 closed
  52. }
  53. connect(timeoutMs = 10000) {
  54. return new Promise((resolve, reject) => {
  55. const key = crypto.randomBytes(16).toString('base64');
  56. const req = http.request({
  57. host: this.url.hostname,
  58. port: this.url.port || 80,
  59. path: this.url.pathname + this.url.search,
  60. method: 'GET',
  61. headers: {
  62. Connection: 'Upgrade',
  63. Upgrade: 'websocket',
  64. 'Sec-WebSocket-Key': key,
  65. 'Sec-WebSocket-Version': '13',
  66. },
  67. timeout: timeoutMs,
  68. });
  69. let settled = false;
  70. const fail = (e) => { if (!settled) { settled = true; reject(e); } };
  71. req.on('upgrade', (res, socket, head) => {
  72. settled = true;
  73. this.sock = socket;
  74. this.readyState = 1;
  75. socket.setNoDelay(true);
  76. socket.on('data', (d) => this._feed(d));
  77. socket.on('close', () => {
  78. const wasOpen = this.readyState === 1;
  79. this.readyState = 3;
  80. this.emit('close');
  81. // 从未 emit 过 error 的连接断掉时,给上层一个明确的失败信号而不是静默
  82. if (wasOpen) this.emit('error', new Error('WebSocket 连接已断开'));
  83. });
  84. socket.on('error', (e) => {
  85. this.readyState = 3;
  86. this.emit('error', e);
  87. });
  88. if (head && head.length) this._feed(head);
  89. resolve(res.headers);
  90. });
  91. req.on('response', (res) => {
  92. // 握手被拒:必须销毁请求释放底层 socket,否则每次探测失败都泄漏一条连接
  93. res.resume();
  94. req.destroy();
  95. fail(new Error(`握手被拒: HTTP ${res.statusCode}`));
  96. });
  97. req.on('error', fail);
  98. req.on('timeout', () => req.destroy(new Error('WebSocket 握手超时')));
  99. req.end();
  100. });
  101. }
  102. _feed(chunk) {
  103. this.buf = Buffer.concat([this.buf, chunk]);
  104. for (;;) {
  105. if (this.buf.length < 2) return;
  106. const b0 = this.buf[0];
  107. const b1 = this.buf[1];
  108. const fin = (b0 & 0x80) !== 0;
  109. const opcode = b0 & 0x0f;
  110. const masked = (b1 & 0x80) !== 0;
  111. let len = b1 & 0x7f;
  112. let pos = 2;
  113. if (len === 126) {
  114. if (this.buf.length < pos + 2) return;
  115. len = this.buf.readUInt16BE(pos); pos += 2;
  116. } else if (len === 127) {
  117. if (this.buf.length < pos + 8) return;
  118. const hi = this.buf.readUInt32BE(pos);
  119. const lo = this.buf.readUInt32BE(pos + 4);
  120. len = hi * 4294967296 + lo; pos += 8;
  121. }
  122. let mask = null;
  123. if (masked) {
  124. if (this.buf.length < pos + 4) return;
  125. mask = this.buf.subarray(pos, pos + 4); pos += 4;
  126. }
  127. if (this.buf.length < pos + len) return;
  128. let payload = this.buf.subarray(pos, pos + len);
  129. this.buf = this.buf.subarray(pos + len);
  130. if (mask) {
  131. payload = Buffer.from(payload); // 复制,避免改动原缓冲
  132. for (let i = 0; i < payload.length; i++) payload[i] ^= mask[i & 3];
  133. }
  134. if (opcode === OP.PING) { this._sendFrame(payload, OP.PONG); continue; }
  135. if (opcode === OP.PONG) continue;
  136. if (opcode === OP.CLOSE) { this.close(); continue; }
  137. if (opcode === OP.CONT) {
  138. this.frag.push(payload);
  139. } else {
  140. this.frag = [payload];
  141. this.fragOp = opcode;
  142. }
  143. if (fin) {
  144. const full = this.frag.length === 1 ? this.frag[0] : Buffer.concat(this.frag);
  145. const startOp = this.fragOp;
  146. this.frag = []; this.fragOp = null;
  147. if (startOp === OP.TEXT) this.emit('message', full.toString('utf8'));
  148. else this.emit('binary', full);
  149. }
  150. }
  151. }
  152. _sendFrame(payload, opcode) {
  153. if (!this.sock || this.readyState !== 1) return;
  154. try { this.sock.write(encodeFrame(payload, opcode)); } catch (e) { this.emit('error', e); }
  155. }
  156. send(text) { this._sendFrame(Buffer.from(text, 'utf8'), OP.TEXT); }
  157. close() {
  158. if (this.readyState === 3) return;
  159. this.readyState = 2;
  160. try { this._sendFrame(Buffer.from([0x03, 0xe8]), OP.CLOSE); } catch { /* ignore */ }
  161. try { this.sock.end(); } catch { /* ignore */ }
  162. this.readyState = 3;
  163. this.emit('close');
  164. }
  165. }
  166. module.exports = { RawWebSocket, encodeFrame };