From 91cd255ba76ff6a780c62740f9f5cd3a76f5d7c7 Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Thu, 15 Oct 2020 11:42:06 +0200 Subject: [PATCH] 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). --- lib/client.ts | 14 ++++++++++ lib/index.ts | 68 ++++++++++++++++++++++++++++++++++++----------- test/socket.io.ts | 19 +++++++++++++ 3 files changed, 85 insertions(+), 16 deletions(-) diff --git a/lib/client.ts b/lib/client.ts index fe69aa2ec1..712d9f0013 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -16,6 +16,7 @@ export class Client { private readonly decoder: Decoder; private sockets: Map = new Map(); private nsps: Map = new Map(); + private connectTimeout: NodeJS.Timeout; /** * Client constructor. @@ -58,6 +59,15 @@ export class Client { this.conn.on("data", this.ondata); this.conn.on("error", this.onerror); this.conn.on("close", this.onclose); + + this.connectTimeout = setTimeout(() => { + if (this.nsps.size === 0) { + debug("no namespace joined yet, close the client"); + this.close(); + } else { + debug("the client has already joined a namespace, nothing to do"); + } + }, this.server._connectTimeout); } /** @@ -97,6 +107,10 @@ export class Client { * @private */ private doConnect(name: string, auth: object) { + if (this.connectTimeout) { + clearTimeout(this.connectTimeout); + this.connectTimeout = null; + } const nsp = this.server.of(name); const socket = nsp.add(this, auth, () => { diff --git a/lib/index.ts b/lib/index.ts index 1c8e30d6d5..91fc2ad03a 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -29,19 +29,23 @@ type Transport = "polling" | "websocket"; interface EngineOptions { /** - * how many ms without a pong packet to consider the connection closed (5000) + * how many ms without a pong packet to consider the connection closed + * @default 5000 */ pingTimeout: number; /** - * how many ms before sending a new ping packet (25000) + * how many ms before sending a new ping packet + * @default 25000 */ pingInterval: number; /** - * how many ms before an uncompleted transport upgrade is cancelled (10000) + * how many ms before an uncompleted transport upgrade is cancelled + * @default 10000 */ upgradeTimeout: number; /** - * how many bytes or characters a message can be, before closing the session (to avoid DoS). Default value is 1E5. + * how many bytes or characters a message can be, before closing the session (to avoid DoS). + * @default 1e5 (100 KB) */ maxHttpBufferSize: number; /** @@ -55,19 +59,23 @@ interface EngineOptions { fn: (err: string | null | undefined, success: boolean) => void ) => void; /** - * to allow connections to (['polling', 'websocket']) + * the low-level transports that are enabled + * @default ["polling", "websocket"] */ transports: Transport[]; /** - * whether to allow transport upgrades (true) + * whether to allow transport upgrades + * @default true */ allowUpgrades: boolean; /** - * parameters of the WebSocket permessage-deflate extension (see ws module api docs). Set to false to disable. (false) + * parameters of the WebSocket permessage-deflate extension (see ws module api docs). Set to false to disable. + * @default false */ perMessageDeflate: boolean | object; /** - * parameters of the http compression for the polling transports (see zlib api docs). Set to false to disable. (true) + * parameters of the http compression for the polling transports (see zlib api docs). Set to false to disable. + * @default true */ httpCompression: boolean | object; /** @@ -82,7 +90,8 @@ interface EngineOptions { initialPacket: any; /** * configuration of the cookie that contains the client sid to send as part of handshake response headers. This cookie - * might be used for sticky-session. Defaults to not sending any cookie (false) + * might be used for sticky-session. Defaults to not sending any cookie. + * @default false */ cookie: CookieSerializeOptions | boolean; /** @@ -93,15 +102,18 @@ interface EngineOptions { interface AttachOptions { /** - * name of the path to capture (/engine.io). + * name of the path to capture + * @default "/engine.io" */ path: string; /** - * destroy unhandled upgrade requests (true) + * destroy unhandled upgrade requests + * @default true */ destroyUpgrade: boolean; /** - * milliseconds after which unhandled requests are ended (1000) + * milliseconds after which unhandled requests are ended + * @default 1000 */ destroyUpgradeTimeout: number; } @@ -110,21 +122,30 @@ interface EngineAttachOptions extends EngineOptions, AttachOptions {} interface ServerOptions extends EngineAttachOptions { /** - * name of the path to capture (/socket.io) + * name of the path to capture + * @default "/socket.io" */ path: string; /** - * whether to serve the client files (true) + * whether to serve the client files + * @default true */ serveClient: boolean; /** - * the adapter to use. Defaults to an instance of the Adapter that ships with socket.io which is memory based. + * the adapter to use + * @default the in-memory adapter (https://github.com/socketio/socket.io-adapter) */ adapter: any; /** - * the parser to use. Defaults to an instance of the Parser that ships with socket.io. + * the parser to use + * @default the default parser (https://github.com/socketio/socket.io-parser) */ parser: any; + /** + * how many ms before a client without namespace is closed + * @default 45000 + */ + connectTimeout: number; } export class Server extends EventEmitter { @@ -154,6 +175,7 @@ export class Server extends EventEmitter { private eio; private engine; private _path: string; + private _connectTimeout: number; private httpServer: http.Server; /** @@ -173,6 +195,7 @@ export class Server extends EventEmitter { srv = null; } this.path(opts.path || "/socket.io"); + this.connectTimeout(opts.connectTimeout || 45000); this.serveClient(false !== opts.serveClient); this._parser = opts.parser || parser; this.encoder = new this._parser.Encoder(); @@ -263,6 +286,19 @@ export class Server extends EventEmitter { return this; } + /** + * Set the delay after which a client without namespace is closed + * @param v + * @public + */ + public connectTimeout(v: number): Server; + public connectTimeout(): number; + public connectTimeout(v?: number): Server | number { + if (v === undefined) return this._connectTimeout; + this._connectTimeout = v; + return this; + } + /** * Sets the adapter for rooms. * diff --git a/test/socket.io.ts b/test/socket.io.ts index 9a75772ef0..8e330d1a58 100644 --- a/test/socket.io.ts +++ b/test/socket.io.ts @@ -701,6 +701,25 @@ describe("socket.io", () => { ); }); + it("should close a client without namespace", done => { + const srv = createServer(); + const sio = new Server(srv, { + connectTimeout: 10 + }); + + srv.listen(() => { + const socket = client(srv); + + socket.io.engine.write = () => {}; // prevent the client from sending a CONNECT packet + + socket.on("disconnect", () => { + socket.close(); + sio.close(); + done(); + }); + }); + }); + describe("dynamic namespaces", () => { it("should allow connections to dynamic namespaces with a regex", done => { const srv = createServer();