ws.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  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', () => { this.readyState = 3; this.emit('close'); });
  78. socket.on('error', (e) => this.emit('error', e));
  79. if (head && head.length) this._feed(head);
  80. resolve(res.headers);
  81. });
  82. req.on('response', (res) => fail(new Error(`握手被拒: HTTP ${res.statusCode}`)));
  83. req.on('error', fail);
  84. req.on('timeout', () => req.destroy(new Error('WebSocket 握手超时')));
  85. req.end();
  86. });
  87. }
  88. _feed(chunk) {
  89. this.buf = Buffer.concat([this.buf, chunk]);
  90. for (;;) {
  91. if (this.buf.length < 2) return;
  92. const b0 = this.buf[0];
  93. const b1 = this.buf[1];
  94. const fin = (b0 & 0x80) !== 0;
  95. const opcode = b0 & 0x0f;
  96. const masked = (b1 & 0x80) !== 0;
  97. let len = b1 & 0x7f;
  98. let pos = 2;
  99. if (len === 126) {
  100. if (this.buf.length < pos + 2) return;
  101. len = this.buf.readUInt16BE(pos); pos += 2;
  102. } else if (len === 127) {
  103. if (this.buf.length < pos + 8) return;
  104. const hi = this.buf.readUInt32BE(pos);
  105. const lo = this.buf.readUInt32BE(pos + 4);
  106. len = hi * 4294967296 + lo; pos += 8;
  107. }
  108. let mask = null;
  109. if (masked) {
  110. if (this.buf.length < pos + 4) return;
  111. mask = this.buf.subarray(pos, pos + 4); pos += 4;
  112. }
  113. if (this.buf.length < pos + len) return;
  114. let payload = this.buf.subarray(pos, pos + len);
  115. this.buf = this.buf.subarray(pos + len);
  116. if (mask) {
  117. payload = Buffer.from(payload); // 复制,避免改动原缓冲
  118. for (let i = 0; i < payload.length; i++) payload[i] ^= mask[i & 3];
  119. }
  120. if (opcode === OP.PING) { this._sendFrame(payload, OP.PONG); continue; }
  121. if (opcode === OP.PONG) continue;
  122. if (opcode === OP.CLOSE) { this.close(); continue; }
  123. if (opcode === OP.CONT) {
  124. this.frag.push(payload);
  125. } else {
  126. this.frag = [payload];
  127. this.fragOp = opcode;
  128. }
  129. if (fin) {
  130. const full = this.frag.length === 1 ? this.frag[0] : Buffer.concat(this.frag);
  131. const startOp = this.fragOp;
  132. this.frag = []; this.fragOp = null;
  133. if (startOp === OP.TEXT) this.emit('message', full.toString('utf8'));
  134. else this.emit('binary', full);
  135. }
  136. }
  137. }
  138. _sendFrame(payload, opcode) {
  139. if (!this.sock || this.readyState !== 1) return;
  140. try { this.sock.write(encodeFrame(payload, opcode)); } catch (e) { this.emit('error', e); }
  141. }
  142. send(text) { this._sendFrame(Buffer.from(text, 'utf8'), OP.TEXT); }
  143. close() {
  144. if (this.readyState === 3) return;
  145. this.readyState = 2;
  146. try { this._sendFrame(Buffer.from([0x03, 0xe8]), OP.CLOSE); } catch { /* ignore */ }
  147. try { this.sock.end(); } catch { /* ignore */ }
  148. this.readyState = 3;
  149. this.emit('close');
  150. }
  151. }
  152. module.exports = { RawWebSocket, encodeFrame };