webgme-engine
Version:
WebGME server and Client API without a GUI
1,100 lines (962 loc) • 46.6 kB
JavaScript
/*globals requireJS*/
/*eslint-env node*/
/**
* @module Server:StandAlone
* @author kecso / https://github.com/kecso
*/
'use strict';
const path = require('path');
const OS = require('os');
const Q = require('q');
const fs = require('fs');
const Express = require('express');
const compression = require('compression');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const methodOverride = require('method-override');
const multipart = require('connect-multiparty');
const cors = require('cors');
const Http = require('http');
const ejs = require('ejs');
const MongoAdapter = require('./storage/mongo');
const RedisAdapter = require('./storage/datastores/redisadapter');
const MemoryAdapter = require('./storage/memory');
const Storage = require('./storage/safestorage');
const WebSocket = require('./storage/websocket');
const WebhookManager = require('./webhooks/WebhookManager');
// Middleware
const BlobServer = require('./middleware/blob/BlobServer');
const ExecutorServer = require('./middleware/executor/ExecutorServer');
const TokenServer = require('./middleware/access-tokens/TokenServer');
const API = require('./api');
const Mailer = require('./middleware/mailer/mailer');
const getClientConfig = require('../../config/getclientconfig');
const GmeAuth = require('./middleware/auth/gmeauth');
const Logger = require('./logger');
const AADClient = require('./middleware/auth/WebgmeAADClient');
const AddOnEventPropagator = require('../addon/addoneventpropagator');
const webgmeUtils = require('../utils');
const servers = [];
const CONSTANTS = requireJS('common/Constants');
const isAbsUrlPath = new RegExp('^(?:[a-z]+:)?//', 'i');
// const jwt = require('jsonwebtoken');
let mainLogger;
const shutdown = () => {
let error;
let numStops = 0;
const serverOnStop = (server) => {
server.stop((err) => {
numStops -= 1;
if (err) {
error = true;
server.logger.error('Stopping server failed', {metadata: err});
} else {
server.logger.info('Server stopped.');
}
if (numStops === 0) {
if (error) {
process.exit(1);
} else {
process.exit(0);
}
}
});
};
servers.forEach(server => {
if (server.isRunning) {
numStops += 1;
}
});
if (numStops === 0) {
process.exit(0);
}
servers.forEach(server => {
if (server.isRunning) {
server.logger.info('Requesting server to stop ...');
serverOnStop(server);
}
});
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
class StandAloneServer {
constructor(gmeConfig) {
// config based includes
const WorkerManager = require(gmeConfig.server.workerManager.path);
// class field initializations
this.__gmeConfig = gmeConfig;
this.__clientConfig = getClientConfig(gmeConfig);
this.__isRunning = false;
this.__httpServer = null;
this.__app = new Express();
this.__gmeAuth = new GmeAuth(null, gmeConfig);
this.__database = null;
this.__serverUrl = '';
this.__routeComponents = [];
this.__tokenServer = null;
this.__executorServer = null;
this.__baseDir = requireJS.s.contexts._.config.baseUrl; // TODO: this is ugly
this.__clientBaseDir = path.resolve(gmeConfig.client.appDir);
this.__nmpPackageJson = webgmeUtils.getPackageJsonSync();
this.__excludeRegExs = [];
this.__sockets = {};
this.__middlewareOptions = {
gmeConfig: gmeConfig,
gmeAuth: this.__gmeAuth,
};
if (!mainLogger) {
mainLogger = Logger.createWithGmeConfig('gme', gmeConfig, true);
mainLogger.info('Node version', process.version);
gmeConfig.server.log.transports.forEach(transport => {
if (transport.transportType === 'File') {
mainLogger.info('Logfile [', transport.options.level, '] :',
path.resolve(transport.options.filename));
}
});
}
this.__logger = mainLogger.fork('server:standalone');
this.__middlewareOptions.logger = this.__logger;
this.__logger.debug('Starting server initialization');
this.__mailer = new Mailer(this.__logger, gmeConfig, this.__gmeAuth);
this.__logger.debug('Initializing worker manager');
this.__workerManager = new WorkerManager({
gmeConfig: gmeConfig,
logger: this.__logger,
});
this.__logger.debug('Initializing storage back-end');
if (gmeConfig.storage.database.type.toLowerCase() === 'mongo') {
this.__database = new MongoAdapter(this.__logger, gmeConfig);
} else if (gmeConfig.storage.database.type.toLowerCase() === 'redis') {
this.__database = new RedisAdapter(this.__logger, gmeConfig);
} else if (gmeConfig.storage.database.type.toLowerCase() === 'memory') {
this.__database = new MemoryAdapter(this.__logger, gmeConfig);
} else {
this.__logger.error(new Error('Unknown storage.database.type in config (config validator not used?)',
gmeConfig.storage.database.type));
}
this.__storage = new Storage(this.__database, this.__logger, gmeConfig, this.__gmeAuth);
this.__webSocket = new WebSocket(
this.__storage,
this.__logger,
gmeConfig,
this.__gmeAuth,
this.__workerManager
);
this.__middlewareOptions.safeStorage = this.__storage;
this.__middlewareOptions.workerManager = this.__workerManager;
this.__middlewareOptions.webSocket = this.__webSocket;
this.__logger.debug('Initializing basic core route functions');
this.__app.use(cors());
this.__app.use(compression());
this.__app.use(cookieParser());
this.__app.use(bodyParser.json(gmeConfig.server.bodyParser.json));
this.__app.use(bodyParser.urlencoded({extended: true}));
this.__app.use(methodOverride());
this.__app.use(multipart({defer: true})); // required to upload files. (body parser should not be used!)
this.__logger.debug('Collecting valid self-addresses');
const networkIfs = OS.networkInterfaces();
this.__addresses = [];
for (let netId in networkIfs) {
networkIfs[netId].forEach(network => {
if (network.family === 'IPv4') {
this.__addresses.push('http://' + network.address + ':' + gmeConfig.server.port);
}
});
}
this.__logger.info('Valid addresses of gme web server: ', this.__addresses.join(' '));
if (gmeConfig.webhooks.enable) {
this.__routeComponents.push(new WebhookManager(this.__storage, this.__logger, gmeConfig));
}
if (gmeConfig.addOn.enable) {
this.__addOnEventPropagator = new AddOnEventPropagator(this.__storage, this.__logger, gmeConfig);
this.__routeComponents.push(this.__addOnEventPropagator);
this.__middlewareOptions.addOnEventPropagator = this.__addOnEventPropagator;
}
this.__middlewareOptions.server = this;
this.__aadClient = new AADClient(gmeConfig, this.__gmeAuth, this.__logger);
}
getUrl() {
if (!this.__serverUrl) {
// use the cached version if we already built the string
this.__serverUrl = 'http://127.0.0.1:' + this.__gmeConfig.server.port;
}
return this.__serverUrl;
}
getAddresses() {
return this.__addresses;
}
getGmeConfig() {
return this.__gmeConfig;
}
getSocketsInfo() {
return Object.keys(this.__sockets)
.map(sid => {
return {
address: this.__sockets[sid].address(),
localAddress: this.__sockets[sid].localAddress,
localPort: this.__sockets[sid].localPort,
remoteAddress: this.__sockets[sid].remoteAddress,
remotePort: this.__sockets[sid].remotePort,
};
});
}
_setIsRunning(value) {
this.__isRunning = value === true;
}
isRunning() {
return this.__isRunning;
}
start(callback) {
const deferred = Q.defer();
const {
__gmeConfig,
__gmeAuth,
__logger,
__app,
__routeComponents,
__baseDir,
__clientBaseDir,
__nmpPackageJson,
__clientConfig,
__excludeRegExs,
__mailer,
__workerManager,
__storage,
__webSocket,
} = this;
const getUserId = req => {
return req.userData && req.userData.userId;
};
const getMountedPath = req => {
return req.header(CONSTANTS.HTTP_HEADERS.MOUNTED_PATH) || '';
};
const getLogInUrl = req => {
if (isAbsUrlPath.test(__gmeConfig.authentication.logInUrl)) {
return __gmeConfig.authentication.logInUrl;
}
return getMountedPath(req) + __gmeConfig.authentication.logInUrl;
};
const getLogOutUrl = req => {
if (isAbsUrlPath.test(__gmeConfig.authentication.logOutUrl)) {
return __gmeConfig.authentication.logOutUrl;
}
return getMountedPath(req) + __gmeConfig.authentication.logOutUrl;
};
const ensureAuthenticated = (req, res, next) => {
const authorization = req.get('Authorization');
let username;
let password;
let token;
let split;
if (__gmeConfig.authentication.enable === false) {
// If authentication is turned off we treat everybody as a guest user.
req.userData = {
userId: __gmeConfig.authentication.guestAccount
};
next();
return;
}
if (authorization && authorization.indexOf('Basic') === 0) {
__logger.debug('Basic authentication request');
// FIXME: ':' should not be in username nor in password
split = Buffer.from(authorization.substr('Basic '.length), 'base64').toString('utf8').split(':');
username = split[0];
password = split[1];
if (username && password) {
__gmeAuth.authenticateUser(username, password)
.then(() => {
req.userData = {
userId: username
};
next();
})
.catch(err => {
__logger.debug('Basic auth failed', {metadata: err});
res.status(401);
next(new Error('Basic authentication failed'));
});
} else {
res.status(401);
next(new Error('Basic authentication failed'));
}
} else if (authorization && authorization.indexOf('Bearer ') === 0) {
__logger.debug('Token Bearer authentication request');
token = authorization.substr('Bearer '.length);
__gmeAuth.verifyJWToken(token)
.then(result => {
if (result.renew === true) {
__gmeAuth.regenerateJWToken(token)
.then(newToken => {
req.userData = {
token: newToken,
newToken: true,
userId: result.content.userId
};
res.header(__gmeConfig.authentication.jwt.cookieId, newToken);
next();
})
.catch(next);
} else {
req.userData = {
token: token,
userId: result.content.userId
};
next();
}
})
.catch(err => {
if (err.name === 'TokenExpiredError') {
if (res.getHeader('X-WebGME-Media-Type') || !__gmeConfig.authentication.logInUrl) {
res.status(401);
next(err);
} else {
res.redirect(getLogInUrl(req));
}
} else {
__logger.error('Cookie verification failed', {metadata: err});
res.status(401);
next(err);
}
});
} else if (req.query.token) {
__logger.debug('jwtoken provided in url query string');
token = req.query.token;
__gmeAuth.verifyJWToken(token)
.then(result => {
req.userData = {
userId: result.content.userId,
};
if (__gmeConfig.authentication.jwt.logOutUrlField &&
result.content[__gmeConfig.authentication.jwt.logOutUrlField]) {
res.cookie(
'webgme-logout-url',
result.content[__gmeConfig.authentication.jwt.logOutUrlField]
);
}
if (result.renew === true) {
__gmeAuth.regenerateJWToken(token)
.then(newToken => {
req.userData.newToken = true;
req.userData.token = newToken;
__logger.debug('generated new token for user', result.content.userId);
res.cookie(__gmeConfig.authentication.jwt.cookieId, newToken);
next();
})
.catch(next);
} else {
req.userData.token = token;
res.cookie(__gmeConfig.authentication.jwt.cookieId, token);
next();
}
})
.catch(err => {
if (err.name === 'TokenExpiredError') {
res.clearCookie(__gmeConfig.authentication.jwt.cookieId);
if (res.getHeader('X-WebGME-Media-Type') || !__gmeConfig.authentication.logInUrl) {
res.status(401);
next(err);
} else {
res.redirect(getLogInUrl(req));
}
} else {
__logger.error('Cookie verification failed', err);
res.status(401);
next(err);
}
});
} else if (req.cookies[__gmeConfig.authentication.jwt.cookieId]) {
//TODO this is a forceful method of trying to renew aad token needs to be harmonized better
let webgmeTokenResult = null;
__logger.debug('jwtoken provided in cookie');
token = req.cookies[__gmeConfig.authentication.jwt.cookieId];
const aToken = req.cookies[__gmeConfig.authentication.azureActiveDirectory.cookieId];
__gmeAuth.verifyJWToken(token)
.then(result => {
webgmeTokenResult = result;
if (__gmeConfig.authentication.azureActiveDirectory.enable) {
return this.__aadClient.getAccessToken(result.content.userId, aToken);
} else {
return Q(null);
}
})
.then(token => {
if (token) {
res.cookie(__gmeConfig.authentication.azureActiveDirectory.cookieId, token.accessToken);
}
if (webgmeTokenResult.renew === true) {
__gmeAuth.regenerateJWToken(token)
.then(newToken => {
req.userData = {
token: newToken,
newToken: true,
userId: webgmeTokenResult.content.userId
};
__logger.debug('generated new token for user', webgmeTokenResult.content.userId);
res.cookie(__gmeConfig.authentication.jwt.cookieId, newToken);
// Status code for new token??
next();
})
.catch(next);
} else {
req.userData = {
token: token,
userId: webgmeTokenResult.content.userId
};
next();
}
})
.catch(err => {
res.clearCookie(__gmeConfig.authentication.jwt.cookieId);
res.clearCookie(__gmeConfig.authentication.azureActiveDirectory.cookieId);
if (err.name === 'TokenExpiredError' || err.name === 'MissingAADAccountForTokenError') {
if (res.getHeader('X-WebGME-Media-Type') || !__gmeConfig.authentication.logInUrl) {
res.status(401);
next(err);
} else {
res.redirect(getLogInUrl(req));
}
} else {
__logger.error('Cookie verification failed', err);
res.status(401);
next(err);
}
});
} else if (__gmeConfig.authentication.allowGuests) {
__logger.debug('jwtoken not provided in cookie - will generate a guest token.');
__gmeAuth.generateJWToken(__gmeConfig.authentication.guestAccount, null)
.then(guestToken => {
req.userData = {
token: guestToken,
newToken: true,
userId: __gmeConfig.authentication.guestAccount
};
res.cookie(__gmeConfig.authentication.jwt.cookieId, guestToken);
next();
})
.catch(next);
} else if (res.getHeader('X-WebGME-Media-Type') || !__gmeConfig.authentication.logInUrl) {
// do not redirect with direct api access or if no login url is specified
res.status(401);
return next(new Error());
} else {
res.redirect(getLogInUrl(req) + webgmeUtils.getRedirectUrlParameter(req));
}
};
const setupExternalRestModules = () => {
const keys = Object.keys(__gmeConfig.rest.components);
__logger.debug('Initializing external REST modules');
keys.forEach(componentId => {
let restComponent;
let mount;
let src;
if (typeof __gmeConfig.rest.components[componentId] === 'string') {
mount = componentId;
src = __gmeConfig.rest.components[componentId];
} else {
mount = __gmeConfig.rest.components[componentId].mount;
src = __gmeConfig.rest.components[componentId].src;
}
restComponent = require(src);
if (restComponent) {
__logger.debug('Mounting external REST component [' + src + '] at /' + mount);
if (Object.hasOwn(restComponent, 'initialize') && Object.hasOwn(restComponent, 'router')) {
restComponent.initialize(this.__middlewareOptions);
__app.use('/' + mount, restComponent.router);
if (Object.hasOwn(restComponent, 'start') && Object.hasOwn(restComponent, 'stop')) {
__routeComponents.push(restComponent);
} else {
__logger.warn('Deprecated restRouter, [' + src + '] does not have start/stop methods.');
}
} else {
__logger.warn('Deprecated restComponent [' + src + '], use the RestRouter instead.');
__app.use('/' + mount, restComponent(__gmeConfig, ensureAuthenticated, __logger));
}
} else {
throw new Error('Loading rest component ' + __gmeConfig.rest.components[componentId] + ' failed.');
}
});
};
const mountUserManagementPage = () => {
var userComponent = require(__gmeConfig.authentication.userManagementPage);
userComponent.initialize(this.__middlewareOptions);
__routeComponents.push(userComponent);
__app.use('/profile', userComponent.router);
};
const processRequestBasedGMEConfigFields = (baseConfig, req) => {
baseConfig.client.mountedPath = getMountedPath(req);
return baseConfig;
};
// starting gmeAuth and the mailer service...
const coreInit = () => {
const deferred = Q.defer();
this.__middlewareOptions.ensureAuthenticated = ensureAuthenticated;
this.__middlewareOptions.getUserId = getUserId;
this.__middlewareOptions.getMountedPath = getMountedPath;
if (__gmeConfig.mailer.enable) {
__mailer.init()
.then(() => {
this.__middlewareOptions.mailer = __mailer;
})
.catch(err => {
__logger.info('Failed to start mail service!', {metadata: err});
})
.finally(deferred.resolve);
} else {
__logger.debug('No mail service was configured');
deferred.resolve();
}
return deferred.promise;
};
// adding all routes
const buildRoutes = () => {
this.__tokenServer = new TokenServer(this.__middlewareOptions);
__app.use('/rest/tokens', this.__tokenServer.router);
this.__middlewareOptions.accessTokens = this.__tokenServer.tokens;
if (__gmeConfig.executor.enable) {
this.__executorServer = new ExecutorServer(this.__middlewareOptions);
__app.use('/rest/executor', this.__executorServer.router);
} else {
__logger.debug('Executor not enabled. Add \'executor.enable: true\' to configuration to activate.');
}
if (__gmeConfig.authentication.enable === true && __gmeConfig.authentication.userManagementPage) {
mountUserManagementPage();
}
setupExternalRestModules();
__app.get(['', '/', '/index.html'], ensureAuthenticated, (req, res) => {
const indexHtmlPath = path.join(__clientBaseDir, 'index.html');
const protocol = __gmeConfig.server.behindSecureProxy ? 'https' : 'http';
const host = protocol + '://' + req.get('host');
const url = host + req.originalUrl;
const imageUrl = host + '/img/gme-logo.png';
const projectId = req.query.project;
__logger.debug('resolved url', url);
fs.readFile(indexHtmlPath, 'utf8', (err, indexTemp) => {
if (err) {
__logger.error(err);
res.sendStatus(404);
} else {
res.contentType('text/html');
//http://stackoverflow.com/questions/49547/how-to-control-web-page-caching-across-all-browsers
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); // HTTP 1.1.
res.setHeader('Pragma', 'no-cache'); // HTTP 1.0.
res.setHeader('Expires', '0'); // Proxies.
res.send(ejs.render(indexTemp, {
webgmeVersion: __nmpPackageJson.version,
appVersion: __gmeConfig.client.appVersion,
mountedPath: req.header('X-Proxy-Mounted-Path') || '',
url: url,
imageUrl: imageUrl,
projectId: projectId ? projectId.replace('+', '/') : 'WebGME',
favicon: __gmeConfig.client.faviconPath,
pageTitle: typeof __gmeConfig.client.pageTitle === 'string' ?
__gmeConfig.client.pageTitle : 'WebGME',
}));
}
});
});
__logger.debug('creating login routing rules for the static server');
__app.get('/logout', (req, res) => {
let redirectUrl;
let logOutUrl = '';
if (__gmeConfig.authentication.enable === false) {
res.sendStatus(404);
} else {
redirectUrl = req.query.redirectUrl;
if (__gmeConfig.authentication.logOutUrl) {
logOutUrl = getLogOutUrl(req);
}
if (__gmeConfig.authentication.jwt.logOutUrlField) {
if (req.cookies['webgme-logout-url']) {
logOutUrl = req.cookies['webgme-logout-url'];
}
}
res.clearCookie(__gmeConfig.authentication.jwt.cookieId);
res.redirect(logOutUrl || redirectUrl || getLogInUrl(req) || '/');
}
});
__app.post('/login', (req, res) => {
const userId = req.body.userId;
const password = req.body.password;
if (__gmeConfig.authentication.enable) {
__gmeAuth.generateJWToken(userId, password)
.then(token => {
res.cookie(__gmeConfig.authentication.jwt.cookieId, token);
res.sendStatus(200);
})
.catch(err => {
__logger.error('Failed login', err);
res.sendStatus(401);
});
} else {
res.sendStatus(404);
}
});
//AzureActiveDirectory direct login
if (this.__gmeConfig.authentication.enable &&
this.__gmeConfig.authentication.azureActiveDirectory.enable) {
__app.get('/aad', (req, res) => {
this.__aadClient.login(req, res);
});
//AAD login response
__app.post('/aad', (req, res) => {
const redirectUrl = req.cookies['webgme-redirect'] || '/';
this.__aadClient.cacheUser(req, res, (err) => {
res.clearCookie('webgme-redirect');
if (err) {
res.sendStatus(401);
} else {
res.redirect(redirectUrl);
}
});
});
//device access to use webgme related services - only available when access Scope is used
if (__gmeConfig.authentication.azureActiveDirectory.accessScope) {
const aadVerify = require('azure-ad-verify-token-commonjs');
const verify = aadVerify.verify;
const voptions = {
jwksUri: __gmeConfig.authentication.azureActiveDirectory.jwksUri,
issuer: __gmeConfig.authentication.azureActiveDirectory.issuer,
audience: __gmeConfig.authentication.azureActiveDirectory.audience
};
__app.get('/aad/device', (req, res) => {
if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
const token = req.headers.authorization.split(' ')[1];
verify(token, voptions)
.then(token => {
if (token.oid) {
return this.__gmeAuth.getUser(
{$exists: true},
{aadId: {$eq: token.oid}}
);
} else {
throw new Error('unknown user device trying to get access!!!', token);
}
})
.then(webgmeUser => {
return this.__gmeAuth.generateJWTokenForAuthenticatedUser(webgmeUser._id);
})
.then(webgmeToken => {
res.cookie(this.__gmeConfig.authentication.jwt.cookieId, webgmeToken);
res.sendStatus(200);
})
.catch(err => {
__logger.error(err);
res.sendStatus(403);
});
} else {
res.sendStatus(403);
}
});
}
}
__app.get('/bin/getconfig.js', ensureAuthenticated, (req, res) => {
res.status(200);
res.setHeader('Content-type', 'application/javascript');
res.end('define([],function(){ return ' +
JSON.stringify(processRequestBasedGMEConfigFields(__clientConfig, req)) + ';});');
});
__logger.debug('creating gmeConfig.json specific routing rules');
__app.get('/gmeConfig.json', (req, res) => {
res.status(200);
res.setHeader('Content-type', 'application/json');
res.end(JSON.stringify(processRequestBasedGMEConfigFields(__clientConfig, req)));
});
__app.get(/^\/(gme-dist)\/.*\.(js|map)$/, (req, res) => {
res.sendFile(path.join(__baseDir, '../dist', req.url.substring('/gme-dist/'.length)));
});
__logger.debug('creating decorator specific routing rules');
__app.get(/^\/decorators\/.*/, ensureAuthenticated, (req, res) => {
const tryNext = (index) => {
let resolvedPath;
if (index < __gmeConfig.visualization.decoratorPaths.length) {
resolvedPath = path.resolve(__gmeConfig.visualization.decoratorPaths[index]);
resolvedPath = path.join(resolvedPath, req.url.substring('/decorators/'.length));
res.sendFile(resolvedPath, (err) => {
__logger.debug('sending decorator', resolvedPath);
if (err && err.code !== 'ECONNRESET') {
tryNext(index + 1);
}
});
} else {
res.sendStatus(404);
}
};
if (__gmeConfig.visualization.decoratorPaths && __gmeConfig.visualization.decoratorPaths.length) {
tryNext(0);
} else {
res.sendStatus(404);
}
});
// Plugin paths
__logger.debug('creating plugin specific routing rules');
__app.get(/^\/plugin\/.*/, webgmeUtils.getGoodExtraAssetRouteFor('plugin',
__gmeConfig.plugin.basePaths, __logger, __baseDir));
// Layout paths
__logger.debug('creating layout specific routing rules');
__app.get(/^\/layout\/.*/, webgmeUtils.getGoodExtraAssetRouteFor('layout',
__gmeConfig.visualization.layout.basePaths, __logger, __baseDir));
// Panel paths
__logger.debug('creating panel specific routing rules');
__app.get(/^\/panel\/.*/,
webgmeUtils.getRouteFor(
'panel',
__gmeConfig.visualization.panelPaths,
__baseDir,
__logger
));
// Assets paths (svgs)
__logger.debug('creating assets/svgs specific routing rules');
__app.get(/^\/assets\/DecoratorSVG\/.*/, ensureAuthenticated, (req, res, next) => {
webgmeUtils.getSVGMap(__gmeConfig, __logger)
.then(svgMap => {
if (Object.hasOwn(svgMap, req.path)) {
res.sendFile(svgMap[req.path]);
} else {
__logger.warn('Requested DecoratorSVG not found', req.path);
res.sendStatus(404);
}
})
.catch(next);
});
__app.get('/assets/decoratorSVGList.json', ensureAuthenticated, (req, res, next) => {
webgmeUtils.getSVGMap(__gmeConfig, __logger)
.then(svgMap => {
res.json(Object.keys(svgMap).map(svgName => {
return svgName.substring('/assets/DecoratorSVG/'.length);
}));
})
.catch(err => {
__logger.error(err);
next(err);
});
});
__logger.debug('creating external library specific routing rules');
__gmeConfig.server.extlibExcludes.forEach(regExStr => {
__logger.debug('Adding exclude rule to "/extlib" path: ', regExStr);
__excludeRegExs.push(new RegExp(regExStr));
});
__app.get(/^\/extlib\/.*/, ensureAuthenticated, (req, res) => {
var i;
for (i = 0; i < __excludeRegExs.length; i += 1) {
if (__excludeRegExs[i].test(req.url)) {
__logger.warn('Request attempted to access excluded path "' + req.url + '", caught by "' +
__gmeConfig.server.extlibExcludes[i] + '" from gmeConfig.');
res.sendStatus(403);
return;
}
}
//first we try to give back the common extlib/modules
var urlArray = req.path.split('/');
urlArray[1] = '.';
urlArray.shift();
var relPath = urlArray.join('/');
var absPath = path.resolve(path.join(process.cwd(), relPath));
// must pass the full path
if (relPath.lastIndexOf('/') === relPath.length - 1) {
// if URL ends with /, append / to support sending index.html
absPath = absPath + '/';
}
webgmeUtils.expressFileSending(res, absPath, __logger);
});
__logger.debug('creating basic static content related routing rules');
//static contents
Express.static.mime.define({'application/wasm': ['wasm']});
__app.get(/^\/(common|client)\/.*\.(js|wasm)$/, Express.static(__baseDir, {index: false}));
//TODO remove this part as this is only temporary!!!
__app.get('/docs/*', Express.static(path.join(__baseDir, '..'), {index: false}));
__app.use('/rest/blob', BlobServer.createExpressBlob(this.__middlewareOptions));
//client contents - js/html/css
__app.get(/^\/.*\.(css|ico|ttf|woff|woff2|js|cur)$/, Express.static(__clientBaseDir));
// There is no secret inside the package.json so it is better if it can be downloaded before
// authentication finishes/
__app.get('/package.json', Express.static(path.join(__baseDir, '..')));
__app.get(/^\/.*\.(_js|html|gif|png|bmp|svg|json|map)$/,
ensureAuthenticated, Express.static(__clientBaseDir));
__logger.debug('creating API related routing rules');
const apiReady = API.createAPI(__app, '/api', this.__middlewareOptions);
// everything else is 404
__logger.debug('creating all other request rule - error 404 -');
__app.use('*', (req, res) => {
res.sendStatus(404);
});
// catches all next(new Error()) from previous rules,
// you can set res.status() before you call next(new Error())
// eslint-disable-next-line
__app.use((err, req, res, next) => {
if (res.statusCode === 200) {
res.status(err.status || 500);
}
res.sendStatus(res.statusCode);
//res.send(err.stack ? err.stack : err); // FIXME: in dev mode
});
__logger.debug('gmeConfig of webgme server', {metadata: __gmeConfig});
__logger.debug('standalone server route initialization completed');
return apiReady;
};
// starting everything else
const startListening = () => {
const deferred = Q.defer();
const handleNewConnection = socket => {
let socketId = socket.remoteAddress + ':' + socket.remotePort;
if (socket.encrypted) { // https://nodejs.org/api/tls.html#tls_tlssocket_encrypted
socketId += ':encrypted';
}
this.__sockets[socketId] = socket;
__logger.debug('socket connected (added to list) ' + socketId);
socket.on('close', () => {
if (Object.hasOwn(this.__sockets, socketId)) {
__logger.debug('socket closed (removed from list) ' + socketId);
delete this.__sockets[socketId];
}
});
};
this.__httpServer = Http.createServer(__app);
if (__gmeConfig.server.timeout > -1) {
this.__httpServer.timeout = __gmeConfig.server.timeout;
}
this.__httpServer.on('connection', handleNewConnection);
this.__httpServer.on('secureConnection', handleNewConnection);
this.__httpServer.on('clientError', (err/*, socket*/) => {
__logger.debug('clientError', err);
});
this.__httpServer.on('error', err => {
if (err.code === 'EADDRINUSE') {
__logger.error('Failed to start server', {metadata: {port: __gmeConfig.server.port, error: err}});
deferred.reject(err);
} else {
__logger.error('Server raised an error', {metadata: {port: __gmeConfig.server.port, error: err}});
}
});
__logger.debug('starting server');
__gmeAuth.connect()
.then(db => {
const promises = [];
__logger.debug('gmeAuth connected');
promises.push(Q.ninvoke(__workerManager, 'start'));
promises.push(__storage.openDatabase());
if (this.__executorServer) {
promises.push(this.__executorServer.start({mongoClient: db}));
}
if (this.__tokenServer) {
promises.push(this.__tokenServer.start({mongoClient: db}));
}
return Q.all(promises);
})
.then(() => {
const promises = [];
__webSocket.start(this.__httpServer);
__routeComponents.forEach(component => {
promises.push(Q.ninvoke(component, 'start'));
});
return Q.all(promises);
})
.then(() => {
// Finally start listening to the server port.
return Q.ninvoke(this.__httpServer, 'listen', __gmeConfig.server.handle || __gmeConfig.server.port);
})
.then(() => {
this.__isRunning = true;
__logger.info('Server is listening ...');
return webgmeUtils.createStartUpProjects(
__gmeConfig,
__gmeAuth,
__storage,
__logger,
this.getUrl()
);
})
.then(() => {
deferred.resolve();
})
.catch(err => {
__logger.error('Cannot start server...', {metadata: err});
if (this.__isRunning) {
//should stop the http server
this.stop()
.finally(() => {
deferred.reject(err);
});
} else {
deferred.reject(err);
}
});
return deferred.promise;
};
// actual server start fucntion
coreInit()
.then(() => {
return buildRoutes();
})
.then(() => {
return startListening();
})
.then(deferred.resolve)
.catch(deferred.reject);
return deferred.promise.nodeify(callback);
}
stop(callback) {
const {
__logger,
__httpServer,
__webSocket,
__routeComponents,
__executorServer,
__storage,
__workerManager,
__gmeAuth,
} = this;
const deferred = Q.defer();
if (this.__isRunning === false) {
// TODO - should this be an error?
deferred.resolve();
} else {
this.__isRunning = false;
// first we have to request the close then we can destroy the sockets.
// this should happen without waiting for http server close.
let numDestroyedSockets = 0;
// destroy all open sockets i.e. keep-alive and socket-io connections, etc.
for (let key in this.__sockets) {
if (Object.hasOwn(this.__sockets, key)) {
this.__sockets[key].destroy();
delete this.__sockets[key];
__logger.debug('destroyed open socket ' + key);
numDestroyedSockets += 1;
}
}
__logger.debug('destroyed # of sockets: ' + numDestroyedSockets);
Q.ninvoke(__httpServer, 'close')
.then(() => {
const promises = [];
__webSocket.stop();
if (__executorServer) {
__executorServer.stop();
}
__routeComponents.forEach(component => {
promises.push(Q.ninvoke(component, 'stop'));
});
return Q.all(promises);
})
.then(() => {
return Q.all([
__storage.closeDatabase(),
Q.ninvoke(__workerManager, 'stop')
]);
})
.then(() => {
return __gmeAuth.unload();
})
.then(deferred.resolve)
.catch(err => {
this.__logger.error('Error at server stop', err);
if (err.code === 'ERR_SERVER_NOT_RUNNING' || err.message.indexOf('Not running') > -1) {
// It's not running which is what we want.
deferred.resolve();
} else {
deferred.reject(err);
}
});
}
return deferred.promise.nodeify(callback);
}
}
module.exports = StandAloneServer;