Skip to content

Commit 0b35dc7

Browse files
committedMar 31, 2022
refactor: make the protocol implementation stricter
This commit handles several edge cases that were silently ignored before: - receiving several CONNECT packets during a session - receiving any packet without CONNECT packet first
1 parent 531104d commit 0b35dc7

File tree

3 files changed

+123
-18
lines changed

3 files changed

+123
-18
lines changed
 

‎lib/client.ts

+25-15
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ export class Client<
248248
try {
249249
this.decoder.add(data);
250250
} catch (e) {
251+
debug("invalid packet format");
251252
this.onerror(e);
252253
}
253254
}
@@ -258,22 +259,31 @@ export class Client<
258259
* @private
259260
*/
260261
private ondecoded(packet: Packet): void {
261-
if (PacketType.CONNECT === packet.type) {
262-
if (this.conn.protocol === 3) {
263-
const parsed = url.parse(packet.nsp, true);
264-
this.connect(parsed.pathname!, parsed.query);
265-
} else {
266-
this.connect(packet.nsp, packet.data);
267-
}
262+
let namespace: string;
263+
let authPayload;
264+
if (this.conn.protocol === 3) {
265+
const parsed = url.parse(packet.nsp, true);
266+
namespace = parsed.pathname!;
267+
authPayload = parsed.query;
268268
} else {
269-
const socket = this.nsps.get(packet.nsp);
270-
if (socket) {
271-
process.nextTick(function () {
272-
socket._onpacket(packet);
273-
});
274-
} else {
275-
debug("no socket for namespace %s", packet.nsp);
276-
}
269+
namespace = packet.nsp;
270+
authPayload = packet.data;
271+
}
272+
const socket = this.nsps.get(namespace);
273+
274+
if (!socket && packet.type === PacketType.CONNECT) {
275+
this.connect(namespace, authPayload);
276+
} else if (
277+
socket &&
278+
packet.type !== PacketType.CONNECT &&
279+
packet.type !== PacketType.CONNECT_ERROR
280+
) {
281+
process.nextTick(function () {
282+
socket._onpacket(packet);
283+
});
284+
} else {
285+
debug("invalid state (packet type: %s)", packet.type);
286+
this.close();
277287
}
278288
}
279289

‎lib/socket.ts

-3
Original file line numberDiff line numberDiff line change
@@ -407,9 +407,6 @@ export class Socket<
407407
case PacketType.DISCONNECT:
408408
this.ondisconnect();
409409
break;
410-
411-
case PacketType.CONNECT_ERROR:
412-
this._onerror(new Error(packet.data));
413410
}
414411
}
415412

‎test/socket.io.ts

+98
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,44 @@ const getPort = (io: Server): number => {
4747
return io.httpServer.address().port;
4848
};
4949

50+
// TODO: update superagent as latest release now supports promises
51+
const eioHandshake = (httpServer): Promise<string> => {
52+
return new Promise((resolve) => {
53+
request(httpServer)
54+
.get("/socket.io/")
55+
.query({ transport: "polling", EIO: 4 })
56+
.end((err, res) => {
57+
const sid = JSON.parse(res.text.substring(1)).sid;
58+
resolve(sid);
59+
});
60+
});
61+
};
62+
63+
const eioPush = (httpServer, sid: string, body: string): Promise<void> => {
64+
return new Promise((resolve) => {
65+
request(httpServer)
66+
.post("/socket.io/")
67+
.send(body)
68+
.query({ transport: "polling", EIO: 4, sid })
69+
.expect(200)
70+
.end(() => {
71+
resolve();
72+
});
73+
});
74+
};
75+
76+
const eioPoll = (httpServer, sid): Promise<string> => {
77+
return new Promise((resolve) => {
78+
request(httpServer)
79+
.get("/socket.io/")
80+
.query({ transport: "polling", EIO: 4, sid })
81+
.expect(200)
82+
.end((err, res) => {
83+
resolve(res.text);
84+
});
85+
});
86+
};
87+
5088
describe("socket.io", () => {
5189
it("should be the same version as client", () => {
5290
const version = require("../package").version;
@@ -378,6 +416,66 @@ describe("socket.io", () => {
378416
exec(fixture("server-close.ts"), done);
379417
});
380418
});
419+
420+
describe("protocol violations", () => {
421+
it("should close the connection when receiving several CONNECT packets", async () => {
422+
const httpServer = createServer();
423+
const io = new Server(httpServer);
424+
425+
httpServer.listen(0);
426+
427+
const sid = await eioHandshake(httpServer);
428+
// send a first CONNECT packet
429+
await eioPush(httpServer, sid, "40");
430+
// send another CONNECT packet
431+
await eioPush(httpServer, sid, "40");
432+
// session is cleanly closed (not discarded, see 'client.close()')
433+
// first, we receive the Socket.IO handshake response
434+
await eioPoll(httpServer, sid);
435+
// then a close packet
436+
const body = await eioPoll(httpServer, sid);
437+
expect(body).to.be("6\u001e1");
438+
439+
io.close();
440+
});
441+
442+
it("should close the connection when receiving an EVENT packet while not connected", async () => {
443+
const httpServer = createServer();
444+
const io = new Server(httpServer);
445+
446+
httpServer.listen(0);
447+
448+
const sid = await eioHandshake(httpServer);
449+
// send an EVENT packet
450+
await eioPush(httpServer, sid, '42["some event"]');
451+
// session is cleanly closed, we receive a close packet
452+
const body = await eioPoll(httpServer, sid);
453+
expect(body).to.be("6\u001e1");
454+
455+
io.close();
456+
});
457+
458+
it("should close the connection when receiving an invalid packet", async () => {
459+
const httpServer = createServer();
460+
const io = new Server(httpServer);
461+
462+
httpServer.listen(0);
463+
464+
const sid = await eioHandshake(httpServer);
465+
// send a CONNECT packet
466+
await eioPush(httpServer, sid, "40");
467+
// send an invalid packet
468+
await eioPush(httpServer, sid, "4abc");
469+
// session is cleanly closed (not discarded, see 'client.close()')
470+
// first, we receive the Socket.IO handshake response
471+
await eioPoll(httpServer, sid);
472+
// then a close packet
473+
const body = await eioPoll(httpServer, sid);
474+
expect(body).to.be("6\u001e1");
475+
476+
io.close();
477+
});
478+
});
381479
});
382480

383481
describe("namespaces", () => {

0 commit comments

Comments
 (0)
Please sign in to comment.