engine.io
Advanced tools
Comparing version 6.3.1 to 6.4.0
/// <reference types="node" /> | ||
import { EventEmitter } from "events"; | ||
import { IncomingMessage, Server as HttpServer } from "http"; | ||
import { CookieSerializeOptions } from "cookie"; | ||
import { CorsOptions, CorsOptionsDelegate } from "cors"; | ||
import type { IncomingMessage, Server as HttpServer, ServerResponse } from "http"; | ||
import type { CookieSerializeOptions } from "cookie"; | ||
import type { CorsOptions, CorsOptionsDelegate } from "cors"; | ||
import type { Duplex } from "stream"; | ||
declare type Transport = "polling" | "websocket"; | ||
@@ -107,2 +108,11 @@ export interface AttachOptions { | ||
} | ||
/** | ||
* An Express-compatible middleware. | ||
* | ||
* Middleware functions are functions that have access to the request object (req), the response object (res), and the | ||
* next middleware function in the application’s request-response cycle. | ||
* | ||
* @see https://expressjs.com/en/guide/using-middleware.html | ||
*/ | ||
declare type Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void; | ||
export declare abstract class BaseServer extends EventEmitter { | ||
@@ -112,3 +122,3 @@ opts: ServerOptions; | ||
private clientsCount; | ||
protected corsMiddleware: Function; | ||
protected middlewares: Middleware[]; | ||
/** | ||
@@ -144,2 +154,22 @@ * Server constructor. | ||
/** | ||
* Adds a new middleware. | ||
* | ||
* @example | ||
* import helmet from "helmet"; | ||
* | ||
* engine.use(helmet()); | ||
* | ||
* @param fn | ||
*/ | ||
use(fn: Middleware): void; | ||
/** | ||
* Apply the middlewares to the request. | ||
* | ||
* @param req | ||
* @param res | ||
* @param callback | ||
* @protected | ||
*/ | ||
protected _applyMiddlewares(req: IncomingMessage, res: ServerResponse, callback: () => void): void; | ||
/** | ||
* Closes all clients. | ||
@@ -210,7 +240,7 @@ * | ||
* | ||
* @param {http.IncomingMessage} request | ||
* @param {http.ServerResponse|http.OutgoingMessage} response | ||
* @param {IncomingMessage} req | ||
* @param {ServerResponse} res | ||
* @api public | ||
*/ | ||
handleRequest(req: any, res: any): void; | ||
handleRequest(req: IncomingMessage, res: ServerResponse): void; | ||
/** | ||
@@ -221,3 +251,3 @@ * Handles an Engine.IO HTTP Upgrade. | ||
*/ | ||
handleUpgrade(req: any, socket: any, upgradeHead: any): void; | ||
handleUpgrade(req: IncomingMessage, socket: Duplex, upgradeHead: Buffer): void; | ||
/** | ||
@@ -224,0 +254,0 @@ * Called upon a ws.io connection. |
@@ -14,2 +14,3 @@ "use strict"; | ||
const debug = (0, debug_1.default)("engine"); | ||
const kResponseHeaders = Symbol("responseHeaders"); | ||
class BaseServer extends events_1.EventEmitter { | ||
@@ -24,2 +25,3 @@ /** | ||
super(); | ||
this.middlewares = []; | ||
this.clients = {}; | ||
@@ -51,3 +53,3 @@ this.clientsCount = 0; | ||
if (this.opts.cors) { | ||
this.corsMiddleware = require("cors")(this.opts.cors); | ||
this.use(require("cors")(this.opts.cors)); | ||
} | ||
@@ -156,2 +158,41 @@ if (opts.perMessageDeflate) { | ||
/** | ||
* Adds a new middleware. | ||
* | ||
* @example | ||
* import helmet from "helmet"; | ||
* | ||
* engine.use(helmet()); | ||
* | ||
* @param fn | ||
*/ | ||
use(fn) { | ||
this.middlewares.push(fn); | ||
} | ||
/** | ||
* Apply the middlewares to the request. | ||
* | ||
* @param req | ||
* @param res | ||
* @param callback | ||
* @protected | ||
*/ | ||
_applyMiddlewares(req, res, callback) { | ||
if (this.middlewares.length === 0) { | ||
debug("no middleware to apply, skipping"); | ||
return callback(); | ||
} | ||
const apply = (i) => { | ||
debug("applying middleware n°%d", i + 1); | ||
this.middlewares[i](req, res, () => { | ||
if (i + 1 < this.middlewares.length) { | ||
apply(i + 1); | ||
} | ||
else { | ||
callback(); | ||
} | ||
}); | ||
}; | ||
apply(0); | ||
} | ||
/** | ||
* Closes all clients. | ||
@@ -299,2 +340,31 @@ * | ||
}; | ||
/** | ||
* Exposes a subset of the http.ServerResponse interface, in order to be able to apply the middlewares to an upgrade | ||
* request. | ||
* | ||
* @see https://nodejs.org/api/http.html#class-httpserverresponse | ||
*/ | ||
class WebSocketResponse { | ||
constructor(req, socket) { | ||
this.req = req; | ||
this.socket = socket; | ||
// temporarily store the response headers on the req object (see the "headers" event) | ||
req[kResponseHeaders] = {}; | ||
} | ||
setHeader(name, value) { | ||
this.req[kResponseHeaders][name] = value; | ||
} | ||
getHeader(name) { | ||
return this.req[kResponseHeaders][name]; | ||
} | ||
removeHeader(name) { | ||
delete this.req[kResponseHeaders][name]; | ||
} | ||
write() { } | ||
writeHead() { } | ||
end() { | ||
// we could return a proper error code, but the WebSocket client will emit an "error" event anyway. | ||
this.socket.destroy(); | ||
} | ||
} | ||
class Server extends BaseServer { | ||
@@ -321,3 +391,4 @@ /** | ||
// we could also try to parse the array and then sync the values, but that will be error-prone | ||
const additionalHeaders = {}; | ||
const additionalHeaders = req[kResponseHeaders] || {}; | ||
delete req[kResponseHeaders]; | ||
const isInitialRequest = !req._query.sid; | ||
@@ -328,2 +399,3 @@ if (isInitialRequest) { | ||
this.emit("headers", additionalHeaders, req); | ||
debug("writing headers: %j", additionalHeaders); | ||
Object.keys(additionalHeaders).forEach((key) => { | ||
@@ -359,4 +431,4 @@ headersArray.push(`${key}: ${additionalHeaders[key]}`); | ||
* | ||
* @param {http.IncomingMessage} request | ||
* @param {http.ServerResponse|http.OutgoingMessage} response | ||
* @param {IncomingMessage} req | ||
* @param {ServerResponse} res | ||
* @api public | ||
@@ -367,2 +439,3 @@ */ | ||
this.prepare(req); | ||
// @ts-ignore | ||
req.res = res; | ||
@@ -380,4 +453,6 @@ const callback = (errorCode, errorContext) => { | ||
} | ||
// @ts-ignore | ||
if (req._query.sid) { | ||
debug("setting new request for existing client"); | ||
// @ts-ignore | ||
this.clients[req._query.sid].transport.onRequest(req); | ||
@@ -387,13 +462,9 @@ } | ||
const closeConnection = (errorCode, errorContext) => abortRequest(res, errorCode, errorContext); | ||
// @ts-ignore | ||
this.handshake(req._query.transport, req, closeConnection); | ||
} | ||
}; | ||
if (this.corsMiddleware) { | ||
this.corsMiddleware.call(null, req, res, () => { | ||
this.verify(req, false, callback); | ||
}); | ||
} | ||
else { | ||
this._applyMiddlewares(req, res, () => { | ||
this.verify(req, false, callback); | ||
} | ||
}); | ||
} | ||
@@ -407,18 +478,24 @@ /** | ||
this.prepare(req); | ||
this.verify(req, true, (errorCode, errorContext) => { | ||
if (errorCode) { | ||
this.emit("connection_error", { | ||
req, | ||
code: errorCode, | ||
message: Server.errorMessages[errorCode], | ||
context: errorContext, | ||
const res = new WebSocketResponse(req, socket); | ||
this._applyMiddlewares(req, res, () => { | ||
this.verify(req, true, (errorCode, errorContext) => { | ||
if (errorCode) { | ||
this.emit("connection_error", { | ||
req, | ||
code: errorCode, | ||
message: Server.errorMessages[errorCode], | ||
context: errorContext, | ||
}); | ||
abortUpgrade(socket, errorCode, errorContext); | ||
return; | ||
} | ||
const head = Buffer.from(upgradeHead); | ||
upgradeHead = null; | ||
// some middlewares (like express-session) wait for the writeHead() call to flush their headers | ||
// see https://github.com/expressjs/session/blob/1010fadc2f071ddf2add94235d72224cf65159c6/index.js#L220-L244 | ||
res.writeHead(); | ||
// delegate to ws | ||
this.ws.handleUpgrade(req, socket, head, (websocket) => { | ||
this.onWebSocket(req, socket, websocket); | ||
}); | ||
abortUpgrade(socket, errorCode, errorContext); | ||
return; | ||
} | ||
const head = Buffer.from(upgradeHead); | ||
upgradeHead = null; | ||
// delegate to ws | ||
this.ws.handleUpgrade(req, socket, head, (websocket) => { | ||
this.onWebSocket(req, socket, websocket); | ||
}); | ||
@@ -425,0 +502,0 @@ }); |
@@ -35,2 +35,3 @@ import { AttachOptions, BaseServer } from "./server"; | ||
attach(app: any, options?: AttachOptions & uOptions): void; | ||
_applyMiddlewares(req: any, res: any, callback: () => void): void; | ||
private handleRequest; | ||
@@ -37,0 +38,0 @@ private handleUpgrade; |
@@ -18,2 +18,3 @@ "use strict"; | ||
req.method = req.getMethod().toUpperCase(); | ||
req.url = req.getUrl(); | ||
const params = new URLSearchParams(req.getQuery()); | ||
@@ -64,2 +65,15 @@ req._query = Object.fromEntries(params.entries()); | ||
} | ||
_applyMiddlewares(req, res, callback) { | ||
if (this.middlewares.length === 0) { | ||
return callback(); | ||
} | ||
// needed to buffer headers until the status is computed | ||
req.res = new ResponseWrapper(res); | ||
super._applyMiddlewares(req, req.res, () => { | ||
// some middlewares (like express-session) wait for the writeHead() call to flush their headers | ||
// see https://github.com/expressjs/session/blob/1010fadc2f071ddf2add94235d72224cf65159c6/index.js#L220-L244 | ||
req.res.writeHead(); | ||
callback(); | ||
}); | ||
} | ||
handleRequest(res, req) { | ||
@@ -69,32 +83,24 @@ debug('handling "%s" http request "%s"', req.getMethod(), req.getUrl()); | ||
req.res = res; | ||
const callback = (errorCode, errorContext) => { | ||
if (errorCode !== undefined) { | ||
this.emit("connection_error", { | ||
req, | ||
code: errorCode, | ||
message: server_1.Server.errorMessages[errorCode], | ||
context: errorContext, | ||
}); | ||
this.abortRequest(req.res, errorCode, errorContext); | ||
return; | ||
} | ||
if (req._query.sid) { | ||
debug("setting new request for existing client"); | ||
this.clients[req._query.sid].transport.onRequest(req); | ||
} | ||
else { | ||
const closeConnection = (errorCode, errorContext) => this.abortRequest(res, errorCode, errorContext); | ||
this.handshake(req._query.transport, req, closeConnection); | ||
} | ||
}; | ||
if (this.corsMiddleware) { | ||
// needed to buffer headers until the status is computed | ||
req.res = new ResponseWrapper(res); | ||
this.corsMiddleware.call(null, req, req.res, () => { | ||
this.verify(req, false, callback); | ||
this._applyMiddlewares(req, res, () => { | ||
this.verify(req, false, (errorCode, errorContext) => { | ||
if (errorCode !== undefined) { | ||
this.emit("connection_error", { | ||
req, | ||
code: errorCode, | ||
message: server_1.Server.errorMessages[errorCode], | ||
context: errorContext, | ||
}); | ||
this.abortRequest(req.res, errorCode, errorContext); | ||
return; | ||
} | ||
if (req._query.sid) { | ||
debug("setting new request for existing client"); | ||
this.clients[req._query.sid].transport.onRequest(req); | ||
} | ||
else { | ||
const closeConnection = (errorCode, errorContext) => this.abortRequest(res, errorCode, errorContext); | ||
this.handshake(req._query.transport, req, closeConnection); | ||
} | ||
}); | ||
} | ||
else { | ||
this.verify(req, false, callback); | ||
} | ||
}); | ||
} | ||
@@ -104,46 +110,49 @@ handleUpgrade(res, req, context) { | ||
this.prepare(req, res); | ||
// @ts-ignore | ||
req.res = res; | ||
this.verify(req, true, async (errorCode, errorContext) => { | ||
if (errorCode) { | ||
this.emit("connection_error", { | ||
req, | ||
code: errorCode, | ||
message: server_1.Server.errorMessages[errorCode], | ||
context: errorContext, | ||
}); | ||
this.abortRequest(res, errorCode, errorContext); | ||
return; | ||
} | ||
const id = req._query.sid; | ||
let transport; | ||
if (id) { | ||
const client = this.clients[id]; | ||
if (!client) { | ||
debug("upgrade attempt for closed client"); | ||
res.close(); | ||
this._applyMiddlewares(req, res, () => { | ||
this.verify(req, true, async (errorCode, errorContext) => { | ||
if (errorCode) { | ||
this.emit("connection_error", { | ||
req, | ||
code: errorCode, | ||
message: server_1.Server.errorMessages[errorCode], | ||
context: errorContext, | ||
}); | ||
this.abortRequest(res, errorCode, errorContext); | ||
return; | ||
} | ||
else if (client.upgrading) { | ||
debug("transport has already been trying to upgrade"); | ||
res.close(); | ||
const id = req._query.sid; | ||
let transport; | ||
if (id) { | ||
const client = this.clients[id]; | ||
if (!client) { | ||
debug("upgrade attempt for closed client"); | ||
res.close(); | ||
} | ||
else if (client.upgrading) { | ||
debug("transport has already been trying to upgrade"); | ||
res.close(); | ||
} | ||
else if (client.upgraded) { | ||
debug("transport had already been upgraded"); | ||
res.close(); | ||
} | ||
else { | ||
debug("upgrading existing transport"); | ||
transport = this.createTransport(req._query.transport, req); | ||
client.maybeUpgrade(transport); | ||
} | ||
} | ||
else if (client.upgraded) { | ||
debug("transport had already been upgraded"); | ||
res.close(); | ||
} | ||
else { | ||
debug("upgrading existing transport"); | ||
transport = this.createTransport(req._query.transport, req); | ||
client.maybeUpgrade(transport); | ||
transport = await this.handshake(req._query.transport, req, (errorCode, errorContext) => this.abortRequest(res, errorCode, errorContext)); | ||
if (!transport) { | ||
return; | ||
} | ||
} | ||
} | ||
else { | ||
transport = await this.handshake(req._query.transport, req, (errorCode, errorContext) => this.abortRequest(res, errorCode, errorContext)); | ||
if (!transport) { | ||
return; | ||
} | ||
} | ||
res.upgrade({ | ||
transport, | ||
}, req.getHeader("sec-websocket-key"), req.getHeader("sec-websocket-protocol"), req.getHeader("sec-websocket-extensions"), context); | ||
// calling writeStatus() triggers the flushing of any header added in a middleware | ||
req.res.writeStatus("101 Switching Protocols"); | ||
res.upgrade({ | ||
transport, | ||
}, req.getHeader("sec-websocket-key"), req.getHeader("sec-websocket-protocol"), req.getHeader("sec-websocket-extensions"), context); | ||
}); | ||
}); | ||
@@ -174,7 +183,24 @@ } | ||
set statusCode(status) { | ||
if (!status) { | ||
return; | ||
} | ||
// FIXME: handle all status codes? | ||
this.writeStatus(status === 200 ? "200 OK" : "204 No Content"); | ||
} | ||
writeHead(status) { | ||
this.statusCode = status; | ||
} | ||
setHeader(key, value) { | ||
this.writeHeader(key, value); | ||
if (Array.isArray(value)) { | ||
value.forEach((val) => { | ||
this.writeHeader(key, val); | ||
}); | ||
} | ||
else { | ||
this.writeHeader(key, value); | ||
} | ||
} | ||
removeHeader() { | ||
// FIXME: not implemented | ||
} | ||
// needed by vary: https://github.com/jshttp/vary/blob/5d725d059b3871025cf753e9dfa08924d0bcfa8f/index.js#L134 | ||
@@ -181,0 +207,0 @@ getHeader() { } |
{ | ||
"name": "engine.io", | ||
"version": "6.3.1", | ||
"version": "6.4.0", | ||
"description": "The realtime engine behind Socket.IO. Provides the foundation of a bidirectional connection between client and server", | ||
@@ -48,5 +48,7 @@ "type": "commonjs", | ||
"eiows": "^4.1.2", | ||
"engine.io-client": "6.3.0", | ||
"engine.io-client": "6.4.0", | ||
"engine.io-client-v3": "npm:engine.io-client@3.5.2", | ||
"expect.js": "^0.3.1", | ||
"express-session": "^1.17.3", | ||
"helmet": "^6.0.1", | ||
"mocha": "^9.1.3", | ||
@@ -53,0 +55,0 @@ "prettier": "^2.8.2", |
154354
4238
13