2
\$\begingroup\$

We're converting the City of Philadelphia's website to be served as static files. In order to support redirects and rewrites (URL masking), we decided to use a Lambda@edge function, which modifies (or responds to) the inbound request. I expect this will be maintained by other people so readability is very important. I would love your feedback on how this could be easier to understand and maintain.

const { URL } = require('url')
const { inspect } = require('util')
const TRAILING_SLASH = /\/$/
let compiledRegexes = {}

exports.handler = createHandler(require('../rules.json')) // expose to lambda
exports.createHandler = createHandler // expose for testing

function createHandler (rules) {
  return async function (event) {
    const request = event.Records[0].cf.request
    const cleanPath = request.uri.toLowerCase().replace(TRAILING_SLASH, '')
    log(event)

    for (const rule of rules) {
      let newPath = null

      if (rule.regex) {
        if (!compiledRegexes.hasOwnProperty(rule.pattern)) {
          compiledRegexes[rule.pattern] = new RegExp(rule.pattern)
        }

        const regex = compiledRegexes[rule.pattern]
        if (regex.test(cleanPath)) {
          newPath = cleanPath.replace(regex, rule.replacement)
        }
      } else if (cleanPath === rule.pattern) {
        newPath = rule.replacement
      }

      if (newPath !== null) {
        if (rule.type === 'rewrite') {
          request.uri = newPath || '/'
          if (rule.origin) setOrigin(request, rule.origin)
          log(request)
          return request
        } else {
          const response = createRedirect(newPath)
          log(response)
          return response
        }
      }
    }

    // If no matches, return unmodified request
    log('no match')
    return request
  }
}

// Mutates request object
function setOrigin (request, origin) {
  const url = new URL(origin)
  const protocol = url.protocol.slice(0, -1) // remove trailing colon
  const path = url.pathname.replace(TRAILING_SLASH, '')

  request.origin = {
    custom: {
      domainName: url.hostname,
      protocol,
      port: (protocol === 'https') ? 443 : 80,
      path,
      sslProtocols: ['TLSv1.2', 'TLSv1.1'],
      readTimeout: 5,
      keepaliveTimeout: 5,
      customHeaders: {}
    }
  }
  request.headers.host = [
    { key: 'host', value: url.hostname }
  ]
}

function createRedirect (newUri) {
  return {
    status: '301',
    statusDescription: 'Moved Permanently',
    headers: {
      location: [
        { key: 'Location', value: newUri }
      ]
    }
  }
}

function log (data) {
  if (process.env.NODE_ENV === 'test') return
  // use util.inspect so objects aren't collapsed
  console.log(inspect(data, false, 10))
}

A few example rules from rules.json:

redirects

{
  "pattern": "/old",
  "replacement": "https://example.com/new",
  "type": "redirect"
}

rewrites

{
  "pattern": "/old(/.+)?",
  "regex": true,
  "replacement": "$1",
  "origin": "https://example.com/new",
  "type": "rewrite"
}

(I definitely don't love the "API" of how these rules are configured, and I think they could be simplified or merged. Would love any ideas here too!)

Thanks so much for your time.

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.