'use strict';
/**
* Main entry point.
* @module AppServer
* @license MIT
* @author Kai KRETSCHMANN <kai@kretschmann.consulting>
*/
// Sentry
const Sentry = require('@sentry/node');
const sentryDSN = process.env.SENTRY_DSN || process.env.SENTRY;
Sentry.init({
dsn: sentryDSN,
environment: process.env.NODE_ENV || /* istanbul ignore next */ 'production',
sendDefaultPii: true,
debug: false,
release: 'bintra@' + process.env.npm_package_version,
tracesSampleRate: 1.0
});
const path = require('path');
const http = require('http');
require('custom-env').env(true);
const log4js = require('log4js');
const logger = log4js.getLogger();
logger.level = process.env.LOGLEVEL || /* istanbul ignore next */ 'warn';
logger.info(`SENTRY dsn=${sentryDSN}`);
// Start of gRPC block
if (process.env.GRPC_BIND_PORT) {
const PROTO_PATH = path.join(__dirname, 'proto/route_guide.proto');
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const addReflection = require('grpc-server-reflection').addReflection;
const protoOptions = {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
};
const packageDefinition = protoLoader.loadSync(PROTO_PATH, protoOptions);
const apiProto = grpc.loadPackageDefinition(packageDefinition);
const grpcControllerAdmins = require('./grpc-controllers/Admins');
const grpcServer = new grpc.Server();
grpcServer.addService(apiProto.routeguide.ApiService.service, {
getCount: grpcControllerAdmins.serviceCountPackage
});
addReflection(grpcServer, 'descriptor_set.bin');
const grpcServerHost = process.env.GRPC_BIND_HOST || '127.0.0.1';
const grpcServerPort = process.env.GRPC_BIND_PORT || /* istanbul ignore next */ 8081;
grpcServer.bindAsync(
`${grpcServerHost}:${grpcServerPort}`,
grpc.ServerCredentials.createInsecure(),
(error, port) => {
if (error) logger.error(error);
logger.info(`gRPC server starting at grpc://${grpcServerHost}:${grpcServerPort}`);
}
);
} else {
logger.warn('gRPC disabled, no GRPC_BIND_PORT was defined');
} // if GRPC_BIND_PORT set, it is enabled
// Start of WEB block
const favicon = require('serve-favicon');
const serveStatic = require('serve-static');
const oas3Tools = require('./myoas/'); // here was required the oas3-tools before
const mongoose = require('mongoose');
const auth = require('./utils/auth');
const webFilterOK = require('./utils/webfilter').webFilterOK;
const express = require('express');
const app = express();
app.disable('x-powered-by');
app.didStart = false;
/* add BEHINDPROXY=uniquelocal to .env for private IP detection of proxy */
const behindProxy = process.env.BEHINDPROXY || '';
if (behindProxy === '') {
logger.warn('Direct serving, no proxy');
} else {
logger.warn(`Behind proxy ${behindProxy}`);
app.set('trust proxy', behindProxy);
}
// Prometheus section
const client = require('prom-client');
const defaultLabels = { NODE_APP_INSTANCE: process.env.NODE_APP_INSTANCE };
const Registry = client.Registry;
const register = new Registry();
register.setDefaultLabels(defaultLabels);
const collectDefaultMetrics = client.collectDefaultMetrics;
const appCounter = new client.Counter({
name: 'bintra_app_requests_counter',
help: 'all api requests',
registers: [register]
});
const pj = require('./package.json');
const myversion = pj.version;
const versionCounter = new client.Gauge({
name: 'bintra_app_version',
help: 'source code version',
registers: [register],
labelNames: ['version']
});
versionCounter.set({ version: myversion }, 1);
collectDefaultMetrics({ prefix: 'bintra_', register });
// next section
const toobusy = require('toobusy-js');
const hpp = require('hpp');
const cors = require('cors');
const swaggerUi = require('swagger-ui-express');
const YAML = require('yamljs');
const pfilter = require('./controllers/pfilter');
const eventEmitter = require('./utils/eventer').em;
eventEmitter.setMaxListeners(0); // temp solution somehow
require('./subscribers/matomo');
require('./subscribers/toot');
require('./subscribers/mqttclient.js');
require('./subscribers/prometheus.js');
const myworker = require('./worker/main');
// Connect to mongo DB
const computeDBurl = require('./conf').computeDBurl;
const mongoUrl = computeDBurl();
logger.debug(`DB used: ${mongoUrl}`);
const db = mongoose.connection;
mongoose.set('strictQuery', true);
db.on('error', console.error.bind(console, 'MongoDB connection error:'));
db.on('connecting', err => { if (err) { logger.error(err); } logger.info('connecting'); });
db.on('connected', err => { if (err) { logger.error(err); } logger.info('DB connected'); });
db.on('open', err => {
if (err) { logger.error(err); }
logger.info('DB opened');
app.emit('dbready');
});
mongoose.connect(mongoUrl, { });
// default CORS domain
const corsWhitelist = ['https://api.bintra.directory', 'https://api.binarytransparency.net', 'https://bintra.directory', 'http://192.168.0.249:8080', 'http://127.0.0.1:8087'];
const corsOptions = {
methods: ['GET', 'HEAD', 'POST', 'DELETE', 'PUT', 'PATCH'],
origin: function (origin, callback) {
if (!(origin)) {
callback(null, true);
} else {
logger.info(`cors check on ${origin}`);
if (corsWhitelist.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
}
};
app.use(hpp());
app.use(cors(corsOptions));
// Add Sentry to app object
app.use(function (req, res, next) {
let realip = req.ip;
if (typeof req.headers['x-real-ip'] !== 'undefined') {
realip = req.headers['x-real-ip'];
}
Sentry.setUser({
ip_address: realip
});
if (typeof req.headers['geoip-country-code'] !== 'undefined') {
Sentry.setContext('GeoIP', {
country: req.headers['geoip-country-code'],
city: req.headers['geoip-city-name'],
zip: req.headers['geoip-zip'],
statecode: req.headers['geoip-state-code']
});
} // if
req.sentry = Sentry;
req.realip = realip;
next();
});
// Redirect root to docs UI
app.get('/', function doRedir (req, res, next) {
if (req.url !== '/') {
next();
} else {
res.writeHead(301, {
Location: '/docs/'
});
res.end();
}
});
toobusy.maxLag(parseInt(process.env.BUSY_LAG) || /* istanbul ignore next */ 70);
toobusy.interval(parseInt(process.env.BUSY_INTERVAL) || /* istanbul ignore next */ 500);
const currentMaxLag = toobusy.maxLag();
const interval = toobusy.interval();
logger.info(`configure toobusy with maxLag=${currentMaxLag}, interval=${interval}`);
app.use(function (req, res, next) {
if (toobusy()) {
const error = new Error('Too busy');
req.sentry.captureException(error);
res.status(503).send("I'm busy right now, sorry.");
} else {
next();
}
});
toobusy.onLag(function (currentLag) {
const sTmp = `Event loop lag detected! Latency: ${currentLag} ms`;
Sentry.captureMessage(sTmp, 'warning');
logger.warn(sTmp);
});
// filter bad requests early
app.use(function (req, res, next) {
logger.debug('use webFilter');
if (!webFilterOK(req)) {
logger.warn('Skip bad request');
const error = new Error('Bad request gets filtered out');
req.sentry.captureException(error);
res.status(400).send('Bad request');
} else {
next();
}
});
app.get('/feed.(rss|atom|json)', (req, res) => res.redirect('/v1/feed.' + req.params[0]));
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
app.use(favicon(path.join(__dirname, 'static', 'favicon.ico')));
app.use(serveStatic(path.join(__dirname, 'static')));
// Add some mongoose data to request for later use
app.use(function (req, res, next) {
req.mcdadmin = mongoose.connection;
req.appCounter = appCounter;
next();
});
// The Swagger document (require it, build it programmatically, fetch it from a URL, ...)
const swaggerDocJson = YAML.load(path.join(__dirname, 'api/swagger.yaml'));
const uioptions = {
customCss: '.swagger-ui .topbar { display: none }',
customJs: '/matomo.js',
customSiteTitle: 'Bintra directory API - binarytransparency',
customfavIcon: '/favicon.ico'
};
app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocJson, uioptions));
// swaggerRouter configuration
const options = {
routing: {
controllers: path.join(__dirname, 'controllers')
},
logging: {
format: 'combined',
errorLimit: 400
},
theapp: app,
openApiValidator: {
validateSecurity: {
handlers: {
bearerauth: auth.verifyToken
}
},
validateRequests: true,
validateResponses: false
}
};
// No "/v1" pattern, so it is wrong from here on
app.use(/^(?!\/v1).+/, function (req, res) {
res.status(404);
res.send('No API call');
req.sentry.captureMessage('No API call');
});
// Filter all parameters known
app.use(pfilter);
oas3Tools.expressAppConfig(path.join(__dirname, 'api/swagger.yaml'), options);
// Attach Sentry to express app
Sentry.setupExpressErrorHandler(app);
/**
* Start the server
*/
let server;
app.on('dbready', function () {
logger.info('db connection seems ready');
logger.info(`MaxSockets: ${http.globalAgent.maxSockets}`);
const serverPort = process.env.BIND_PORT;
const serverHost = process.env.BIND_HOST;
logger.info(`Bind to ${serverHost}:${serverPort}`);
server = http.createServer(app).listen(serverPort, serverHost, function () {
logger.info(`Your server is listening on port ${serverPort} (http://${serverHost}:${serverPort})`);
logger.info(`Swagger-ui is available on http://${serverHost}:${serverPort}/docs`);
app.didStart = true;
app.emit('appStarted');
});
});
/* istanbul ignore next */
async function workerStop () {
await myworker.queue.end();
await myworker.Scheduler.end();
await myworker.Worker.end();
}
process.on('SIGINT', function () /* istanbul ignore next */ {
logger.error('SIGINT received, quit');
app.didStart = false;
server.close();
(async () => workerStop())();
// calling .shutdown allows your process to exit normally
toobusy.shutdown();
process.exit();
});
module.exports = {
app,
mongoose
};