"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const decorators_1 = require("../decorators");
const Logger_1 = require("../shared/Logger");
const Server_1 = require("../Server");
const errors = require("../errors");
const Token_1 = require("../shared/Token");
const util_1 = require("../shared/util");
const http = require("http");
const httpProxy = require("http-proxy");
let SyncProxyService = class SyncProxyService {
    constructor() {
        this.logger = new Logger_1.MuteLogger();
        this.backendPromises = {};
        this.nextSocketID = 0;
    }
    start(server) {
        return __awaiter(this, void 0, void 0, function* () {
            this.server = server;
            this.activeConnectionsGauge = this.stats.gauge({
                name: "ros_sync_proxy_active_connections",
                help: "Number of current active sync proxy connections",
                labelNames: ["identity"],
            });
            this.connectionDurationHistogram = this.stats.histogram({
                name: "ros_sync_proxy_connection_durations",
                help: "Duration of sync proxy connections",
                labelNames: ["identity"],
            });
        });
    }
    stop() {
        return __awaiter(this, void 0, void 0, function* () {
            for (const label in this.backendPromises) {
                try {
                    const backend = yield util_1.timeout(this.backendPromises[label], 0);
                    this.closeBackend(backend);
                    delete this.backendPromises[label];
                }
                catch (err) {
                }
            }
        });
    }
    setLogger(logger) {
        this.logger = logger;
    }
    syncMasterChanged(backend, handle) {
        const labelTags = handle.tags.filter((t) => t.startsWith("label="));
        if (labelTags.length !== 1) {
            this.logger.error("master sync service should have exactly one label tag.");
            return;
        }
        const label = labelTags[0].slice("label=".length);
        const oldHandle = backend.handle;
        if (oldHandle) {
            if (oldHandle.address === handle.address && oldHandle.port === handle.port) {
                this.logger.debug("ignore repeated 'available' message from service watch");
                return;
            }
        }
        this.closeBackend(backend);
        backend.handle = handle;
        const target = `http://${handle.address}:${handle.port}`;
        this.logger.debug(`new target for ${label} is ${target}`);
        backend.sockets = {};
        backend.proxy = httpProxy.createProxy({ ws: true, target });
        backend.proxy.on('error', (err) => {
            this.logger.detail(`sync proxy error: ${err.message}`);
        });
        backend.proxy.on('close', () => {
            this.logger.debug("sync proxy closed");
        });
    }
    closeBackend(backend) {
        if (backend.proxy) {
            backend.proxy.close();
            delete backend.proxy;
        }
        if (backend.sockets) {
            for (const id in backend.sockets) {
                const socket = backend.sockets[id];
                socket.destroy();
                this.logger.debug(`destroyed connection from ${socket.remoteAddress}:${socket.remotePort}`);
            }
            backend.sockets = {};
        }
    }
    parseTokenAndGetLabel(req, socket, head) {
        return __awaiter(this, void 0, void 0, function* () {
            const authorization = req.headers[this.server["serverConfig"].authorizationHeaderName] || req.headers["authorization"];
            if (!authorization) {
                throw new errors.JSONError({
                    title: "Authorization header was not provided",
                    status: 400,
                });
            }
            const matches = authorization.match(/^Realm\-Access\-Token version=(\d+) token="(.*)"$/);
            if (!matches) {
                throw new errors.JSONError({
                    title: "Authorization header is not in a valid format",
                    status: 400,
                });
            }
            const tokenVersion = matches[1];
            const tokenData = matches[2];
            let token;
            try {
                token = Token_1.Token.parse(tokenData, this.server.publicKey, false);
            }
            catch (err) {
                throw new errors.realm.InvalidCredentials();
            }
            const pathParam = util_1.validateRealmPath(req.params.path, token.identity);
            if (!(token instanceof Token_1.AccessToken)) {
                throw new errors.realm.InvalidParameters(`The provided token (${token.data()}) is not an instanceof AccessToken.`);
            }
            const accessToken = token;
            if (accessToken.path !== pathParam && !accessToken.isAdminToken()) {
                throw new errors.realm.InvalidParameters(`The path in the provided token ${accessToken.path} is not the same as that in the URL (${pathParam}).`);
            }
            let label = accessToken.syncLabel;
            if (!label || label === "") {
                const shouldCreate = false;
                const response = yield util_1.timeout(this.server.realmDirectoryClient.findByPath({
                    realmPath: pathParam,
                    shouldCreate: shouldCreate,
                    token: tokenData
                }), 1000);
                if (!response || !response.exists || !response.syncLabel) {
                    throw new errors.JSONError({
                        title: "AccessToken is not valid: sync label was not found in token",
                        status: 400,
                    });
                }
                label = response.syncLabel;
            }
            return {
                label,
                identity: accessToken.identity,
                authorization
            };
        });
    }
    websocketHandler(req, socket, head) {
        return __awaiter(this, void 0, void 0, function* () {
            const socketID = this.nextSocketID++;
            this.logger.debug(`Proxy upgrade: [${socketID}] new connection from ${socket.remoteAddress}:${socket.remotePort}`);
            try {
                const { label, identity, authorization } = yield this.parseTokenAndGetLabel(req, socket, head);
                const backend = yield this.getBackend(label);
                if (!backend.proxy) {
                    throw new errors.realm.ServiceUnavailable();
                }
                socket.identity = identity;
                const end = this.connectionDurationHistogram.startTimer({ identity });
                backend.sockets[socketID] = socket;
                this.activeConnectionsGauge.inc({ identity });
                this.logger.debug(`The Realm Object Server proxy has ${Object.keys(backend.sockets).length} open connections to the sync server`);
                socket.on('close', () => {
                    this.logger.debug(`[${socketID}] closed connection from ${socket.remoteAddress}:${socket.remotePort}`);
                    if (backend.sockets) {
                        delete backend.sockets[socketID];
                    }
                    this.activeConnectionsGauge.dec({ identity });
                    this.logger.debug(`The Realm Object Server proxy has ${Object.keys(backend.sockets).length} open connections to the sync server`);
                    end();
                });
                delete req.headers[this.server["serverConfig"].authorizationHeaderName];
                req.headers["authorization"] = authorization;
                backend.proxy.ws(req, socket, head);
            }
            catch (error) {
                if (error instanceof util_1.TimeoutError) {
                    throw new errors.realm.ServiceUnavailable();
                }
                throw error;
            }
        });
    }
    getBackend(syncLabel) {
        if (!this.backendPromises[syncLabel]) {
            const labelTag = `label=${syncLabel}`;
            this.backendPromises[syncLabel] = util_1.timeout(this.server.discovery.waitForService("sync", ["role=master", labelTag]), 1000)
                .then((handle) => {
                const backend = {};
                this.syncMasterChanged(backend, handle);
                const watch = this.server.discovery.watchService("sync", ["role=master", labelTag]);
                watch.on('available', (handle) => this.syncMasterChanged(backend, handle));
                watch.on('unavailable', (handle) => {
                    this.logger.debug("ignore 'unavailable' message from service watch");
                });
                return backend;
            });
        }
        return this.backendPromises[syncLabel];
    }
};
__decorate([
    decorators_1.Start(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Server_1.Server]),
    __metadata("design:returntype", Promise)
], SyncProxyService.prototype, "start", null);
__decorate([
    decorators_1.Stop(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", Promise)
], SyncProxyService.prototype, "stop", null);
__decorate([
    decorators_1.Unmute(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Logger_1.Logger]),
    __metadata("design:returntype", void 0)
], SyncProxyService.prototype, "setLogger", null);
__decorate([
    decorators_1.Upgrade('/:path'),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [http.IncomingMessage, Object, Buffer]),
    __metadata("design:returntype", Promise)
], SyncProxyService.prototype, "websocketHandler", null);
SyncProxyService = __decorate([
    decorators_1.BaseRoute('/realm-sync')
], SyncProxyService);
exports.SyncProxyService = SyncProxyService;
//# sourceMappingURL=SyncProxyService.js.map