flex-template-web/server/index.js
Mikko Koski 0c6cb3c950 Use http.Agent with keepalive set to true
Instantiate HTTP(S) Agents with keepAlive set to true.
This will reduce the request time for consecutive requests by
reusing the existing TCP connection, thus eliminating the time used
for setting up new TCP connections. The reduced response time can
be as much as 200ms.
2018-03-19 09:15:02 +02:00

256 lines
8.5 KiB
JavaScript

/**
* This is the main server to run the production application.
*
* Running the server requires that `npm run build` is run so that the
* production JS bundle can be imported.
*
* This server renders the requested URL in the server side for
* performance, SEO, etc., and properly handles redirects, HTTP status
* codes, and serving the static assets.
*
* When the application is loaded in a browser, the client side app
* takes control and all the functionality such as routing is handled
* in the client.
*/
// This enables nice stacktraces from the minified production bundle
require('source-map-support').install();
// Configure process.env with .env.* files
require('./env').configureEnv();
const http = require('http');
const https = require('https');
const express = require('express');
const helmet = require('helmet');
const compression = require('compression');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const enforceSsl = require('express-enforces-ssl');
const path = require('path');
const sharetribeSdk = require('sharetribe-sdk');
const Decimal = require('decimal.js');
const sitemap = require('express-sitemap');
const auth = require('./auth');
const renderer = require('./renderer');
const dataLoader = require('./dataLoader');
const fs = require('fs');
const log = require('./log');
const { sitemapStructure } = require('./sitemap');
const csp = require('./csp');
const buildPath = path.resolve(__dirname, '..', 'build');
const env = process.env.REACT_APP_ENV;
const dev = process.env.REACT_APP_ENV === 'development';
const PORT = parseInt(process.env.PORT, 10);
const CLIENT_ID = process.env.REACT_APP_SHARETRIBE_SDK_CLIENT_ID;
const BASE_URL = process.env.REACT_APP_SHARETRIBE_SDK_BASE_URL;
const USING_SSL = process.env.REACT_APP_SHARETRIBE_USING_SSL === 'true';
const TRUST_PROXY = process.env.SERVER_SHARETRIBE_TRUST_PROXY || null;
const CSP = process.env.REACT_APP_CSP;
const cspReportUrl = '/csp-report';
const cspEnabled = CSP === 'block' || CSP === 'report';
const app = express();
const errorPage = fs.readFileSync(path.join(buildPath, '500.html'), 'utf-8');
// load sitemap and robots file structure
// and write those into files
sitemap(sitemapStructure()).toFile();
// Setup error logger
log.setup();
// Add logger request handler. In case Sentry is set up
// request information is added to error context when sent
// to Sentry.
app.use(log.requestHandler());
// The helmet middleware sets various HTTP headers to improve security.
// See: https://www.npmjs.com/package/helmet
app.use(helmet());
if (cspEnabled) {
// When a CSP directive is violated, the browser posts a JSON body
// to the defined report URL and we need to parse this body.
app.use(
bodyParser.json({
type: ['json', 'application/csp-report'],
})
);
// CSP can be turned on in report or block mode. In report mode, the
// browser checks the policy and calls the report URL when the
// policy is violated, but doesn't block any requests. In block
// mode, the browser also blocks the requests.
const reportOnly = CSP === 'report';
app.use(csp(cspReportUrl, USING_SSL, reportOnly));
}
// Redirect HTTP to HTTPS if USING_SSL is `true`.
// This also works behind reverse proxies (load balancers) as they are for example used by Heroku.
// In such cases, however, the TRUST_PROXY parameter has to be set (see below)
//
// Read more: https://github.com/aredo/express-enforces-ssl
//
if (USING_SSL) {
app.use(enforceSsl());
}
// Set the TRUST_PROXY when running the app behind a reverse proxy.
//
// For example, when running the app in Heroku, set TRUST_PROXY to `true`.
//
// Read more: https://expressjs.com/en/guide/behind-proxies.html
//
if (TRUST_PROXY === 'true') {
app.enable('trust proxy');
} else if (TRUST_PROXY === 'false') {
app.disable('trust proxy');
} else if (TRUST_PROXY !== null) {
app.set('trust proxy', TRUST_PROXY);
}
app.use(compression());
app.use('/static', express.static(path.join(buildPath, 'static')));
// server robots.txt from the root
app.use('/robots.txt', express.static(path.join(buildPath, 'robots.txt')));
app.use(cookieParser());
// Use basic authentication when not in dev mode. This is
// intentionally after the static middleware to skip basic auth for
// static resources.
if (!dev) {
const USERNAME = process.env.BASIC_AUTH_USERNAME;
const PASSWORD = process.env.BASIC_AUTH_PASSWORD;
const hasUsername = typeof USERNAME === 'string' && USERNAME.length > 0;
const hasPassword = typeof PASSWORD === 'string' && PASSWORD.length > 0;
// If BASIC_AUTH_USERNAME and BASIC_AUTH_PASSWORD have been set - let's use them
if (hasUsername && hasPassword) {
app.use(auth.basicAuth(USERNAME, PASSWORD));
}
}
const noCacheHeaders = {
'Cache-control': 'no-cache, no-store, must-revalidate',
};
// Instantiate HTTP(S) Agents with keepAlive set to true.
// This will reduce the request time for consecutive requests by
// reusing the existing TCP connection, thus eliminating the time used
// for setting up new TCP connections.
const httpAgent = new http.Agent({ keepAlive: true });
const httpsAgent = new https.Agent({ keepAlive: true });
app.get('*', (req, res) => {
if (req.url.startsWith('/static/')) {
// The express.static middleware only handles static resources
// that it finds, otherwise passes them through. However, we don't
// want to render the app for missing static resources and can
// just return 404 right away.
return res.status(404).send('Static asset not found.');
}
if (req.url === '/_status.json') {
return res.status(200).send({ status: 'ok' });
}
const context = {};
// Get handle to tokenStore
// We check in unauthorized cases if requests have set tokens to cookies
const tokenStore = sharetribeSdk.tokenStore.expressCookieStore({
clientId: CLIENT_ID,
req,
res,
secure: USING_SSL,
});
const sdk = sharetribeSdk.createInstance({
clientId: CLIENT_ID,
baseUrl: BASE_URL,
httpAgent: httpAgent,
httpsAgent: httpsAgent,
tokenStore,
typeHandlers: [
{
type: sharetribeSdk.types.BigDecimal,
customType: Decimal,
writer: v => new sharetribeSdk.types.BigDecimal(v.toString()),
reader: v => new Decimal(v.value),
},
],
});
// Until we have a better plan for caching dynamic content and we
// make sure that no sensitive data can appear in the prefetched
// data, let's disable response caching altogether.
res.set(noCacheHeaders);
dataLoader
.loadData(req.url, sdk)
.then(preloadedState => {
const html = renderer.render(req.url, context, preloadedState);
if (dev) {
const debugData = {
url: req.url,
context,
};
console.log(`\nRender info:\n${JSON.stringify(debugData, null, ' ')}`);
}
if (context.unauthorized) {
// Routes component injects the context.unauthorized when the
// user isn't logged in to view the page that requires
// authentication.
const token = tokenStore.getToken();
const refreshTokenExists = !!token && !!token.refresh_token;
if (refreshTokenExists) {
// If refresh token exists, we assume that client can handle the situation
// TODO: improve by checking if the token is valid (needs an API call)
res.status(200).send(html);
} else {
res.status(401).send(html);
}
} else if (context.forbidden) {
res.status(403).send(html);
} else if (context.url) {
// React Router injects the context.url if a redirect was rendered
res.redirect(context.url);
} else if (context.notfound) {
// NotFoundPage component injects the context.notfound when a
// 404 should be returned
res.status(404).send(html);
} else {
res.send(html);
}
})
.catch(e => {
log.error(e, 'server-side-render-failed');
res.status(500).send(errorPage);
});
});
// Set error handler. If Sentry is set up, all error responses
// will be logged there.
app.use(log.errorHandler());
if (cspEnabled) {
// Handler for CSP violation reports.
app.post(cspReportUrl, (req, res) => {
const report = req.body ? req.body['csp-report'] : null;
log.error(new Error('CSP violation'), 'csp-violation', report);
res.status(204).end();
});
}
app.listen(PORT, () => {
const mode = dev ? 'development' : 'production';
console.log(`Listening to port ${PORT} in ${mode} mode`);
if (dev) {
console.log(`Open http://localhost:${PORT}/ and start hacking!`);
}
});