Compare commits

...

10 Commits
4.7.4 ... 4.7.5

Author SHA1 Message Date
Damien Arrachequesne
50176812a1 chore(release): 4.7.5
Diff: https://github.com/socketio/socket.io/compare/4.7.4...4.7.5
2024-03-14 17:55:23 +01:00
Damien Arrachequesne
bf64870957 fix: close the adapters when the server is closed
Related:

- https://github.com/socketio/socket.io-mongo-adapter/issues/9
- https://github.com/socketio/socket.io-postgres-adapter/issues/13
- 0e23ff0cc6
2024-02-23 12:10:29 +01:00
Damien Arrachequesne
748e18c22e ci: test with older TypeScript version
Related: https://github.com/socketio/socket.io/issues/3891
2024-02-22 10:12:18 +01:00
Wang Guan
b9ce6a25d1 refactor: create specific adapter for parent namespaces (#4950) 2024-02-19 22:01:29 +01:00
Damien Arrachequesne
54dabe5bff ci: upgrade to actions/checkout@4 and actions/setup-node@4
Reference: https://github.blog/changelog/2023-09-22-github-actions-transitioning-from-node-16-to-node-20/
2024-02-12 18:16:10 +01:00
Damien Arrachequesne
e426f3e8e1 fix: remove duplicate pipeline when serving bundle
Related: https://github.com/socketio/socket.io/issues/4946
2024-02-12 18:11:38 +01:00
Damien Arrachequesne
e36062ca2d docs: update the webtransport example
Reference: https://github.com/fails-components/webtransport#changes-for-version-1xx
2024-01-17 13:01:57 +01:00
Damien Arrachequesne
0bbe8aec77 docs: only execute the passport middleware once
Before this change, the session and user context were retrieved once
per HTTP request and not once per session.
2024-01-13 17:56:17 +01:00
Damien Arrachequesne
914a8bd2b9 docs: add example with JWT
Related: https://github.com/socketio/socket.io/issues/4910
2024-01-13 17:14:01 +01:00
Damien Arrachequesne
d943c3e0b0 docs: update the Passport.js example 2024-01-12 17:08:19 +01:00
46 changed files with 1764 additions and 218 deletions

View File

@@ -22,16 +22,20 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Install TypeScript 4.2
run: npm i typescript@4.2
if: ${{ matrix.node-version == '16' }}
- name: Run tests
run: npm test
env:
@@ -54,10 +58,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use Node.js 20
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 20

View File

@@ -2,6 +2,7 @@
## 2024
- [4.7.5](#475-2024-03-14) (Mar 2024)
- [4.7.4](#474-2024-01-12) (Jan 2024)
- [4.7.3](#473-2024-01-03) (Jan 2024)
@@ -66,6 +67,22 @@
# Release notes
## [4.7.5](https://github.com/socketio/socket.io/compare/4.7.4...4.7.5) (2024-03-14)
### Bug Fixes
* close the adapters when the server is closed ([bf64870](https://github.com/socketio/socket.io/commit/bf64870957e626a73e0544716a1a41a4ba5093bb))
* remove duplicate pipeline when serving bundle ([e426f3e](https://github.com/socketio/socket.io/commit/e426f3e8e1bfea5720c32d30a3663303200ee6ad))
### Dependencies
- [`engine.io@~6.5.2`](https://github.com/socketio/engine.io/releases/tag/6.5.2) (no change)
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
## [4.7.4](https://github.com/socketio/socket.io/compare/4.7.3...4.7.4) (2024-01-12)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
/*!
* Socket.IO v4.7.4
* Socket.IO v4.7.5
* (c) 2014-2024 Guillermo Rauch
* Released under the MIT License.
*/
@@ -3022,6 +3022,29 @@
*/
_this._queueSeq = 0;
_this.ids = 0;
/**
* A map containing acknowledgement handlers.
*
* The `withError` attribute is used to differentiate handlers that accept an error as first argument:
*
* - `socket.emit("test", (err, value) => { ... })` with `ackTimeout` option
* - `socket.timeout(5000).emit("test", (err, value) => { ... })`
* - `const value = await socket.emitWithAck("test")`
*
* From those that don't:
*
* - `socket.emit("test", (value) => { ... });`
*
* In the first case, the handlers will be called with an error when:
*
* - the timeout is reached
* - the socket gets disconnected
*
* In the second case, the handlers will be simply discarded upon disconnection, since the client will never receive
* an acknowledgement from the server.
*
* @private
*/
_this.acks = {};
_this.flags = {};
_this.io = io;
@@ -3216,14 +3239,16 @@
}
ack.call(_this2, new Error("operation has timed out"));
}, timeout);
this.acks[id] = function () {
var fn = function fn() {
// @ts-ignore
_this2.io.clearTimeoutFn(timer);
for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
args[_key3] = arguments[_key3];
}
ack.apply(_this2, [null].concat(args));
ack.apply(_this2, args);
};
fn.withError = true;
this.acks[id] = fn;
}
/**
* Emits an event and waits for an acknowledgement
@@ -3248,16 +3273,12 @@
for (var _len4 = arguments.length, args = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {
args[_key4 - 1] = arguments[_key4];
}
// the timeout flag is optional
var withErr = this.flags.timeout !== undefined || this._opts.ackTimeout !== undefined;
return new Promise(function (resolve, reject) {
args.push(function (arg1, arg2) {
if (withErr) {
return arg1 ? reject(arg1) : resolve(arg2);
} else {
return resolve(arg1);
}
});
var fn = function fn(arg1, arg2) {
return arg1 ? reject(arg1) : resolve(arg2);
};
fn.withError = true;
args.push(fn);
_this3.emit.apply(_this3, [ev].concat(args));
});
}
@@ -3405,6 +3426,31 @@
this.connected = false;
delete this.id;
this.emitReserved("disconnect", reason, description);
this._clearAcks();
}
/**
* Clears the acknowledgement handlers upon disconnection, since the client will never receive an acknowledgement from
* the server.
*
* @private
*/
}, {
key: "_clearAcks",
value: function _clearAcks() {
var _this6 = this;
Object.keys(this.acks).forEach(function (id) {
var isBuffered = _this6.sendBuffer.some(function (packet) {
return String(packet.id) === id;
});
if (!isBuffered) {
// note: handlers that do not accept an error as first argument are ignored here
var ack = _this6.acks[id];
delete _this6.acks[id];
if (ack.withError) {
ack.call(_this6, new Error("socket has been disconnected"));
}
}
});
}
/**
* Called with socket packet.
@@ -3512,7 +3558,7 @@
};
}
/**
* Called upon a server acknowlegement.
* Called upon a server acknowledgement.
*
* @param packet
* @private
@@ -3521,10 +3567,16 @@
key: "onack",
value: function onack(packet) {
var ack = this.acks[packet.id];
if ("function" === typeof ack) {
ack.apply(this, packet.data);
delete this.acks[packet.id];
if (typeof ack !== "function") {
return;
}
delete this.acks[packet.id];
// @ts-ignore FIXME ack is incorrectly inferred as 'never'
if (ack.withError) {
packet.data.unshift(null);
}
// @ts-ignore
ack.apply(this, packet.data);
}
/**
* Called upon server connect.
@@ -3550,14 +3602,14 @@
}, {
key: "emitBuffered",
value: function emitBuffered() {
var _this6 = this;
var _this7 = this;
this.receiveBuffer.forEach(function (args) {
return _this6.emitEvent(args);
return _this7.emitEvent(args);
});
this.receiveBuffer = [];
this.sendBuffer.forEach(function (packet) {
_this6.notifyOutgoingListeners(packet);
_this6.packet(packet);
_this7.notifyOutgoingListeners(packet);
_this7.packet(packet);
});
this.sendBuffer = [];
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,8 @@ This example shows how to retrieve the authentication context from a basic [Expr
![Passport example](assets/passport_example.gif)
Please read the related guide: https://socket.io/how-to/use-with-passport
## How to use
```
@@ -12,3 +14,33 @@ $ npm ci && npm start
```
And point your browser to `http://localhost:3000`. Optionally, specify a port by supplying the `PORT` env variable.
## How it works
The Socket.IO server retrieves the user context from the session:
```js
function onlyForHandshake(middleware) {
return (req, res, next) => {
const isHandshake = req._query.sid === undefined;
if (isHandshake) {
middleware(req, res, next);
} else {
next();
}
};
}
io.engine.use(onlyForHandshake(sessionMiddleware));
io.engine.use(onlyForHandshake(passport.session()));
io.engine.use(
onlyForHandshake((req, res, next) => {
if (req.user) {
next();
} else {
res.writeHead(401);
res.end();
}
}),
);
```

View File

@@ -0,0 +1,55 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Passport example</title>
</head>
<body>
<p>Authenticated!</p>
<table>
<tbody>
<tr>
<td>Status</td>
<td><span id="status">Disconnected</span></td>
</tr>
<tr>
<td>Socket ID</td>
<td><span id="socket-id"></span></td>
</tr>
<tr>
<td>Username</td>
<td><span id="username"></span></td>
</tr>
</tbody>
</table>
<form action="/logout" method="post">
<div>
<input type="submit" value="Log out" />
</div>
</form>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
const socketIdSpan = document.getElementById('socket-id');
const usernameSpan = document.getElementById('username');
const statusSpan = document.getElementById('status');
socket.on('connect', () => {
statusSpan.innerText = 'connected';
socketIdSpan.innerText = socket.id;
socket.emit('whoami', (username) => {
usernameSpan.innerText = username;
});
});
socket.on('disconnect', () => {
statusSpan.innerText = 'disconnected';
socketIdSpan.innerText = '-';
});
</script>
</body>
</html>

View File

@@ -0,0 +1,117 @@
const express = require("express");
const { createServer } = require("node:http");
const { Server } = require("socket.io");
const session = require("express-session");
const bodyParser = require("body-parser");
const passport = require("passport");
const LocalStrategy = require("passport-local").Strategy;
const { join } = require("node:path");
const port = process.env.PORT || 3000;
const app = express();
const httpServer = createServer(app);
const sessionMiddleware = session({
secret: "changeit",
resave: true,
saveUninitialized: true,
});
app.use(sessionMiddleware);
app.use(bodyParser.urlencoded({ extended: false }));
app.use(passport.session());
app.get("/", (req, res) => {
if (!req.user) {
return res.redirect("/login");
}
res.sendFile(join(__dirname, "index.html"));
});
app.get("/login", (req, res) => {
if (req.user) {
return res.redirect("/");
}
res.sendFile(join(__dirname, "login.html"));
});
app.post(
"/login",
passport.authenticate("local", {
successRedirect: "/",
failureRedirect: "/",
}),
);
app.post("/logout", (req, res) => {
const sessionId = req.session.id;
req.session.destroy(() => {
// disconnect all Socket.IO connections linked to this session ID
io.to(`session:${sessionId}`).disconnectSockets();
res.status(204).end();
});
});
passport.use(
new LocalStrategy((username, password, done) => {
if (username === "john" && password === "changeit") {
console.log("authentication OK");
return done(null, { id: 1, username });
} else {
console.log("wrong credentials");
return done(null, false);
}
}),
);
passport.serializeUser((user, cb) => {
console.log(`serializeUser ${user.id}`);
cb(null, user);
});
passport.deserializeUser((user, cb) => {
console.log(`deserializeUser ${user.id}`);
cb(null, user);
});
const io = new Server(httpServer);
function onlyForHandshake(middleware) {
return (req, res, next) => {
const isHandshake = req._query.sid === undefined;
if (isHandshake) {
middleware(req, res, next);
} else {
next();
}
};
}
io.engine.use(onlyForHandshake(sessionMiddleware));
io.engine.use(onlyForHandshake(passport.session()));
io.engine.use(
onlyForHandshake((req, res, next) => {
if (req.user) {
next();
} else {
res.writeHead(401);
res.end();
}
}),
);
io.on("connection", (socket) => {
const req = socket.request;
socket.join(`session:${req.session.id}`);
socket.join(`user:${req.user.id}`);
socket.on("whoami", (cb) => {
cb(req.user.username);
});
});
httpServer.listen(port, () => {
console.log(`application is running at: http://localhost:${port}`);
});

View File

@@ -8,17 +8,17 @@
<p>Not authenticated</p>
<form action="/login" method="post">
<div>
<label>Username:</label>
<input type="text" name="username" />
<label for="username">Username:</label>
<input type="text" id="username" name="username" />
<br/>
</div>
<div>
<label>Password:</label>
<input type="password" name="password" />
<label for="password">Password:</label>
<input type="password" id="password" name="password" />
</div>
<div>
<input type="submit" value="Submit" />
</div>
</form>
</body>
</html>
</html>

View File

@@ -0,0 +1,20 @@
{
"name": "passport-example",
"version": "0.0.1",
"private": true,
"type": "commonjs",
"description": "Example with passport (https://www.passportjs.org)",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "~4.17.3",
"express-session": "~1.17.2",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
"socket.io": "^4.7.2"
},
"devDependencies": {
"prettier": "^3.1.1"
}
}

View File

@@ -0,0 +1,55 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Passport example</title>
</head>
<body>
<p>Authenticated!</p>
<table>
<tbody>
<tr>
<td>Status</td>
<td><span id="status">Disconnected</span></td>
</tr>
<tr>
<td>Socket ID</td>
<td><span id="socket-id"></span></td>
</tr>
<tr>
<td>Username</td>
<td><span id="username"></span></td>
</tr>
</tbody>
</table>
<form action="/logout" method="post">
<div>
<input type="submit" value="Log out" />
</div>
</form>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
const socketIdSpan = document.getElementById('socket-id');
const usernameSpan = document.getElementById('username');
const statusSpan = document.getElementById('status');
socket.on('connect', () => {
statusSpan.innerText = 'connected';
socketIdSpan.innerText = socket.id;
socket.emit('whoami', (username) => {
usernameSpan.innerText = username;
});
});
socket.on('disconnect', () => {
statusSpan.innerText = 'disconnected';
socketIdSpan.innerText = '-';
});
</script>
</body>
</html>

View File

@@ -0,0 +1,120 @@
import express from "express";
import { createServer } from "http";
import { Server } from "socket.io";
import session from "express-session";
import bodyParser from "body-parser";
import passport from "passport";
import { Strategy as LocalStrategy } from "passport-local";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const port = process.env.PORT || 3000;
const app = express();
const httpServer = createServer(app);
const sessionMiddleware = session({
secret: "changeit",
resave: true,
saveUninitialized: true,
});
app.use(sessionMiddleware);
app.use(bodyParser.urlencoded({ extended: false }));
app.use(passport.session());
const __dirname = dirname(fileURLToPath(import.meta.url));
app.get("/", (req, res) => {
if (!req.user) {
return res.redirect("/login");
}
res.sendFile(join(__dirname, "index.html"));
});
app.get("/login", (req, res) => {
if (req.user) {
return res.redirect("/");
}
res.sendFile(join(__dirname, "login.html"));
});
app.post(
"/login",
passport.authenticate("local", {
successRedirect: "/",
failureRedirect: "/",
}),
);
app.post("/logout", (req, res) => {
const sessionId = req.session.id;
req.session.destroy(() => {
// disconnect all Socket.IO connections linked to this session ID
io.to(`session:${sessionId}`).disconnectSockets();
res.status(204).end();
});
});
passport.use(
new LocalStrategy((username, password, done) => {
if (username === "john" && password === "changeit") {
console.log("authentication OK");
return done(null, { id: 1, username });
} else {
console.log("wrong credentials");
return done(null, false);
}
}),
);
passport.serializeUser((user, cb) => {
console.log(`serializeUser ${user.id}`);
cb(null, user);
});
passport.deserializeUser((user, cb) => {
console.log(`deserializeUser ${user.id}`);
cb(null, user);
});
const io = new Server(httpServer);
function onlyForHandshake(middleware) {
return (req, res, next) => {
const isHandshake = req._query.sid === undefined;
if (isHandshake) {
middleware(req, res, next);
} else {
next();
}
};
}
io.engine.use(onlyForHandshake(sessionMiddleware));
io.engine.use(onlyForHandshake(passport.session()));
io.engine.use(
onlyForHandshake((req, res, next) => {
if (req.user) {
next();
} else {
res.writeHead(401);
res.end();
}
}),
);
io.on("connection", (socket) => {
const req = socket.request;
socket.join(`session:${req.session.id}`);
socket.join(`user:${req.user.id}`);
socket.on("whoami", (cb) => {
cb(req.user.username);
});
});
httpServer.listen(port, () => {
console.log(`application is running at: http://localhost:${port}`);
});

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Passport example</title>
</head>
<body>
<p>Not authenticated</p>
<form action="/login" method="post">
<div>
<label for="username">Username:</label>
<input type="text" id="username" name="username" />
<br/>
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password" />
</div>
<div>
<input type="submit" value="Submit" />
</div>
</form>
</body>
</html>

View File

@@ -0,0 +1,20 @@
{
"name": "passport-example",
"version": "0.0.1",
"private": true,
"type": "module",
"description": "Example with passport (https://www.passportjs.org)",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "~4.17.3",
"express-session": "~1.17.2",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
"socket.io": "^4.7.2"
},
"devDependencies": {
"prettier": "^3.1.1"
}
}

View File

@@ -1,31 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Passport example</title>
</head>
<body>
<p>Authenticated!</p>
<p>Socket ID: <span id="socketId"></span></p>
<p>Username: <span id="username"></span></p>
<form action="/logout" method="post">
<div>
<input type="submit" value="Log out" />
</div>
</form>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
const socketIdSpan = document.getElementById("socketId");
const usernameSpan = document.getElementById("username");
socket.on('connect', () => {
socketIdSpan.innerText = socket.id;
socket.emit('whoami', (username) => {
usernameSpan.innerText = username;
});
});
</script>
</body>
</html>

View File

@@ -1,104 +0,0 @@
const app = require("express")();
const server = require("http").createServer(app);
const port = process.env.PORT || 3000;
const session = require("express-session");
const bodyParser = require("body-parser");
const passport = require("passport");
const LocalStrategy = require("passport-local").Strategy;
const sessionMiddleware = session({ secret: "changeit", resave: false, saveUninitialized: false });
app.use(sessionMiddleware);
app.use(bodyParser.urlencoded({ extended: false }));
app.use(passport.initialize());
app.use(passport.session());
const DUMMY_USER = {
id: 1,
username: "john",
};
passport.use(
new LocalStrategy((username, password, done) => {
if (username === "john" && password === "doe") {
console.log("authentication OK");
return done(null, DUMMY_USER);
} else {
console.log("wrong credentials");
return done(null, false);
}
})
);
app.get("/", (req, res) => {
const isAuthenticated = !!req.user;
if (isAuthenticated) {
console.log(`user is authenticated, session is ${req.session.id}`);
} else {
console.log("unknown user");
}
res.sendFile(isAuthenticated ? "index.html" : "login.html", { root: __dirname });
});
app.post(
"/login",
passport.authenticate("local", {
successRedirect: "/",
failureRedirect: "/",
})
);
app.post("/logout", (req, res) => {
console.log(`logout ${req.session.id}`);
const socketId = req.session.socketId;
if (socketId && io.of("/").sockets.get(socketId)) {
console.log(`forcefully closing socket ${socketId}`);
io.of("/").sockets.get(socketId).disconnect(true);
}
req.logout();
res.cookie("connect.sid", "", { expires: new Date() });
res.redirect("/");
});
passport.serializeUser((user, cb) => {
console.log(`serializeUser ${user.id}`);
cb(null, user.id);
});
passport.deserializeUser((id, cb) => {
console.log(`deserializeUser ${id}`);
cb(null, DUMMY_USER);
});
const io = require('socket.io')(server);
// convert a connect middleware to a Socket.IO middleware
const wrap = middleware => (socket, next) => middleware(socket.request, {}, next);
io.use(wrap(sessionMiddleware));
io.use(wrap(passport.initialize()));
io.use(wrap(passport.session()));
io.use((socket, next) => {
if (socket.request.user) {
next();
} else {
next(new Error('unauthorized'))
}
});
io.on('connect', (socket) => {
console.log(`new connection ${socket.id}`);
socket.on('whoami', (cb) => {
cb(socket.request.user ? socket.request.user.username : '');
});
const session = socket.request.session;
console.log(`saving sid ${socket.id} in session ${session.id}`);
session.socketId = socket.id;
session.save();
});
server.listen(port, () => {
console.log(`application is running at: http://localhost:${port}`);
});

View File

@@ -1,16 +0,0 @@
{
"name": "passport-example",
"version": "0.0.1",
"description": "Example with Passport (http://www.passportjs.org/)",
"dependencies": {
"body-parser": "~1.19.0",
"express": "~4.17.1",
"express-session": "~1.17.1",
"passport": "~0.4.1",
"passport-local": "~1.0.0",
"socket.io": "^4.0.0"
},
"scripts": {
"start": "node index.js"
}
}

View File

@@ -0,0 +1,55 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Passport example</title>
</head>
<body>
<p>Authenticated!</p>
<table>
<tbody>
<tr>
<td>Status</td>
<td><span id="status">Disconnected</span></td>
</tr>
<tr>
<td>Socket ID</td>
<td><span id="socket-id"></span></td>
</tr>
<tr>
<td>Username</td>
<td><span id="username"></span></td>
</tr>
</tbody>
</table>
<form action="/logout" method="post">
<div>
<input type="submit" value="Log out" />
</div>
</form>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
const socketIdSpan = document.getElementById('socket-id');
const usernameSpan = document.getElementById('username');
const statusSpan = document.getElementById('status');
socket.on('connect', () => {
statusSpan.innerText = 'connected';
socketIdSpan.innerText = socket.id;
socket.emit('whoami', (username) => {
usernameSpan.innerText = username;
});
});
socket.on('disconnect', () => {
statusSpan.innerText = 'disconnected';
socketIdSpan.innerText = '-';
});
</script>
</body>
</html>

View File

@@ -0,0 +1,137 @@
import express = require("express");
import { createServer } from "http";
import { Server } from "socket.io";
import session from "express-session";
import { type Request, type Response } from "express";
import bodyParser = require("body-parser");
import passport = require("passport");
import { Strategy as LocalStrategy } from "passport-local";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
declare global {
namespace Express {
interface User {
id: number;
username: string;
}
}
}
const port = process.env.PORT || 3000;
const app = express();
const httpServer = createServer(app);
const sessionMiddleware = session({
secret: "changeit",
resave: true,
saveUninitialized: true,
});
app.use(sessionMiddleware);
app.use(bodyParser.urlencoded({ extended: false }));
app.use(passport.initialize());
app.use(passport.session());
const __dirname = dirname(fileURLToPath(import.meta.url));
app.get("/", (req, res) => {
if (!req.user) {
return res.redirect("/login");
}
res.sendFile(join(__dirname, "index.html"));
});
app.get("/login", (req, res) => {
if (req.user) {
return res.redirect("/");
}
res.sendFile(join(__dirname, "login.html"));
});
app.post(
"/login",
passport.authenticate("local", {
successRedirect: "/",
failureRedirect: "/",
}),
);
app.post("/logout", (req, res) => {
const sessionId = req.session.id;
req.session.destroy(() => {
// disconnect all Socket.IO connections linked to this session ID
io.to(`session:${sessionId}`).disconnectSockets();
res.status(204).end();
});
});
passport.use(
new LocalStrategy((username, password, done) => {
if (username === "john" && password === "changeit") {
console.log("authentication OK");
return done(null, { id: 1, username });
} else {
console.log("wrong credentials");
return done(null, false);
}
}),
);
passport.serializeUser((user, cb) => {
console.log(`serializeUser ${user.id}`);
cb(null, user);
});
passport.deserializeUser((user: Express.User, cb) => {
console.log(`deserializeUser ${user.id}`);
cb(null, user);
});
const io = new Server(httpServer);
function onlyForHandshake(
middleware: (req: Request, res: Response, next: any) => void,
) {
return (
req: Request & { _query: Record<string, string> },
res: Response,
next: (err?: Error) => void,
) => {
const isHandshake = req._query.sid === undefined;
if (isHandshake) {
middleware(req, res, next);
} else {
next();
}
};
}
io.engine.use(onlyForHandshake(sessionMiddleware));
io.engine.use(onlyForHandshake(passport.session()));
io.engine.use(
onlyForHandshake((req, res, next) => {
if (req.user) {
next();
} else {
res.writeHead(401);
res.end();
}
}),
);
io.on("connection", (socket) => {
const req = socket.request as Request & { user: Express.User };
socket.join(`session:${req.session.id}`);
socket.join(`user:${req.user.id}`);
socket.on("whoami", (cb) => {
cb(req.user.username);
});
});
httpServer.listen(port, () => {
console.log(`application is running at: http://localhost:${port}`);
});

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Passport example</title>
</head>
<body>
<p>Not authenticated</p>
<form action="/login" method="post">
<div>
<label for="username">Username:</label>
<input type="text" id="username" name="username" />
<br/>
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password" />
</div>
<div>
<input type="submit" value="Submit" />
</div>
</form>
</body>
</html>

View File

@@ -0,0 +1,27 @@
{
"name": "passport-example",
"version": "0.0.1",
"private": true,
"type": "module",
"description": "Example with passport (https://www.passportjs.org)",
"scripts": {
"start": "ts-node index.ts"
},
"dependencies": {
"@types/express": "^4.17.17",
"@types/express-session": "^1.17.7",
"@types/node": "^20.6.0",
"@types/passport": "^1.0.16",
"express": "~4.17.3",
"express-session": "~1.17.2",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
"socket.io": "^4.7.2",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
},
"devDependencies": {
"@types/passport-local": "^1.0.38",
"prettier": "^3.1.1"
}
}

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"strict": true
},
"ts-node": {
"esm": true
}
}

View File

@@ -0,0 +1,41 @@
# Example with [`passport-jwt`](https://www.passportjs.org/packages/passport-jwt/)
This example shows how to retrieve the authentication context from a basic [Express](http://expressjs.com/) + [Passport](http://www.passportjs.org/) application.
![Passport example](assets/passport_example.gif)
Please read the related guide: https://socket.io/how-to/use-with-jwt
## How to use
```
$ npm ci && npm start
```
And point your browser to `http://localhost:3000`. Optionally, specify a port by supplying the `PORT` env variable.
## How it works
The client sends the JWT in the headers:
```js
const socket = io({
extraHeaders: {
authorization: `bearer token`
}
});
```
And the Socket.IO server then parses the token and retrieves the user context:
```js
io.engine.use((req, res, next) => {
const isHandshake = req._query.sid === undefined;
if (isHandshake) {
passport.authenticate("jwt", { session: false })(req, res, next);
} else {
next();
}
});
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -0,0 +1,154 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Passport JWT example</title>
</head>
<body>
<div id="login-panel" style="display: none">
<p>Not authenticated</p>
<form id="login-form">
<div>
<label for="username">Username:</label>
<input type="text" id="username" name="username" value="john" />
<br/>
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password" value="changeit" />
</div>
<div>
<input type="submit" value="Submit" />
</div>
</form>
</div>
<div id="home-panel" style="display: none">
<p>Authenticated!</p>
<table>
<tbody>
<tr>
<td>Status</td>
<td><span id="status">Disconnected</span></td>
</tr>
<tr>
<td>Socket ID</td>
<td><span id="socket-id"></span></td>
</tr>
<tr>
<td>Username</td>
<td><span id="name"></span></td>
</tr>
</tbody>
</table>
<form id="logout-form">
<div>
<input type="submit" value="Log out" />
</div>
</form>
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
const loginPanel = document.getElementById('login-panel');
const homePanel = document.getElementById('home-panel');
const loginForm = document.getElementById('login-form');
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const statusSpan = document.getElementById('status');
const socketIdSpan = document.getElementById('socket-id');
const usernameSpan = document.getElementById('name');
const logoutForm = document.getElementById('logout-form');
let socket;
async function main() {
const token = localStorage.getItem('token');
if (!token) {
return showLoginPanel();
}
const res = await fetch('/self', {
headers: {
authorization: `bearer ${token}`
}
});
if (res.status === 200) {
showHomePanel();
} else {
showLoginPanel();
}
}
function showHomePanel() {
loginPanel.style.display = 'none';
homePanel.style.display = 'block';
// this will only work if HTTP long-polling is enabled, since WebSockets do not support providing additional headers
socket = io({
extraHeaders: {
authorization: `bearer ${localStorage.getItem('token')}`
}
});
socket.on('connect', () => {
statusSpan.innerText = 'connected';
socketIdSpan.innerText = socket.id;
socket.emit('whoami', (username) => {
usernameSpan.innerText = username;
});
});
socket.on('disconnect', () => {
statusSpan.innerText = 'disconnected';
socketIdSpan.innerText = '-';
});
}
function showLoginPanel() {
loginPanel.style.display = 'block';
homePanel.style.display = 'none';
}
loginForm.onsubmit = async function (e) {
e.preventDefault();
const res = await fetch('/login', {
method: 'post',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
username: usernameInput.value,
password: passwordInput.value
})
})
if (res.status === 200) {
const { token } = await res.json();
localStorage.setItem('token', token);
showHomePanel();
} else {
passwordInput.value = '';
}
}
logoutForm.onsubmit = function (e) {
e.preventDefault();
socket.disconnect();
localStorage.removeItem('token');
showLoginPanel();
}
main();
</script>
</body>
</html>

View File

@@ -0,0 +1,100 @@
const express = require("express");
const { createServer } = require("node:http");
const { join } = require("node:path");
const passport = require("passport");
const passportJwt = require("passport-jwt");
const JwtStrategy = passportJwt.Strategy;
const ExtractJwt = passportJwt.ExtractJwt;
const bodyParser = require("body-parser");
const { Server } = require("socket.io");
const jwt = require("jsonwebtoken");
const port = process.env.PORT || 3000;
const jwtSecret = "Mys3cr3t";
const app = express();
const httpServer = createServer(app);
app.use(bodyParser.json());
app.get("/", (req, res) => {
res.sendFile(join(__dirname, "index.html"));
});
app.get(
"/self",
passport.authenticate("jwt", { session: false }),
(req, res) => {
if (req.user) {
res.send(req.user);
} else {
res.status(401).end();
}
},
);
app.post("/login", (req, res) => {
if (req.body.username === "john" && req.body.password === "changeit") {
console.log("authentication OK");
const user = {
id: 1,
username: "john",
};
const token = jwt.sign(
{
data: user,
},
jwtSecret,
{
issuer: "accounts.examplesoft.com",
audience: "yoursite.net",
expiresIn: "1h",
},
);
res.json({ token });
} else {
console.log("wrong credentials");
res.status(401).end();
}
});
const jwtDecodeOptions = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: jwtSecret,
issuer: "accounts.examplesoft.com",
audience: "yoursite.net",
};
passport.use(
new JwtStrategy(jwtDecodeOptions, (payload, done) => {
return done(null, payload.data);
}),
);
const io = new Server(httpServer);
io.engine.use((req, res, next) => {
const isHandshake = req._query.sid === undefined;
if (isHandshake) {
passport.authenticate("jwt", { session: false })(req, res, next);
} else {
next();
}
});
io.on("connection", (socket) => {
const req = socket.request;
socket.join(`user:${req.user.id}`);
socket.on("whoami", (cb) => {
cb(req.user.username);
});
});
httpServer.listen(port, () => {
console.log(`application is running at: http://localhost:${port}`);
});

View File

@@ -0,0 +1,21 @@
{
"name": "passport-jwt-example",
"version": "0.0.1",
"private": true,
"type": "commonjs",
"description": "Example with passport and JWT (https://www.passportjs.org/packages/passport-jwt/)",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"body-parser": "^1.20.2",
"express": "~4.17.3",
"jsonwebtoken": "^9.0.2",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"socket.io": "^4.7.2"
},
"devDependencies": {
"prettier": "^3.1.1"
}
}

View File

@@ -0,0 +1,154 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Passport JWT example</title>
</head>
<body>
<div id="login-panel" style="display: none">
<p>Not authenticated</p>
<form id="login-form">
<div>
<label for="username">Username:</label>
<input type="text" id="username" name="username" value="john" />
<br/>
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password" value="changeit" />
</div>
<div>
<input type="submit" value="Submit" />
</div>
</form>
</div>
<div id="home-panel" style="display: none">
<p>Authenticated!</p>
<table>
<tbody>
<tr>
<td>Status</td>
<td><span id="status">Disconnected</span></td>
</tr>
<tr>
<td>Socket ID</td>
<td><span id="socket-id"></span></td>
</tr>
<tr>
<td>Username</td>
<td><span id="name"></span></td>
</tr>
</tbody>
</table>
<form id="logout-form">
<div>
<input type="submit" value="Log out" />
</div>
</form>
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
const loginPanel = document.getElementById('login-panel');
const homePanel = document.getElementById('home-panel');
const loginForm = document.getElementById('login-form');
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const statusSpan = document.getElementById('status');
const socketIdSpan = document.getElementById('socket-id');
const usernameSpan = document.getElementById('name');
const logoutForm = document.getElementById('logout-form');
let socket;
async function main() {
const token = localStorage.getItem('token');
if (!token) {
return showLoginPanel();
}
const res = await fetch('/self', {
headers: {
authorization: `bearer ${token}`
}
});
if (res.status === 200) {
showHomePanel();
} else {
showLoginPanel();
}
}
function showHomePanel() {
loginPanel.style.display = 'none';
homePanel.style.display = 'block';
// this will only work if HTTP long-polling is enabled, since WebSockets do not support providing additional headers
socket = io({
extraHeaders: {
authorization: `bearer ${localStorage.getItem('token')}`
}
});
socket.on('connect', () => {
statusSpan.innerText = 'connected';
socketIdSpan.innerText = socket.id;
socket.emit('whoami', (username) => {
usernameSpan.innerText = username;
});
});
socket.on('disconnect', () => {
statusSpan.innerText = 'disconnected';
socketIdSpan.innerText = '-';
});
}
function showLoginPanel() {
loginPanel.style.display = 'block';
homePanel.style.display = 'none';
}
loginForm.onsubmit = async function (e) {
e.preventDefault();
const res = await fetch('/login', {
method: 'post',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
username: usernameInput.value,
password: passwordInput.value
})
})
if (res.status === 200) {
const { token } = await res.json();
localStorage.setItem('token', token);
showHomePanel();
} else {
passwordInput.value = '';
}
}
logoutForm.onsubmit = function (e) {
e.preventDefault();
socket.disconnect();
localStorage.removeItem('token');
showLoginPanel();
}
main();
</script>
</body>
</html>

View File

@@ -0,0 +1,101 @@
import express from "express";
import { createServer } from "node:http";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import passport from "passport";
import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt";
import bodyParser from "body-parser";
import { Server } from "socket.io";
import jwt from "jsonwebtoken";
const port = process.env.PORT || 3000;
const jwtSecret = "Mys3cr3t";
const app = express();
const httpServer = createServer(app);
app.use(bodyParser.json());
const __dirname = dirname(fileURLToPath(import.meta.url));
app.get("/", (req, res) => {
res.sendFile(join(__dirname, "index.html"));
});
app.get(
"/self",
passport.authenticate("jwt", { session: false }),
(req, res) => {
if (req.user) {
res.send(req.user);
} else {
res.status(401).end();
}
},
);
app.post("/login", (req, res) => {
if (req.body.username === "john" && req.body.password === "changeit") {
console.log("authentication OK");
const user = {
id: 1,
username: "john",
};
const token = jwt.sign(
{
data: user,
},
jwtSecret,
{
issuer: "accounts.examplesoft.com",
audience: "yoursite.net",
expiresIn: "1h",
},
);
res.json({ token });
} else {
console.log("wrong credentials");
res.status(401).end();
}
});
const jwtDecodeOptions = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: jwtSecret,
issuer: "accounts.examplesoft.com",
audience: "yoursite.net",
};
passport.use(
new JwtStrategy(jwtDecodeOptions, (payload, done) => {
return done(null, payload.data);
}),
);
const io = new Server(httpServer);
io.engine.use((req, res, next) => {
const isHandshake = req._query.sid === undefined;
if (isHandshake) {
passport.authenticate("jwt", { session: false })(req, res, next);
} else {
next();
}
});
io.on("connection", (socket) => {
const req = socket.request;
socket.join(`user:${req.user.id}`);
socket.on("whoami", (cb) => {
cb(req.user.username);
});
});
httpServer.listen(port, () => {
console.log(`application is running at: http://localhost:${port}`);
});

View File

@@ -0,0 +1,21 @@
{
"name": "passport-jwt-example",
"version": "0.0.1",
"private": true,
"type": "module",
"description": "Example with passport and JWT (https://www.passportjs.org/packages/passport-jwt/)",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"body-parser": "^1.20.2",
"express": "~4.17.3",
"jsonwebtoken": "^9.0.2",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"socket.io": "^4.7.2"
},
"devDependencies": {
"prettier": "^3.1.1"
}
}

View File

@@ -0,0 +1,154 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Passport JWT example</title>
</head>
<body>
<div id="login-panel" style="display: none">
<p>Not authenticated</p>
<form id="login-form">
<div>
<label for="username">Username:</label>
<input type="text" id="username" name="username" value="john" />
<br/>
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password" value="changeit" />
</div>
<div>
<input type="submit" value="Submit" />
</div>
</form>
</div>
<div id="home-panel" style="display: none">
<p>Authenticated!</p>
<table>
<tbody>
<tr>
<td>Status</td>
<td><span id="status">Disconnected</span></td>
</tr>
<tr>
<td>Socket ID</td>
<td><span id="socket-id"></span></td>
</tr>
<tr>
<td>Username</td>
<td><span id="name"></span></td>
</tr>
</tbody>
</table>
<form id="logout-form">
<div>
<input type="submit" value="Log out" />
</div>
</form>
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
const loginPanel = document.getElementById('login-panel');
const homePanel = document.getElementById('home-panel');
const loginForm = document.getElementById('login-form');
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const statusSpan = document.getElementById('status');
const socketIdSpan = document.getElementById('socket-id');
const usernameSpan = document.getElementById('name');
const logoutForm = document.getElementById('logout-form');
let socket;
async function main() {
const token = localStorage.getItem('token');
if (!token) {
return showLoginPanel();
}
const res = await fetch('/self', {
headers: {
authorization: `bearer ${token}`
}
});
if (res.status === 200) {
showHomePanel();
} else {
showLoginPanel();
}
}
function showHomePanel() {
loginPanel.style.display = 'none';
homePanel.style.display = 'block';
// this will only work if HTTP long-polling is enabled, since WebSockets do not support providing additional headers
socket = io({
extraHeaders: {
authorization: `bearer ${localStorage.getItem('token')}`
}
});
socket.on('connect', () => {
statusSpan.innerText = 'connected';
socketIdSpan.innerText = socket.id;
socket.emit('whoami', (username) => {
usernameSpan.innerText = username;
});
});
socket.on('disconnect', () => {
statusSpan.innerText = 'disconnected';
socketIdSpan.innerText = '-';
});
}
function showLoginPanel() {
loginPanel.style.display = 'block';
homePanel.style.display = 'none';
}
loginForm.onsubmit = async function (e) {
e.preventDefault();
const res = await fetch('/login', {
method: 'post',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
username: usernameInput.value,
password: passwordInput.value
})
})
if (res.status === 200) {
const { token } = await res.json();
localStorage.setItem('token', token);
showHomePanel();
} else {
passwordInput.value = '';
}
}
logoutForm.onsubmit = function (e) {
e.preventDefault();
socket.disconnect();
localStorage.removeItem('token');
showLoginPanel();
}
main();
</script>
</body>
</html>

View File

@@ -0,0 +1,113 @@
import express from "express";
import { type Request, type Response } from "express";
import { createServer } from "node:http";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import passport from "passport";
import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt";
import bodyParser from "body-parser";
import { Server } from "socket.io";
import jwt from "jsonwebtoken";
declare global {
namespace Express {
interface User {
id: number;
username: string;
}
}
}
const port = process.env.PORT || 3000;
const jwtSecret = "Mys3cr3t";
const app = express();
const httpServer = createServer(app);
app.use(bodyParser.json());
const __dirname = dirname(fileURLToPath(import.meta.url));
app.get("/", (req, res) => {
res.sendFile(join(__dirname, "index.html"));
});
app.get(
"/self",
passport.authenticate("jwt", { session: false }),
(req, res) => {
if (req.user) {
res.send(req.user);
} else {
res.status(401).end();
}
},
);
app.post("/login", (req, res) => {
if (req.body.username === "john" && req.body.password === "changeit") {
console.log("authentication OK");
const user = {
id: 1,
username: "john",
};
const token = jwt.sign(
{
data: user,
},
jwtSecret,
{
issuer: "accounts.examplesoft.com",
audience: "yoursite.net",
expiresIn: "1h",
},
);
res.json({ token });
} else {
console.log("wrong credentials");
res.status(401).end();
}
});
const jwtDecodeOptions = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: jwtSecret,
issuer: "accounts.examplesoft.com",
audience: "yoursite.net",
};
passport.use(
new JwtStrategy(jwtDecodeOptions, (payload, done) => {
return done(null, payload.data);
}),
);
const io = new Server(httpServer);
io.engine.use(
(req: { _query: Record<string, string> }, res: Response, next: Function) => {
const isHandshake = req._query.sid === undefined;
if (isHandshake) {
passport.authenticate("jwt", { session: false })(req, res, next);
} else {
next();
}
},
);
io.on("connection", (socket) => {
const req = socket.request as Request & { user: Express.User };
socket.join(`user:${req.user.id}`);
socket.on("whoami", (cb) => {
cb(req.user.username);
});
});
httpServer.listen(port, () => {
console.log(`application is running at: http://localhost:${port}`);
});

View File

@@ -0,0 +1,27 @@
{
"name": "passport-jwt-example",
"version": "0.0.1",
"private": true,
"type": "module",
"description": "Example with passport and JWT (https://www.passportjs.org/packages/passport-jwt/)",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.5",
"@types/passport": "^1.0.16",
"@types/passport-jwt": "^4.0.0",
"body-parser": "^1.20.2",
"express": "~4.17.3",
"jsonwebtoken": "^9.0.2",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"socket.io": "^4.7.2",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
},
"devDependencies": {
"prettier": "^3.1.1"
}
}

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"strict": true
},
"ts-node": {
"esm": true
}
}

View File

@@ -5,7 +5,8 @@
"private": true,
"type": "module",
"dependencies": {
"@fails-components/webtransport": "^0.1.7",
"socket.io": "^4.7.1"
"@fails-components/webtransport": "^1.0.8",
"@fails-components/webtransport-transport-http3-quiche": "^1.0.8",
"socket.io": "^4.7.4"
}
}

View File

@@ -625,7 +625,6 @@ export class Server<
switch (encoding) {
case "br":
res.writeHead(200, { "content-encoding": "br" });
readStream.pipe(createBrotliCompress()).pipe(res);
pipeline(readStream, createBrotliCompress(), res, onError);
break;
case "gzip":
@@ -743,13 +742,17 @@ export class Server<
* @param [fn] optional, called as `fn([err])` on error OR all conns closed
*/
public close(fn?: (err?: Error) => void): void {
for (const socket of this.sockets.sockets.values()) {
socket._onclose("server shutting down");
}
this._nsps.forEach((nsp) => {
nsp.sockets.forEach((socket) => {
socket._onclose("server shutting down");
});
nsp.adapter.close();
});
this.engine.close();
// restore the Adapter prototype
// restore the Adapter prototype, when the Socket.IO server was attached to a uWebSockets.js server
restoreAdapter();
if (this.httpServer) {

View File

@@ -6,6 +6,7 @@ import type {
DefaultEventsMap,
EventNamesWithoutAck,
} from "./typed-events";
import { Adapter } from "socket.io-adapter";
import type { BroadcastOptions } from "socket.io-adapter";
import debugModule from "debug";
@@ -33,7 +34,7 @@ export class ParentNamespace<
SocketData = any
> extends Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
private static count: number = 0;
private children: Set<
private readonly children: Set<
Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Set();
@@ -47,13 +48,7 @@ export class ParentNamespace<
* @private
*/
_initAdapter(): void {
const broadcast = (packet: any, opts: BroadcastOptions) => {
this.children.forEach((nsp) => {
nsp.adapter.broadcast(packet, opts);
});
};
// @ts-ignore FIXME is there a way to declare an inner class in TypeScript?
this.adapter = { broadcast };
this.adapter = new ParentBroadcastAdapter(this, this.children);
}
public emit<Ev extends EventNamesWithoutAck<EmitEvents>>(
@@ -112,3 +107,19 @@ export class ParentNamespace<
throw new Error("fetchSockets() is not supported on parent namespaces");
}
}
/**
* A dummy adapter that only supports broadcasting to child (concrete) namespaces.
* @private file
*/
class ParentBroadcastAdapter extends Adapter {
constructor(parentNsp: any, private readonly children: Set<Namespace>) {
super(parentNsp);
}
broadcast(packet: any, opts: BroadcastOptions) {
this.children.forEach((nsp) => {
nsp.adapter.broadcast(packet, opts);
});
}
}

18
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "socket.io",
"version": "4.7.3",
"version": "4.7.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "socket.io",
"version": "4.7.3",
"version": "4.7.5",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.4",
@@ -24,7 +24,7 @@
"nyc": "^15.1.0",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
"socket.io-client": "4.7.4",
"socket.io-client": "4.7.5",
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
"superagent": "^8.0.0",
"supertest": "^6.1.6",
@@ -3478,9 +3478,9 @@
}
},
"node_modules/socket.io-client": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.4.tgz",
"integrity": "sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==",
"version": "4.7.5",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz",
"integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==",
"dev": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
@@ -6942,9 +6942,9 @@
}
},
"socket.io-client": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.4.tgz",
"integrity": "sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==",
"version": "4.7.5",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz",
"integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==",
"dev": true,
"requires": {
"@socket.io/component-emitter": "~3.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "socket.io",
"version": "4.7.4",
"version": "4.7.5",
"description": "node.js realtime framework server",
"keywords": [
"realtime",
@@ -61,7 +61,7 @@
"nyc": "^15.1.0",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
"socket.io-client": "4.7.4",
"socket.io-client": "4.7.5",
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
"superagent": "^8.0.0",
"supertest": "^6.1.6",

View File

@@ -221,11 +221,11 @@ describe("connection state recovery", () => {
const httpServer = createServer().listen(0);
class DummyAdapter extends Adapter {
override persistSession(session) {
persistSession(session) {
expect().fail();
}
override restoreSession(pid, offset) {
restoreSession(pid, offset) {
expect().fail();
return Promise.reject("should not happen");
}

View File

@@ -70,6 +70,21 @@ describe("server attachment", () => {
});
});
it("should serve client (br)", (done) => {
const srv = createServer();
new Server(srv);
request(srv)
.get("/socket.io/socket.io.js")
.set("accept-encoding", "br")
.buffer(true)
.end((err, res) => {
if (err) return done(err);
expect(res.headers["content-encoding"]).to.be("br");
expect(res.status).to.be(200);
done();
});
});
it("should serve client with necessary CORS headers", (done) => {
const srv = createServer();
new Server(srv, {