Compare commits

...

16 Commits
4.2.0 ... 2.5.1

Author SHA1 Message Date
Damien Arrachequesne
88b2cdb6ab chore(release): 2.5.1
Release notes: https://github.com/socketio/socket.io/releases/tag/2.5.1
Diff: https://github.com/socketio/socket.io/compare/2.5.0...2.5.1
2024-06-19 10:48:24 +02:00
Damien Arrachequesne
d30630ba10 fix: add a noop handler for the error event
Backported from main: 15af22fc22
2024-06-19 10:46:29 +02:00
Damien Arrachequesne
f927ba29ef test: fix tests on Node.js > 18
Reference: https://nodejs.org/api/buffer.html#class-blob
2024-06-19 10:44:04 +02:00
Damien Arrachequesne
baa6804440 chore(release): 2.5.0
Release notes: https://github.com/socketio/socket.io/releases/tag/2.5.0
Diff: https://github.com/socketio/socket.io/compare/2.4.1...2.5.0
2022-06-26 09:49:21 +02:00
Damien Arrachequesne
f223178eb6 fix: prevent the socket from joining a room after disconnection
Calling `socket.join()` after disconnection would lead to a memory
leak, because the room was never removed from the memory:

```js
io.on("connection", (socket) => {
  socket.disconnect();
  socket.join("room1"); // leak
});
```

Related:

- https://github.com/socketio/socket.io/issues/4067
- https://github.com/socketio/socket.io/issues/4380

Backported from 18f3fdab12
2022-06-26 08:54:51 +02:00
Damien Arrachequesne
226cc16165 fix: only set 'connected' to true after middleware execution
The Socket instance is only considered connected when the "connection"
event is emitted, and not during the middleware(s) execution.

```js
io.use((socket, next) => {
  console.log(socket.connected); // prints "false"
  next();
});

io.on("connection", (socket) => {
  console.log(socket.connected); // prints "true"
});
```

Related: https://github.com/socketio/socket.io/issues/4129

Backported from 02b0f73e2c
2022-06-26 08:46:28 +02:00
Damien Arrachequesne
05e1278cfa fix: fix race condition in dynamic namespaces
Using an async operation with `io.use()` could lead to the creation of
several instances of a same namespace, each of them overriding the
previous one.

Example:

```js
io.use(async (nsp, auth, next) => {
  await anOperationThatTakesSomeTime();
  next();
});
```

Related: https://github.com/socketio/socket.io/issues/4136

Backported from 9d86397243
2022-06-26 08:41:16 +02:00
Damien Arrachequesne
22d4bdf00d fix: ignore packet received after disconnection
Related: https://github.com/socketio/socket.io/issues/3095

Backported from 494c64e44f
2022-06-26 08:35:42 +02:00
Damien Arrachequesne
dfded53593 chore: update engine.io version to 3.6.0
Release notes: https://github.com/socketio/engine.io/releases/tag/3.6.0
Diff: https://github.com/socketio/engine.io/compare/3.5.0...3.6.0
2022-06-26 08:13:06 +02:00
Damien Arrachequesne
e6b869738c chore(release): 2.4.1
Diff: https://github.com/socketio/socket.io/compare/2.4.0...2.4.1
2021-01-07 10:59:46 +01:00
Damien Arrachequesne
a169050947 revert: fix(security): do not allow all origins by default
This reverts commit f78a575f66.

This commit contains a breaking change which deviates from semver,
which we try to follow as closely as possible. That's why this change
is reverted and we will rather suggest users to upgrade to v3.

Related: https://github.com/socketio/socket.io/discussions/3741
2021-01-07 10:51:55 +01:00
Damien Arrachequesne
873fdc55ed chore(release): 2.4.0
Diff: https://github.com/socketio/socket.io/compare/2.3.0...2.4.0
2021-01-05 00:27:13 +01:00
Damien Arrachequesne
f78a575f66 fix(security): do not allow all origins by default
BREAKING CHANGE: previously, all origins were allowed by default, which
meant that a Socket.IO server sent the necessary CORS headers
(`Access-Control-Allow-xxx`) to any domain by default.

Please note that you are not impacted if:

- you are using Socket.IO v2 and the `origins` option to restrict the list of allowed domains
- you are using Socket.IO v3 (disabled by default)

This commit also removes the support for '*' matchers and protocol-less
URL:

```
io.origins('https://example.com:443'); => io.origins(['https://example.com']);
io.origins('localhost:3000');          => io.origins(['http://localhost:3000']);
io.origins('http://localhost:*');      => io.origins(['http://localhost:3000']);
io.origins('*:3000');                  => io.origins(['http://localhost:3000']);
```

To restore the previous behavior (please use with caution):

```js
io.origins((_, callback) => {
  callback(null, true);
});
```

