UNPKG

express-gateway

Version:

A microservices API gateway built on top of ExpressJS

276 lines (222 loc) 8.47 kB
const express = require('express'); const flatMap = require('lodash.flatmap'); const vhost = require('vhost'); const chalk = require('chalk'); const log = require('../logger').gateway; const policies = require('../policies'); const EgContextBase = require('./context'); const ActionParams = require('./actionParams'); const { conditions } = require('../conditions'); const conditionDefinitions = require('../conditions/predefined'); module.exports.bootstrap = async function ({ app, config }) { if (!normalizeGatewayConfig(config)) { return app; } const apiEndpointToPipelineMap = {}; for (const name in config.gatewayConfig.pipelines) { log.verbose(`processing pipeline ${name}`); const pipeline = config.gatewayConfig.pipelines[name]; const configuredPipeline = configurePipeline(pipeline.policies || [], config); const router = configuredPipeline instanceof Promise ? await configuredPipeline : configuredPipeline; for (const apiName of pipeline.apiEndpoints) { apiEndpointToPipelineMap[apiName] = router; } } const apiEndpoints = processApiEndpoints(config.gatewayConfig.apiEndpoints); for (const el in apiEndpoints) { const host = el; const hostConfig = apiEndpoints[el]; const router = express.Router(); log.debug('processing vhost %s %j', host, hostConfig.routes); for (const route of hostConfig.routes) { let mountPaths = []; route.paths = route.paths || route.path; if (route.pathRegex) { mountPaths.push(RegExp(route.pathRegex)); } else if (route.paths && route.paths.length) { mountPaths = mountPaths.concat(route.paths); } else { mountPaths.push('*'); } for (const path of mountPaths) { log.debug('mounting routes for apiEndpointName %s, mount %s', route.apiEndpointName, path); const handler = generatePipelineHandler({ path, pipeline: apiEndpointToPipelineMap[route.apiEndpointName], route }); if (route.methods && route.methods !== '*') { log.debug('methods specified, registering for each method individually'); const methods = Array.isArray(route.methods) ? route.methods : route.methods.split(','); for (const method of methods) { const m = method.trim().toLowerCase(); if (m) { router[m](path, handler); } } } else { log.debug('no methods specified. handle all mode.'); router.all(path, handler); } } } if (!host || host === '*') { app.use(router); } else { const virtualHost = hostConfig.isRegex ? new RegExp(host) : host; app.use(vhost(virtualHost, router)); } } if (process.env.LOG_LEVEL === 'debug') { app.use((req, res, next) => { if (!req.route) { log.debug('No pipeline matched, returning 404.'); } next(); }); } return app; }; const processApiEndpoints = (apiEndpoints) => { const cfg = {}; log.debug('loading apiEndpoints %j', apiEndpoints); for (const el in apiEndpoints) { const apiEndpointName = el; let endpointConfigs = apiEndpoints[el]; // apiEndpoint can be array or object {host, paths, methods, ...} endpointConfigs = Array.isArray(endpointConfigs) ? endpointConfigs : [endpointConfigs]; endpointConfigs.forEach(endpointConfig => { let host = endpointConfig.hostRegex; let isRegex = true; if (!host) { host = endpointConfig.host || '*'; isRegex = false; } cfg[host] = cfg[host] || { isRegex, routes: [] }; log.debug('processing host: %s, isRegex: %s', host, cfg[host].isRegex); const route = Object.assign({ apiEndpointName }, endpointConfig); log.debug('adding route to host: %s, %j', host, route); cfg[host].routes.push(route); }); } return cfg; }; function configurePipeline(pipelinePoliciesConfig, config) { const router = express.Router({ mergeParams: true }); if (!Array.isArray(pipelinePoliciesConfig)) { pipelinePoliciesConfig = [pipelinePoliciesConfig]; } validatePipelinePolicies(pipelinePoliciesConfig, config.gatewayConfig.policies || []); pipelinePoliciesConfig.forEach(policyConfig => { const policyName = Object.keys(policyConfig)[0]; let policySteps = policyConfig[policyName]; if (!policySteps) { policySteps = []; } else if (!Array.isArray(policySteps)) { policySteps = [policySteps]; } const policy = policies.resolve(policyName).policy; if (policySteps.length === 0) { policySteps.push({}); } const middlewares = flatMap(policySteps, policyStep => { const conditionConfig = policyStep.condition; let condition; let middlewares; if (conditionConfig) { condition = conditions[conditionConfig.name](conditionConfig); const cd = conditionDefinitions.find(cd => cd.name === conditionConfig.name); if (cd) { middlewares = cd.middlewares; } } const action = Object.assign({}, policyStep.action, ActionParams.prototype); const policyMiddleware = policy(action, config); if (condition instanceof Promise) { return condition.then(condFn => createConditionAndActionMiddleware(conditionConfig, condFn, policyName, policyMiddleware, middlewares)); } return createConditionAndActionMiddleware(conditionConfig, condition, policyName, policyMiddleware, middlewares); }); return Promise.all(middlewares).then(resolvedMiddlewares => router.use(resolvedMiddlewares)); }); return router; } function normalizeGatewayConfig(config) { if (!config || !config.gatewayConfig) { throw new Error('No config provided'); } const gatewayConfig = config.gatewayConfig; if (!gatewayConfig.pipelines) { if (gatewayConfig.pipeline) { gatewayConfig.pipelines = Array.isArray(gatewayConfig.pipeline) ? gatewayConfig.pipeline : [gatewayConfig.pipeline]; } else { return false; } } if (!gatewayConfig.apiEndpoints) { if (gatewayConfig.apiEndpoint) { gatewayConfig.apiEndpoints = Array.isArray(gatewayConfig.apiEndpoint) ? gatewayConfig.apiEndpoint : [gatewayConfig.apiEndpoint]; } else { return false; } } for (const name in gatewayConfig.pipelines) { const pipeline = gatewayConfig.pipelines[name]; if (!pipeline.apiEndpoints) { pipeline.apiEndpoints = pipeline.apiEndpoint; } if (!Array.isArray(pipeline.apiEndpoints)) { pipeline.apiEndpoints = [pipeline.apiEndpoints]; } if (!pipeline.policies) { pipeline.policies = pipeline.policy; } if (!Array.isArray(pipeline.policies)) { pipeline.policies = [pipeline.policies]; } } return true; } const validatePipelinePolicies = (policies, avaiablePolicies) => { policies.forEach(policyObj => { const policyNames = Object.keys(policyObj); if (avaiablePolicies.indexOf(policyNames[0]) === -1) { log.error(`${policyNames[0]} policy not declared in the 'policies' gateway.config section`); throw new Error('POLICY_NOT_DECLARED'); } }); }; const generatePipelineHandler = ({ path, pipeline, route }) => { if (!pipeline) { log.debug(`No suitable pipeline found for ${route.apiEndpointName}`); return (req, res, next) => res.sendStatus(404); } log.debug('executing pipeline for api %s, mounted at %s', route.apiEndpointName, path); if (!route.scopes) { route.scopes = []; } else if (!Array.isArray(route.scopes)) { route.scopes = [route.scopes]; } return (req, res, next) => { req.egContext = Object.create(new EgContextBase()); req.egContext.req = req; req.egContext.res = res; req.egContext.apiEndpoint = route; return pipeline(req, res, next); }; }; function createConditionAndActionMiddleware(conditionConfig, conditionFunction, policyName, policyMiddleware, middlewares) { const fn = (req, res, next) => { if (conditionConfig) { if (conditionFunction(req)) { log.debug(`request matched condition in ${chalk.green(policyName)} policy `); return policyMiddleware(req, res, next); } else { log.debug(`request did not match condition in ${chalk.green(policyName)} policy`); return next(); } } return policyMiddleware(req, res, next); }; if (middlewares) { return [...middlewares, fn]; } return fn; }