Skip to content

Commit 1b858fb

Browse files
authored
fix(websocket): run parser in loop, instead of recursively (nodejs#1828)
1 parent 2856e4b commit 1b858fb

File tree

1 file changed

+156
-153
lines changed

1 file changed

+156
-153
lines changed

lib/websocket/receiver.js

Lines changed: 156 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -48,201 +48,204 @@ class ByteParser extends Writable {
4848
* or not enough bytes are buffered to parse.
4949
*/
5050
run (callback) {
51-
if (this.#state === parserStates.INFO) {
52-
// If there aren't enough bytes to parse the payload length, etc.
53-
if (this.#byteOffset < 2) {
54-
return callback()
55-
}
51+
while (true) {
52+
if (this.#state === parserStates.INFO) {
53+
// If there aren't enough bytes to parse the payload length, etc.
54+
if (this.#byteOffset < 2) {
55+
return callback()
56+
}
5657

57-
const buffer = this.consume(2)
58+
const buffer = this.consume(2)
5859

59-
this.#info.fin = (buffer[0] & 0x80) !== 0
60-
this.#info.opcode = buffer[0] & 0x0F
60+
this.#info.fin = (buffer[0] & 0x80) !== 0
61+
this.#info.opcode = buffer[0] & 0x0F
6162

62-
// If we receive a fragmented message, we use the type of the first
63-
// frame to parse the full message as binary/text, when it's terminated
64-
this.#info.originalOpcode ??= this.#info.opcode
63+
// If we receive a fragmented message, we use the type of the first
64+
// frame to parse the full message as binary/text, when it's terminated
65+
this.#info.originalOpcode ??= this.#info.opcode
6566

66-
this.#info.fragmented = !this.#info.fin && this.#info.opcode !== opcodes.CONTINUATION
67+
this.#info.fragmented = !this.#info.fin && this.#info.opcode !== opcodes.CONTINUATION
6768

68-
if (this.#info.fragmented && this.#info.opcode !== opcodes.BINARY && this.#info.opcode !== opcodes.TEXT) {
69-
// Only text and binary frames can be fragmented
70-
failWebsocketConnection(this.ws, 'Invalid frame type was fragmented.')
71-
return
72-
}
69+
if (this.#info.fragmented && this.#info.opcode !== opcodes.BINARY && this.#info.opcode !== opcodes.TEXT) {
70+
// Only text and binary frames can be fragmented
71+
failWebsocketConnection(this.ws, 'Invalid frame type was fragmented.')
72+
return
73+
}
7374

74-
const payloadLength = buffer[1] & 0x7F
75+
const payloadLength = buffer[1] & 0x7F
7576

76-
if (payloadLength <= 125) {
77-
this.#info.payloadLength = payloadLength
78-
this.#state = parserStates.READ_DATA
79-
} else if (payloadLength === 126) {
80-
this.#state = parserStates.PAYLOADLENGTH_16
81-
} else if (payloadLength === 127) {
82-
this.#state = parserStates.PAYLOADLENGTH_64
83-
}
77+
if (payloadLength <= 125) {
78+
this.#info.payloadLength = payloadLength
79+
this.#state = parserStates.READ_DATA
80+
} else if (payloadLength === 126) {
81+
this.#state = parserStates.PAYLOADLENGTH_16
82+
} else if (payloadLength === 127) {
83+
this.#state = parserStates.PAYLOADLENGTH_64
84+
}
8485

85-
if (this.#info.fragmented && payloadLength > 125) {
86-
// A fragmented frame can't be fragmented itself
87-
failWebsocketConnection(this.ws, 'Fragmented frame exceeded 125 bytes.')
88-
return
89-
} else if (
90-
(this.#info.opcode === opcodes.PING ||
91-
this.#info.opcode === opcodes.PONG ||
92-
this.#info.opcode === opcodes.CLOSE) &&
93-
payloadLength > 125
94-
) {
95-
// Control frames can have a payload length of 125 bytes MAX
96-
failWebsocketConnection(this.ws, 'Payload length for control frame exceeded 125 bytes.')
97-
return
98-
} else if (this.#info.opcode === opcodes.CLOSE) {
99-
if (payloadLength === 1) {
100-
failWebsocketConnection(this.ws, 'Received close frame with a 1-byte body.')
86+
if (this.#info.fragmented && payloadLength > 125) {
87+
// A fragmented frame can't be fragmented itself
88+
failWebsocketConnection(this.ws, 'Fragmented frame exceeded 125 bytes.')
10189
return
102-
}
90+
} else if (
91+
(this.#info.opcode === opcodes.PING ||
92+
this.#info.opcode === opcodes.PONG ||
93+
this.#info.opcode === opcodes.CLOSE) &&
94+
payloadLength > 125
95+
) {
96+
// Control frames can have a payload length of 125 bytes MAX
97+
failWebsocketConnection(this.ws, 'Payload length for control frame exceeded 125 bytes.')
98+
return
99+
} else if (this.#info.opcode === opcodes.CLOSE) {
100+
if (payloadLength === 1) {
101+
failWebsocketConnection(this.ws, 'Received close frame with a 1-byte body.')
102+
return
103+
}
103104

104-
const body = this.consume(payloadLength)
105+
const body = this.consume(payloadLength)
106+
107+
this.#info.closeInfo = this.parseCloseBody(false, body)
108+
109+
if (!this.ws[kSentClose]) {
110+
// If an endpoint receives a Close frame and did not previously send a
111+
// Close frame, the endpoint MUST send a Close frame in response. (When
112+
// sending a Close frame in response, the endpoint typically echos the
113+
// status code it received.)
114+
const body = Buffer.allocUnsafe(2)
115+
body.writeUInt16BE(this.#info.closeInfo.code, 0)
116+
const closeFrame = new WebsocketFrameSend(body)
117+
118+
this.ws[kResponse].socket.write(
119+
closeFrame.createFrame(opcodes.CLOSE),
120+
(err) => {
121+
if (!err) {
122+
this.ws[kSentClose] = true
123+
}
124+
}
125+
)
126+
}
105127

106-
this.#info.closeInfo = this.parseCloseBody(false, body)
128+
// Upon either sending or receiving a Close control frame, it is said
129+
// that _The WebSocket Closing Handshake is Started_ and that the
130+
// WebSocket connection is in the CLOSING state.
131+
this.ws[kReadyState] = states.CLOSING
132+
this.ws[kReceivedClose] = true
107133

108-
if (!this.ws[kSentClose]) {
109-
// If an endpoint receives a Close frame and did not previously send a
110-
// Close frame, the endpoint MUST send a Close frame in response. (When
111-
// sending a Close frame in response, the endpoint typically echos the
112-
// status code it received.)
113-
const body = Buffer.allocUnsafe(2)
114-
body.writeUInt16BE(this.#info.closeInfo.code, 0)
115-
const closeFrame = new WebsocketFrameSend(body)
134+
this.end()
116135

117-
this.ws[kResponse].socket.write(
118-
closeFrame.createFrame(opcodes.CLOSE),
119-
(err) => {
120-
if (!err) {
121-
this.ws[kSentClose] = true
122-
}
123-
}
124-
)
125-
}
136+
return
137+
} else if (this.#info.opcode === opcodes.PING) {
138+
// Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
139+
// response, unless it already received a Close frame.
140+
// A Pong frame sent in response to a Ping frame must have identical
141+
// "Application data"
142+
143+
const body = this.consume(payloadLength)
126144

127-
// Upon either sending or receiving a Close control frame, it is said
128-
// that _The WebSocket Closing Handshake is Started_ and that the
129-
// WebSocket connection is in the CLOSING state.
130-
this.ws[kReadyState] = states.CLOSING
131-
this.ws[kReceivedClose] = true
145+
if (!this.ws[kReceivedClose]) {
146+
const frame = new WebsocketFrameSend(body)
132147

133-
this.end()
148+
this.ws[kResponse].socket.write(frame.createFrame(opcodes.PONG))
134149

135-
return
136-
} else if (this.#info.opcode === opcodes.PING) {
137-
// Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
138-
// response, unless it already received a Close frame.
139-
// A Pong frame sent in response to a Ping frame must have identical
140-
// "Application data"
150+
if (channels.ping.hasSubscribers) {
151+
channels.ping.publish({
152+
payload: body
153+
})
154+
}
155+
}
141156

142-
const body = this.consume(payloadLength)
157+
this.#state = parserStates.INFO
143158

144-
if (!this.ws[kReceivedClose]) {
145-
const frame = new WebsocketFrameSend(body)
159+
if (this.#byteOffset > 0) {
160+
continue
161+
} else {
162+
callback()
163+
return
164+
}
165+
} else if (this.#info.opcode === opcodes.PONG) {
166+
// A Pong frame MAY be sent unsolicited. This serves as a
167+
// unidirectional heartbeat. A response to an unsolicited Pong frame is
168+
// not expected.
146169

147-
this.ws[kResponse].socket.write(frame.createFrame(opcodes.PONG))
170+
const body = this.consume(payloadLength)
148171

149-
if (channels.ping.hasSubscribers) {
150-
channels.ping.publish({
172+
if (channels.pong.hasSubscribers) {
173+
channels.pong.publish({
151174
payload: body
152175
})
153176
}
154-
}
155-
156-
this.#state = parserStates.INFO
157177

158-
if (this.#byteOffset > 0) {
159-
return this.run(callback)
160-
} else {
161-
callback()
162-
return
178+
if (this.#byteOffset > 0) {
179+
continue
180+
} else {
181+
callback()
182+
return
183+
}
184+
}
185+
} else if (this.#state === parserStates.PAYLOADLENGTH_16) {
186+
if (this.#byteOffset < 2) {
187+
return callback()
163188
}
164-
} else if (this.#info.opcode === opcodes.PONG) {
165-
// A Pong frame MAY be sent unsolicited. This serves as a
166-
// unidirectional heartbeat. A response to an unsolicited Pong frame is
167-
// not expected.
168189

169-
const body = this.consume(payloadLength)
190+
const buffer = this.consume(2)
170191

171-
if (channels.pong.hasSubscribers) {
172-
channels.pong.publish({
173-
payload: body
174-
})
192+
this.#info.payloadLength = buffer.readUInt16BE(0)
193+
this.#state = parserStates.READ_DATA
194+
} else if (this.#state === parserStates.PAYLOADLENGTH_64) {
195+
if (this.#byteOffset < 8) {
196+
return callback()
175197
}
176198

177-
if (this.#byteOffset > 0) {
178-
return this.run(callback)
179-
} else {
180-
callback()
199+
const buffer = this.consume(8)
200+
const upper = buffer.readUInt32BE(0)
201+
202+
// 2^31 is the maxinimum bytes an arraybuffer can contain
203+
// on 32-bit systems. Although, on 64-bit systems, this is
204+
// 2^53-1 bytes.
205+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length
206+
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275
207+
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e
208+
if (upper > 2 ** 31 - 1) {
209+
failWebsocketConnection(this.ws, 'Received payload length > 2^31 bytes.')
181210
return
182211
}
183-
}
184-
} else if (this.#state === parserStates.PAYLOADLENGTH_16) {
185-
if (this.#byteOffset < 2) {
186-
return callback()
187-
}
188-
189-
const buffer = this.consume(2)
190212

191-
this.#info.payloadLength = buffer.readUInt16BE(0)
192-
this.#state = parserStates.READ_DATA
193-
} else if (this.#state === parserStates.PAYLOADLENGTH_64) {
194-
if (this.#byteOffset < 8) {
195-
return callback()
196-
}
197-
198-
const buffer = this.consume(8)
199-
const upper = buffer.readUInt32BE(0)
200-
201-
// 2^31 is the maxinimum bytes an arraybuffer can contain
202-
// on 32-bit systems. Although, on 64-bit systems, this is
203-
// 2^53-1 bytes.
204-
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length
205-
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275
206-
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e
207-
if (upper > 2 ** 31 - 1) {
208-
failWebsocketConnection(this.ws, 'Received payload length > 2^31 bytes.')
209-
return
210-
}
213+
const lower = buffer.readUInt32BE(4)
211214

212-
const lower = buffer.readUInt32BE(4)
215+
this.#info.payloadLength = (upper << 8) + lower
216+
this.#state = parserStates.READ_DATA
217+
} else if (this.#state === parserStates.READ_DATA) {
218+
if (this.#byteOffset < this.#info.payloadLength) {
219+
// If there is still more data in this chunk that needs to be read
220+
return callback()
221+
} else if (this.#byteOffset >= this.#info.payloadLength) {
222+
// If the server sent multiple frames in a single chunk
213223

214-
this.#info.payloadLength = (upper << 8) + lower
215-
this.#state = parserStates.READ_DATA
216-
} else if (this.#state === parserStates.READ_DATA) {
217-
if (this.#byteOffset < this.#info.payloadLength) {
218-
// If there is still more data in this chunk that needs to be read
219-
return callback()
220-
} else if (this.#byteOffset >= this.#info.payloadLength) {
221-
// If the server sent multiple frames in a single chunk
224+
const body = this.consume(this.#info.payloadLength)
222225

223-
const body = this.consume(this.#info.payloadLength)
226+
this.#fragments.push(body)
224227

225-
this.#fragments.push(body)
228+
// If the frame is unfragmented, or a fragmented frame was terminated,
229+
// a message was received
230+
if (!this.#info.fragmented || (this.#info.fin && this.#info.opcode === opcodes.CONTINUATION)) {
231+
const fullMessage = Buffer.concat(this.#fragments)
226232

227-
// If the frame is unfragmented, or a fragmented frame was terminated,
228-
// a message was received
229-
if (!this.#info.fragmented || (this.#info.fin && this.#info.opcode === opcodes.CONTINUATION)) {
230-
const fullMessage = Buffer.concat(this.#fragments)
233+
websocketMessageReceived(this.ws, this.#info.originalOpcode, fullMessage)
231234

232-
websocketMessageReceived(this.ws, this.#info.originalOpcode, fullMessage)
235+
this.#info = {}
236+
this.#fragments.length = 0
237+
}
233238

234-
this.#info = {}
235-
this.#fragments.length = 0
239+
this.#state = parserStates.INFO
236240
}
237-
238-
this.#state = parserStates.INFO
239241
}
240-
}
241242

242-
if (this.#byteOffset > 0) {
243-
return this.run(callback)
244-
} else {
245-
callback()
243+
if (this.#byteOffset > 0) {
244+
continue
245+
} else {
246+
callback()
247+
break
248+
}
246249
}
247250
}
248251

0 commit comments

Comments
 (0)