Source: addMiddlewares.js


'use strict'

// pour json et urlencode on prend celui inclut dans express,
// mais y'a pas text-plain, on prend body-parser
const bodyParser = require('body-parser')
const express = require('express')
const { formatTime } = require('sesajs-date')
const { hasProp } = require('sesajstools')

const tools = require('./lib/tools')
const config = require('./config')
const applog = require('an-log')(config.application.name)

// 10min pour la majorité du statique
const shortStaticTtl = 600
// 24h pour ce qui sort de webpack (y'a etag qui jouera aussi)
const staticTtl = 3600 * 24
// const publicTtl = 3600 * 4 // 4h seulement pour les résultats de recherche ou les ressources

/**
 * Ajoute notre bodyParser après le middleware cookie
 * @param rail
 */
function addBodyParsers (rail) {
  if (config.$rail.noBodyParser) {
    const dateRegExp = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/
    const bodyParserSettings = config.$rail.bodyParser || {
      reviver: (key, value) => (typeof value === 'string' && dateRegExp.exec(value)) ? new Date(value) : value
    }
    if (!bodyParserSettings.limit) bodyParserSettings.limit = '10mb'
    // ça c'est juste pour urlencoded, à priori on devrait pouvoir avoir des tableaux avec false
    // mais ça plante par ex sur un post de form avec `categories[2]: true`
    if (!hasProp(bodyParserSettings, 'extended')) bodyParserSettings.extended = true
    const jsonMiddleware = express.json(bodyParserSettings)
    const urlencodedMiddleware = express.urlencoded(bodyParserSettings)
    const textMiddleware = bodyParser.text(bodyParserSettings)

    rail.use('/api', jsonMiddleware)
    rail.use(['/groupe', '/ressource'], urlencodedMiddleware)
    // lui doit aussi accepter du text/plain (envoyé par sendBeacon)
    rail.use('/api/deferPost', textMiddleware)
  } else {
    log.error('Il manque le settings $rail.noBodyParser pour mettre nos propres parsers')
  }
} // addBodyParsers

/**
 * Ajoute sur le rail les requetes en console (en dev), CORS, expires, access.log et perf.log
 * @param {Object} rail le rail express
 */
