Files
socket.io/lib/client.ts
Damien Arrachequesne 3289f7ec37 feat: remove the implicit connection to the default namespace
In previous versions, a client was always connected to the default
namespace, even if it requested access to another namespace.

This meant that the middlewares registered for the default namespace
were triggered in any case, which is a surprising behavior for end
users.

This also meant that the query option of the Socket on the client-side
was not sent in the Socket.IO CONNECT packet for the default namespace:

```js
// default namespace: query sent in the query params
const socket = io({
  query: {
    abc: "def"
  }
});

// another namespace: query sent in the query params + the CONNECT packet
const socket = io("/admin", {
  query: {
    abc: "def"
  }
});
```

The client will now send a CONNECT packet in any case, and the query
option of the Socket is renamed to "auth", in order to make a clear
distinction with the query option of the Manager (included in the query
parameters of the HTTP requests).

```js
// server-side
io.use((socket, next) => {
  // not triggered anymore
});

io.of("/admin").use((socket, next => {
  // triggered
  console.log(socket.handshake.query.abc); // "def"
  console.log(socket.handshake.auth.abc); // "123"
});

// client-side
const socket = io("/admin", {
  query: {
    abc: "def"
  },
  auth: {
    abc: "123"
  }
});
```
2020-10-13 23:02:07 +02:00

248 lines
5.9 KiB
TypeScript

import { Decoder, Encoder, Packet, PacketType } from "socket.io-parser";
import debugModule = require("debug");
import { IncomingMessage } from "http";
import { Server } from "./index";
import { Socket } from "./socket";
import { SocketId } from "socket.io-adapter";
const debug = debugModule("socket.io:client");
export class Client {
public readonly conn;
/** @package */
public readonly id: string;
private readonly server;
private readonly encoder: Encoder;
private readonly decoder: Decoder;
private sockets: Map<SocketId, Socket> = new Map();
private nsps: Map<string, Socket> = new Map();
/**
* Client constructor.
*
* @param {Server} server instance
* @param {Socket} conn
* @package
*/
constructor(server: Server, conn) {
this.server = server;
this.conn = conn;
this.encoder = server.encoder;
this.decoder = new server.parser.Decoder();
this.id = conn.id;
this.setup();
}
/**
* @return the reference to the request that originated the Engine.IO connection
*/
public get request(): IncomingMessage {
return this.conn.request;
}
/**
* Sets up event listeners.
*/
private setup() {
this.onclose = this.onclose.bind(this);
this.ondata = this.ondata.bind(this);
this.onerror = this.onerror.bind(this);
this.ondecoded = this.ondecoded.bind(this);
// @ts-ignore
this.decoder.on("decoded", this.ondecoded);
this.conn.on("data", this.ondata);
this.conn.on("error", this.onerror);
this.conn.on("close", this.onclose);
}
/**
* Connects a client to a namespace.
*
* @param {String} name - the namespace
* @param {Object} auth - the auth parameters
* @package
*/
public connect(name: string, auth: object = {}) {
if (this.server.nsps.has(name)) {
debug("connecting to namespace %s", name);
return this.doConnect(name, auth);
}
this.server.checkNamespace(name, auth, dynamicNsp => {
if (dynamicNsp) {
debug("dynamic namespace %s was created", dynamicNsp.name);
this.doConnect(name, auth);
} else {
debug("creation of namespace %s was denied", name);
this.packet({
type: PacketType.ERROR,
nsp: name,
data: "Invalid namespace"
});
}
});
}
/**
* Connects a client to a namespace.
*
* @param {String} name - the namespace
* @param {Object} auth - the auth parameters
*/
private doConnect(name: string, auth: object) {
const nsp = this.server.of(name);
const socket = nsp.add(this, auth, () => {
this.sockets.set(socket.id, socket);
this.nsps.set(nsp.name, socket);
});
}
/**
* Disconnects from all namespaces and closes transport.
*
* @package
*/
public disconnect() {
for (const socket of this.sockets.values()) {
socket.disconnect();
}
this.sockets.clear();
this.close();
}
/**
* Removes a socket. Called by each `Socket`.
*
* @package
*/
public remove(socket: Socket) {
if (this.sockets.has(socket.id)) {
const nsp = this.sockets.get(socket.id).nsp.name;
this.sockets.delete(socket.id);
this.nsps.delete(nsp);
} else {
debug("ignoring remove for %s", socket.id);
}
}
/**
* Closes the underlying connection.
*/
private close() {
if ("open" == this.conn.readyState) {
debug("forcing transport close");
this.conn.close();
this.onclose("forced server close");
}
}
/**
* Writes a packet to the transport.
*
* @param {Object} packet object
* @param {Object} opts
* @package
*/
public packet(packet, opts?) {
opts = opts || {};
const self = this;
// this writes to the actual connection
function writeToEngine(encodedPackets) {
if (opts.volatile && !self.conn.transport.writable) return;
for (let i = 0; i < encodedPackets.length; i++) {
self.conn.write(encodedPackets[i], { compress: opts.compress });
}
}
if ("open" == this.conn.readyState) {
debug("writing packet %j", packet);
if (!opts.preEncoded) {
// not broadcasting, need to encode
writeToEngine(this.encoder.encode(packet)); // encode, then write results to engine
} else {
// a broadcast pre-encodes a packet
writeToEngine(packet);
}
} else {
debug("ignoring packet write %j", packet);
}
}
/**
* Called with incoming transport data.
*/
private ondata(data) {
// try/catch is needed for protocol violations (GH-1880)
try {
this.decoder.add(data);
} catch (e) {
this.onerror(e);
}
}
/**
* Called when parser fully decodes a packet.
*/
private ondecoded(packet: Packet) {
if (PacketType.CONNECT == packet.type) {
this.connect(packet.nsp, packet.data);
} else {
const socket = this.nsps.get(packet.nsp);
if (socket) {
process.nextTick(function() {
socket.onpacket(packet);
});
} else {
debug("no socket for namespace %s", packet.nsp);
}
}
}
/**
* Handles an error.
*
* @param {Object} err object
*/
private onerror(err) {
for (const socket of this.sockets.values()) {
socket.onerror(err);
}
this.conn.close();
}
/**
* Called upon transport close.
*
* @param reason
*/
private onclose(reason: string) {
debug("client close with reason %s", reason);
// ignore a potential subsequent `close` event
this.destroy();
// `nsps` and `sockets` are cleaned up seamlessly
for (const socket of this.sockets.values()) {
socket.onclose(reason);
}
this.sockets.clear();
this.decoder.destroy(); // clean up decoder
}
/**
* Cleans up event listeners.
*/
private destroy() {
this.conn.removeListener("data", this.ondata);
this.conn.removeListener("error", this.onerror);
this.conn.removeListener("close", this.onclose);
// @ts-ignore
this.decoder.removeListener("decoded", this.ondecoded);
}
}