diff --git a/lib/web/websocket/frame.js b/lib/web/websocket/frame.js index c29ab46096a..53e91191db8 100644 --- a/lib/web/websocket/frame.js +++ b/lib/web/websocket/frame.js @@ -20,6 +20,31 @@ function generateMask () { return [buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++]] } +function maskPayload (source, target, targetOffset, maskKey) { + const length = source.byteLength + const mask0 = maskKey[0] + const mask1 = maskKey[1] + const mask2 = maskKey[2] + const mask3 = maskKey[3] + const unrolledEnd = length & ~7 + + let i = 0 + for (; i < unrolledEnd; i += 8) { + target[targetOffset + i] = source[i] ^ mask0 + target[targetOffset + i + 1] = source[i + 1] ^ mask1 + target[targetOffset + i + 2] = source[i + 2] ^ mask2 + target[targetOffset + i + 3] = source[i + 3] ^ mask3 + target[targetOffset + i + 4] = source[i + 4] ^ mask0 + target[targetOffset + i + 5] = source[i + 5] ^ mask1 + target[targetOffset + i + 6] = source[i + 6] ^ mask2 + target[targetOffset + i + 7] = source[i + 7] ^ mask3 + } + + for (; i < length; ++i) { + target[targetOffset + i] = source[i] ^ maskKey[i & 3] + } +} + class WebsocketFrameSend { /** * @param {Buffer|undefined} data @@ -71,8 +96,8 @@ class WebsocketFrameSend { buffer[1] |= 0x80 // MASK // mask body - for (let i = 0; i < bodyLength; ++i) { - buffer[offset + i] = frameData[i] ^ maskKey[i & 3] + if (bodyLength !== 0) { + maskPayload(frameData, buffer, offset, maskKey) } return buffer @@ -87,9 +112,7 @@ class WebsocketFrameSend { const bodyLength = buffer.length // mask body - for (let i = 0; i < bodyLength; ++i) { - buffer[i] ^= maskKey[i & 3] - } + maskPayload(buffer, buffer, 0, maskKey) let payloadLength = bodyLength let offset = 6 diff --git a/test/websocket/frame.js b/test/websocket/frame.js index b75d79aff4b..8b3a37e8dfe 100644 --- a/test/websocket/frame.js +++ b/test/websocket/frame.js @@ -37,3 +37,29 @@ test('Writing 16-bit frame length value at correct offset when buffer has a non- t.assert.strictEqual(frame[3], payloadLength & 0xff) t.assert.strictEqual(smallBuffer.length, 1) // ensure smallBuffer can't be garbage-collected too soon }) + +test('Masks aligned and unaligned payloads without changing frame semantics', (t) => { + for (const length of [0, 1, 3, 4, 5, 7, 8, 9, 125, 126, 127, 65535, 65536]) { + const payload = Buffer.alloc(length) + for (let i = 0; i < payload.length; ++i) payload[i] = i & 0xff + const originalPayload = Buffer.from(payload) + const frame = new WebsocketFrameSend(payload).createFrame(opcodes.BINARY) + const offset = length > 65535 ? 14 : length > 125 ? 8 : 6 + const mask = frame.subarray(offset - 4, offset) + const decoded = Buffer.alloc(length) + for (let i = 0; i < length; ++i) decoded[i] = frame[offset + i] ^ mask[i & 3] + t.assert.deepStrictEqual(decoded, originalPayload) + t.assert.deepStrictEqual(payload, originalPayload) + } + + const text = Buffer.from('héllo websocket 🌍'.repeat(2)) + const backing = Buffer.alloc(text.length + 1) + text.copy(backing, 1) + const view = backing.subarray(1) + const original = Buffer.from(view) + const [head, body] = WebsocketFrameSend.createFastTextFrame(view) + const mask = head.subarray(head.length - 4) + const decoded = Buffer.alloc(body.length) + for (let i = 0; i < body.length; ++i) decoded[i] = body[i] ^ mask[i & 3] + t.assert.deepStrictEqual(decoded, original) +})