| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626 | "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");class BaseServer extends events_1.EventEmitter {    /**     * Server constructor.     *     * @param {Object} opts - options     * @api public     */    constructor(opts = {}) {        super();        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: "/",                // @ts-ignore                httpOnly: opts.cookie.path !== false,                sameSite: "lax"            }, opts.cookie);        }        if (this.opts.cors) {            this.corsMiddleware = require("cors")(this.opts.cors);        }        if (opts.perMessageDeflate) {            this.opts.perMessageDeflate = Object.assign({                threshold: 1024            }, opts.perMessageDeflate);        }        this.init();    }    /**     * Returns a list of available transports for upgrade given a certain transport.     *     * @return {Array}     * @api public     */    upgrades(transport) {        if (!this.opts.allowUpgrades)            return [];        return transports_1.default[transport].upgradesTo || [];    }    /**     * Verifies a request.     *     * @param {http.IncomingMessage}     * @return {Boolean} whether the request is valid     * @api private     */    verify(req, upgrade, fn) {        // transport check        const transport = req._query.transport;        if (!~this.opts.transports.indexOf(transport)) {            debug('unknown transport "%s"', transport);            return fn(Server.errors.UNKNOWN_TRANSPORT, { transport });        }        // 'Origin' header check        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            });        }        // sid check        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 {            // handshake is GET only            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();    }    /**     * Closes all clients.     *     * @api public     */    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;    }    /**     * generate a socket id.     * Overwrite this method to generate your custom socket id     *     * @param {Object} request object     * @api public     */    generateId(req) {        return base64id.generateId();    }    /**     * Handshakes a new client.     *     * @param {String} transport name     * @param {Object} request object     * @param {Function} closeConnection     *     * @api protected     */    async handshake(transportName, req, closeConnection) {        const protocol = req._query.EIO === "4" ? 4 : 3; // 3rd revision by default        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"] = [                        // @ts-ignore                        (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;/** * Protocol errors mappings. */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 Server extends BaseServer {    /**     * Initialize websocket server     *     * @api protected     */    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) => {                // note: 'ws' uses an array of headers, while Engine.IO uses an object (response.writeHead() accepts both formats)                // we could also try to parse the array and then sync the values, but that will be error-prone                const additionalHeaders = {};                const isInitialRequest = !req._query.sid;                if (isInitialRequest) {                    this.emit("initial_headers", additionalHeaders, req);                }                this.emit("headers", additionalHeaders, req);                Object.keys(additionalHeaders).forEach(key => {                    headersArray.push(`${key}: ${additionalHeaders[key]}`);                });            });        }    }    cleanup() {        if (this.ws) {            debug("closing webSocketServer");            this.ws.close();            // don't delete this.ws because it can be used again if the http server starts listening again        }    }    /**     * Prepares a request by processing the query string.     *     * @api private     */    prepare(req) {        // try to leverage pre-existing `req._query` (e.g: from connect)        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);    }    /**     * Handles an Engine.IO HTTP request.     *     * @param {http.IncomingMessage} request     * @param {http.ServerResponse|http.OutgoingMessage} response     * @api public     */    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);            }        };        if (this.corsMiddleware) {            this.corsMiddleware.call(null, req, res, () => {                this.verify(req, false, callback);            });        }        else {            this.verify(req, false, callback);        }    }    /**     * Handles an Engine.IO HTTP Upgrade.     *     * @api public     */    handleUpgrade(req, socket, upgradeHead) {        this.prepare(req);        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); // eslint-disable-line node/no-deprecated-api            upgradeHead = null;            // delegate to ws            this.ws.handleUpgrade(req, socket, head, websocket => {                this.onWebSocket(req, socket, websocket);            });        });    }    /**     * Called upon a ws.io connection.     *     * @param {ws.Socket} websocket     * @api private     */    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;        }        // get client id        const id = req._query.sid;        // keep a reference to the ws.Socket        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");                // transport error handling takes over                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");            // websocket.close() not needed        }    }    /**     * Captures upgrade requests for a http.Server.     *     * @param {http.Server} server     * @param {Object} options     * @api public     */    attach(server, options = {}) {        let path = (options.path || "/engine.io").replace(/\/$/, "");        const destroyUpgradeTimeout = options.destroyUpgradeTimeout || 1000;        // normalize path        path += "/";        function check(req) {            return path === req.url.substr(0, path.length);        }        // cache and clean up listeners        const listeners = server.listeners("request").slice(0);        server.removeAllListeners("request");        server.on("close", this.close.bind(this));        server.on("listening", this.init.bind(this));        // add request handler        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) {                    // default node behavior is to disconnect when no handlers                    // but by adding a handler, we prevent that                    // and if no eio thing handles the upgrade                    // then the socket needs to die!                    setTimeout(function () {                        // @ts-ignore                        if (socket.writable && socket.bytesWritten <= 0) {                            return socket.end();                        }                    }, destroyUpgradeTimeout);                }            });        }    }}exports.Server = Server;/** * Close the HTTP long-polling request * * @param res - the response object * @param errorCode - the error code * @param errorContext - additional error context * * @api private */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    }));}/** * Close the WebSocket connection * * @param {net.Socket} socket * @param {string} errorCode - the error code * @param {object} errorContext - additional error context * * @api private */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();}/* eslint-disable *//** * From https://github.com/nodejs/node/blob/v8.4.0/lib/_http_common.js#L303-L354 * * True if val contains an invalid field-vchar *  field-value    = *( field-content / obs-fold ) *  field-content  = field-vchar [ 1*( SP / HTAB ) field-vchar ] *  field-vchar    = VCHAR / obs-text * * checkInvalidHeaderChar() is currently designed to be inlinable by v8, * so take care when making changes to the implementation so that the source * code size does not exceed v8's default max_inlined_source_size setting. **/// prettier-ignoreconst 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 // ... 255];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;}
 |