|
"use strict"; |
|
Object.defineProperty(exports, "__esModule", { value: true }); |
|
exports.Server = exports.BaseServer = void 0; |
|
const qs = require("querystring"); |
|
const url_1 = require("url"); |
|
const base64id = require("base64id"); |
|
const transports_1 = require("./transports"); |
|
const events_1 = require("events"); |
|
const socket_1 = require("./socket"); |
|
const debug_1 = require("debug"); |
|
const cookie_1 = require("cookie"); |
|
const ws_1 = require("ws"); |
|
const debug = (0, debug_1.default)("engine"); |
|
const kResponseHeaders = Symbol("responseHeaders"); |
|
class BaseServer extends events_1.EventEmitter { |
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(opts = {}) { |
|
super(); |
|
this.middlewares = []; |
|
this.clients = {}; |
|
this.clientsCount = 0; |
|
this.opts = Object.assign({ |
|
wsEngine: ws_1.Server, |
|
pingTimeout: 20000, |
|
pingInterval: 25000, |
|
upgradeTimeout: 10000, |
|
maxHttpBufferSize: 1e6, |
|
transports: Object.keys(transports_1.default), |
|
allowUpgrades: true, |
|
httpCompression: { |
|
threshold: 1024, |
|
}, |
|
cors: false, |
|
allowEIO3: false, |
|
}, opts); |
|
if (opts.cookie) { |
|
this.opts.cookie = Object.assign({ |
|
name: "io", |
|
path: "/", |
|
|
|
httpOnly: opts.cookie.path !== false, |
|
sameSite: "lax", |
|
}, opts.cookie); |
|
} |
|
if (this.opts.cors) { |
|
this.use(require("cors")(this.opts.cors)); |
|
} |
|
if (opts.perMessageDeflate) { |
|
this.opts.perMessageDeflate = Object.assign({ |
|
threshold: 1024, |
|
}, opts.perMessageDeflate); |
|
} |
|
this.init(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
_computePath(options) { |
|
let path = (options.path || "/engine.io").replace(/\/$/, ""); |
|
if (options.addTrailingSlash !== false) { |
|
|
|
path += "/"; |
|
} |
|
return path; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
upgrades(transport) { |
|
if (!this.opts.allowUpgrades) |
|
return []; |
|
return transports_1.default[transport].upgradesTo || []; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
verify(req, upgrade, fn) { |
|
|
|
const transport = req._query.transport; |
|
if (!~this.opts.transports.indexOf(transport)) { |
|
debug('unknown transport "%s"', transport); |
|
return fn(Server.errors.UNKNOWN_TRANSPORT, { transport }); |
|
} |
|
|
|
const isOriginInvalid = checkInvalidHeaderChar(req.headers.origin); |
|
if (isOriginInvalid) { |
|
const origin = req.headers.origin; |
|
req.headers.origin = null; |
|
debug("origin header invalid"); |
|
return fn(Server.errors.BAD_REQUEST, { |
|
name: "INVALID_ORIGIN", |
|
origin, |
|
}); |
|
} |
|
|
|
const sid = req._query.sid; |
|
if (sid) { |
|
if (!this.clients.hasOwnProperty(sid)) { |
|
debug('unknown sid "%s"', sid); |
|
return fn(Server.errors.UNKNOWN_SID, { |
|
sid, |
|
}); |
|
} |
|
const previousTransport = this.clients[sid].transport.name; |
|
if (!upgrade && previousTransport !== transport) { |
|
debug("bad request: unexpected transport without upgrade"); |
|
return fn(Server.errors.BAD_REQUEST, { |
|
name: "TRANSPORT_MISMATCH", |
|
transport, |
|
previousTransport, |
|
}); |
|
} |
|
} |
|
else { |
|
|
|
if ("GET" !== req.method) { |
|
return fn(Server.errors.BAD_HANDSHAKE_METHOD, { |
|
method: req.method, |
|
}); |
|
} |
|
if (transport === "websocket" && !upgrade) { |
|
debug("invalid transport upgrade"); |
|
return fn(Server.errors.BAD_REQUEST, { |
|
name: "TRANSPORT_HANDSHAKE_ERROR", |
|
}); |
|
} |
|
if (!this.opts.allowRequest) |
|
return fn(); |
|
return this.opts.allowRequest(req, (message, success) => { |
|
if (!success) { |
|
return fn(Server.errors.FORBIDDEN, { |
|
message, |
|
}); |
|
} |
|
fn(); |
|
}); |
|
} |
|
fn(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
use(fn) { |
|
this.middlewares.push(fn); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_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); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
close() { |
|
debug("closing all open clients"); |
|
for (let i in this.clients) { |
|
if (this.clients.hasOwnProperty(i)) { |
|
this.clients[i].close(true); |
|
} |
|
} |
|
this.cleanup(); |
|
return this; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
generateId(req) { |
|
return base64id.generateId(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async handshake(transportName, req, closeConnection) { |
|
const protocol = req._query.EIO === "4" ? 4 : 3; |
|
if (protocol === 3 && !this.opts.allowEIO3) { |
|
debug("unsupported protocol version"); |
|
this.emit("connection_error", { |
|
req, |
|
code: Server.errors.UNSUPPORTED_PROTOCOL_VERSION, |
|
message: Server.errorMessages[Server.errors.UNSUPPORTED_PROTOCOL_VERSION], |
|
context: { |
|
protocol, |
|
}, |
|
}); |
|
closeConnection(Server.errors.UNSUPPORTED_PROTOCOL_VERSION); |
|
return; |
|
} |
|
let id; |
|
try { |
|
id = await this.generateId(req); |
|
} |
|
catch (e) { |
|
debug("error while generating an id"); |
|
this.emit("connection_error", { |
|
req, |
|
code: Server.errors.BAD_REQUEST, |
|
message: Server.errorMessages[Server.errors.BAD_REQUEST], |
|
context: { |
|
name: "ID_GENERATION_ERROR", |
|
error: e, |
|
}, |
|
}); |
|
closeConnection(Server.errors.BAD_REQUEST); |
|
return; |
|
} |
|
debug('handshaking client "%s"', id); |
|
try { |
|
var transport = this.createTransport(transportName, req); |
|
if ("polling" === transportName) { |
|
transport.maxHttpBufferSize = this.opts.maxHttpBufferSize; |
|
transport.httpCompression = this.opts.httpCompression; |
|
} |
|
else if ("websocket" === transportName) { |
|
transport.perMessageDeflate = this.opts.perMessageDeflate; |
|
} |
|
if (req._query && req._query.b64) { |
|
transport.supportsBinary = false; |
|
} |
|
else { |
|
transport.supportsBinary = true; |
|
} |
|
} |
|
catch (e) { |
|
debug('error handshaking to transport "%s"', transportName); |
|
this.emit("connection_error", { |
|
req, |
|
code: Server.errors.BAD_REQUEST, |
|
message: Server.errorMessages[Server.errors.BAD_REQUEST], |
|
context: { |
|
name: "TRANSPORT_HANDSHAKE_ERROR", |
|
error: e, |
|
}, |
|
}); |
|
closeConnection(Server.errors.BAD_REQUEST); |
|
return; |
|
} |
|
const socket = new socket_1.Socket(id, this, transport, req, protocol); |
|
transport.on("headers", (headers, req) => { |
|
const isInitialRequest = !req._query.sid; |
|
if (isInitialRequest) { |
|
if (this.opts.cookie) { |
|
headers["Set-Cookie"] = [ |
|
|
|
(0, cookie_1.serialize)(this.opts.cookie.name, id, this.opts.cookie), |
|
]; |
|
} |
|
this.emit("initial_headers", headers, req); |
|
} |
|
this.emit("headers", headers, req); |
|
}); |
|
transport.onRequest(req); |
|
this.clients[id] = socket; |
|
this.clientsCount++; |
|
socket.once("close", () => { |
|
delete this.clients[id]; |
|
this.clientsCount--; |
|
}); |
|
this.emit("connection", socket); |
|
return transport; |
|
} |
|
} |
|
exports.BaseServer = BaseServer; |
|
|
|
|
|
|
|
BaseServer.errors = { |
|
UNKNOWN_TRANSPORT: 0, |
|
UNKNOWN_SID: 1, |
|
BAD_HANDSHAKE_METHOD: 2, |
|
BAD_REQUEST: 3, |
|
FORBIDDEN: 4, |
|
UNSUPPORTED_PROTOCOL_VERSION: 5, |
|
}; |
|
BaseServer.errorMessages = { |
|
0: "Transport unknown", |
|
1: "Session ID unknown", |
|
2: "Bad handshake method", |
|
3: "Bad request", |
|
4: "Forbidden", |
|
5: "Unsupported protocol version", |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
class WebSocketResponse { |
|
constructor(req, socket) { |
|
this.req = req; |
|
this.socket = socket; |
|
|
|
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() { |
|
|
|
this.socket.destroy(); |
|
} |
|
} |
|
class Server extends BaseServer { |
|
|
|
|
|
|
|
|
|
|
|
init() { |
|
if (!~this.opts.transports.indexOf("websocket")) |
|
return; |
|
if (this.ws) |
|
this.ws.close(); |
|
this.ws = new this.opts.wsEngine({ |
|
noServer: true, |
|
clientTracking: false, |
|
perMessageDeflate: this.opts.perMessageDeflate, |
|
maxPayload: this.opts.maxHttpBufferSize, |
|
}); |
|
if (typeof this.ws.on === "function") { |
|
this.ws.on("headers", (headersArray, req) => { |
|
|
|
|
|
const additionalHeaders = req[kResponseHeaders] || {}; |
|
delete req[kResponseHeaders]; |
|
const isInitialRequest = !req._query.sid; |
|
if (isInitialRequest) { |
|
this.emit("initial_headers", additionalHeaders, req); |
|
} |
|
this.emit("headers", additionalHeaders, req); |
|
debug("writing headers: %j", additionalHeaders); |
|
Object.keys(additionalHeaders).forEach((key) => { |
|
headersArray.push(`${key}: ${additionalHeaders[key]}`); |
|
}); |
|
}); |
|
} |
|
} |
|
cleanup() { |
|
if (this.ws) { |
|
debug("closing webSocketServer"); |
|
this.ws.close(); |
|
|
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
prepare(req) { |
|
|
|
if (!req._query) { |
|
req._query = ~req.url.indexOf("?") ? qs.parse((0, url_1.parse)(req.url).query) : {}; |
|
} |
|
} |
|
createTransport(transportName, req) { |
|
return new transports_1.default[transportName](req); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handleRequest(req, res) { |
|
debug('handling "%s" http request "%s"', req.method, req.url); |
|
this.prepare(req); |
|
|
|
req.res = res; |
|
const callback = (errorCode, errorContext) => { |
|
if (errorCode !== undefined) { |
|
this.emit("connection_error", { |
|
req, |
|
code: errorCode, |
|
message: Server.errorMessages[errorCode], |
|
context: errorContext, |
|
}); |
|
abortRequest(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) => abortRequest(res, errorCode, errorContext); |
|
|
|
this.handshake(req._query.transport, req, closeConnection); |
|
} |
|
}; |
|
this._applyMiddlewares(req, res, () => { |
|
this.verify(req, false, callback); |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
handleUpgrade(req, socket, upgradeHead) { |
|
this.prepare(req); |
|
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; |
|
|
|
|
|
res.writeHead(); |
|
|
|
this.ws.handleUpgrade(req, socket, head, (websocket) => { |
|
this.onWebSocket(req, socket, websocket); |
|
}); |
|
}); |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
onWebSocket(req, socket, websocket) { |
|
websocket.on("error", onUpgradeError); |
|
if (transports_1.default[req._query.transport] !== undefined && |
|
!transports_1.default[req._query.transport].prototype.handlesUpgrades) { |
|
debug("transport doesnt handle upgraded requests"); |
|
websocket.close(); |
|
return; |
|
} |
|
|
|
const id = req._query.sid; |
|
|
|
req.websocket = websocket; |
|
if (id) { |
|
const client = this.clients[id]; |
|
if (!client) { |
|
debug("upgrade attempt for closed client"); |
|
websocket.close(); |
|
} |
|
else if (client.upgrading) { |
|
debug("transport has already been trying to upgrade"); |
|
websocket.close(); |
|
} |
|
else if (client.upgraded) { |
|
debug("transport had already been upgraded"); |
|
websocket.close(); |
|
} |
|
else { |
|
debug("upgrading existing transport"); |
|
|
|
websocket.removeListener("error", onUpgradeError); |
|
const transport = this.createTransport(req._query.transport, req); |
|
if (req._query && req._query.b64) { |
|
transport.supportsBinary = false; |
|
} |
|
else { |
|
transport.supportsBinary = true; |
|
} |
|
transport.perMessageDeflate = this.opts.perMessageDeflate; |
|
client.maybeUpgrade(transport); |
|
} |
|
} |
|
else { |
|
const closeConnection = (errorCode, errorContext) => abortUpgrade(socket, errorCode, errorContext); |
|
this.handshake(req._query.transport, req, closeConnection); |
|
} |
|
function onUpgradeError() { |
|
debug("websocket error before upgrade"); |
|
|
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
attach(server, options = {}) { |
|
const path = this._computePath(options); |
|
const destroyUpgradeTimeout = options.destroyUpgradeTimeout || 1000; |
|
function check(req) { |
|
|
|
return path === req.url.slice(0, path.length); |
|
} |
|
|
|
const listeners = server.listeners("request").slice(0); |
|
server.removeAllListeners("request"); |
|
server.on("close", this.close.bind(this)); |
|
server.on("listening", this.init.bind(this)); |
|
|
|
server.on("request", (req, res) => { |
|
if (check(req)) { |
|
debug('intercepting request for path "%s"', path); |
|
this.handleRequest(req, res); |
|
} |
|
else { |
|
let i = 0; |
|
const l = listeners.length; |
|
for (; i < l; i++) { |
|
listeners[i].call(server, req, res); |
|
} |
|
} |
|
}); |
|
if (~this.opts.transports.indexOf("websocket")) { |
|
server.on("upgrade", (req, socket, head) => { |
|
if (check(req)) { |
|
this.handleUpgrade(req, socket, head); |
|
} |
|
else if (false !== options.destroyUpgrade) { |
|
|
|
|
|
|
|
|
|
setTimeout(function () { |
|
|
|
if (socket.writable && socket.bytesWritten <= 0) { |
|
socket.on("error", (e) => { |
|
debug("error while destroying upgrade: %s", e.message); |
|
}); |
|
return socket.end(); |
|
} |
|
}, destroyUpgradeTimeout); |
|
} |
|
}); |
|
} |
|
} |
|
} |
|
exports.Server = Server; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function abortRequest(res, errorCode, errorContext) { |
|
const statusCode = errorCode === Server.errors.FORBIDDEN ? 403 : 400; |
|
const message = errorContext && errorContext.message |
|
? errorContext.message |
|
: Server.errorMessages[errorCode]; |
|
res.writeHead(statusCode, { "Content-Type": "application/json" }); |
|
res.end(JSON.stringify({ |
|
code: errorCode, |
|
message, |
|
})); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function abortUpgrade(socket, errorCode, errorContext = {}) { |
|
socket.on("error", () => { |
|
debug("ignoring error from closed connection"); |
|
}); |
|
if (socket.writable) { |
|
const message = errorContext.message || Server.errorMessages[errorCode]; |
|
const length = Buffer.byteLength(message); |
|
socket.write("HTTP/1.1 400 Bad Request\r\n" + |
|
"Connection: close\r\n" + |
|
"Content-type: text/html\r\n" + |
|
"Content-Length: " + |
|
length + |
|
"\r\n" + |
|
"\r\n" + |
|
message); |
|
} |
|
socket.destroy(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const validHdrChars = [ |
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, |
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, |
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, |
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, |
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, |
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, |
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, |
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, |
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, |
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, |
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, |
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, |
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, |
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, |
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, |
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 |
|
]; |
|
function checkInvalidHeaderChar(val) { |
|
val += ""; |
|
if (val.length < 1) |
|
return false; |
|
if (!validHdrChars[val.charCodeAt(0)]) { |
|
debug('invalid header, index 0, char "%s"', val.charCodeAt(0)); |
|
return true; |
|
} |
|
if (val.length < 2) |
|
return false; |
|
if (!validHdrChars[val.charCodeAt(1)]) { |
|
debug('invalid header, index 1, char "%s"', val.charCodeAt(1)); |
|
return true; |
|
} |
|
if (val.length < 3) |
|
return false; |
|
if (!validHdrChars[val.charCodeAt(2)]) { |
|
debug('invalid header, index 2, char "%s"', val.charCodeAt(2)); |
|
return true; |
|
} |
|
if (val.length < 4) |
|
return false; |
|
if (!validHdrChars[val.charCodeAt(3)]) { |
|
debug('invalid header, index 3, char "%s"', val.charCodeAt(3)); |
|
return true; |
|
} |
|
for (let i = 4; i < val.length; ++i) { |
|
if (!validHdrChars[val.charCodeAt(i)]) { |
|
debug('invalid header, index "%i", char "%s"', i, val.charCodeAt(i)); |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
|