apps/mountebank-mock/mountebank-source/src/util/middleware.js (181 lines of code) (raw):

'use strict'; const helpers = require('../util/helpers.js'), errors = require('./errors.js'); /** * Express middleware functions to inject into the HTTP processing * @module */ /** * Returns a middleware function to transforms all outgoing relative links in the response body * to absolute URLs, incorporating the current host name and port * @param {number} port - The port of the current instance * @returns {Function} */ function useAbsoluteUrls (port) { return function (request, response, next) { const setHeaderOriginal = response.setHeader, sendOriginal = response.send, host = request.headers.host || `localhost:${port}`, absolutize = link => `http://${host}${link}`, isObject = helpers.isObject; response.setHeader = function () { const args = Array.prototype.slice.call(arguments); if (args[0] && args[0].toLowerCase() === 'location') { args[1] = absolutize(args[1]); } setHeaderOriginal.apply(this, args); }; response.send = function () { const args = Array.prototype.slice.call(arguments), body = args[0], changeLinks = function (obj) { if (obj._links) { Object.keys(obj._links).forEach(function (rel) { if (obj._links[rel].href) { obj._links[rel].href = absolutize(obj._links[rel].href); } }); } }, traverse = function (obj, fn, parent) { if (parent === 'stubs' || parent === 'response') { // Don't change _links within stubs or within the response // sent back to protocol implementations return; } fn(obj); Object.keys(obj).forEach(key => { if (obj[key] && isObject(obj[key])) { traverse(obj[key], fn, key); } }); }; if (isObject(body)) { traverse(body, changeLinks); // Special case stubs _links. Hard to manage in the traverse function because stubs is an array // and we want to change stubs[]._links but not stubs[]._responses.is.body._links if (Array.isArray(body.stubs)) { body.stubs.forEach(changeLinks); } else if (Array.isArray(body.imposters)) { body.imposters.forEach(imposter => { if (Array.isArray(imposter.stubs)) { imposter.stubs.forEach(changeLinks); } }); } } sendOriginal.apply(this, args); }; next(); }; } /** * Returns a middleware function to return a 404 if the imposter does not exist * @param {Object} imposters - The imposters repository * @returns {Function} */ function createImposterValidator (imposters) { return async function validateImposterExists (request, response, next) { const exists = await imposters.exists(request.params.id); if (exists) { next(); } else { response.statusCode = 404; response.send({ errors: [errors.MissingResourceError('Try POSTing to /imposters first?')] }); } }; } /** * Returns a middleware function that logs the requests made to the server * @param {Object} log - The logger * @param {string} format - The log format * @returns {Function} */ function logger (log, format) { function shouldLog (request) { const isStaticAsset = (['.js', '.css', '.gif', '.png', '.ico'].some(function (fileType) { return request.url.indexOf(fileType) >= 0; })), isHtmlRequest = (request.headers.accept || '').indexOf('html') >= 0, isXHR = request.headers['x-requested-with'] === 'XMLHttpRequest'; return !(isStaticAsset || isHtmlRequest || isXHR); } return function (request, response, next) { if (shouldLog(request)) { const message = format.replace(':method', request.method).replace(':url', request.url); if (request.url.indexOf('_requests') > 0) { // Protocol implementations communicating with mountebank log.debug(message); } else { log.info(message); } } next(); }; } /** * Returns a middleware function that passes global variables to all render calls without * having to pass them explicitly * @param {Object} vars - the global variables to pass * @returns {Function} */ function globals (vars) { return function (request, response, next) { const originalRender = response.render; response.render = function () { const args = Array.prototype.slice.call(arguments), variables = args[1] || {}; Object.keys(vars).forEach(function (name) { variables[name] = vars[name]; }); args[1] = variables; originalRender.apply(this, args); }; next(); }; } /** * The mountebank server uses header-based content negotiation to return either HTML or JSON * for each URL. This breaks down on IE browsers as they fail to send the correct Accept header, * and since we default to JSON (to make the API easier to use), that leads to a poor experience * for IE users. We special case IE to html by inspecting the user agent, making sure not to * interfere with XHR requests that do add the Accept header * @param {Object} request - The http request * @param {Object} response - The http response * @param {Function} next - The next middleware function to call */ function defaultIEtoHTML (request, response, next) { // IE has inconsistent Accept headers, often defaulting to */* // Our default is JSON, which fails to render in the browser on content-negotiated pages if (request.headers['user-agent'] && request.headers['user-agent'].indexOf('MSIE') >= 0) { if (!(request.headers.accept && request.headers.accept.match(/application\/json/))) { request.headers.accept = 'text/html'; } } next(); } /** * Returns a middleware function that defaults the content type to JSON if not set to make * command line testing easier (e.g. you don't have to set the Accept header with curl) and * parses the JSON before reaching a controller, handling errors gracefully. * @param {Object} log - The logger * @returns {Function} */ function json (log) { return function (request, response, next) { // Disable body parsing, if already parsed if (request.headers['content-type'] === 'application/json' && helpers.isObject(request.body)) { next(); return; } request.body = ''; request.setEncoding('utf8'); request.on('data', chunk => { request.body += chunk; }); request.on('end', function () { if (request.body === '') { next(); } else { try { request.body = JSON.parse(request.body); request.headers['content-type'] = 'application/json'; next(); } catch (e) { log.error('Invalid JSON: ' + request.body); response.statusCode = 400; response.send({ errors: [errors.InvalidJSONError({ source: request.body })] }); } } }); }; } function validateApiKey (expectedApiKey, log) { return function (request, response, next) { if (!expectedApiKey) { next(); return; } if (!request.headers['x-api-key']) { log.error('The x-api-key header is required but was not provided'); response.statusCode = 401; response.send({ errors: [errors.UnauthorizedError()] }); return; } const crypto = require('crypto'); const hash = crypto.createHash('sha512'); if (crypto.timingSafeEqual( hash.copy().update(request.headers['x-api-key']).digest(), hash.copy().update(expectedApiKey).digest() )) { next(); } else { log.error('The x-api-key header value does not match the expected API key'); response.statusCode = 401; response.send({ errors: [errors.UnauthorizedError()] }); } }; } module.exports = { useAbsoluteUrls, createImposterValidator, logger, globals, defaultIEtoHTML, json, validateApiKey };