UNPKG

express-gateway

Version:

A microservices API gateway built on top of ExpressJS

250 lines (215 loc) 7.93 kB
const log = require('../logger').gateway; const policies = require('../policies'); const EgContextBase = require('./context'); const ActionParams = require('./actionParams'); const express = require('express'); const vhost = require('vhost'); const ConfigurationError = require('../errors').ConfigurationError; /* * A pipeline consists of an apiEndpoint and a set of policies. * To bootstrap, iterate through all pipelines and create a specific router for each. * Each router will handle requests to the apiEndpoints specified in its pipeline. * * An example of pipelines config: * pipelines: { * pipeline1: { * apiEndpoints: ['parrots'], * policies: [ * { * test: [ * { * action: { * param1: 'black_beak', * param2: 'someOtherParam' * } * }, * { * action: { * param1: 'red_beak', * param2: 'someOtherParam' * } * } * ] * } * ] * } * } * } */ module.exports.bootstrap = function ({app, config}) { if (!validateGatewayConfig(config)) { return app; } const apiEndpointToPipelineMap = {}; for (const name in config.gatewayConfig.pipelines) { log.info(`processing pipeline ${name}`); const pipeline = config.gatewayConfig.pipelines[name]; const router = configurePipeline(pipeline.policies || [], config); 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 = (req, res, next) => { log.debug('executing pipeline for api %s, mounted at %s', route.apiEndpointName, path); req.egContext = Object.create(new EgContextBase()); req.egContext.req = req; req.egContext.res = res; req.egContext.apiEndpoint = route; req.egContext.apiEndpoint.scopes = req.egContext.apiEndpoint.scopes || []; if (!Array.isArray(req.egContext.apiEndpoint.scopes)) { req.egContext.apiEndpoint.scopes = [req.egContext.apiEndpoint.scopes]; } return apiEndpointToPipelineMap[route.apiEndpointName](req, res, next); }; 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)); } } return app; }; function 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); pipelinePoliciesConfig.forEach(policyConfig => { const policyName = Object.keys(policyConfig)[0]; let policySteps = policyConfig[policyName]; if (!policySteps) { policySteps = []; } if (!Array.isArray(policySteps)) { policySteps = [ policySteps ]; } const policy = policies.resolve(policyName).policy; if (policySteps.length === 0) { policySteps.push({}); } for (const policyStep of policySteps) { const condition = policyStep.condition; // parameters that we pass to the policy at time of execution const action = policyStep.action || {}; Object.assign(action, ActionParams.prototype); const policyMiddleware = policy(action, config); router.use((req, res, next) => { if (!condition || req.matchEGCondition(condition)) { log.debug('request matched condition for action', policyStep.action, 'in policy', policyName); policyMiddleware(req, res, next); } else { log.debug(`request did not matched condition for action`, policyStep.action, 'in policy', policyName); next(); } }); } }); return router; } function validateGatewayConfig (config) { if (!config || !config.gatewayConfig) { throw new ConfigurationError('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; } function validatePipelinePolicies (policies, config) { const policiesConfig = config.gatewayConfig.policies || []; policies.forEach(policyObj => { const policyNames = Object.keys(policyObj); if (policyNames.length !== 1) { throw new ConfigurationError('there should be one and only one policy per policy-object in pipeline configuration'); } if (policiesConfig.indexOf(policyNames[0]) === -1) { throw new ConfigurationError(`${policyNames[0]} policy not declared`); } }); }