realm-object-server
Version:
322 lines • 15.9 kB
JavaScript
;
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