Skip to content

Commit 91cd255

Browse files
committedOct 15, 2020
fix: close clients with no namespace
After a given timeout, a client that did not join any namespace will be closed in order to prevent malicious clients from using the server resources. The timeout defaults to 45 seconds, in order not to interfere with the Engine.IO heartbeat mechanism (30 seconds).
1 parent 58b66f8 commit 91cd255

File tree

3 files changed

+85
-16
lines changed

3 files changed

+85
-16
lines changed
 

‎lib/client.ts

+14
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export class Client {
1616
private readonly decoder: Decoder;
1717
private sockets: Map<SocketId, Socket> = new Map();
1818
private nsps: Map<string, Socket> = new Map();
19+
private connectTimeout: NodeJS.Timeout;
1920

2021
/**
2122
* Client constructor.
@@ -58,6 +59,15 @@ export class Client {
5859
this.conn.on("data", this.ondata);
5960
this.conn.on("error", this.onerror);
6061
this.conn.on("close", this.onclose);
62+
63+
this.connectTimeout = setTimeout(() => {
64+
if (this.nsps.size === 0) {
65+
debug("no namespace joined yet, close the client");
66+
this.close();
67+
} else {
68+
debug("the client has already joined a namespace, nothing to do");
69+
}
70+
}, this.server._connectTimeout);
6171
}
6272

6373
/**
@@ -97,6 +107,10 @@ export class Client {
97107
* @private
98108
*/
99109
private doConnect(name: string, auth: object) {
110+
if (this.connectTimeout) {
111+
clearTimeout(this.connectTimeout);
112+
this.connectTimeout = null;
113+
}
100114
const nsp = this.server.of(name);
101115

102116
const socket = nsp.add(this, auth, () => {

‎lib/index.ts

+52-16
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,23 @@ type Transport = "polling" | "websocket";
2929

3030
interface EngineOptions {
3131
/**
32-
* how many ms without a pong packet to consider the connection closed (5000)
32+
* how many ms without a pong packet to consider the connection closed
33+
* @default 5000
3334
*/
3435
pingTimeout: number;
3536
/**
36-
* how many ms before sending a new ping packet (25000)
37+
* how many ms before sending a new ping packet
38+
* @default 25000
3739
*/
3840
pingInterval: number;
3941
/**
40-
* how many ms before an uncompleted transport upgrade is cancelled (10000)
42+
* how many ms before an uncompleted transport upgrade is cancelled
43+
* @default 10000
4144
*/
4245
upgradeTimeout: number;
4346
/**
44-
* how many bytes or characters a message can be, before closing the session (to avoid DoS). Default value is 1E5.
47+
* how many bytes or characters a message can be, before closing the session (to avoid DoS).
48+
* @default 1e5 (100 KB)
4549
*/
4650
maxHttpBufferSize: number;
4751
/**
@@ -55,19 +59,23 @@ interface EngineOptions {
5559
fn: (err: string | null | undefined, success: boolean) => void
5660
) => void;
5761
/**
58-
* to allow connections to (['polling', 'websocket'])
62+
* the low-level transports that are enabled
63+
* @default ["polling", "websocket"]
5964
*/
6065
transports: Transport[];
6166
/**
62-
* whether to allow transport upgrades (true)
67+
* whether to allow transport upgrades
68+
* @default true
6369
*/
6470
allowUpgrades: boolean;
6571
/**
66-
* parameters of the WebSocket permessage-deflate extension (see ws module api docs). Set to false to disable. (false)
72+
* parameters of the WebSocket permessage-deflate extension (see ws module api docs). Set to false to disable.
73+
* @default false
6774
*/
6875
perMessageDeflate: boolean | object;
6976
/**
70-
* parameters of the http compression for the polling transports (see zlib api docs). Set to false to disable. (true)
77+
* parameters of the http compression for the polling transports (see zlib api docs). Set to false to disable.
78+
* @default true
7179
*/
7280
httpCompression: boolean | object;
7381
/**
@@ -82,7 +90,8 @@ interface EngineOptions {
8290
initialPacket: any;
8391
/**
8492
* configuration of the cookie that contains the client sid to send as part of handshake response headers. This cookie
85-
* might be used for sticky-session. Defaults to not sending any cookie (false)
93+
* might be used for sticky-session. Defaults to not sending any cookie.
94+
* @default false
8695
*/
8796
cookie: CookieSerializeOptions | boolean;
8897
/**
@@ -93,15 +102,18 @@ interface EngineOptions {
93102

94103
interface AttachOptions {
95104
/**
96-
* name of the path to capture (/engine.io).
105+
* name of the path to capture
106+
* @default "/engine.io"
97107
*/
98108
path: string;
99109
/**
100-
* destroy unhandled upgrade requests (true)
110+
* destroy unhandled upgrade requests
111+
* @default true
101112
*/
102113
destroyUpgrade: boolean;
103114
/**
104-
* milliseconds after which unhandled requests are ended (1000)
115+
* milliseconds after which unhandled requests are ended
116+
* @default 1000
105117
*/
106118
destroyUpgradeTimeout: number;
107119
}
@@ -110,21 +122,30 @@ interface EngineAttachOptions extends EngineOptions, AttachOptions {}
110122

111123
interface ServerOptions extends EngineAttachOptions {
112124
/**
113-
* name of the path to capture (/socket.io)
125+
* name of the path to capture
126+
* @default "/socket.io"
114127
*/
115128
path: string;
116129
/**
117-
* whether to serve the client files (true)
130+
* whether to serve the client files
131+
* @default true
118132
*/
119133
serveClient: boolean;
120134
/**
121-
* the adapter to use. Defaults to an instance of the Adapter that ships with socket.io which is memory based.
135+
* the adapter to use
136+
* @default the in-memory adapter (https://github.com/socketio/socket.io-adapter)
122137
*/
123138
adapter: any;
124139
/**
125-
* the parser to use. Defaults to an instance of the Parser that ships with socket.io.
140+
* the parser to use
141+
* @default the default parser (https://github.com/socketio/socket.io-parser)
126142
*/
127143
parser: any;
144+
/**
145+
* how many ms before a client without namespace is closed
146+
* @default 45000
147+
*/
148+
connectTimeout: number;
128149
}
129150

130151
export class Server extends EventEmitter {
@@ -154,6 +175,7 @@ export class Server extends EventEmitter {
154175
private eio;
155176
private engine;
156177
private _path: string;
178+
private _connectTimeout: number;
157179
private httpServer: http.Server;
158180

159181
/**
@@ -173,6 +195,7 @@ export class Server extends EventEmitter {
173195
srv = null;
174196
}
175197
this.path(opts.path || "/socket.io");
198+
this.connectTimeout(opts.connectTimeout || 45000);
176199
this.serveClient(false !== opts.serveClient);
177200
this._parser = opts.parser || parser;
178201
this.encoder = new this._parser.Encoder();
@@ -263,6 +286,19 @@ export class Server extends EventEmitter {
263286
return this;
264287
}
265288

289+
/**
290+
* Set the delay after which a client without namespace is closed
291+
* @param v
292+
* @public
293+
*/
294+
public connectTimeout(v: number): Server;
295+
public connectTimeout(): number;
296+
public connectTimeout(v?: number): Server | number {
297+
if (v === undefined) return this._connectTimeout;
298+
this._connectTimeout = v;
299+
return this;
300+
}
301+
266302
/**
267303
* Sets the adapter for rooms.
268304
*

‎test/socket.io.ts

+19
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,25 @@ describe("socket.io", () => {
701701
);
702702
});
703703

704+
it("should close a client without namespace", done => {
705+
const srv = createServer();
706+
const sio = new Server(srv, {
707+
connectTimeout: 10
708+
});
709+
710+
srv.listen(() => {
711+
const socket = client(srv);
712+
713+
socket.io.engine.write = () => {}; // prevent the client from sending a CONNECT packet
714+
715+
socket.on("disconnect", () => {
716+
socket.close();
717+
sio.close();
718+
done();
719+
});
720+
});
721+
});
722+
704723
describe("dynamic namespaces", () => {
705724
it("should allow connections to dynamic namespaces with a regex", done => {
706725
const srv = createServer();

0 commit comments

Comments
 (0)
Please sign in to comment.