reserve
Version:
Lightweight http server statically configurable using regular expressions
229 lines (210 loc) • 6.71 kB
JavaScript
const fs = require('fs')
const path = require('path')
const util = require('util')
const IConfiguration = require('./iconfiguration')
const { check } = require('./mapping')
const checkMethod = require('./checkMethod')
const { parse } = require('./schema')
const {
$configurationInterface,
$configurationRequests,
$handlerMethod,
$handlerSchema
} = require('./symbols')
const readFileAsync = util.promisify(fs.readFile)
const statAsync = util.promisify(fs.stat)
const defaultHandlers = {
custom: require('./handlers/custom'),
file: require('./handlers/file'),
status: require('./handlers/status'),
url: require('./handlers/url'),
use: require('./handlers/use')
}
const defaults = {
hostname: undefined,
port: 5000,
'max-redirect': 10,
listeners: [],
mappings: [{
match: /^\/proxy\/(https?)\/(.*)/,
url: '$1://$2',
'unsecure-cookies': true
}, {
match: '(.*)',
file: './$1'
}]
}
function applyDefaults (configuration) {
Object.keys(defaults).forEach(property => {
if (!Object.prototype.hasOwnProperty.call(configuration, property)) {
configuration[property] = defaults[property]
}
})
}
function getHandler (handlers, types, mapping) {
for (let index = 0; index < types.length; ++index) {
const type = types[index]
const redirect = mapping[type]
if (redirect !== undefined) {
return {
handler: handlers[type],
redirect,
type
}
}
}
return {}
}
function checkHandler (handler, type) {
if (handler.schema) {
handler[$handlerSchema] = parse(handler.schema)
delete handler.schema
}
if (handler.method) {
checkMethod(handler, $handlerMethod)
delete handler.method
}
if (typeof handler.redirect !== 'function') {
throw new Error('Invalid "' + type + '" handler: redirect is not a function')
}
}
function validateHandler (type) {
const handlers = this.handlers
let handler = handlers[type]
if (typeof handler === 'string') {
handler = require(handler)
handlers[type] = handler
}
checkHandler(handler, type)
Object.freeze(handler)
}
function setHandlers (configuration) {
if (configuration.handlers) {
// Default hanlders can't be overridden
configuration.handlers = Object.assign({}, configuration.handlers, defaultHandlers)
} else {
configuration.handlers = Object.assign({}, defaultHandlers)
}
Object.keys(configuration.handlers).forEach(validateHandler.bind(configuration))
configuration.handler = getHandler.bind(null, configuration.handlers, Object.keys(configuration.handlers))
}
function invalidListeners () {
throw new Error('Invalid listeners member, must be an array of functions')
}
function checkListeners (configuration) {
const listeners = configuration.listeners
if (!Array.isArray(listeners)) {
invalidListeners()
}
configuration.listeners = listeners.map(register => {
let registerType = typeof register
if (registerType === 'string') {
register = require(path.join(configuration.cwd || process.cwd(), register))
registerType = typeof register
}
if (registerType !== 'function') {
invalidListeners()
}
return register
})
}
async function readSslFile (configuration, filePath) {
if (path.isAbsolute(filePath)) {
return (await readFileAsync(filePath)).toString()
}
return (await readFileAsync(path.join(configuration.ssl.cwd, filePath))).toString()
}
async function checkProtocol (configuration) {
if (configuration.ssl) {
configuration.ssl.cert = await readSslFile(configuration, configuration.ssl.cert)
configuration.ssl.key = await readSslFile(configuration, configuration.ssl.key)
configuration.protocol = 'https'
} else {
configuration.protocol = 'http'
}
if (![true, false, undefined].includes(configuration.http2)) {
throw new Error('Invalid http2 setting')
}
if (!configuration.http2) {
configuration.http2 = false
}
}
async function checkMappings (configuration) {
const configurationInterface = new IConfiguration(configuration)
configuration[$configurationInterface] = configurationInterface
for await (const mapping of configuration.mappings) {
await check(configuration, mapping)
}
}
function setCwd (folderPath, configuration) {
if (configuration.handlers) {
Object.keys(configuration.handlers).forEach(prefix => {
const handler = configuration.handlers[prefix]
if (typeof handler === 'string' && handler.match(/^\.\.?\//)) {
configuration.handlers[prefix] = path.join(folderPath, handler)
}
})
}
if (configuration.mappings) {
configuration.mappings.forEach(mapping => {
if (!mapping.cwd) {
mapping.cwd = folderPath
}
})
}
if (configuration.ssl && !configuration.ssl.cwd) {
configuration.ssl.cwd = folderPath
}
configuration.cwd = folderPath
}
function extend (filePath, configuration) {
const folderPath = path.dirname(filePath)
setCwd(folderPath, configuration)
if (configuration.extend) {
const basefilePath = path.join(folderPath, configuration.extend)
delete configuration.extend
return readFileAsync(basefilePath)
.then(buffer => JSON.parse(buffer.toString()))
.then(baseConfiguration => {
// Only merge mappings
const baseMappings = baseConfiguration.mappings
const mergedConfiguration = Object.assign(baseConfiguration, configuration)
if (baseMappings && baseMappings !== mergedConfiguration.mappings) {
mergedConfiguration.mappings = [...configuration.mappings, ...baseMappings]
}
return extend(basefilePath, mergedConfiguration)
})
}
return configuration
}
module.exports = {
async check (configuration) {
if (typeof configuration !== 'object' || configuration === null) {
throw new Error('Configuration must be an object')
}
const checkedConfiguration = Object.assign({}, configuration)
applyDefaults(checkedConfiguration)
setHandlers(checkedConfiguration)
checkListeners(checkedConfiguration)
await checkProtocol(checkedConfiguration)
await checkMappings(checkedConfiguration)
checkedConfiguration[$configurationRequests] = {
lastId: 0,
holding: Promise.resolve(),
contexts: []
}
return checkedConfiguration
},
async read (fileName) {
let filePath
if (path.isAbsolute(fileName)) {
filePath = fileName
} else {
filePath = path.join(process.cwd(), fileName)
}
return statAsync(filePath)
.then(() => readFileAsync(filePath).then(buffer => JSON.parse(buffer.toString())))
.then(configuration => extend(filePath, configuration))
}
}