"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const types_1 = require("@deepstream/types");
const constants_1 = require("../../../constants");
const utils_1 = require("../../../utils/utils");
const fileUtils = require("../../../config/file-utils");
const HTTPStatus = require("http-status");
class UWSHTTP extends types_1.DeepstreamPlugin {
    constructor(pluginOptions, services, config) {
        super();
        this.pluginOptions = pluginOptions;
        this.services = services;
        this.description = 'UWS HTTP Service';
        this.isReady = false;
        this.connections = new Map();
        this.isGettingReady = false;
        this.methods = ['GET', 'POST', 'OPTIONS'];
        this.methodsStr = this.methods.join(', ');
        this.headers = ['X-Requested-With', 'X-HTTP-Method-Override', 'Content-Type', 'Accept'];
        this.headersLower = this.headers.map((header) => header.toLowerCase());
        this.headersStr = this.headers.join(', ');
        if (this.pluginOptions.allowAllOrigins === false) {
            if (!this.pluginOptions.origins) {
                this.services.logger.fatal(types_1.EVENT.INVALID_CONFIG_DATA, 'HTTP allowAllOrigins set to false but no origins provided');
            }
        }
        // alias require to trick nexe from bundling it
        const req = require;
        try {
            this.uWS = req('uWebSockets.js');
        }
        catch (e) {
            this.uWS = req(fileUtils.lookupLibRequirePath('uWebSockets.js'));
        }
        const sslParams = this.getSLLParams(pluginOptions);
        if (sslParams) {
            this.server = this.uWS.SSLApp({
                ...pluginOptions,
                ...sslParams
            });
        }
        else {
            this.server = this.uWS.App(pluginOptions);
        }
    }
    async whenReady() {
        if (this.isReady || this.isGettingReady) {
            return;
        }
        this.isGettingReady = true;
        return new Promise((resolve) => {
            this.server.listen(this.pluginOptions.host, this.pluginOptions.port, (token) => {
                /* Save the listen socket for later shut down */
                this.listenSocket = token;
                // handle options requests
                this.server.options('/*', (response, request) => {
                    if (!this.pluginOptions.allowAllOrigins) {
                        if (!this.verifyOrigin(response, request)) {
                            return;
                        }
                        this.handleOptions(response, request);
                    }
                    else {
                        response.writeHeader('Access-Control-Allow-Origin', '*');
                        this.handleOptions(response, request);
                    }
                });
                if (!!token) {
                    resolve();
                    return;
                }
                this.services.logger.fatal(constants_1.STATES.SERVICE_INIT, `Failed to listen to port: ${this.pluginOptions.port}`);
            });
        });
    }
    async close() {
        const closePromises = [];
        this.connections.forEach((conn) => {
            if (!conn.isClosed) {
                closePromises.push(new Promise((resolve) => conn.onClose(resolve)));
                conn.destroy();
            }
        });
        await Promise.all(closePromises);
        this.connections.clear();
        this.uWS.us_listen_socket_close(this.listenSocket);
        await utils_1.PromiseDelay(2000);
    }
    registerPostPathPrefix(prefix, handler) {
        this.server.post(prefix, (response, request) => {
            /* Register error cb */
            response.onAborted((err) => {
                console.log('error with post', err);
            });
            const meta = { headers: this.getHeaders(request), url: request.getUrl() };
            if (!this.pluginOptions.allowAllOrigins) {
                if (!this.verifyOrigin(response, request)) {
                    return;
                }
            }
            else {
                response.writeHeader('Access-Control-Allow-Origin', '*');
            }
            readJson(response, (body) => {
                handler(body, meta, this.sendResponse.bind(this, response));
            }, () => {
                this.terminateResponse(response, HTTPStatus.BAD_REQUEST, 'Failed to parse body of request');
            });
        });
    }
    registerGetPathPrefix(prefix, handler) {
        this.server.get(prefix, (response, request) => {
            /* Register error cb */
            response.onAborted((err) => {
                console.log('error with get', err);
            });
            if (!this.pluginOptions.allowAllOrigins) {
                if (!this.verifyOrigin(response, request)) {
                    return;
                }
            }
            else {
                response.writeHeader('Access-Control-Allow-Origin', '*');
            }
            handler({ headers: this.getHeaders(request), url: request.getUrl() }, this.sendResponse.bind(this, response));
        });
    }
    sendWebsocketMessage(socket, message, isBinary) {
        socket.send(message, isBinary);
    }
    getSocketWrappersForUserId(userId) {
        return [...this.connections.values()].filter((socketWrapper) => socketWrapper.userId === userId);
    }
    registerWebsocketEndpoint(path, createSocketWrapper, webSocketConnectionEndpoint) {
        this.server.ws(path, {
            /* Options */
            compression: 0,
            maxPayloadLength: webSocketConnectionEndpoint.wsOptions.maxMessageSize,
            idleTimeout: webSocketConnectionEndpoint.wsOptions.heartBeatInterval * 2,
            open: (websocket, request) => {
                const handshakeData = {
                    remoteAddress: new Uint8Array(websocket.getRemoteAddress()).join('.'),
                    headers: this.getHeaders(request),
                    referer: request.getHeader('referer')
                };
                const socketWrapper = createSocketWrapper(websocket, handshakeData, this.services, webSocketConnectionEndpoint.wsOptions, webSocketConnectionEndpoint);
                this.connections.set(websocket, socketWrapper);
                webSocketConnectionEndpoint.onConnection.call(webSocketConnectionEndpoint, socketWrapper);
            },
            message: (ws, message, isBinary) => {
                const socketWrapper = this.connections.get(ws);
                const messages = socketWrapper.parseMessage(isBinary ? new Uint8Array(message) : Buffer.from(message).toString());
                if (messages.length > 0) {
                    socketWrapper.onMessage(messages);
                }
            },
            drain: () => {
            },
            close: (ws) => {
                webSocketConnectionEndpoint.onSocketClose.call(webSocketConnectionEndpoint, this.connections.get(ws));
                this.connections.delete(ws);
            }
        });
    }
    terminateResponse(response, code, message) {
        response.writeHeader('Content-Type', 'text/plain; charset=utf-8');
        response.writeStatus(code.toString());
        if (message) {
            response.end(`${message}\r\n\r\n`);
        }
        else {
            response.end();
        }
    }
    sendResponse(response, err, data) {
        if (err) {
            const statusCode = err.statusCode || HTTPStatus.BAD_REQUEST;
            this.terminateResponse(response, statusCode, err.message);
            return;
        }
        response.writeHeader('Content-Type', 'application/json; charset=utf-8');
        response.writeStatus(HTTPStatus.OK.toString());
        if (data) {
            response.end(`${JSON.stringify(data)}\r\n\r\n`);
        }
        else {
            response.end();
        }
    }
    getHeaders(req) {
        const headers = {};
        for (const wantedHeader of this.pluginOptions.headers) {
            headers[wantedHeader] = req.getHeader(wantedHeader).toString();
        }
        return headers;
    }
    getSLLParams(options) {
        if (!options.ssl) {
            return null;
        }
        // tslint:disable-next-line: variable-name
        const { key: key_file_name, cert: cert_file_name, dhParams: dh_params_file_name, passphrase } = options.ssl;
        if (key_file_name || cert_file_name) {
            if (!key_file_name) {
                this.services.logger.fatal(types_1.EVENT.PLUGIN_INITIALIZATION_ERROR, 'Must also include ssl key in order to use SSL');
            }
            if (!cert_file_name) {
                this.services.logger.fatal(types_1.EVENT.PLUGIN_INITIALIZATION_ERROR, 'Must also include ssl cert in order to use SSL');
            }
            return {
                key_file_name,
                cert_file_name,
                dh_params_file_name,
                passphrase,
            };
        }
        return null;
    }
    verifyOrigin(response, request) {
        const requestOriginUrl = request.getHeader('origin') || request.getHeader('referer');
        const requestHostUrl = request.getHeader('host');
        if (this.pluginOptions.hostUrl && requestHostUrl !== this.pluginOptions.hostUrl) {
            this.terminateResponse(response, HTTPStatus.FORBIDDEN, 'Forbidden Host.');
            return false;
        }
        if (this.origins.indexOf(requestOriginUrl) === -1) {
            if (!requestOriginUrl) {
                this.terminateResponse(response, HTTPStatus.FORBIDDEN, 'CORS is configured for this. All requests must set a valid "Origin" header.');
            }
            else {
                this.terminateResponse(response, HTTPStatus.FORBIDDEN, `Origin "${requestOriginUrl}" is forbidden.`);
            }
            return false;
        }
        // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
        response.writeHeader('Access-Control-Allow-Origin', requestOriginUrl);
        response.writeHeader('Access-Control-Allow-Credentials', 'true');
        response.writeHeader('Vary', 'Origin');
        return true;
    }
    handleOptions(response, request) {
        const requestMethod = request.getHeader('access-control-request-method');
        if (!requestMethod) {
            this.terminateResponse(response, HTTPStatus.BAD_REQUEST, 'Missing header "Access-Control-Request-Method".');
            return;
        }
        if (this.methods.indexOf(requestMethod) === -1) {
            this.terminateResponse(response, HTTPStatus.FORBIDDEN, `Method ${requestMethod} is forbidden. Supported methods: ${this.methodsStr}`);
            return;
        }
        const requestHeadersRaw = request.getHeader('access-control-request-headers');
        if (!requestHeadersRaw) {
            this.terminateResponse(response, HTTPStatus.BAD_REQUEST, 'Missing header "Access-Control-Request-Headers".');
            return;
        }
        const requestHeaders = requestHeadersRaw.split(',');
        for (let i = 0; i < requestHeaders.length; i++) {
            if (this.headersLower.indexOf(requestHeaders[i].trim().toLowerCase()) === -1) {
                this.terminateResponse(response, HTTPStatus.FORBIDDEN, `Header ${requestHeaders[i]} is forbidden. Supported headers: ${this.headersStr}`);
                return;
            }
        }
        response.writeHeader('Access-Control-Allow-Methods', this.methodsStr);
        response.writeHeader('Access-Control-Allow-Headers', this.headersStr);
        this.terminateResponse(response, HTTPStatus.NO_CONTENT);
    }
}
exports.UWSHTTP = UWSHTTP;
/* Helper function for reading a posted JSON body */
function readJson(res, cb, err) {
    let buffer;
    res.onData((ab, isLast) => {
        const chunk = Buffer.from(ab);
        if (isLast) {
            let json;
            if (buffer) {
                try {
                    json = JSON.parse(Buffer.concat([buffer, chunk]).toString());
                }
                catch (e) {
                    /* res.close calls onAborted */
                    res.close();
                    return;
                }
                cb(json);
            }
            else {
                try {
                    json = JSON.parse(chunk.toString());
                }
                catch (e) {
                    /* res.close calls onAborted */
                    res.close();
                    return;
                }
                cb(json);
            }
        }
        else {
            if (buffer) {
                buffer = Buffer.concat([buffer, chunk]);
            }
            else {
                buffer = Buffer.concat([chunk]);
            }
        }
    });
}
//# sourceMappingURL=uws-http.js.map