|
| 1 | +/** |
| 2 | + * Copyright (c) Jonathan Cardoso Machado. All Rights Reserved. |
| 3 | + * |
| 4 | + * This source code is licensed under the MIT license found in the |
| 5 | + * LICENSE file in the root directory of this source tree. |
| 6 | + */ |
| 7 | + |
| 8 | +/* |
| 9 | + * For the next methods reading this is very important: https://tools.ietf.org/html/rfc6455#section-5 |
| 10 | + * |
| 11 | + * 0 1 2 3 |
| 12 | + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 |
| 13 | + * +-+-+-+-+-------+-+-------------+-------------------------------+ |
| 14 | + * |F|R|R|R| opcode|M| Payload len | Extended payload length | |
| 15 | + * |I|S|S|S| (4) |A| (7) | (16/64) | |
| 16 | + * |N|V|V|V| |S| | (if payload len==126/127) | |
| 17 | + * | |1|2|3| |K| | | |
| 18 | + * +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + |
| 19 | + * | Extended payload length continued, if payload len == 127 | |
| 20 | + * + - - - - - - - - - - - - - - - +-------------------------------+ |
| 21 | + * | |Masking-key, if MASK set to 1 | |
| 22 | + * +-------------------------------+-------------------------------+ |
| 23 | + * | Masking-key (continued) | Payload Data | |
| 24 | + * +-------------------------------- - - - - - - - - - - - - - - - + |
| 25 | + * : Payload Data continued ... : |
| 26 | + * + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + |
| 27 | + * | Payload Data continued ... | |
| 28 | + * +---------------------------------------------------------------+ |
| 29 | + * |
| 30 | + */ |
| 31 | + |
| 32 | +/** |
| 33 | + * TODO: Handle message fragmentation? |
| 34 | + * |
| 35 | + * @param {Buffer} frame |
| 36 | + */ |
| 37 | +function readFrame(frame) { |
| 38 | + const firstByte = frame.readUInt8(0) |
| 39 | + |
| 40 | + const fin = ((firstByte & 0b10000000) === 0b10000000) | 0 |
| 41 | + const rs1 = firstByte & 0b01000000 |
| 42 | + const rs2 = firstByte & 0b00100000 |
| 43 | + const rs3 = firstByte & 0b00010000 |
| 44 | + const opc = firstByte & 0b00001111 |
| 45 | + |
| 46 | + let result = { |
| 47 | + fin, |
| 48 | + rs1, |
| 49 | + rs2, |
| 50 | + rs3, |
| 51 | + opc, |
| 52 | + } |
| 53 | + |
| 54 | + const secondByte = frame.readUInt8(1) |
| 55 | + const mask = secondByte & 0b10000000 |
| 56 | + const length = secondByte & 0b01111111 |
| 57 | + |
| 58 | + let payloadLength = length |
| 59 | + |
| 60 | + // 2 is the default for when the payload length <= 125 |
| 61 | + // in this case, the masking key (if it exists) is |
| 62 | + // in the third byte (index 2) |
| 63 | + let byteOffset = 2 |
| 64 | + |
| 65 | + // if length === 126, the length itself is in the next 16 bits |
| 66 | + // so we must bump the offset by 8 bytes (16 bits) |
| 67 | + if (length === 126) { |
| 68 | + byteOffset += 2 |
| 69 | + // all data in the websockets message frame uses network byte order |
| 70 | + // which is big endian |
| 71 | + payloadLength = frame.readUInt16BE(2) |
| 72 | + // otherwise, if length === 127, the length is in the next 64 bits |
| 73 | + // so we must bump the offset by 8 bytes (64 bits) |
| 74 | + } else if (length === 127) { |
| 75 | + byteOffset += 8 |
| 76 | + |
| 77 | + const payloadLength32 = frame.readUInt32BE(2) |
| 78 | + // max safe integer in JavaScript is 2^53 - 1 |
| 79 | + // this was taken from the ws package |
| 80 | + if (payloadLength32 > Math.pow(2, 53 - 32) - 1) { |
| 81 | + throw new WebSocketError( |
| 82 | + 'Unsupported WebSocket frame: payload length > 2^53 - 1', |
| 83 | + 1009, |
| 84 | + ) |
| 85 | + } |
| 86 | + |
| 87 | + payloadLength = payloadLength32 * Math.pow(2, 32) + frame.readUInt32BE(6) |
| 88 | + } |
| 89 | + |
| 90 | + let maskingKey = null |
| 91 | + if (mask) { |
| 92 | + // masking key takes 32 bits in length |
| 93 | + // so 4 bytes |
| 94 | + maskingKey = frame.slice(byteOffset, byteOffset + 4) |
| 95 | + byteOffset += 4 |
| 96 | + } |
| 97 | + |
| 98 | + let payload = null |
| 99 | + |
| 100 | + if (length) { |
| 101 | + payload = frame.slice(byteOffset, byteOffset + payloadLength) |
| 102 | + |
| 103 | + if (maskingKey) { |
| 104 | + // this is the way to unmask the value |
| 105 | + // see https://tools.ietf.org/html/rfc6455#section-5.3 |
| 106 | + for (var i = 0; i < payload.length; i++) { |
| 107 | + // we could use bitwise and here, & 3 (or & 0x3) |
| 108 | + payload[i] = payload[i] ^ maskingKey[i % 4] |
| 109 | + } |
| 110 | + } |
| 111 | + } |
| 112 | + |
| 113 | + const remaining = frame.slice(byteOffset + payloadLength) |
| 114 | + |
| 115 | + return { |
| 116 | + ...result, |
| 117 | + payload, |
| 118 | + remaining: remaining && remaining.length ? remaining : null, |
| 119 | + } |
| 120 | +} |
| 121 | + |
| 122 | +/** |
| 123 | + * @param {Buffer} data |
| 124 | + * @param {Number} type This is the first byte of the frame, it includes the fin, rsvn and opcode |
| 125 | + */ |
| 126 | +function packFrame(data, type) { |
| 127 | + const length = data.length |
| 128 | + // initial offset is 6 |
| 129 | + // 1 byte for the first part (fin, rsvn, opcode) |
| 130 | + // 1 byte for the mask and payload length(first part) |
| 131 | + // 4 bytes for the mask key |
| 132 | + // if the length is less than or equal 125, we are able to store in the 7 bits |
| 133 | + // we have in the first length part of the frame, so no need to change the offset |
| 134 | + let payloadOffset = 6 |
| 135 | + let payloadLength = data.length |
| 136 | + |
| 137 | + // our key |
| 138 | + const maskingKey = new Uint8Array([0x12, 0x34, 0x56, 0x78]) |
| 139 | + // const maskingKey = Buffer.alloc(4) |
| 140 | + // crypto.randomFillSync(maskingKey, 0, 4); |
| 141 | + |
| 142 | + // if the length is bigger than 65535 |
| 143 | + // then we will need to use all the payload storage we have in the frame |
| 144 | + // which means adding more 8 bytes (64 bits) |
| 145 | + if (length > 0xffff) { |
| 146 | + payloadOffset += 8 |
| 147 | + payloadLength = 127 |
| 148 | + // if the length is bigger than 125, then we just need |
| 149 | + // more 2 bytes (16 bits) |
| 150 | + } else if (length >= 126) { |
| 151 | + payloadOffset += 2 |
| 152 | + payloadLength = 126 |
| 153 | + } |
| 154 | + |
| 155 | + const frame = Buffer.alloc(length + payloadOffset) |
| 156 | + |
| 157 | + // fin, rsvn and opcode |
| 158 | + frame[0] = type |
| 159 | + |
| 160 | + // first part of the payload length, including mask bit set |
| 161 | + frame[1] = payloadLength | 0b10000000 |
| 162 | + |
| 163 | + if (payloadLength === 126) { |
| 164 | + frame.writeUInt16BE(data.length, 2) |
| 165 | + } else if (payloadLength === 127) { |
| 166 | + frame.writeUInt32BE(0, 2) |
| 167 | + frame.writeUInt32BE(data.length, 6) |
| 168 | + } |
| 169 | + |
| 170 | + // masking key |
| 171 | + frame[payloadOffset - 4] = maskingKey[0] |
| 172 | + frame[payloadOffset - 3] = maskingKey[1] |
| 173 | + frame[payloadOffset - 2] = maskingKey[2] |
| 174 | + frame[payloadOffset - 1] = maskingKey[3] |
| 175 | + |
| 176 | + for (let i = 0; i < length; i++) { |
| 177 | + // we could use bitwise and here, & 3 (or & 0x3) |
| 178 | + frame[payloadOffset + i] = data[i] ^ maskingKey[i % 4] |
| 179 | + } |
| 180 | + |
| 181 | + return frame |
| 182 | +} |
| 183 | + |
| 184 | +const packMessageFrame = (data) => packFrame(data, 0b10000001) |
| 185 | +const packPingFrame = (data = Buffer.alloc(0)) => packFrame(data, 0b10001001) |
| 186 | +const packPongFrame = (data = Buffer.alloc(0)) => packFrame(data, 0b10001010) |
| 187 | +const packCloseFrame = (code = 1000, message = Buffer.alloc(0)) => { |
| 188 | + // TODO: add validation for the code and message here. |
| 189 | + // example: the message itself cannot be bigger than 123 bytes. |
| 190 | + |
| 191 | + const data = Buffer.alloc(2 + message.length) |
| 192 | + |
| 193 | + data.writeUInt16BE(code, 0) |
| 194 | + if (message.length) { |
| 195 | + message.copy(data, 2) |
| 196 | + } |
| 197 | + |
| 198 | + return packFrame(data, 0b10000000 | 0x8) |
| 199 | +} |
| 200 | +const WEBSOCKET_FRAME_OPCODE = { |
| 201 | + CONT: 0x0, |
| 202 | + NON_CONTROL_TEXT: 0x1, |
| 203 | + NON_CONTROL_BINARY: 0x2, |
| 204 | + CONTROL_CLOSE: 0x8, |
| 205 | + CONTROL_PING: 0x9, |
| 206 | + CONTROL_PONG: 0xa, |
| 207 | +} |
| 208 | + |
| 209 | +class WebSocketError extends Error { |
| 210 | + constructor(message, code) { |
| 211 | + super(message) |
| 212 | + this.code = code |
| 213 | + } |
| 214 | +} |
| 215 | + |
| 216 | +module.exports = { |
| 217 | + packCloseFrame, |
| 218 | + packFrame, |
| 219 | + packMessageFrame, |
| 220 | + packPingFrame, |
| 221 | + packPongFrame, |
| 222 | + readFrame, |
| 223 | + WEBSOCKET_FRAME_OPCODE, |
| 224 | + WebSocketError, |
| 225 | +} |
0 commit comments