UNPKG

realm-object-server

Version:

Realm Object Server

322 lines 15.9 kB
"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 httpProxy = require("http-proxy"); const events_1 = require("events"); let SyncProxyService = class SyncProxyService extends events_1.EventEmitter { constructor() { super(...arguments); 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.connectionsCounter = this.stats.counter({ name: "ros_sync_proxy_connections_total", help: "Number of total connections received", labelNames: ["protocol"], }); this.activeConnectionsGauge = this.stats.gauge({ name: "ros_sync_proxy_active_connections", help: "Number of current active sync proxy connections", labelNames: ["identity", "protocol"], }); this.connectionDurationHistogram = this.stats.histogram({ name: "ros_sync_proxy_connection_durations", help: "Duration of sync proxy connections", labelNames: ["identity", "protocol"], }); this.backendConnectionErrorCounter = this.stats.gauge({ name: "ros_sync_proxy_backend_connection_errors", help: "A counter of sync proxy backend connection errors", labelNames: ["error", "syncLabel"], }); }); } stop() { return __awaiter(this, void 0, void 0, function* () { for (const label in this.backendPromises) { try { const backend = yield util_1.getValueIfResolved(this.backendPromises[label]); 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("Repeated 'available' message from service watch"); } } this.closeBackend(backend); backend.handle = handle; const serviceAgent = this.server["serverConfig"].httpsAgentForInternalComponents; const scheme = serviceAgent ? "https" : "http"; const target = `${scheme}://${handle.address}:${handle.port}`; this.logger.debug(`new target for ${label} is ${target}`); backend.sockets = {}; const proxyOptions = { ws: true, target }; if (serviceAgent) { proxyOptions.agent = serviceAgent; } backend.proxy = httpProxy.createProxy(proxyOptions); backend.proxy.on("proxyReqWs", (proxyReq, req, socket, options, head) => { proxyReq.on("socket", (proxySocket) => { socket.on("error", (error) => { this.logger.detail(req.logMessage(`Error on connection to sync server. src=${proxySocket.localAddress}:${proxySocket.localPort} dst=${proxySocket.remoteAddress}:${proxySocket.remotePort} : ${error.message}`)); }); socket.on("end", () => { this.logger.detail(req.logMessage("Connection to sync server closed.")); }); }); proxyReq.on("error", (error) => { this.logger.detail(req.logMessage(`Error on connection to sync server. ${error.message}`)); }); proxyReq.on("upgrade", (proxyRes, proxySocket, proxyHead) => { this.logger.detail(req.logMessage(`Connection to sync server established. src=${proxySocket.localAddress}:${proxySocket.localPort} dst=${proxySocket.remoteAddress}:${proxySocket.remotePort}`)); }); }); backend.proxy.on("error", (err, req, socket) => { if (err.message !== "read ECONNRESET") { this.logger.detail(req.logMessage(`Error on connection to sync server: ${err.message}`)); this.backendConnectionErrorCounter.inc({ syncLabel: backend.syncLabel, error: err.message, }); } }); } closeBackend(backend) { if (!backend) { return; } 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) { 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 tokenData = matches[2]; let token; try { token = this.server.tokenValidator.parse(tokenData, { ignoreExpiration: true }); } catch (err) { this.logger.detail(`[SyncProxy] Failed to parse token data: ${err.stack}`, err); throw new errors.realm.InvalidCredentials({ title: "Invalid credentials - failed to parse token data" }); } 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, path: pathParam, }; }); } websocketHandler(req, socket, head) { return __awaiter(this, void 0, void 0, function* () { req.socketID = this.nextSocketID++; req.logMessage = (message) => `[${req.socketID}]: ${message}`; this.logger.debug(req.logMessage(`New connection from client at ${socket.remoteAddress}:${socket.remotePort}`)); const protocol = String(this.enforceMinimumProtocolVersion(req)); try { const { label, identity, authorization, path } = yield this.parseTokenAndGetLabel(req); const backend = yield util_1.timeout(this.getBackend(label), 1000); if (!backend || !backend.proxy) { throw new errors.realm.ServiceUnavailable({ detail: `No backend proxy for '${label}'` }); } socket.identity = identity; const end = this.connectionDurationHistogram.startTimer({ identity, protocol }); this.connectionsCounter.inc({ protocol }); backend.sockets[req.socketID] = socket; this.activeConnectionsGauge.inc({ identity, protocol }); this.logger.debug(`The Realm Object Server proxy has ${Object.keys(backend.sockets).length} open connections to the '${backend.syncLabel}' sync server`); socket.on("error", (error) => { this.logger.detail(req.logMessage(`Error on connection to client: ${error.message}`), { syncLabel: backend.syncLabel, error }); this.backendConnectionErrorCounter.inc({ syncLabel: backend.syncLabel, error: error.message, }); }); const userAgent = req.headers["user-agent"]; socket.on("close", () => { this.logger.debug(req.logMessage(`Closed connection to client ${socket.remoteAddress}:${socket.remotePort}`)); if (backend.sockets) { delete backend.sockets[req.socketID]; } this.activeConnectionsGauge.dec({ identity, protocol }); this.logger.debug(`The Realm Object Server proxy has ${Object.keys(backend.sockets).length} open connections to the '${backend.syncLabel}' sync server`); end(); this.emit("socketDisconnected", { path, socketId: req.socketID, userAgent }); }); delete req.headers[this.server["serverConfig"].authorizationHeaderName]; req.headers["authorization"] = authorization; backend.proxy.ws(req, socket, head); this.emit("socketConnected", { path, socketId: req.socketID, userAgent }); } catch (error) { if (error instanceof util_1.TimeoutError) { throw new errors.realm.ServiceUnavailable({ detail: "Timeout occured upgrading websocket request" }); } throw error; } }); } getBackend(syncLabel) { if (!this.backendPromises[syncLabel]) { const labelTag = `label=${syncLabel}`; this.backendPromises[syncLabel] = this.server.discovery.waitForService("sync", ["role=master", labelTag]) .then((handle) => { const backend = { syncLabel }; 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; }) .catch((err) => { this.logger.error("An error occurred while trying to resolve a sync service", err); delete this.backendPromises[syncLabel]; return undefined; }); } return this.backendPromises[syncLabel]; } enforceMinimumProtocolVersion(req) { const protocolHeader = req.headers["sec-websocket-protocol"]; if (typeof protocolHeader !== "string") { throw new errors.realm.NotSupported({ detail: "No Sec-Websocket-Protocol header specified in request." }); } const matches = protocolHeader.match(/io\.realm\.sync\.(\d+)/); if (!matches) { throw new errors.realm.NotSupported({ detail: `Protocol '${protocolHeader}' not supported.` }); } const protocolVersion = Number(matches[1]); if (!this.server["serverConfig"].minimumSupportedSyncProtocolVersion) { return protocolVersion; } if (protocolVersion < this.server["serverConfig"].minimumSupportedSyncProtocolVersion) { throw new errors.realm.NotSupported({ detail: `Unsupported sync protocol version ${protocolVersion}` }); } return protocolVersion; } }; __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", [Object, Object, Buffer]), __metadata("design:returntype", Promise) ], SyncProxyService.prototype, "websocketHandler", null); SyncProxyService = __decorate([ decorators_1.BaseRoute("/realm-sync", { allowAnonymous: false }) ], SyncProxyService); exports.SyncProxyService = SyncProxyService; //# sourceMappingURL=SyncProxyService.js.map