UNPKG

@luminati-io/mountebank

Version:

Over the wire test doubles

208 lines (174 loc) 7.95 kB
'use strict'; /** * The base implementation of http/s servers * @module */ const net = require('net'), headersMap = require('./headersMap.js'), errors = require('../../util/errors.js'), httpProxy = require('./httpProxy.js'), httpRequest = require('./httpRequest.js'), helpers = require('../../util/helpers.js'); module.exports = function (createBaseServer) { function create (options, logger, responseFn) { const connections = {}, defaultResponse = options.defaultResponse || {}; function postProcess (stubResponse, request) { /* eslint complexity: 0 */ const defaultHeaders = defaultResponse.headers || {}, response = { statusCode: stubResponse.statusCode || defaultResponse.statusCode || 200, headers: stubResponse.headers || defaultHeaders, body: stubResponse.body || defaultResponse.body || '', _mode: stubResponse._mode || defaultResponse._mode || 'text' }, responseHeaders = headersMap.of(response.headers), encoding = response._mode === 'binary' ? 'base64' : 'utf8', isObject = helpers.isObject; if (isObject(response.body)) { // Support JSON response bodies response.body = JSON.stringify(response.body, null, 4); } if (options.allowCORS) { const requestHeaders = headersMap.of(request.headers), isCrossOriginPreflight = request.method === 'OPTIONS' && requestHeaders.get('Access-Control-Request-Headers') && requestHeaders.get('Access-Control-Request-Method') && requestHeaders.get('Origin'); if (isCrossOriginPreflight) { responseHeaders.set('Access-Control-Allow-Headers', requestHeaders.get('Access-Control-Request-Headers')); responseHeaders.set('Access-Control-Allow-Methods', requestHeaders.get('Access-Control-Request-Method')); responseHeaders.set('Access-Control-Allow-Origin', requestHeaders.get('Origin')); } } if (encoding === 'base64') { // ensure the base64 has no newlines or other non // base64 chars that will cause the body to be garbled. response.body = response.body.replace(/[^A-Za-z0-9=+/]+/g, ''); } if (!responseHeaders.has('Connection')) { responseHeaders.set('Connection', 'close'); } if (responseHeaders.has('Content-Length')) { responseHeaders.set('Content-Length', Buffer.byteLength(response.body, encoding)); } return response; } const baseServer = createBaseServer(options), server = baseServer.createNodeServer(); // Allow long wait behaviors server.timeout = 0; server.on('connect', (response, client, head) => { const host = response.socket.servername; const port = response.socket.localPort; // may we prefer this method instead? const proxyServer = net.createConnection({ host, port }, () => { client.on('error', err => { logger.warn('CLIENT TO PROXY ERROR: [%s] %s', err.code, err.message); logger.debug('%s', err.stack); }); proxyServer.on('error', err => { logger.warn('SERVER PROXY ERROR: [%s] %s', err.code, err.message); logger.debug('%s', err.stack); }); logger.info('PROXY TO SERVER SET UP TO %s ON %s', host, port); client.write('HTTP/1.1 200 Connection established\r\n\r\n'); proxyServer.write(head); proxyServer.pipe(client); client.pipe(proxyServer); }); }); server.on('connection', socket => { const name = helpers.socketName(socket); logger.debug('%s ESTABLISHED', name); if (socket.on) { connections[name] = socket; socket.on('error', error => { logger.error('%s transmission error X=> %s', name, JSON.stringify(error)); }); socket.on('end', () => { logger.debug('%s LAST-ACK', name); }); socket.on('close', () => { logger.debug('%s CLOSED', name); delete connections[name]; }); } }); server.on('request', async (request, response) => { const clientName = helpers.socketName(request.socket); logger.info(`${clientName} => ${request.method} ${request.url}`); try { const simplifiedRequest = await httpRequest.createFrom(request); logger.debug('%s => %s', clientName, JSON.stringify(simplifiedRequest)); const mbResponse = await responseFn(simplifiedRequest, { rawUrl: request.url }), stubResponse = postProcess(mbResponse, simplifiedRequest), encoding = stubResponse._mode === 'binary' ? 'base64' : 'utf8'; if (mbResponse.blocked) { request.socket.destroy(); return; } if (helpers.simulateFault(request.socket, mbResponse.fault, logger)) { return; } response.writeHead(stubResponse.statusCode, stubResponse.headers); response.end(stubResponse.body.toString(), encoding); if (stubResponse) { logger.debug('%s <= %s', clientName, JSON.stringify(stubResponse)); } } catch (error) { const exceptions = errors; logger.error('%s X=> %s', clientName, JSON.stringify(exceptions.details(error))); response.writeHead(500, { 'content-type': 'application/json' }); response.end(JSON.stringify({ errors: [exceptions.details(error)] }), 'utf8'); } }); return new Promise((resolve, reject) => { server.on('error', error => { if (error.errno === 'EADDRINUSE') { reject(errors.ResourceConflictError(`Port ${options.port} is already in use`)); } else if (error.errno === 'EACCES') { reject(errors.InsufficientAccessError()); } else { reject(error); } }); // Bind the socket to a port (the || 0 bit auto-selects a port if one isn't provided) server.listen(options.port || 0, options.host, () => { resolve({ port: server.address().port, metadata: baseServer.metadata, close: callback => { server.close(callback); Object.keys(connections).forEach(socket => { connections[socket].destroy(); }); }, proxy: httpProxy.create(logger), encoding: 'utf8' }); }); }); } return { testRequest: { requestFrom: '', method: 'GET', path: '/', query: {}, headers: {}, form: {}, body: '' }, testProxyResponse: { statusCode: 200, headers: {}, body: '' }, create: create, validate: undefined }; };