2022-02-08 21:30:07 +03:00
import fs from 'fs'
import path from 'path'
2021-07-14 23:49:18 +03:00
import express from 'express'
import instrument from '../lib/instrument-middleware.js'
import haltOnDroppedConnection from './halt-on-dropped-connection.js'
import abort from './abort.js'
import timeout from './timeout.js'
import morgan from 'morgan'
import datadog from './connect-datadog.js'
import cors from './cors.js'
import helmet from 'helmet'
import csp from './csp.js'
import cookieParser from './cookie-parser.js'
import csrf from './csrf.js'
import handleCsrfErrors from './handle-csrf-errors.js'
2022-02-08 03:01:38 +03:00
import { setDefaultFastlySurrogateKey } from './set-fastly-surrogate-key.js'
2021-07-14 23:49:18 +03:00
import setFastlyCacheHeaders from './set-fastly-cache-headers.js'
import reqUtils from './req-utils.js'
import recordRedirect from './record-redirect.js'
import connectSlashes from 'connect-slashes'
import handleErrors from './handle-errors.js'
import handleInvalidPaths from './handle-invalid-paths.js'
import handleNextDataPath from './handle-next-data-path.js'
import detectLanguage from './detect-language.js'
import context from './context.js'
import shortVersions from './contextualizers/short-versions.js'
import redirectsExternal from './redirects/external.js'
import languageCodeRedirects from './redirects/language-code-redirects.js'
import handleRedirects from './redirects/handle-redirects.js'
import findPage from './find-page.js'
2021-12-15 23:21:15 +03:00
import spotContentFlaws from './spot-content-flaws.js'
2021-07-14 23:49:18 +03:00
import blockRobots from './block-robots.js'
import archivedEnterpriseVersionsAssets from './archived-enterprise-versions-assets.js'
import events from './events.js'
import search from './search.js'
2021-12-15 00:24:10 +03:00
import healthz from './healthz.js'
2022-02-23 19:46:29 +03:00
import remoteIP from './remote-ip.js'
2021-07-14 23:49:18 +03:00
import archivedEnterpriseVersions from './archived-enterprise-versions.js'
import robots from './robots.js'
import earlyAccessLinks from './contextualizers/early-access-links.js'
import categoriesForSupport from './categories-for-support.js'
import loaderio from './loaderio-verification.js'
import triggerError from './trigger-error.js'
import releaseNotes from './contextualizers/release-notes.js'
import whatsNewChangelog from './contextualizers/whats-new-changelog.js'
import graphQL from './contextualizers/graphql.js'
import webhooks from './contextualizers/webhooks.js'
import layout from './contextualizers/layout.js'
import currentProductTree from './contextualizers/current-product-tree.js'
import genericToc from './contextualizers/generic-toc.js'
import breadcrumbs from './contextualizers/breadcrumbs.js'
import features from './contextualizers/features.js'
import productExamples from './contextualizers/product-examples.js'
import featuredLinks from './featured-links.js'
import learningTrack from './learning-track.js'
import next from './next.js'
import renderPage from './render-page.js'
2021-12-10 16:01:48 +03:00
import assetPreprocessing from './asset-preprocessing.js'
2022-02-04 19:32:01 +03:00
import archivedAssetRedirects from './archived-asset-redirects.js'
2022-02-16 00:35:08 +03:00
import favicons from './favicons.js'
2022-02-08 03:01:38 +03:00
import setStaticAssetCaching from './static-asset-caching.js'
2020-09-27 15:10:11 +03:00
2022-01-05 21:06:11 +03:00
const { DEPLOYMENT _ENV , NODE _ENV } = process . env
2021-03-10 20:40:25 +03:00
const isDevelopment = NODE _ENV === 'development'
2022-01-05 21:06:11 +03:00
const isAzureDeployment = DEPLOYMENT _ENV === 'azure'
2021-03-10 20:40:25 +03:00
const isTest = NODE _ENV === 'test' || process . env . GITHUB _ACTIONS === 'true'
2020-09-27 15:10:11 +03:00
// Catch unhandled promise rejections and passing them to Express's error handler
// https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016
2021-07-15 00:35:01 +03:00
const asyncMiddleware = ( fn ) => ( req , res , next ) => {
Promise . resolve ( fn ( req , res , next ) ) . catch ( next )
}
2020-09-27 15:10:11 +03:00
2022-02-24 20:54:09 +03:00
// By default, `:remote-addr` is described as following in the morgon docs:
//
// The remote address of the request. This will use req.ip, otherwise
// the standard req.connection.remoteAddress value (socket address).
//
// But in production, by default, `req.ip` is the IP of the Azure machine
// which is something like "104.156.87.177:28244" which is *not* the
// end user. BUT! Because we configure `app.set('trust proxy', true)`
// *before* morgain is enabled, it will use the first entry from
// the `x-forwarded-for` header which is looking like this:
// "75.40.90.27, 157.52.111.52, 104.156.87.177:5786" which is
// "{USER'S IP}, {FASTLY'S POP IP}, {AZURE'S IP}".
// Incidentally, that first IP in the comma separated list is the
// same as the value of `req.headers['fastly-client-ip']` but
// Fastly will put that into the X-Forwarded-IP.
// By leaning in to X-Forwarded-IP (*and* the use
// `app.set('trust proxy', true)`) we can express ourselves here
// without having to use vendor specific headers.
const productionLogFormat = ` :remote-addr - ":method :url" :status - :response-time ms `
2022-01-31 19:54:39 +03:00
2021-07-14 23:49:18 +03:00
export default function ( app ) {
2021-03-09 22:14:02 +03:00
// *** Request connection management ***
2021-06-22 21:27:48 +03:00
if ( ! isTest ) app . use ( timeout )
app . use ( abort )
2021-03-09 22:14:02 +03:00
2022-02-24 20:54:09 +03:00
// Don't use the proxy's IP, use the requester's for rate limiting or
// logging.
// See https://expressjs.com/en/guide/behind-proxies.html
// Essentially, setting this means it believe that the IP is the
// first of the `X-Forwarded-For` header values.
// If it was 0 (or false), the value would be that
// of `req.socket.remoteAddress`.
// Now, the `req.ip` becomes the first entry from x-forwarded-for
// and falls back on `req.socket.remoteAddress` in all other cases.
// Their documentation says:
//
// If true, the client's IP address is understood as the
// left-most entry in the X-Forwarded-For header.
//
app . set ( 'trust proxy' , true )
2022-01-05 21:06:11 +03:00
// *** Request logging ***
// Enabled in development and azure deployed environments
// Not enabled in Heroku because the Heroku router + papertrail already logs the request information
app . use (
2022-01-31 19:54:39 +03:00
morgan ( isAzureDeployment ? productionLogFormat : 'dev' , {
2022-01-05 21:06:11 +03:00
skip : ( req , res ) => ! ( isDevelopment || isAzureDeployment ) ,
} )
)
2020-11-17 20:49:10 +03:00
2020-12-11 21:13:18 +03:00
// *** Observability ***
if ( process . env . DD _API _KEY ) {
2021-06-22 21:27:48 +03:00
app . use ( datadog )
2020-12-11 21:13:18 +03:00
}
2021-12-06 16:00:34 +03:00
// Must appear before static assets and all other requests
// otherwise we won't be able to benefit from that functionality
// for static assets as well.
2021-12-08 22:55:12 +03:00
app . use ( setDefaultFastlySurrogateKey )
2021-12-06 16:00:34 +03:00
// Must come before `csrf` otherwise you get a Set-Cookie on successful
// asset requests. And it can come before `rateLimit` because if it's a
// 200 OK, the rate limiting won't matter anyway.
// archivedEnterpriseVersionsAssets must come before static/assets
app . use (
asyncMiddleware (
instrument ( archivedEnterpriseVersionsAssets , './archived-enterprise-versions-assets' )
)
)
2022-02-03 03:11:59 +03:00
2022-02-16 00:35:08 +03:00
app . use ( favicons )
2022-02-03 03:11:59 +03:00
2022-02-08 03:01:38 +03:00
// Any static URL that contains some sort of checksum that makes it
// unique gets the "manual" surrogate key. If it's checksummed,
// it's bound to change when it needs to change. Otherwise,
// we want to make sure it doesn't need to be purged just because
// there's a production deploy.
// Note, for `/assets/cb-*...` requests,
// this needs to come before `assetPreprocessing` because
2022-02-04 19:55:40 +03:00
// the `assetPreprocessing` middleware will rewrite `req.url` if
// it applies.
2022-02-08 03:01:38 +03:00
app . use ( setStaticAssetCaching )
2022-02-04 19:55:40 +03:00
2022-02-04 19:32:01 +03:00
// Must come before any other middleware for assets
app . use ( archivedAssetRedirects )
2021-12-10 16:01:48 +03:00
// This must come before the express.static('assets') middleware.
app . use ( assetPreprocessing )
2022-02-04 19:55:40 +03:00
2021-12-06 16:00:34 +03:00
app . use (
2021-12-10 16:01:48 +03:00
'/assets/' ,
2021-12-06 16:00:34 +03:00
express . static ( 'assets' , {
index : false ,
etag : false ,
2021-12-10 16:01:48 +03:00
// Can be aggressive because images inside the content get unique
// URLs with a cache busting prefix.
maxAge : '7 days' ,
2022-02-08 21:30:07 +03:00
immutable : process . env . NODE _ENV !== 'development' ,
// This means, that if you request a file that starts with /assets/
// any file doesn't exist, don't bother (NextJS) rendering a
// pretty HTML error page.
fallthrough : false ,
2021-12-06 16:00:34 +03:00
} )
)
app . use (
2021-12-10 16:01:48 +03:00
'/public/' ,
2021-12-06 16:00:34 +03:00
express . static ( 'data/graphql' , {
index : false ,
etag : false ,
maxAge : '7 days' , // A bit longer since releases are more sparse
2022-02-08 21:30:07 +03:00
// See note about about use of 'fallthrough'
fallthrough : false ,
2021-12-06 16:00:34 +03:00
} )
)
2022-02-08 21:30:07 +03:00
// In development, let NextJS on-the-fly serve the static assets.
// But in production, don't let NextJS handle any static assets
// because they are costly to generate (the 404 HTML page)
// and it also means that a CSRF cookie has to be generated.
if ( process . env . NODE _ENV !== 'development' ) {
const assetDir = path . join ( '.next' , 'static' )
if ( ! fs . existsSync ( assetDir ) )
throw new Error ( ` ${ assetDir } directory has not been generated. Run 'npm run build' first. ` )
app . use (
'/_next/static/' ,
express . static ( assetDir , {
index : false ,
etag : false ,
maxAge : '365 days' ,
immutable : true ,
// See note about about use of 'fallthrough'
fallthrough : false ,
} )
)
}
2020-11-17 20:49:10 +03:00
// *** Early exits ***
2021-06-18 22:14:08 +03:00
app . use ( instrument ( handleInvalidPaths , './handle-invalid-paths' ) )
2022-01-26 02:10:18 +03:00
app . use ( asyncMiddleware ( instrument ( handleNextDataPath , './handle-next-data-path' ) ) )
2020-11-17 20:49:10 +03:00
// *** Security ***
2021-06-22 21:27:48 +03:00
app . use ( cors )
2021-07-15 00:35:01 +03:00
app . use (
helmet ( {
// Override referrerPolicy to match the browser's default: "strict-origin-when-cross-origin".
// Helmet now defaults to "no-referrer", which is a problem for our archived assets proxying.
referrerPolicy : {
policy : 'strict-origin-when-cross-origin' ,
} ,
} )
)
2021-06-22 21:27:48 +03:00
app . use ( csp ) // Must come after helmet
app . use ( cookieParser ) // Must come before csrf
2020-11-17 20:49:10 +03:00
app . use ( express . json ( ) ) // Must come before csrf
2021-06-22 21:27:48 +03:00
app . use ( csrf )
app . use ( handleCsrfErrors ) // Must come before regular handle-errors
2020-11-17 20:49:10 +03:00
// *** Headers ***
2021-02-04 01:16:54 +03:00
app . set ( 'etag' , false ) // We will manage our own ETags if desired
2020-11-17 20:49:10 +03:00
// *** Config and context for redirects ***
2021-06-22 21:27:48 +03:00
app . use ( reqUtils ) // Must come before record-redirect and events
app . use ( recordRedirect )
2021-06-18 22:14:08 +03:00
app . use ( instrument ( detectLanguage , './detect-language' ) ) // Must come before context, breadcrumbs, find-page, handle-errors, homepages
app . use ( asyncMiddleware ( instrument ( context , './context' ) ) ) // Must come before early-access-*, handle-redirects
app . use ( asyncMiddleware ( instrument ( shortVersions , './contextualizers/short-versions' ) ) ) // Support version shorthands
2020-11-17 20:49:10 +03:00
2022-02-14 23:19:10 +03:00
// Must come before handleRedirects.
// This middleware might either redirect to serve something.
app . use ( asyncMiddleware ( instrument ( archivedEnterpriseVersions , './archived-enterprise-versions' ) ) )
2020-11-17 20:49:10 +03:00
// *** Redirects, 3xx responses ***
// I ordered these by use frequency
2021-06-22 21:27:48 +03:00
app . use ( connectSlashes ( false ) )
2021-06-18 22:14:08 +03:00
app . use ( instrument ( redirectsExternal , './redirects/external' ) )
app . use ( instrument ( languageCodeRedirects , './redirects/language-code-redirects' ) ) // Must come before contextualizers
app . use ( instrument ( handleRedirects , './redirects/handle-redirects' ) ) // Must come before contextualizers
2020-11-17 20:49:10 +03:00
// *** Config and context for rendering ***
2021-06-18 22:14:08 +03:00
app . use ( asyncMiddleware ( instrument ( findPage , './find-page' ) ) ) // Must come before archived-enterprise-versions, breadcrumbs, featured-links, products, render-page
2021-12-15 23:21:15 +03:00
app . use ( asyncMiddleware ( instrument ( spotContentFlaws , './spot-content-flaws' ) ) ) // Must come after findPage
2021-06-18 22:14:08 +03:00
app . use ( instrument ( blockRobots , './block-robots' ) )
2020-11-17 20:49:10 +03:00
2021-03-09 22:14:02 +03:00
// Check for a dropped connection before proceeding
app . use ( haltOnDroppedConnection )
2020-11-17 20:49:10 +03:00
// *** Rendering, 2xx responses ***
2021-06-18 22:14:08 +03:00
app . use ( '/events' , asyncMiddleware ( instrument ( events , './events' ) ) )
app . use ( '/search' , asyncMiddleware ( instrument ( search , './search' ) ) )
2021-12-15 00:24:10 +03:00
app . use ( '/healthz' , asyncMiddleware ( instrument ( healthz , './healthz' ) ) )
2022-02-23 19:46:29 +03:00
app . get ( '/_ip' , asyncMiddleware ( instrument ( remoteIP , './remoteIP' ) ) )
2021-06-28 22:31:54 +03:00
// Check for a dropped connection before proceeding (again)
app . use ( haltOnDroppedConnection )
2021-06-18 22:14:08 +03:00
app . use ( instrument ( robots , './robots' ) )
2021-07-15 00:35:01 +03:00
app . use (
/(\/.*)?\/early-access$/ ,
instrument ( earlyAccessLinks , './contextualizers/early-access-links' )
)
app . use (
'/categories.json' ,
asyncMiddleware ( instrument ( categoriesForSupport , './categories-for-support' ) )
)
2021-06-18 22:14:08 +03:00
app . use ( instrument ( loaderio , './loaderio-verification' ) )
app . get ( '/_500' , asyncMiddleware ( instrument ( triggerError , './trigger-error' ) ) )
2020-11-17 20:49:10 +03:00
2021-03-09 22:14:02 +03:00
// Check for a dropped connection before proceeding (again)
app . use ( haltOnDroppedConnection )
2021-05-20 17:01:33 +03:00
// *** Preparation for render-page: contextualizers ***
2021-06-18 22:14:08 +03:00
app . use ( asyncMiddleware ( instrument ( releaseNotes , './contextualizers/release-notes' ) ) )
app . use ( instrument ( graphQL , './contextualizers/graphql' ) )
app . use ( instrument ( webhooks , './contextualizers/webhooks' ) )
app . use ( asyncMiddleware ( instrument ( whatsNewChangelog , './contextualizers/whats-new-changelog' ) ) )
app . use ( instrument ( layout , './contextualizers/layout' ) )
app . use ( instrument ( currentProductTree , './contextualizers/current-product-tree' ) )
app . use ( asyncMiddleware ( instrument ( genericToc , './contextualizers/generic-toc' ) ) )
app . use ( asyncMiddleware ( instrument ( breadcrumbs , './contextualizers/breadcrumbs' ) ) )
2021-06-23 22:59:37 +03:00
app . use ( asyncMiddleware ( instrument ( features , './contextualizers/features' ) ) )
2021-06-18 22:14:08 +03:00
app . use ( asyncMiddleware ( instrument ( productExamples , './contextualizers/product-examples' ) ) )
app . use ( asyncMiddleware ( instrument ( featuredLinks , './featured-links' ) ) )
app . use ( asyncMiddleware ( instrument ( learningTrack , './learning-track' ) ) )
2020-11-17 20:49:10 +03:00
2021-02-03 00:37:41 +03:00
// *** Headers for pages only ***
2021-06-22 21:27:48 +03:00
app . use ( setFastlyCacheHeaders )
2021-02-03 00:37:41 +03:00
2021-05-05 18:23:46 +03:00
// handle serving NextJS bundled code (/_next/*)
2021-07-17 00:54:25 +03:00
app . use ( instrument ( next , './next' ) )
2021-05-05 18:23:46 +03:00
2021-03-09 22:14:02 +03:00
// Check for a dropped connection before proceeding (again)
app . use ( haltOnDroppedConnection )
// *** Rendering, must go almost last ***
2021-06-18 22:14:08 +03:00
app . get ( '/*' , asyncMiddleware ( instrument ( renderPage , './render-page' ) ) )
2021-03-09 22:14:02 +03:00
// *** Error handling, must go last ***
2021-06-22 21:27:48 +03:00
app . use ( handleErrors )
2020-09-27 15:10:11 +03:00
}