See also:

- https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
- https://socket.io/docs/v3/handling-cors/
- https://socket.io/docs/v3/migrating-from-2-x-to-3-0/#CORS-handling

Thanks a lot to https://github.com/ni8walk3r for the security report.
2021-01-04 22:34:09 +01:00
Sebastiaan Marynissen
d33a619905 fix: properly overwrite the query sent in the handshake
The `query` option of the Manager had the priority over the one of the
Socket instance, which meant updating the Socket#query object on the
client-side was not reflected in the Socket#handshake object on the
server-side.

Please note that the behavior of the `query` option is still a bit
weird in Socket.IO v2, as it only applies to non-default namespace.
This is fixed in v3:

- https://socket.io/docs/v3/migrating-from-2-x-to-3-0/#Add-a-clear-distinction-between-the-Manager-query-option-and-the-Socket-query-option
- https://socket.io/docs/v3/middlewares/#Sending-credentials

Fixes https://github.com/socketio/socket.io/issues/3495
2021-01-04 11:34:24 +01:00
Damien Arrachequesne
3951a79359 chore: bump engine.io version
Diff: https://github.com/socketio/engine.io/compare/3.4.2...3.5.0
2021-01-04 10:50:13 +01:00
Damien Arrachequesne
6fa026fc94 ci: migrate to GitHub Actions
Due to the recent changes to the Travis CI platform (see [1]), we will
now use GitHub Actions to run the tests.

Reference: https://docs.github.com/en/free-pro-team@latest/actions/guides/building-and-testing-nodejs

[1]: https://blog.travis-ci.com/2020-11-02-travis-ci-new-billing
2021-01-04 10:46:44 +01:00
11 changed files with 3609 additions and 51 deletions

24
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: CI
on:
push:
pull_request:
jobs:
test-node:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
env:
CI: true

View File

@@ -1,12 +0,0 @@
language: node_js
sudo: false
node_js:
- '8'
- '10'
notifications:
irc: "irc.freenode.org#socket.io"
git:
depth: 1
cache:
directories:
- node_modules

36
CHANGELOG.md Normal file
View File

