"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const types_1 = require("@deepstream/types");
// @ts-ignore
const httpShutdown = require("http-shutdown");
const http = require("http");
const https = require("https");
const HTTPStatus = require("http-status");
const contentType = require("content-type");
const bodyParser = require("body-parser");
const events_1 = require("events");
const WebSocket = require("ws");
class NodeHTTP extends types_1.DeepstreamPlugin {
    constructor(pluginOptions, services, config) {
        super();
        this.pluginOptions = pluginOptions;
        this.services = services;
        this.description = 'NodeJS HTTP Service';
        this.isReady = 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(', ');
        this.postPaths = new Map();
        this.getPaths = new Map();
        this.upgradePaths = new Map();
        this.sortedPostPaths = [];
        this.sortedGetPaths = [];
        this.sortedUpgradePaths = [];
        this.connections = new Map();
        this.emitter = new events_1.EventEmitter();
        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');
            }
        }
        this.jsonBodyParser = bodyParser.json({
            inflate: true,
            limit: `${pluginOptions.maxMessageSize / 1024}mb`
        });
    }
    async whenReady() {
        if (this.isReady) {
            return;
        }
        if (!this.server) {
            const server = this.createHttpServer();
            this.server = httpShutdown(server);
            this.server.on('request', this.onRequest.bind(this));
            this.server.on('upgrade', this.onUpgrade.bind(this));
            this.server.listen(this.pluginOptions.port, this.pluginOptions.host, () => {
                const serverAddress = this.server.address();
                const address = serverAddress.address;
                const port = serverAddress.port;
                this.services.logger.info(types_1.EVENT.INFO, `Listening for http connections on ${address}:${port}`);
                this.services.logger.info(types_1.EVENT.INFO, `Listening for health checks on path ${this.pluginOptions.healthCheckPath}`);
                this.registerGetPathPrefix(this.pluginOptions.healthCheckPath, (meta, response) => {
                    response(null);
                });
                this.isReady = true;
                this.emitter.emit('ready');
            });
        }
        return new Promise((resolve) => this.emitter.once('ready', resolve));
    }
    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();
        // @ts-ignore
        return new Promise((resolve) => this.server.shutdown(resolve));
    }
    sendWebsocketMessage(socket, message, isBinary) {
        socket.send(message);
    }
    getSocketWrappersForUserId(userId) {
        return [...this.connections.values()].filter((socketWrapper) => socketWrapper.userId === userId);
    }
    registerPostPathPrefix(prefix, handler) {
        this.postPaths.set(prefix, handler);
        this.sortedPostPaths = [...this.postPaths.keys()].sort().reverse();
    }
    registerGetPathPrefix(prefix, handler) {
        this.getPaths.set(prefix, handler);
        this.sortedGetPaths = [...this.getPaths.keys()].sort().reverse();
    }
    registerWebsocketEndpoint(path, createSocketWrapper, webSocketConnectionEndpoint) {
        const server = new WebSocket.Server({ noServer: true, maxPayload: webSocketConnectionEndpoint.wsOptions.maxMessageSize });
        server.on('connection', (websocket, handshakeData) => {
            websocket.on('error', (error) => {
                this.services.logger.error(types_1.EVENT.ERROR, `Error on websocket: ${error.message}`);
            });
            const socketWrapper = createSocketWrapper(websocket, handshakeData, this.services, webSocketConnectionEndpoint.wsOptions, webSocketConnectionEndpoint);
            this.connections.set(websocket, socketWrapper);
            websocket.on('close', () => {
                webSocketConnectionEndpoint.onSocketClose.call(webSocketConnectionEndpoint, socketWrapper);
                this.connections.delete(websocket);
            });
            websocket.on('message', (msg) => {
                const messages = socketWrapper.parseMessage(msg);
                if (messages.length > 0) {
                    socketWrapper.onMessage(messages);
                }
            });
            webSocketConnectionEndpoint.onConnection.call(webSocketConnectionEndpoint, socketWrapper);
        });
        this.upgradePaths.set(path, server);
        this.sortedUpgradePaths = [...this.upgradePaths.keys()].sort().reverse();
    }
    createHttpServer() {
        if (this.pluginOptions.ssl) {
            const { key, cert, ca } = this.pluginOptions.ssl;
            if (!key || !cert) {
                this.services.logger.fatal(types_1.EVENT.PLUGIN_INITIALIZATION_ERROR, 'To enable HTTP please provide a key and cert');
            }
            return new https.Server({ key, cert, ca });
        }
        return new http.Server();
    }
    onUpgrade(request, socket, head) {
        for (const path of this.sortedUpgradePaths) {
            if (request.url === path) {
                const wss = this.upgradePaths.get(path);
                wss.handleUpgrade(request, socket, head, (ws) => {
                    wss.emit('connection', ws, {
                        remoteAddress: request.headers['x-forwarded-for'] || request.connection.remoteAddress,
                        headers: request.headers,
                        referer: request.headers.referer
                    });
                });
                return;
            }
        }
        socket.destroy();
    }
    onRequest(request, response) {
        if (!this.pluginOptions.allowAllOrigins) {
            if (!this.verifyOrigin(request, response)) {
                return;
            }
        }
        else {
            response.setHeader('Access-Control-Allow-Origin', '*');
        }
        switch (request.method) {
            case 'POST':
                this.handlePost(request, response);
                break;
            case 'GET':
                this.handleGet(request, response);
                break;
            case 'OPTIONS':
                this.handleOptions(request, response);
                break;
            default:
                this.terminateResponse(response, HTTPStatus.METHOD_NOT_ALLOWED, `Unsupported method. Supported methods: ${this.methodsStr}`);
        }
    }
    handlePost(request, response) {
        let parsedContentType;
        try {
            parsedContentType = contentType.parse(request);
        }
        catch (typeError) {
            parsedContentType = { type: null };
        }
        if (parsedContentType.type !== 'application/json') {
            this.terminateResponse(response, HTTPStatus.UNSUPPORTED_MEDIA_TYPE, 'Invalid "Content-Type" header. Supported media types: "application/json"');
            return;
        }
        this.jsonBodyParser(request, response, (err) => {
            if (err) {
                this.terminateResponse(response, HTTPStatus.BAD_REQUEST, `Failed to parse body of request: ${err.message}`);
                return;
            }
            for (const path of this.sortedPostPaths) {
                if (request.url.startsWith(path)) {
                    this.postPaths.get(path)(request.body, { headers: request.headers, url: request.url }, this.sendResponse.bind(this, response));
                    return;
                }
            }
            this.terminateResponse(response, HTTPStatus.NOT_FOUND, 'Endpoint not found.');
        });
    }
    handleGet(request, response) {
        for (const path of this.sortedGetPaths) {
            if (request.url.startsWith(path)) {
                this.getPaths.get(path)({ headers: request.headers, url: request.url }, this.sendResponse.bind(this, response));
                return;
            }
        }
        this.terminateResponse(response, HTTPStatus.NOT_FOUND, 'Endpoint not found.');
    }
    handleOptions(request, response) {
        const requestMethod = request.headers['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.headers['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.setHeader('Access-Control-Allow-Methods', this.methodsStr);
        response.setHeader('Access-Control-Allow-Headers', this.headersStr);
        this.terminateResponse(response, HTTPStatus.NO_CONTENT);
    }
    verifyOrigin(request, response) {
        const requestOriginUrl = request.headers.origin || request.headers.referer;
        const requestHostUrl = request.headers.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.setHeader('Access-Control-Allow-Origin', requestOriginUrl);
        response.setHeader('Access-Control-Allow-Credentials', 'true');
        response.setHeader('Vary', 'Origin');
        return true;
    }
    terminateResponse(response, code, message) {
        response.setHeader('Content-Type', 'text/plain; charset=utf-8');
        response.writeHead(code);
        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.setHeader('Content-Type', 'application/json; charset=utf-8');
        response.writeHead(HTTPStatus.OK);
        if (data) {
            response.end(`${JSON.stringify(data)}\r\n\r\n`);
        }
        else {
            response.end();
        }
    }
}
exports.NodeHTTP = NodeHTTP;
//# sourceMappingURL=node-http.js.map