"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../../constants");
const types_1 = require("@deepstream/types");
const OPEN = 'OPEN';
/**
 * This is the frontmost class of deepstream's message pipeline. It receives
 * connections and authentication requests, authenticates sockets and
 * forwards messages it receives from authenticated sockets.
 */
class BaseWebsocketConnectionEndpoint extends types_1.DeepstreamPlugin {
    constructor(options, services, dsOptions) {
        super();
        this.options = options;
        this.services = services;
        this.dsOptions = dsOptions;
        this.description = 'WebSocket Connection Endpoint';
        this.initialized = false;
        this.flushTimeout = null;
        this.authenticatedSocketWrappers = new Set();
        this.scheduledSocketWrapperWrites = new Set();
        this.logInvalidAuthData = false;
        this.maxAuthAttempts = 3;
        this.unauthenticatedClientTimeout = false;
        this.flushSockets = this.flushSockets.bind(this);
    }
    async whenReady() {
        await this.services.httpService.whenReady();
    }
    createWebsocketServer() {
    }
    closeWebsocketServer() {
    }
    onSocketWrapperClosed(socketWrapper) {
        socketWrapper.close();
    }
    setConnectionListener(connectionListener) {
        this.connectionListener = connectionListener;
    }
    /**
     * Called for every message that's received
     * from an authenticated socket
     *
     * This method will be overridden by an external class and is used instead
     * of an event emitter to improve the performance of the messaging pipeline
     */
    onMessages(socketWrapper, messages) {
    }
    /**
     * initialize and setup the http and WebSocket servers.
     */
    init() {
        if (this.initialized) {
            throw new Error('init() must only be called once');
        }
        this.initialized = true;
        this.maxAuthAttempts = this.options.maxAuthAttempts;
        this.logInvalidAuthData = this.options.logInvalidAuthData;
        this.unauthenticatedClientTimeout = this.options.unauthenticatedClientTimeout;
        this.createWebsocketServer();
    }
    /**
     * Called from a socketWrapper. This method tells the connection endpoint
     * to flush the socket after a certain amount of time, used to low priority
     * messages
     */
    scheduleFlush(socketWrapper) {
        this.scheduledSocketWrapperWrites.add(socketWrapper);
        if (!this.flushTimeout) {
            this.flushTimeout = setTimeout(this.flushSockets, this.options.outgoingBufferTimeout);
        }
    }
    /**
     * Called when the flushTimeout occurs in order to send  all pending socket acks
     */
    flushSockets() {
        for (const socketWrapper of this.scheduledSocketWrapperWrites) {
            socketWrapper.flush();
        }
        this.scheduledSocketWrapperWrites.clear();
        this.flushTimeout = null;
    }
    getOption(option) {
        return this.options[option];
    }
    handleParseErrors(socketWrapper, parseResults) {
        const messages = [];
        for (const parseResult of parseResults) {
            if (parseResult.parseError) {
                this.services.logger.warn(constants_1.PARSER_ACTION[constants_1.PARSER_ACTION.MESSAGE_PARSE_ERROR], 'error parsing connection message');
                socketWrapper.sendMessage({
                    topic: constants_1.TOPIC.PARSER,
                    action: parseResult.action,
                    data: parseResult.raw,
                    originalTopic: parseResult.parsedMessage.topic,
                    originalAction: parseResult.parsedMessage.action
                }, false);
                socketWrapper.destroy();
                continue;
            }
            const message = parseResult;
            if (message.topic === constants_1.TOPIC.CONNECTION &&
                message.action === constants_1.CONNECTION_ACTION.PONG) {
                continue;
            }
            messages.push(message);
        }
        return messages;
    }
    /**
     * Receives a connected socket, wraps it in a SocketWrapper, sends a connection ack to the user
     * and subscribes to authentication messages.
     */
    onConnection(socketWrapper) {
        const handshakeData = socketWrapper.getHandshakeData();
        this.services.logger.info(types_1.EVENT.INCOMING_CONNECTION, `from ${handshakeData.referer} (${handshakeData.remoteAddress})`);
        let disconnectTimer;
        if (this.unauthenticatedClientTimeout !== null && this.unauthenticatedClientTimeout !== false) {
            const timeout = this.unauthenticatedClientTimeout;
            disconnectTimer = setTimeout(this.processConnectionTimeout.bind(this, socketWrapper), timeout);
            socketWrapper.onClose(clearTimeout.bind(null, disconnectTimer));
        }
        socketWrapper.authCallback = this.authenticateConnection.bind(this, socketWrapper, disconnectTimer);
        socketWrapper.onMessage = this.processConnectionMessage.bind(this, socketWrapper);
    }
    /**
     * Always challenges the client that connects. This will be opened up later to allow users
     * to put in their own challenge authentication.
     */
    processConnectionMessage(socketWrapper, parsedMessages) {
        const msg = parsedMessages[0];
        if (msg.topic !== constants_1.TOPIC.CONNECTION) {
            this.services.logger.warn(constants_1.CONNECTION_ACTION[constants_1.CONNECTION_ACTION.INVALID_MESSAGE], 'invalid connection message');
            socketWrapper.sendMessage({
                topic: constants_1.TOPIC.CONNECTION,
                action: constants_1.CONNECTION_ACTION.INVALID_MESSAGE,
                originalTopic: msg.topic,
                originalAction: msg.action
            }, false);
            return;
        }
        if (msg.action === constants_1.CONNECTION_ACTION.PING) {
            return;
        }
        if (msg.action === constants_1.CONNECTION_ACTION.CHALLENGE) {
            socketWrapper.onMessage = socketWrapper.authCallback;
            socketWrapper.sendMessage({
                topic: constants_1.TOPIC.CONNECTION,
                action: constants_1.CONNECTION_ACTION.ACCEPT
            }, false);
            return;
        }
        this.services.logger.error(constants_1.PARSER_ACTION[constants_1.PARSER_ACTION.UNKNOWN_ACTION], '', { message: msg });
    }
    /**
     * Callback for the first message that's received from the socket.
     * This is expected to be an auth-message. This method makes sure that's
     * the case and - if so - forwards it to the permission handler for authentication
     */
    authenticateConnection(socketWrapper, disconnectTimeout, parsedMessages) {
        const msg = parsedMessages[0];
        let errorMsg;
        if (msg.topic === constants_1.TOPIC.CONNECTION && msg.action === constants_1.CONNECTION_ACTION.PING) {
            return;
        }
        if (msg.topic !== constants_1.TOPIC.AUTH) {
            this.services.logger.warn(constants_1.AUTH_ACTION[constants_1.AUTH_ACTION.INVALID_MESSAGE], `invalid auth message: ${JSON.stringify(msg)}`);
            socketWrapper.sendMessage({
                topic: constants_1.TOPIC.AUTH,
                action: constants_1.AUTH_ACTION.INVALID_MESSAGE,
                originalTopic: msg.topic,
                originalAction: msg.action
            }, false);
            return;
        }
        /**
         * Log the authentication attempt
         */
        const logMsg = socketWrapper.getHandshakeData().remoteAddress;
        this.services.logger.debug(constants_1.AUTH_ACTION[constants_1.AUTH_ACTION.REQUEST], logMsg);
        /**
         * Ensure the message is a valid authentication message
         */
        if (msg.action !== constants_1.AUTH_ACTION.REQUEST) {
            errorMsg = this.logInvalidAuthData === true ? JSON.stringify(msg.parsedData) : '';
            this.sendInvalidAuthMsg(socketWrapper, errorMsg, msg.action);
            return;
        }
        /**
         * Ensure the authentication data is valid JSON
         */
        const result = socketWrapper.parseData(msg);
        if (result instanceof Error || !msg.parsedData || typeof msg.parsedData !== 'object') {
            errorMsg = 'Error parsing auth message';
            if (this.logInvalidAuthData === true) {
                errorMsg += ` "${msg.data}": ${result.toString()}`;
            }
            this.sendInvalidAuthMsg(socketWrapper, errorMsg, msg.action);
            return;
        }
        /**
         * Forward for authentication
         */
        this.services.authentication.isValidUser(socketWrapper.getHandshakeData(), msg.parsedData, this.processAuthResult.bind(this, msg.parsedData, socketWrapper, disconnectTimeout));
    }
    /**
     * Will be called for syntactically incorrect auth messages. Logs
     * the message, sends an error to the client and closes the socket
     */
    sendInvalidAuthMsg(socketWrapper, msg, originalAction) {
        this.services.logger.warn(constants_1.AUTH_ACTION[constants_1.AUTH_ACTION.INVALID_MESSAGE_DATA], this.logInvalidAuthData ? msg : '');
        socketWrapper.sendMessage({
            topic: constants_1.TOPIC.AUTH,
            action: constants_1.AUTH_ACTION.INVALID_MESSAGE_DATA,
            originalAction
        }, false);
        socketWrapper.destroy();
    }
    /**
     * Callback for succesfully validated sockets. Removes
     * all authentication specific logic and registeres the
     * socket with the authenticated sockets
     */
    registerAuthenticatedSocket(unauthenticatedSocketWrapper, userData) {
        const socketWrapper = this.appendDataToSocketWrapper(unauthenticatedSocketWrapper, userData);
        delete unauthenticatedSocketWrapper.authCallback;
        unauthenticatedSocketWrapper.onMessage = (parsedMessages) => {
            this.onMessages(socketWrapper, parsedMessages);
        };
        this.authenticatedSocketWrappers.add(socketWrapper);
        socketWrapper.sendMessage({
            topic: constants_1.TOPIC.AUTH,
            action: constants_1.AUTH_ACTION.AUTH_SUCCESSFUL,
            parsedData: userData.clientData
        });
        this.connectionListener.onClientConnected(socketWrapper);
        this.services.logger.info(constants_1.AUTH_ACTION[constants_1.AUTH_ACTION.AUTH_SUCCESSFUL], socketWrapper.userId);
    }
    /**
     * Append connection data to the socket wrapper
     */
    appendDataToSocketWrapper(socketWrapper, userData) {
        const authenticatedSocketWrapper = socketWrapper;
        authenticatedSocketWrapper.userId = userData.id || OPEN;
        authenticatedSocketWrapper.serverData = userData.serverData || null;
        authenticatedSocketWrapper.clientData = userData.clientData || null;
        return authenticatedSocketWrapper;
    }
    /**
     * Callback for invalid credentials. Will notify the client
     * of the invalid auth attempt. If the number of invalid attempts
     * exceed the threshold specified in options.maxAuthAttempts
     * the client will be notified and the socket destroyed.
     */
    processInvalidAuth(clientData, authData, socketWrapper) {
        let logMsg = 'invalid authentication data';
        if (this.logInvalidAuthData === true) {
            logMsg += `: ${JSON.stringify(authData)}`;
        }
        this.services.logger.info(constants_1.AUTH_ACTION[constants_1.AUTH_ACTION.AUTH_UNSUCCESSFUL], logMsg);
        socketWrapper.sendMessage({
            topic: constants_1.TOPIC.AUTH,
            action: constants_1.AUTH_ACTION.AUTH_UNSUCCESSFUL,
            parsedData: clientData
        }, false);
        socketWrapper.authAttempts++;
        if (socketWrapper.authAttempts >= this.maxAuthAttempts) {
            this.services.logger.info(constants_1.AUTH_ACTION[constants_1.AUTH_ACTION.TOO_MANY_AUTH_ATTEMPTS], 'too many authentication attempts');
            socketWrapper.sendMessage({
                topic: constants_1.TOPIC.AUTH,
                action: constants_1.AUTH_ACTION.TOO_MANY_AUTH_ATTEMPTS
            }, false);
            setTimeout(() => socketWrapper.destroy(), 10);
        }
    }
    /**
     * Callback for connections that have not authenticated succesfully within
     * the expected timeframe
     */
    processConnectionTimeout(socketWrapper) {
        const log = 'connection has not authenticated successfully in the expected time';
        this.services.logger.info(constants_1.CONNECTION_ACTION[constants_1.CONNECTION_ACTION.AUTHENTICATION_TIMEOUT], log);
        socketWrapper.sendMessage({
            topic: constants_1.TOPIC.CONNECTION,
            action: constants_1.CONNECTION_ACTION.AUTHENTICATION_TIMEOUT
        }, false);
        socketWrapper.destroy();
    }
    /**
     * Callback for the results returned by the permission service
     */
    processAuthResult(authData, socketWrapper, disconnectTimeout, isAllowed, userData) {
        this.services.monitoring.onLogin(isAllowed, 'websocket');
        userData = userData || {};
        if (disconnectTimeout) {
            clearTimeout(disconnectTimeout);
        }
        if (isAllowed === true) {
            this.registerAuthenticatedSocket(socketWrapper, userData);
        }
        else {
            this.processInvalidAuth(userData.clientData, authData, socketWrapper);
        }
    }
    /**
     * Notifies the (optional) onClientDisconnect method of the permission
     * that the specified client has disconnected
     */
    onSocketClose(socketWrapper) {
        this.scheduledSocketWrapperWrites.delete(socketWrapper);
        this.onSocketWrapperClosed(socketWrapper);
        if (this.authenticatedSocketWrappers.delete(socketWrapper)) {
            const authenticatedSocketWrapper = socketWrapper;
            if (this.services.authentication.onClientDisconnect) {
                this.services.authentication.onClientDisconnect(authenticatedSocketWrapper.userId);
            }
            this.connectionListener.onClientDisconnected(authenticatedSocketWrapper);
        }
    }
    /**
     * Closes the ws server connection. The ConnectionEndpoint
     * will emit a close event once succesfully shut down
     */
    async close() {
        await this.closeWebsocketServer();
    }
}
exports.default = BaseWebsocketConnectionEndpoint;
//# sourceMappingURL=connection-endpoint.js.map