@@ -0,0 +1,36 @@
## [2.5.1](https://github.com/socketio/socket.io/compare/2.5.0...2.5.1) (2024-06-19)
### Bug Fixes
* add a noop handler for the error event ([d30630b](https://github.com/socketio/socket.io/commit/d30630ba10562bf987f4d2b42440fc41a828119c))
# [2.5.0](https://github.com/socketio/socket.io/compare/2.4.1...2.5.0) (2022-06-26)
### Bug Fixes
* fix race condition in dynamic namespaces ([05e1278](https://github.com/socketio/socket.io/commit/05e1278cfa99f3ecf3f8f0531ffe57d850e9a05b))
* ignore packet received after disconnection ([22d4bdf](https://github.com/socketio/socket.io/commit/22d4bdf00d1a03885dc0171125faddfaef730066))
* only set 'connected' to true after middleware execution ([226cc16](https://github.com/socketio/socket.io/commit/226cc16165f9fe60f16ff4d295fb91c8971cde35))
* prevent the socket from joining a room after disconnection ([f223178](https://github.com/socketio/socket.io/commit/f223178eb655a7713303b21a78f9ef9e161d6458))
## [2.4.1](https://github.com/socketio/socket.io/compare/2.4.0...2.4.1) (2021-01-07)
### Reverts
* fix(security): do not allow all origins by default ([a169050](https://github.com/socketio/socket.io/commit/a1690509470e9dd5559cec4e60908ca6c23e9ba0))
# [2.4.0](https://github.com/socketio/socket.io/compare/2.3.0...2.4.0) (2021-01-04)
### Bug Fixes
* **security:** do not allow all origins by default ([f78a575](https://github.com/socketio/socket.io/commit/f78a575f66ab693c3ea96ea88429ddb1a44c86c7))
* properly overwrite the query sent in the handshake ([d33a619](https://github.com/socketio/socket.io/commit/d33a619905a4905c153d4fec337c74da5b533a9e))

View File

@@ -2,7 +2,7 @@
# socket.io
[![Backers on Open Collective](https://opencollective.com/socketio/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/socketio/sponsors/badge.svg)](#sponsors)
[![Build Status](https://secure.travis-ci.org/socketio/socket.io.svg?branch=master)](https://travis-ci.org/socketio/socket.io)
[![Build Status](https://github.com/socketio/socket.io/workflows/CI/badge.svg)](https://github.com/socketio/socket.io/actions)
[![Dependency Status](https://david-dm.org/socketio/socket.io.svg)](https://david-dm.org/socketio/socket.io)
[![devDependency Status](https://david-dm.org/socketio/socket.io/dev-status.svg)](https://david-dm.org/socketio/socket.io#info=devDependencies)
[![NPM version](https://badge.fury.io/js/socket.io.svg)](https://www.npmjs.com/package/socket.io)

View File

@@ -68,7 +68,6 @@ Client.prototype.connect = function(name, query){
this.server.checkNamespace(name, query, (dynamicNsp) => {
if (dynamicNsp) {
debug('dynamic namespace %s was created', dynamicNsp.name);
this.doConnect(name, query);
} else {
debug('creation of namespace %s was denied', name);

View File

@@ -182,11 +182,17 @@ Server.prototype.checkNamespace = function(name, query, fn){
return fn(false);
}
nextFn.value(name, query, (err, allow) => {
if (err || !allow) {
run();
} else {
fn(this.parentNsps.get(nextFn.value).createChild(name));
if (err || !allow) {
return run();
}
if (this.nsps[name]) {
// the namespace was created in the meantime
debug("dynamic namespace %s already exists", name);
return fn(this.nsps[name]);
}
const namespace = this.parentNsps.get(nextFn.value).createChild(name);
debug("dynamic namespace %s was created", name);
fn(namespace);
});
};

View File

@@ -163,25 +163,31 @@ Namespace.prototype.add = function(client, query, fn){
var self = this;
this.run(socket, function(err){
process.nextTick(function(){
if ('open' == client.conn.readyState) {
if (err) return socket.error(err.data || err.message);
// track socket
self.sockets[socket.id] = socket;
// it's paramount that the internal `onconnect` logic
// fires before user-set events to prevent state order
// violations (such as a disconnection before the connection
// logic is complete)
socket.onconnect();
if (fn) fn();
// fire user-set events
self.emit('connect', socket);
self.emit('connection', socket);
} else {
debug('next called after client was closed - ignoring socket');
if ("open" !== client.conn.readyState) {
debug("next called after client was closed - ignoring socket");
socket._cleanup();
return;
}
if (err) {
debug("middleware error, sending CONNECT_ERROR packet to the client");
socket._cleanup();
return socket.error(err.data || err.message);
}
// track socket
self.sockets[socket.id] = socket;
// it's paramount that the internal `onconnect` logic
// fires before user-set events to prevent state order
// violations (such as a disconnection before the connection
// logic is complete)
socket.onconnect();
if (fn) fn();
// fire user-set events
self.emit('connect', socket);
self.emit('connection', socket);
});
});
return socket;

View File

@@ -49,6 +49,8 @@ var flags = [
var emit = Emitter.prototype.emit;
function noop() {}
/**
* Interface to a `Client` for a given `Namespace`.
*
@@ -66,12 +68,15 @@ function Socket(nsp, client, query){
this.conn = client.conn;
this.rooms = {};
this.acks = {};
this.connected = true;
this.disconnected = false;
this.connected = false;
this.disconnected = true;
this.handshake = this.buildHandshake(query);
this.fns = [];
this.flags = {};
this._rooms = [];
// prevents crash when the socket receives an "error" event without listener
this.on('error', noop);
}
/**
@@ -116,7 +121,7 @@ Socket.prototype.buildHandshake = function(query){
function buildQuery(){
var requestQuery = url.parse(self.request.url, true).query;
//if socket-specific query exist, replace query strings in requestQuery
return Object.assign({}, query, requestQuery);
return Object.assign({}, requestQuery, query);
}
return {
headers: this.request.headers,
@@ -300,6 +305,8 @@ Socket.prototype.leaveAll = function(){
Socket.prototype.onconnect = function(){
debug('socket connected - writing packet');
this.connected = true;
this.disconnected = false;
this.nsp.connected[this.id] = this;
this.join(this.id);
var skip = this.nsp.name === '/' && this.nsp.fns.length === 0;
@@ -425,12 +432,7 @@ Socket.prototype.ondisconnect = function(){
*/
Socket.prototype.onerror = function(err){
if (this.listeners('error').length) {
this.emit('error', err);
} else {
console.error('Missing error handler on `socket`.');
console.error(err.stack);
}
this.emit('error', err);
};
/**
@@ -445,7 +447,7 @@ Socket.prototype.onclose = function(reason){
if (!this.connected) return this;
debug('closing socket - reason %s', reason);
this.emit('disconnecting', reason);
this.leaveAll();
this._cleanup();
this.nsp.remove(this);
this.client.remove(this);
this.connected = false;
@@ -525,7 +527,11 @@ Socket.prototype.dispatch = function(event){
if (err) {
return self.error(err.data || err.message);
}
emit.apply(self, event);
if (self.connected) {
emit.apply(self, event);
} else {
debug("ignore packet received after disconnection");
}
});
}
this.run(event, dispatchSocket);
@@ -570,3 +576,8 @@ Socket.prototype.run = function(event, fn){
run(0);
};
Socket.prototype._cleanup = function () {
this.leaveAll();
this.join = function noop() {};
}

3352
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "socket.io",
"version": "2.3.0",
"version": "2.5.1",
"description": "node.js realtime framework server",
"keywords": [
"realtime",
@@ -25,10 +25,10 @@
},
"dependencies": {
"debug": "~4.1.0",
"engine.io": "~3.4.0",
"engine.io": "~3.6.0",
"has-binary2": "~1.0.2",
"socket.io-adapter": "~1.1.0",
"socket.io-client": "2.3.0",
"socket.io-client": "2.5.0",
"socket.io-parser": "~3.4.0"
},
"devDependencies": {

View File

@@ -945,6 +945,42 @@ describe('socket.io', function(){
});
});
});
it("should handle race conditions with dynamic namespaces (#4136)", (done) => {
const srv = http();
const sio = io(srv);
const counters = {
connected: 0,
created: 0,
events: 0,
};
const buffer = [];
srv.listen(() => {
const handler = () => {
if (++counters.events === 2) {
done();
}
};
sio
.of((name, query, next) => {
buffer.push(next);
if (buffer.length === 2) {
buffer.forEach((next) => next(null, true));
}
})
.on("connection", (socket) => {
if (++counters.connected === 2) {
sio.of("/dynamic-101").emit("message");
}
});
let one = client(srv, "/dynamic-101");
let two = client(srv, "/dynamic-101");
one.on("message", handler);
two.on("message", handler);
});
});
});
});
@@ -1621,8 +1657,25 @@ describe('socket.io', function(){
expect(s.handshake.query.key2).to.be('&=bb');
done();
});
});
it('should see the query options sent in the Socket.IO handshake (specific to the given socket)', (done) => {
const srv = http();
const sio = io(srv);
const socket = client(srv, '/namespace',{ query: { key1: 'a', key2: 'b' }}); // manager-specific query option
socket.query = { key2: 'c' }; // socket-specific query option
const success = () => {
sio.close();
socket.close();
done();
}
sio.of('/namespace').on('connection', (s) => {
expect(s.handshake.query.key1).to.be('a'); // in the query params
expect(s.handshake.query.key2).to.be('c'); // in the Socket.IO handshake
success();
});
});
it('should handle very large json', function(done){
@@ -1805,7 +1858,7 @@ describe('socket.io', function(){
it('should not crash when messing with Object prototype (and other globals)', function(done){
Object.prototype.foo = 'bar';
global.File = '';
global.Blob = [];
// global.Blob = [];
var srv = http();
var sio = io(srv);
srv.listen(function(){
@@ -1821,6 +1874,70 @@ describe('socket.io', function(){
});
});
it("should ignore a packet received after disconnection", (done) => {
const srv = http();
const sio = io(srv);
srv.listen(() => {
const clientSocket = client(srv);
const success = () => {
clientSocket.close();
sio.close();
done();
};
sio.on("connection", (socket) => {
socket.on("test", () => {
done(new Error("should not happen"));
});
socket.on("disconnect", success);
});
clientSocket.on("connect", () => {
clientSocket.emit("test", Buffer.alloc(10));
clientSocket.disconnect();
});
});
});
it("should leave all rooms joined after a middleware failure", (done) => {
const srv = http().listen(0);
const sio = io(srv);
const clientSocket = client(srv, "/");
sio.use((socket, next) => {
socket.join("room1");
next(new Error("nope"));
});
clientSocket.on("error", () => {
expect(sio.of("/").adapter.rooms).to.eql(0);
clientSocket.disconnect();
sio.close();
done();
});
});
it("should not join rooms after disconnection", (done) => {
const srv = http().listen(0);
const sio = io(srv);
const clientSocket = client(srv, "/");
sio.on("connection", (socket) => {
socket.disconnect();
socket.join("room1");
});
clientSocket.on("disconnect", () => {
expect(sio.of("/").adapter.rooms).to.eql(0);
sio.close();
done();
});
});
it('should always trigger the callback (if provided) when joining a room', function(done){
var srv = http();
var sio = io(srv);
@@ -2362,6 +2479,25 @@ describe('socket.io', function(){
if (++count === 2) done();
});
});
it("should only set `connected` to true after the middleware execution", (done) => {
const httpServer = http();
const sio = io(httpServer);
const clientSocket = client(httpServer, "/");
sio.use((socket, next) => {
expect(socket.connected).to.be(false);
expect(socket.disconnected).to.be(true);
next();
});
sio.on("connection", (socket) => {
expect(socket.connected).to.be(true);
expect(socket.disconnected).to.be(false);
done();
});
});
});
describe('socket middleware', function(done){