blob: 8cad789c07a0fc2f2ce6e4391029dad53f8e9aba [file] [log] [blame] [raw]
#!/usr/bin/env node
// Copyright (c) 2018, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
// Initialise options and properties. Don't load any handlers here; they
// may need an initialised properties library.
const nopt = require('nopt'),
os = require('os'),
props = require('./lib/properties'),
child_process = require('child_process'),
fs = require('fs-extra'),
systemdSocket = require('systemd-socket'),
_ = require('underscore'),
express = require('express'),
Raven = require('raven'),
{execScript} = require("./lib/exec"),
logger = require('./lib/logger').logger,
utils = require('./lib/utils');
// Parse arguments from command line 'node ./app.js args...'
const opts = nopt({
env: [String, Array],
rootDir: [String],
host: [String],
port: [Number],
propDebug: [Boolean],
debug: [Boolean]
});
if (opts.debug) logger.level = 'debug';
// Use the canned git_hash if provided
let gitReleaseName = '';
if (opts.static && fs.existsSync(opts.static + "/git_hash")) {
gitReleaseName = fs.readFileSync(opts.static + "/git_hash").toString().trim();
} else if (fs.existsSync('.git/')) { // Just if we have been cloned and not downloaded (Thanks David!)
gitReleaseName = child_process.execSync('git rev-parse HEAD').toString().trim();
}
// Set default values for omitted arguments
const defArgs = {
rootDir: opts.rootDir || './etc',
env: opts.env || ['dev'],
hostname: opts.host,
port: opts.port || 10241,
gitReleaseName: gitReleaseName
};
const isDevMode = () => process.env.NODE_ENV === "DEV";
const propHierarchy = _.flatten([
'defaults',
defArgs.env,
_.map(defArgs.env, e => e + '.' + process.platform),
process.platform,
os.hostname(),
'local']);
logger.info("properties hierarchy: " + propHierarchy.join(', '));
// Propagate debug mode if need be
if (opts.propDebug) props.setDebug(true);
// *All* files in config dir are parsed
props.initialize(defArgs.rootDir + '/config', propHierarchy);
const ceProps = props.propsFor("compiler-explorer");
const maxUploadSize = ceProps('maxUploadSize', '1mb');
// Now load up our libraries.
const aws = require('./lib/aws');
const awsProps = props.propsFor("aws");
class ExecHandler {
constructor() {
}
handle(req, res, next) {
if (!req.is('json')) return next();
res.set('Content-Type', 'application/json');
if (!req.body.script) {
const response = {ok: false, result: {}, error: "Missing script"};
res.end(JSON.stringify(response));
}
execScript(req.body.script)
.then(result => {
const response = {ok: true, result: result || {}, error: null};
res.end(JSON.stringify(response));
})
.catch(error => {
logger.error("Error handling", req.body.script, ":\n", error);
const response = {ok: false, result: {}, error: error.message};
res.end(JSON.stringify(response));
});
}
}
aws.initConfig(awsProps)
.then(() => {
function startListening(server) {
const ss = systemdSocket();
let _port;
if (ss) {
// ms (5 min default)
const idleTimeout = process.env.IDLE_TIMEOUT;
const timeout = (typeof idleTimeout !== 'undefined' ? idleTimeout : 300) * 1000;
if (idleTimeout) {
let exit = () => {
logger.info("Inactivity timeout reached, exiting.");
process.exit(0);
};
let idleTimer = setTimeout(exit, timeout);
let reset = () => {
clearTimeout(idleTimer);
idleTimer = setTimeout(exit, timeout);
};
server.all('*', reset);
logger.info(` IDLE_TIMEOUT: ${idleTimeout}`);
}
_port = ss;
} else {
_port = defArgs.port;
}
logger.info(` Listening on http://${defArgs.hostname || 'localhost'}:${_port}/`);
logger.info("=======================================");
server.listen(_port, defArgs.hostname);
}
const ravenPrivateEndpoint = aws.getConfig('ravenPrivateEndpoint');
if (ravenPrivateEndpoint) {
Raven.config(ravenPrivateEndpoint, {
release: gitReleaseName,
environment: defArgs.env
}).install();
logger.info("Configured with raven endpoint", ravenPrivateEndpoint);
} else {
Raven.config(false).install();
}
const webServer = express(),
morgan = require('morgan'),
bodyParser = require('body-parser'),
compression = require('compression');
logger.info("=======================================");
if (gitReleaseName) logger.info(` git release ${gitReleaseName}`);
const healthCheck = require('./lib/handlers/health-check');
morgan.token('gdpr_ip', req => utils.anonymizeIp(req.ip));
// Based on combined format, but: GDPR compliant IP, no timestamp & no unused fields for our usecase
const morganFormat = isDevMode() ? 'dev' : ':gdpr_ip ":method :url" :status';
const handler = express.Router();
const execHandler = new ExecHandler();
handler.post('/execute', execHandler.handle.bind(execHandler));
webServer
.use(Raven.requestHandler())
.set('trust proxy', true)
// before morgan so healthchecks aren't logged
.use('/healthcheck', new healthCheck.HealthCheckHandler().handle)
.use(morgan(morganFormat, {
stream: logger.stream,
// Skip for non errors (2xx, 3xx)
skip: (req, res) => res.statusCode >= 400
}))
.use(morgan(morganFormat, {
stream: logger.warnStream,
// Skip for non user errors (4xx)
skip: (req, res) => res.statusCode < 400 || res.statusCode >= 500
}))
.use(morgan(morganFormat, {
stream: logger.errStream,
// Skip for non server errors (5xx)
skip: (req, res) => res.statusCode < 500
}))
.use(compression())
.use(bodyParser.json({limit: ceProps('bodyParserLimit', maxUploadSize)}))
.use('/', handler)
.use((req, res, next) => {
next({status: 404, message: `page "${req.path}" could not be found`});
})
.use((err, req, res, next) => {
Raven.errorHandler()(err, req, res, next);
})
// eslint-disable-next-line no-unused-vars
.use((err, req, res, next) => {
const status =
err.status ||
err.statusCode ||
err.status_code ||
(err.output && err.output.statusCode) ||
500;
const message = err.message || 'Internal Server Error';
res.status(status);
res.end(`Error ${status}\n${message}`);
})
.on('error', err => logger.error('Caught error:', err, "(in web handler; continuing)"));
startListening(webServer);
})
.catch(err => {
logger.error("AWS Init Promise error", err, "(shutting down)");
process.exit(1);
});