realm-object-server
Version:
921 lines • 42.6 kB
JavaScript
"use strict";
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 discovery_1 = require("./discovery");
const realmUtil_1 = require("./shared/realmUtil");
const uuid = require("uuid");
const _ = require("lodash");
const Logger_1 = require("./shared/Logger");
const Token_1 = require("./shared/Token");
const errors = require("./errors");
const RealmFactory_1 = require("./RealmFactory");
const stats_1 = require("./stats");
const pathMatcher_1 = require("./shared/pathMatcher");
const shared_1 = require("./shared");
const Service_1 = require("./Service");
const Express = require("express");
const http = require("http");
const https = require("https");
const bodyParser = require("body-parser");
const morgan = require("morgan");
const cors = require("cors");
const fs = require("fs-extra");
const path = require("path");
const URI = require("urijs");
const NodeRSA = require("node-rsa");
const semver = require("semver");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const httpUtil = require("./shared/httpUtil");
const openFiles = require("./shared/openFiles");
const AuthClient_1 = require("./service-clients/AuthClient");
const PermissionsClient_1 = require("./service-clients/PermissionsClient");
const RealmDirectoryClient_1 = require("./service-clients/RealmDirectoryClient");
const HealthClient_1 = require("./service-clients/HealthClient");
const RealmType_1 = require("./realms/RealmType");
const RealmHoover_1 = require("./shared/RealmHoover");
const SyncClient_1 = require("./service-clients/SyncClient");
class ServerValidationError extends Error {
}
exports.ServerValidationError = ServerValidationError;
class ServerStartError extends Error {
}
exports.ServerStartError = ServerStartError;
class Server {
constructor() {
this.services = [];
this.version = require("../package.json").version;
this.numberOfOpenFiles = 0;
}
get started() {
return this._started;
}
get dataPath() {
return this.serverConfig.dataPath;
}
get discovery() {
return this.serverConfig.discovery;
}
get logger() {
return this.serverConfig ? this.serverConfig.logger : null;
}
get publicKeyPath() {
return this.serverConfig.publicKeyPath;
}
get privateKeyPath() {
return this.serverConfig.privateKeyPath;
}
start(config) {
return __awaiter(this, void 0, void 0, function* () {
realmUtil_1.Realm.Sync.setUserAgent(`ROS/${this.version}`);
realmUtil_1.Realm.Sync.enableSessionMultiplexing();
let mixpanelId;
let company;
const featureToken = config.featureToken || process.env.SYNC_WORKER_FEATURE_TOKEN;
if (featureToken) {
try {
const decoded = jwt.decode(featureToken);
if (decoded && typeof decoded !== "string" && decoded.jti) {
mixpanelId = decoded.jti;
company = decoded.company;
}
}
catch (_a) { }
if (!mixpanelId) {
mixpanelId = crypto.createHash("md5").update(featureToken).digest("hex");
}
}
shared_1.mixpanel.identify(mixpanelId, company || "Unknown Company");
if (this.serverConfig !== undefined) {
throw new Error("The server has already been started.");
}
const validatedConfig = yield this.validateStartConfig(config);
this.serverConfig = validatedConfig.config;
this.privateKey = validatedConfig.privateKey;
this.statsSink = this.serverConfig.statsSink;
this.statsStorage = this.serverConfig.statsStorage;
realmUtil_1.Realm.Sync.setLogger((level, message) => {
const syncLogLevel = Logger_1.SyncLogLevel[level];
const mapping = ["all", "trace", "debug", "detail", "detail", "detail", "detail", "detail", "detail"];
const realLevel = mapping[level];
if (this.serverConfig) {
this.logger[realLevel](`sync-client: ${message}`, { level: syncLogLevel });
}
});
this.checkNodeVersion();
this.logger.info(`Realm Object Server version ${this.version} is starting`);
this.publicKey = yield fs.readFile(this.serverConfig.publicKeyPath, "utf-8");
this.adminToken = Token_1.generateAdminToken({
privateKey: this.privateKey,
canSkipRevocationCheck: true,
});
const serviceAgent = this.serverConfig.httpsAgentForInternalComponents;
this.authClient = new AuthClient_1.AuthClient(this.discovery, this.adminToken, this.serverConfig.authorizationHeaderName, serviceAgent);
this.permissionsClient = new PermissionsClient_1.PermissionsClient(this.discovery, this.adminToken, this.serverConfig.authorizationHeaderName, serviceAgent);
this.realmDirectoryClient = new RealmDirectoryClient_1.RealmDirectoryClient(this.discovery, this.adminToken, this.serverConfig.authorizationHeaderName, serviceAgent);
this.healthClient = new HealthClient_1.HealthClient(this.discovery, this.adminToken, this.serverConfig.authorizationHeaderName, serviceAgent);
const realmFactorySSLSettings = {
agent: this.serverConfig.httpsAgentForInternalComponents,
httpsCertificatePath: this.serverConfig.httpsCertChainPath
};
this.realmFactory = new RealmFactory_1.RealmFactory({
discovery: this.discovery,
adminToken: this.adminToken,
dataPath: this.dataPath,
logger: this.logger,
realmDirectoryClient: this.realmDirectoryClient,
ssl: this.serverConfig.httpsAgentForInternalComponents ? realmFactorySSLSettings : null,
realmsEncryptionKey: this.serverConfig.realmsEncryptionKey,
});
if (this.serverConfig.vacuumIntervalInSeconds > 0) {
this.realmHoover = new RealmHoover_1.RealmHoover({
logger: this.logger,
realmFactory: this.realmFactory,
intervalSeconds: this.serverConfig.vacuumIntervalInSeconds,
getSyncClient: (tags) => new SyncClient_1.SyncClient(this.discovery, this.adminToken, this.serverConfig.authorizationHeaderName, this.serverConfig.serviceAgent, tags),
});
}
this.tokenValidator = new shared_1.TokenValidator({
logger: this.logger,
publicKey: this.publicKey,
realmFactory: this.realmFactory,
disableRevocation: this.serverConfig.disableTokenRevocation,
refreshTokenValidators: this.serverConfig.refreshTokenValidators,
});
this.injectLogger();
this.expressApp = Express();
this.expressApp.disable("x-powered-by");
this.expressApp.use((req, res, next) => {
req["id"] = uuid.v4();
next();
});
this.expressApp.use(bodyParser.json({
limit: this.serverConfig.jsonBodyLimit || "100kb"
}));
this.expressApp.use((_, res, next) => {
res.header("Server", httpUtil.makeResponseHeaderServer(this.version));
next();
});
this.expressApp.use((req, res, next) => {
if (Logger_1.SyncLogLevel[this.serverConfig.logLevel] <= Logger_1.SyncLogLevel.trace) {
const msg = "\nRequest: " + req["id"] + "\n" + httpUtil.stringifyRequest(req);
this.httpAccessLogger.trace(msg);
}
next();
});
if (this.serverConfig.middlewares) {
this.expressApp.use(this.serverConfig.middlewares);
}
this.httpBytesSentCounter = this.statsSink.counter({
name: "ros_http_bytes_sent",
help: "Realm Object Server Sent HTTP size counter in bytes",
labelNames: [],
});
this.httpBytesReceivedCounter = this.statsSink.counter({
name: "ros_http_bytes_received",
help: "Realm Object Server Received HTTP size counter in bytes",
labelNames: [],
});
this.httpServer = http.createServer(this.expressApp);
this.httpServer.on("upgrade", this.upgradeHandler.bind(this));
this.httpServer.on("connection", this.connectionHandler.bind(this));
if (this.serverConfig.https) {
const key = yield fs.readFile(this.serverConfig.httpsKeyPath);
try {
new NodeRSA(key);
}
catch (err) {
throw new ServerStartError("Could not create HTTPS server: Private key is malformed.");
}
const cert = yield fs.readFile(this.serverConfig.httpsCertChainPath, "utf-8");
const regex = /-+BEGIN CERTIFICATE-+([\s\S]+?)-+END CERTIFICATE-+/;
if (!regex.test(cert)) {
throw new ServerStartError("Could not create HTTPS server: Certificate is malformed.");
}
try {
this.httpsServer = https.createServer({ key, cert }, this.expressApp);
}
catch (err) {
throw new ServerStartError(`Could not create HTTPS server: ${err.stack}`);
}
this.httpsServer.on("upgrade", this.upgradeHandler.bind(this));
this.httpsServer.on("connection", this.connectionHandler.bind(this));
}
this.httpAccessLogger = this.logger.withContext({ service: "http" });
const formatFunction = morgan.compile(":remote-addr :remote-user :method :url HTTP/:http-version :status :res[content-length] - :response-time ms");
const generateLog = (tokens, req, res) => {
let result = formatFunction(tokens, req, res);
try {
const enhancement = this.services.map(s => {
return s.enhanceLogFunctionName && s[s.enhanceLogFunctionName]((token) => tokens[token](req, res), req.body);
}).filter(l => l).join(" ");
if (enhancement) {
result += ` ${enhancement}`;
}
}
catch (err) {
this.logger.error("Error while logging request metadata", err);
}
return result;
};
this.expressApp.use(morgan(generateLog, {
skip: (req, res) => res.statusCode >= 400,
stream: {
write: (msg) => {
this.httpAccessLogger.debug(msg.trim());
}
}
}));
this.expressApp.use(morgan(generateLog, {
skip: (req, res) => res.statusCode < 400,
stream: {
write: (msg) => {
this.httpAccessLogger.detail(msg.trim());
}
}
}));
this.expressApp.use(this.authenticationMiddleware.bind(this));
this.expressApp.set("view engine", "html");
this.expressApp.engine("html", require("hbs").__express);
yield shared_1.Promisify(this.httpServer.listen.bind(this.httpServer), this.serverConfig.port, this.serverConfig.address);
if (this.httpsServer) {
yield shared_1.Promisify(this.httpsServer.listen.bind(this.httpsServer), this.serverConfig.httpsPort, this.serverConfig.httpsAddress);
}
yield this.startServices();
this.logger.detail("All services have started");
yield this.tokenValidator.start().catch((err) => {
this.logger.error(`Token Validator failed to start: ${err}`);
});
this.expressApp.use(this.errorHandler.bind(this));
this._started = true;
const address = this.httpServer.address();
for (const service of this.services) {
const serverStartedFunctionName = service.serverStartedFunctionName;
if (serverStartedFunctionName) {
yield Promise.resolve(service[serverStartedFunctionName](this));
}
}
const listeningOn = [`http://${address.address}:${address.port}`];
if (this.httpsServer) {
const httpsAddress = this.httpsServer.address();
listeningOn.push(`https://${httpsAddress.address}:${httpsAddress.port}`);
}
this.periodicLoggingOfOpenFiles();
this.logger.info(`Realm Object Server has started and is listening on ${listeningOn.join(" and ")}`);
});
}
get address() {
if (this.httpServer) {
const address = this.httpServer.address();
return `${address.address}:${address.port}`;
}
return undefined;
}
get secureAddress() {
if (this.httpsServer) {
const address = this.httpsServer.address();
return `${address.address}:${address.port}`;
}
return undefined;
}
shutdown() {
return __awaiter(this, void 0, void 0, function* () {
if (this.realmHoover) {
yield this.realmHoover.stop();
delete this.realmHoover;
}
yield Promise.all(this.services
.filter(service => service.stoppingFunctionName)
.map(service => shared_1.timeout(Promise.resolve(service[service.stoppingFunctionName]()), 30000).catch()));
if (this.tokenValidator) {
this.tokenValidator.stop();
}
yield Promise.all(this.services.map((service) => __awaiter(this, void 0, void 0, function* () {
service.state = Service_1.ServiceState.Stopping;
const serviceName = service.constructor.serviceName;
if (serviceName) {
this.logger.detail(`Stopping service ${serviceName}`);
}
yield this.discovery.deregisterService(service);
if (service.stopFunctionName) {
yield Promise.resolve(service[service.stopFunctionName]());
}
service.state = Service_1.ServiceState.Stopped;
if (serviceName) {
this.logger.detail(`Stopped service ${serviceName}`);
}
})));
this.services = [];
if (this.httpServer) {
yield shared_1.Promisify(this.httpServer.close.bind(this.httpServer)).catch((err) => { });
delete this.httpServer;
}
if (this.httpsServer) {
yield shared_1.Promisify(this.httpsServer.close.bind(this.httpsServer)).catch((err) => { });
delete this.httpsServer;
}
if (this.realmFactory) {
yield this.realmFactory.close();
delete this.realmFactory;
}
if (this.logger) {
this.logger.info("Realm Object Server has stopped");
}
delete this.tokenValidator;
delete this.serverConfig;
delete this.expressApp;
this._started = false;
});
}
addServices(...services) {
services.forEach(s => this.addService(s));
return this;
}
addService(service, config) {
if (this.started) {
throw new Error("Services can only be added before starting the server.");
}
let instance;
if (typeof (service) === "string") {
const Services = require("./services");
instance = new Services[service](config);
}
else {
instance = service;
}
this.services.push(instance);
return this;
}
removeService(service) {
if (this.started) {
throw new Error("Services can only be removed before starting the server.");
}
if (typeof service === "string") {
const survivingServices = [];
for (const instance of this.services) {
if (instance.constructor.serviceName !== service) {
survivingServices.push(service);
}
}
this.services = survivingServices;
}
else {
const index = this.services.indexOf(service);
if (index > -1) {
this.services.splice(index, 1);
}
}
return this;
}
getService(name) {
return this.services.find((s) => s.constructor.serviceName === name);
}
upgradeHandler(req, socket, head) {
return __awaiter(this, void 0, void 0, function* () {
const requestPath = URI.parse(req.url).path;
req["id"] = uuid.v4();
if (Logger_1.SyncLogLevel[this.serverConfig.logLevel] <= Logger_1.SyncLogLevel.trace) {
const msg = "\nRequest: " + req["id"] + "\n" + httpUtil.stringifyRequest(req);
this.httpAccessLogger.trace(msg);
}
let numberOfMatches = 0;
for (const service of this.services) {
const baseRoute = service.constructor.baseRoute;
if (!baseRoute) {
continue;
}
for (const upgradeRoute of (service.upgradeRoutes || [])) {
let concatenatedServicePath = baseRoute;
if (upgradeRoute.path !== "/") {
concatenatedServicePath = URI.joinPaths(baseRoute, upgradeRoute.path).toString();
}
const params = pathMatcher_1.pathMatcher(concatenatedServicePath, requestPath);
if (params === false) {
continue;
}
req.params = params;
numberOfMatches = numberOfMatches + 1;
let parameterArguments = service.parameterArguments || [];
parameterArguments = parameterArguments.filter(p => p.functionName === upgradeRoute.functionName);
const handleError = (err) => {
if (socket.destroyed) {
return;
}
const problem = this.processException(err);
const headers = {
"Server": httpUtil.makeResponseHeaderServer(this.version)
};
const response = httpUtil.makeResponse(problem.status, headers);
const msg = "HTTP upgrade failed (service did not respond properly) " +
JSON.stringify(err) + "\n" +
"Request: " + req["id"] + "\n" +
httpUtil.stringifyRequest(req) + "\nResponse:\n" + response;
this.httpAccessLogger.error(msg);
socket.write(response);
socket.end();
};
try {
if (parameterArguments.length > 0) {
throw new Error("Decorator Annotations for @Upgrade are not yet implemented");
}
if (service.state !== Service_1.ServiceState.Running) {
throw new errors.realm.ServiceUnavailable();
}
const promise = service[upgradeRoute.functionName](req, socket, head);
yield Promise.resolve(promise);
}
catch (err) {
handleError(err);
}
}
}
if (numberOfMatches === 0 && !socket.destroyed) {
const headers = {
"Server": httpUtil.makeResponseHeaderServer(this.version)
};
const response = httpUtil.makeResponse(404, headers);
const msg = "HTTP upgrade failed(no service found)\nRequest: " + req["id"] + "\n" +
httpUtil.stringifyRequest(req) + "\nResponse:\n" + response;
this.httpAccessLogger.error(msg);
socket.write(response);
socket.end();
}
});
}
connectionHandler(socket) {
let sent = 0;
let received = 0;
const flushStats = () => {
if (this.shouldIgnoreTrafficForSocket(socket)) {
clearInterval(timer);
}
else {
this.httpBytesReceivedCounter.inc({}, socket.bytesRead - received);
received = socket.bytesRead;
this.httpBytesSentCounter.inc({}, socket.bytesWritten - sent);
sent = socket.bytesWritten;
}
};
const timer = setInterval(flushStats, 30 * 1000);
socket.on("close", () => {
clearInterval(timer);
flushStats();
});
}
shouldIgnoreTrafficForSocket(socket) {
if (socket.ignoreHttpTraffic !== undefined) {
return socket.ignoreHttpTraffic;
}
return this.serverConfig && this.serverConfig.internalIdentities.indexOf(socket.identity) > -1;
}
injectLogger() {
for (const service of this.services) {
let functionName = service.unmuteFunctionName;
if (functionName && service[functionName]) {
service[functionName](this.logger);
}
functionName = service.unmuteContextualFunctionName;
if (functionName && service[functionName]) {
const serviceName = service.constructor.serviceName;
service[functionName](this.logger.withContext({ service: serviceName }));
}
}
}
addServiceEndpoints(service) {
const serviceConstructor = service.constructor;
const baseRoute = serviceConstructor.baseRoute;
const routes = service.routes || [];
const serviceStaticRoutes = service.serveStaticRoutes || [];
const corsPaths = serviceConstructor.corsPaths || {};
if (serviceStaticRoutes.length === 0 && routes.length === 0) {
return;
}
else if (!baseRoute) {
throw new Error(`${service.constructor.name} does not have a base route`);
}
const router = Express.Router();
const middlewares = serviceConstructor.middlewaresBeforeService;
if (middlewares) {
router.use(middlewares);
}
router.use((req, res, next) => {
if (service.state !== Service_1.ServiceState.Running) {
throw new errors.realm.ServiceUnavailable();
}
next();
});
for (const serviceStaticRoute of serviceStaticRoutes) {
router.use(serviceStaticRoute.path, Express.static(serviceStaticRoute.staticRoot));
}
for (const path in corsPaths) {
const options = corsPaths[path] || {};
router.use(path, cors(options));
}
for (const route of routes) {
const m = service.middlewaresBeforeFunction || [];
const middlewaresForThisFunction = m.filter(m => m.functionName === route.functionName);
const middlewares = [].concat.apply([], middlewaresForThisFunction.map(x => x.middlewares));
const handler = (req, res, next) => __awaiter(this, void 0, void 0, function* () {
const functionName = route.functionName;
let parameterArgumentMetaData = service.parameterArguments || [];
parameterArgumentMetaData = parameterArgumentMetaData.filter(x => x.functionName === functionName);
try {
let allowAnonymous = route.allowAnonymous;
if (allowAnonymous === undefined) {
allowAnonymous = serviceConstructor.allowAnonymous;
}
if (!allowAnonymous && !req.authToken) {
throw new errors.realm.InvalidCredentials({ title: "Missing authorization header" });
}
let result;
if (parameterArgumentMetaData.length > 0) {
const args = [];
for (const p of parameterArgumentMetaData) {
switch (p.type) {
case "body":
args[p.argumentIndex] = p.keyPath ? _.get(req.body, p.keyPath) : req.body;
break;
case "query":
args[p.argumentIndex] = p.keyPath ? _.get(req.query, p.keyPath) : req.query;
break;
case "headers":
args[p.argumentIndex] = p.keyPath ? req.headers[p.keyPath.toLowerCase()] : req.headers;
break;
case "params":
args[p.argumentIndex] = p.keyPath ? _.get(req.params, p.keyPath) : req.params;
break;
case "request":
args[p.argumentIndex] = req;
break;
case "response":
args[p.argumentIndex] = res;
break;
case "next":
args[p.argumentIndex] = next;
break;
}
}
result = yield Promise.resolve(service[functionName](...args));
if (result) {
if (result.toJSON) {
res.json(result.toJSON());
}
else {
res.json(result);
}
}
}
else {
result = yield Promise.resolve(service[functionName](req, res, next));
if (result) {
res.json(result);
}
}
if (Logger_1.SyncLogLevel[this.serverConfig.logLevel] <= Logger_1.SyncLogLevel.trace) {
const msg = "\nResponse: " + req["id"] + "\n" + httpUtil.stringifyResponse(res, req.baseUrl, result);
this.httpAccessLogger.trace(msg);
}
}
catch (err) {
const msg = "HTTP response: " + req["id"] + "\n" + JSON.stringify(err);
this.httpAccessLogger.error(msg);
next(err);
}
});
router[route.httpMethod](route.path, middlewares, handler);
}
this.expressApp.use(baseRoute, router);
}
startServices() {
return __awaiter(this, void 0, void 0, function* () {
yield Promise.all(this.services.map(this.startService.bind(this)));
});
}
startService(service) {
return __awaiter(this, void 0, void 0, function* () {
service.state = Service_1.ServiceState.Starting;
service.stats = this.statsSink;
service.statsStorage = this.statsStorage;
const serviceName = service.constructor.serviceName;
if (serviceName) {
this.logger.detail(`Starting service ${serviceName}`);
}
const startFunctionName = service.startFunctionName;
if (startFunctionName && service[startFunctionName]) {
yield Promise.resolve(service[startFunctionName](this));
}
this.addServiceEndpoints(service);
service.state = Service_1.ServiceState.Running;
let address;
const addressFunctionName = service.addressFunctionName;
if (addressFunctionName) {
address = service[addressFunctionName]();
}
else if (this.serverConfig.httpsForInternalComponents) {
address = this.httpsServer.address();
}
else {
address = this.httpServer.address();
}
yield this.discovery.registerService(service, address.address, address.port);
if (serviceName) {
this.logger.detail(`Started service ${serviceName}`);
}
});
}
authenticationMiddleware(req, res, next) {
try {
const authorization = req.headers[this.serverConfig.authorizationHeaderName] || req.headers["authorization"] || req.headers["x-realm-access-token"];
if (authorization) {
const tokenData = authorization;
req.authToken = this.tokenValidator.parse(tokenData);
req.socket.identity = req.authToken.identity;
}
next();
}
catch (err) {
if (!(err instanceof errors.realm.InvalidCredentials) &&
!(err instanceof errors.realm.AccessDenied)) {
err = new errors.realm.InvalidCredentials({
detail: err.message,
});
}
next(err);
}
}
processException(error) {
if (error instanceof errors.JSONError) {
this.logger.debug("Returning error response", Object.assign({}, error, { class: error.constructor.name }));
this.logger.debug(error.stack);
return error;
}
else {
const status = error.status || error.statusCode;
if (status) {
return new errors.JSONError({
status: status,
stack: error.stack,
title: error.message,
});
}
this.logger.error(`Internal server error: ${error.stack}`);
return new errors.JSONError({
status: 500,
stack: error.stack,
title: "Internal Server Error",
});
}
}
errorHandler(error, req, res, next) {
if (res.headersSent) {
next(error);
return;
}
const jsonError = this.processException(error);
res.setHeader("Content-Type", "application/json");
res.status(jsonError.status).json(jsonError.toJSON());
}
validateStartConfig(config) {
return __awaiter(this, void 0, void 0, function* () {
if (!config.discovery) {
config.discovery = new discovery_1.SingleProcessDiscovery();
}
if (!config.logger) {
config.logger = new Logger_1.ConsoleLogger(config.logLevel || "info");
}
if (!config.address) {
config.address = "0.0.0.0";
}
if (!config.httpsAddress) {
config.httpsAddress = config.address;
}
if (config.port === undefined) {
config.port = 9080;
}
if (config.httpsPort === undefined) {
config.httpsPort = 9443;
}
if (config.autoKeyGen === undefined) {
config.autoKeyGen = true;
}
if (config.writeAdminTokenToJson === undefined) {
config.writeAdminTokenToJson = true;
}
if (fs.existsSync(config.dataPath)) {
const stat = fs.statSync(config.dataPath);
if (!stat.isDirectory()) {
throw new ServerValidationError(`The data directory '${config.dataPath}' is not a directory`);
}
try {
fs.accessSync(config.dataPath, fs.constants.R_OK | fs.constants.W_OK | fs.constants.X_OK);
}
catch (e) {
throw new ServerValidationError(`The data directory '${config.dataPath}' does not have 'rwx' permissions`);
}
}
else {
try {
fs.mkdirSync(config.dataPath, 0o700);
}
catch (e) {
throw new ServerValidationError(`The data directory '${config.dataPath}' cannot be created.`);
}
}
if (!config.privateKeyPath) {
config.privateKeyPath = path.join(config.dataPath, "keys", "auth.key");
}
let privateKey;
try {
privateKey = yield fs.readFile(config.privateKeyPath, "utf-8");
}
catch (e) {
if (config.autoKeyGen) {
const key = new NodeRSA({ b: config.autoKeyGenBits || 2048 });
privateKey = key.exportKey("pkcs8-private-pem");
yield fs.mkdirp(path.dirname(config.privateKeyPath));
yield fs.writeFile(config.privateKeyPath, privateKey);
}
else {
throw new ServerValidationError(`The private key '${config.privateKeyPath}' does not exist or is not readable.`);
}
}
const adminJsonPath = path.join(config.dataPath, "keys", "admin.json");
if (config.writeAdminTokenToJson && !fs.pathExistsSync(adminJsonPath)) {
const adminToken = Token_1.generateAdminToken({
privateKey: privateKey,
});
const stringifiedJSON = JSON.stringify({
"ADMIN_TOKEN": adminToken
}, null, 4);
yield fs.mkdirp(path.dirname(adminJsonPath));
yield fs.writeFile(adminJsonPath, stringifiedJSON);
}
if (!config.publicKeyPath) {
config.publicKeyPath = path.join(config.dataPath, "keys", "auth.pub");
}
try {
fs.openSync(config.publicKeyPath, "r");
}
catch (e) {
if (config.autoKeyGen) {
const privateKeyData = yield fs.readFile(config.privateKeyPath);
const privateKey = new NodeRSA(privateKeyData);
const keyData = privateKey.exportKey("pkcs8-public-pem");
yield fs.mkdirp(path.dirname(config.publicKeyPath));
yield fs.writeFile(config.publicKeyPath, keyData);
}
else {
throw new ServerValidationError(`The public key '${config.publicKeyPath}' does not exist or is not readable.`);
}
}
if (config.https) {
if (!config.httpsKeyPath) {
throw new ServerValidationError("HTTPS was enabled but a path to the key was not provided.");
}
try {
fs.openSync(config.httpsKeyPath, "r");
}
catch (e) {
throw new ServerValidationError("The HTTPS key could not be read.");
}
if (!config.httpsCertChainPath) {
throw new ServerValidationError("HTTPS was enabled but a path to the certificate chain was not provided.");
}
try {
fs.openSync(config.httpsCertChainPath, "r");
}
catch (e) {
throw new ServerValidationError("The HTTPS certificate chain could not be read.");
}
}
if (config.httpsForInternalComponents && !config.https) {
throw new ServerValidationError("HTTPS communication between internal components was enabled, but server HTTPS was not.");
}
if ((config.statsSink && !config.statsStorage) || (config.statsStorage && !config.statsSink)) {
throw new ServerValidationError("You must supply statsSink and statsStorage together");
}
if (!config.statsSink && !config.statsStorage) {
config.statsSink = config.statsStorage = new stats_1.StandaloneStats();
}
if (!config.authorizationHeaderName) {
config.authorizationHeaderName = "authorization";
}
else {
config.authorizationHeaderName = config.authorizationHeaderName.toLowerCase();
}
if (!config.internalIdentities) {
config.internalIdentities = new Array();
}
if (config.serviceAgent && !config.httpsAgentForInternalComponents) {
config.httpsAgentForInternalComponents = config.serviceAgent;
}
if (config.httpsForInternalComponents && !config.httpsAgentForInternalComponents) {
config.httpsAgentForInternalComponents = new https.Agent();
}
if (config.vacuumIntervalInSeconds === undefined) {
config.vacuumIntervalInSeconds = -1;
}
if (config.refreshTokenValidators) {
const issuersSet = new Set();
for (const validator of config.refreshTokenValidators) {
if (validator.issuer === "realm") {
throw new ServerValidationError("'realm' is not a valid value for a CustomTokenValidatorConfig.issuer");
}
if (issuersSet.has(validator.issuer)) {
throw new ServerValidationError(`Duplicate issuer set in the 'refreshTokenValidators' collection: '${validator.issuer}'`);
}
issuersSet.add(validator.issuer);
}
}
return { config, privateKey };
});
}
periodicLoggingOfOpenFiles() {
const server = this;
setInterval(function () {
return __awaiter(this, void 0, void 0, function* () {
const summary = yield openFiles.getSummary();
if (summary.total !== server.numberOfOpenFiles) {
server.numberOfOpenFiles = summary.total;
if (server.logger) {
server.logger.debug(`Open files in Realm Object Server: ${JSON.stringify(summary)}`);
}
}
});
}, 10000);
}
openRealm(pathOrDefinition, schema) {
if (typeof pathOrDefinition === "string") {
return this.realmFactory.open({
remotePath: pathOrDefinition,
schema: schema
});
}
return this.realmFactory.open(pathOrDefinition);
}
applyPermissions(condition, realmPath, accessLevel) {
const accessLevels = ["none", "read", "write", "admin"];
if (!realmPath) {
throw new errors.realm.MissingParameters("realmPath");
}
if (!condition) {
throw new errors.realm.MissingParameters("condition");
}
if (accessLevels.indexOf(accessLevel) === -1) {
throw new errors.realm.InvalidParameters("accessLevel");
}
return this.permissionsClient.applyPermissions({
condition,
realmPath,
accessLevel
});
}
ensureRealmExists(realmPath, ownerId) {
return __awaiter(this, void 0, void 0, function* () {
if (!realmPath) {
throw new errors.realm.MissingParameters("realmPath");
}
if (ownerId) {
const homeFolder = new URI(realmPath).segment(0);
if (homeFolder !== ownerId && homeFolder !== "~") {
throw new errors.realm.InvalidParameters({
name: "realmPath",
reason: "realmPath must point to a Realm in the owner's home directory (/ownerId/some-realm)."
});
}
}
else if (realmPath.indexOf("~") !== -1) {
throw new errors.realm.InvalidParameters({
name: "realmPath",
reason: "realmPath must not contain ~ when ownerId is not specified"
});
}
const shouldCreate = true;
const realmType = RealmType_1.RealmType.full;
return this.realmDirectoryClient.findByPath({
realmPath: realmPath,
shouldCreate: shouldCreate,
realmType: realmType,
ownerId: ownerId
});
});
}
checkNodeVersion() {
const minNodeVersion = "8.9.0";
if (semver.lt(process.versions.node, minNodeVersion)) {
const logMessage = `Support for Node ${process.versions.node} has been deprecated. Please update at least to Node ${minNodeVersion}.`;
this.logger.warn(logMessage);
setTimeout(() => this.logger.warn(logMessage), 5000);
setTimeout(() => this.logger.warn(logMessage), 15000);
}
}
}
exports.Server = Server;
//# sourceMappingURL=Server.js.map