function addCorsAndLog (rail) {
  // ajout d'express en global sur lassi (utilisé dans les tests pour le passer à supertest)
  lassi.express = rail
  /**
   * Ajout du CORS (et timestamp dans res.locals.start)
   */
  applog('adding middleware', 'CORS')
  const knownOrigins = {}
  rail.use('/', function (req, res, next) {
    // un timestamp
    req.start = formatTime()
    const origin = req.header('Origin')
    // le public est mis en cache, faut donc autoriser pour tout le monde (sinon faut filtrer sur varnish)
    if (tools.isStatic(req.url) || tools.isPublic(req.url)) {
      res.header('Access-Control-Allow-Origin', '*')
    } else if (origin) {
      // ça dépend de l'appelant, on regarde ceux que l'on a déjà autorisé
      let isKnown = knownOrigins[origin]
      // ceux-là sont toujours autorisés
      if (!isKnown) {
        isKnown = /https?:\/\/([^/]+\.)?(sesamath\.net|sesamath\.dev|local|localhost)(:[0-9]+)?(\/|$)/.test(origin)
        // si pas trouvé, on autorise aussi les sesalab déclarés en configuration
        if (!isKnown && config.sesalabs && config.sesalabs.length) isKnown = config.sesalabs.some((sesalab) => sesalab.baseUrl && sesalab.baseUrl === origin + '/')
        // et on ajoute si trouvé pour pas chercher la prochaine fois
        if (isKnown) knownOrigins[origin] = true
      }
      if (isKnown) {
        res.header('Access-Control-Allow-Origin', origin)
        res.header('Access-Control-Allow-Credentials', 'true')

        // cf https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
        // If the server specifies an origin host rather than '*', then it must also include Origin in the Vary response header
        // to indicate to clients that server responses will differ based on the value of the Origin request header.
        res.header('Vary', 'Origin')

        // ça aide pour ff ? http://stackoverflow.com/a/17957579
        // res.header('Access-Control-Expose-Headers','Access-Control-Allow-Origin');

        /* Apparemmet pas utile
        if (req.headers['access-control-request-method']) {
          res.header('Access-Control-Allow-Methods', req.headers['access-control-request-method'])
        } else {
          res.header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS')
        } /* on laisse le classique */
        res.header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS')

        // sans cela sur l'options du preflight firefox refuse de faire le post (ça répond 'pas de connexion réseau' car xhr.status vaut 0)
        if (req.method === 'OPTIONS' && req.headers['access-control-request-headers']) {
          // ici, on pourrait filtrer pour n'autoriser que certains header, mais on autorise le navigateur
          // à nous envoyer ce qu'il veut comme header, au pire on les ignore…
          res.header('Access-Control-Allow-Headers', req.headers['access-control-request-headers'])
        } /* */
      } else if (origin.substr(0, 7) === 'file://') {
        // pour le moment on accepte les requete depuis du file:// pour autoriser editgraphe de j3p en local
        res.header('Access-Control-Allow-Origin', origin)
        res.header('Access-Control-Allow-Credentials', 'true')
        res.header('Vary', 'Origin,Cookie')
      } else {
        log.debug('cors avec ' + origin + ' refusé')
      }
    }
    next()
  })

  /**
   * headers expires sur le statique ou le json public
   * @todo le mettre aussi sur le html public (quand le source sera indépendant de la session)
   */
  applog('adding middleware', 'expires')
  rail.use('/', function (req, res, next) {
    // pas de cache sur le display, car coté client élève dans sesalab y'a pas moyen de connaître ressource.inc
    // pour en déduire un $displayUrl fiable (le rid est enregistré dans la séquence)
    // idem pour l'api, on laisse express gérer le etag (ça fait du 304 not modified si y'a pas de changement)
    if (tools.isStatic(req.url) || /\/public\//.test(req.url)) {
      // avant 2020-04-07 on avait expires au format de la RFC 1123, on ne garde que max-age car il doit plus rester bcp de http/1.0
      // On ne met 24h de cache que sur ce qui sort de webpack (nom de fichier avec minuscules et chiffres de longueur 20 minimum)
      // ça match pas les urls /api/public/xxxxx qui n'ont pas de point dans l'id xxxxx
      if (/\/[a-z0-9]{20,}\./.test(req.url)) {
        res.header('Cache-Control', `public, max-age=${staticTtl}, stale-while-revalidate=${shortStaticTtl}`)
      } else {
        // sinon on met un délai bcp plus court (etag & 304 continueront de jouer leur role ensuite, et varnish fera un check toutes les 10min)
        // cf https://www.keycdn.com/blog/keycdn-supports-stale-while-revalidate
        res.header('Cache-Control', `public, max-age=${shortStaticTtl}, stale-while-revalidate=${shortStaticTtl}`)
      }
    }
    next()
  })

  // access.log géré par lassi

  /**
   * En dev, ajout des requetes http en console et dans le log de debug
   */
  if (!global.isProd) {
    applog('adding middleware', 'ajout access log en console (car on est pas en prod)')
    rail.use('/', function (req, res, next) {
      // les requetes non statiques en console et debug
      if (!/\.(js|css|png|jpg|jpeg)/.exec(req.originalUrl)) {
        applog(req.method, req.originalUrl)
        log.debug('requete ' + req.method + ' ' + req.originalUrl)
      }
      next()
    })
  }

  /**
   * perf.log
   */
  if (log.perf.out) {
    // on veut logger les perfs, on ajoute response.perf, msg sera écrit dans le log après les contrôleurs
    applog('adding middleware', 'perf.log')
    rail.use('/', function (request, response, next) {
      response.perf = {
        // message stocké en context qui sera écrit dans le listener beforeTransport
        msg: request.method + ' ' + request.originalUrl,
        start: Date.now()
      }
      // on est après body-parser (-2 pour le {} ajouté au stringify)
      // if (request.body) response.perf.msg += '\treceived: ' +(tools.stringify(request.body).length -2)
      const received = request.headers['content-length']
      if (received) response.perf.msg += '\treceived: ' + received
      // on peut pas mettre de middleware après les controlleur, car response.end() sera appelé et les middleware ignorés
      // on ajoute donc un listener sur finish (appelé sans arguments, et c'est le seul event de response)
      response.on('finish', function () {
        const cl = this.get('Content-Length')
        if (cl) response.perf.msg += '\tsent:' + cl
        log.perf.out(response)
      })
      next()
    })
  }
} // addCorsAndLog

/**
 * Hooks qui seront ajoutés sur le rail par app/server/index.js
 */
module.exports = {
  addBodyParsers,
  addCorsAndLog
}