Skip to content

Commit abede57

Browse files
committed
chore: add websockets example [skip ci]
1 parent 15e35fb commit abede57

File tree

6 files changed

+801
-0
lines changed

6 files changed

+801
-0
lines changed

Diff for: examples/20-http2-server-push.js

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
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+
18
const fs = require('fs')
29
const path = require('path')
310

Diff for: examples/21-websockets-client-helpers/index.js

+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
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

Comments
 (0)