/** * 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-flex-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 TRANSIT_VERBOSE = process.env.REACT_APP_SHARETRIBE_SDK_TRANSIT_VERBOSE === 'true'; 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 baseUrl = BASE_URL ? { baseUrl: BASE_URL } : {}; const sdk = sharetribeSdk.createInstance({ transitVerbose: TRANSIT_VERBOSE, clientId: CLIENT_ID, 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), }, ], ...baseUrl, }); // 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) { // Dig out the value of the given CSP report key from the request body. const reportValue = (req, key) => { const report = req.body ? req.body['csp-report'] : null; return report && report[key] ? report[key] : key; }; // Handler for CSP violation reports. app.post(cspReportUrl, (req, res) => { const effectiveDirective = reportValue(req, 'effective-directive'); const blockedUri = reportValue(req, 'blocked-uri'); const msg = `CSP: ${effectiveDirective} doesn't allow ${blockedUri}`; log.error(new Error(msg), 'csp-violation'); 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!`); } });