fxa-content-server/server/bin/fxa-content-server.js

280 строки
8.0 KiB
JavaScript
Executable File

#!/usr/bin/env node
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
// setup version first for the rest of the modules
const logger = require('../lib/logging/log')('server.main');
const version = require('../lib/version');
logger.info(`source set to: ${version.source}`);
logger.info(`version set to: ${version.version}`);
logger.info(`commit hash set to: ${version.commit}`);
logger.info(`fxa-content-server-l10n commit hash set to: ${version.l10n}`);
logger.info(`tos-pp (legal-docs) commit hash set to: ${version.tosPp}`);
const bodyParser = require('body-parser');
const celebrate = require('celebrate');
const consolidate = require('consolidate');
const cookieParser = require('cookie-parser');
const cors = require('cors');
const express = require('express');
const fs = require('fs');
const helmet = require('helmet');
const https = require('https');
const path = require('path');
const serveStatic = require('serve-static');
const config = require('../lib/configuration');
const raven = require('../lib/raven');
const userAgent = require('../lib/user-agent');
if (! userAgent.isToVersionStringSupported()) {
// npm@3 installs the incorrect version of node-uap, one without `toVersionString`.
// To ensure the correct version is installed, check toVersionString is available.
logger.critical('dependency.version.error', {
error: 'node-uap does not support toVersionString()'
});
process.exit(1);
}
// This can't possibly be best way to librar-ify this module.
const isMain = process.argv[1] === __filename;
if (isMain) {
// ./server is our current working directory
process.chdir(path.dirname(__dirname));
}
const i18n = require('../lib/i18n')(config.get('i18n'));
const routes = require('../lib/routes')(config, i18n);
// Side effect - Adds default_fxa and dev_fxa to express.logger formats
const routeLogging = require('../lib/logging/route_logging');
const noindex = require('../lib/noindex');
const fourOhFour = require('../lib/404');
const serverErrorHandler = require('../lib/500');
const localizedRender = require('../lib/localized-render');
const csp = require('../lib/csp');
const cspRulesBlocking = require('../lib/csp/blocking')(config);
const cspRulesReportOnly = require('../lib/csp/report-only')(config);
const frameGuard = require('../lib/frame-guard')(config);
const STATIC_DIRECTORY =
path.join(__dirname, '..', '..', config.get('static_directory'));
const PAGE_TEMPLATE_DIRECTORY =
path.join(config.get('page_template_root'), config.get('page_template_subdirectory'));
logger.info('page_template_directory: %s', PAGE_TEMPLATE_DIRECTORY);
function makeApp() {
const app = express();
if (config.get('env') === 'development') {
const webpack = require('webpack');
const webpackConfig = require('../../webpack.config.js');
const webpackMiddleware = require('webpack-dev-middleware');
const webpackCompiler = webpack(webpackConfig);
app.use(webpackMiddleware(webpackCompiler, {
publicPath: '/bundle/',
writeToDisk: true
}));
}
app.engine('html', consolidate.handlebars);
app.set('view engine', 'html');
app.set('views', PAGE_TEMPLATE_DIRECTORY);
// The request handler must be the first item
app.use(raven.ravenModule.requestHandler());
// i18n adds metadata to a request to help
// with translating templates on the server.
app.use(i18n);
// render the correct template for the locale.
app.use(localizedRender({ i18n: i18n }));
app.use(frameGuard);
app.use(helmet.xssFilter());
app.use(helmet.hsts({
force: true,
includeSubdomains: true,
maxAge: config.get('hsts_max_age')
}));
app.use(helmet.noSniff());
if (config.get('csp.enabled')) {
app.use(csp({ rules: cspRulesBlocking }));
}
if (config.get('csp.reportOnlyEnabled')) {
// There has to be more than a `reportUri`
// to enable reportOnly CSP.
if (Object.keys(cspRulesReportOnly.directives).length > 1) {
app.use(csp({ rules: cspRulesReportOnly }));
}
}
app.disable('x-powered-by');
app.use(routeLogging());
app.use(cookieParser());
app.use(bodyParser.text({
type: 'text/plain'
}));
// chrome sends 'application/csp-report' and firefox < 48 sends
// 'application/json'. 'application/csp-report' is correct:
// https://w3c.github.io/webappsec/specs/content-security-policy/
// https://bugzilla.mozilla.org/show_bug.cgi?id=1192840
app.use(bodyParser.json({
// the 3 entries:
// json file types,
// all json content-types
// csp reports
type: ['json', '*/json', 'application/csp-report']
}));
if (isCorsRequired()) {
// JS, CSS and web font resources served from a CDN
// will be ignored unless CORS headers are present.
const corsOptions = {
origin: config.get('public_url')
};
app.route(/\.(js|css|woff|woff2|eot|ttf)$/)
.get(cors(corsOptions));
}
app.use(noindex);
routes(app);
app.use(serveStatic(STATIC_DIRECTORY, {
maxAge: config.get('static_max_age')
}));
// it's a four-oh-four not found.
app.use(fourOhFour);
// Handler for CORS errors
app.use((err, req, res, next) => {
if (err.message === 'CORS Error') {
res.status(401).json({
error: 'Unauthorized',
message: 'CORS Error',
statusCode: 401
});
} else {
next(err);
}
});
// The error handler must be before any other error middleware
app.use(raven.ravenModule.errorHandler());
// log any joi validation errors
app.use((err, req, res, next) => {
if (err && err.isJoi) {
logger.error('validation.error', {
error: err.details.map(details => details.message).join(','),
path: req.path,
});
// capture validation errors
raven.ravenModule.captureException(err);
}
next(err);
});
// convert joi validation errors to a JSON response
app.use(celebrate.errors());
// server error!
app.use(serverErrorHandler);
return app;
}
let app;
let port;
function catchStartUpErrors(e) {
if ('EACCES' === e.code) {
logger.error('Permission Denied, maybe you should run this with sudo?');
} else if ('EADDRINUSE' === e.code) {
logger.error('Unable to listen for connections, this service might already be running?');
}
console.error(e);
process.exit(1);
}
function listen(theApp) {
app = theApp || app;
if (config.get('use_https')) {
// Development only... Ops runs this behind nginx
port = config.get('port');
const tlsoptions = {
cert: fs.readFileSync(config.get('cert_path')),
key: fs.readFileSync(config.get('key_path'))
};
https.createServer(tlsoptions, app).listen(port);
app.on('error', catchStartUpErrors);
} else {
port = config.get('port');
app.listen(port, '0.0.0.0').on('error', catchStartUpErrors);
}
if (isMain) {
logger.info('Firefox Account Content server listening on port', port);
}
return true;
}
function makeHttpRedirectApp () {
const redirectProtocol = config.get('use_https') ? 'https://' : 'http://';
const redirectPort = port === 443 ? '' : ':' + port;
const httpApp = express();
httpApp.get('*', function (req, res) {
const redirectTo = redirectProtocol + req.host + redirectPort + req.url;
res.redirect(301, redirectTo);
});
return httpApp;
}
function listenHttpRedirectApp(httpApp) {
const httpPort = config.get('use_https') ? config.get('redirect_port') : config.get('http_port');
httpApp.listen(httpPort, '0.0.0.0');
if (isMain) {
logger.info('Firefox Account HTTP redirect server listening on port', httpPort);
}
}
function isCorsRequired() {
return config.get('static_resource_url') !== config.get('public_url');
}
if (isMain) {
app = makeApp();
listen(app);
const httpApp = makeHttpRedirectApp();
listenHttpRedirectApp(httpApp);
} else {
module.exports = {
listen: listen,
listenHttpRedirectApp: listenHttpRedirectApp,
makeApp: makeApp,
makeHttpRedirectApp: makeHttpRedirectApp
};
}