postman-runtime
Version:
Underlying library of executing Postman Collections (used by Newman)
342 lines (285 loc) • 9.73 kB
JavaScript
const fs = require('fs'),
net = require('net'),
path = require('path'),
http = require('http'),
https = require('https'),
GraphQL = require('graphql'),
enableServerDestroy = require('server-destroy');
/**
* Echo raw request message to test
* - Body for HTTP methods like GET, HEAD
* - Custom HTTP methods
*
* @example
* var s = createRawEchoServer();
*
* s.listen(3000, function() {
* console.log(s.port);
* s.close();
* });
*
* @note For HEAD request, read body from `raw-request` response header
*/
function createRawEchoServer () {
var server;
// Reasons behiend creating custom echo server:
// - Node's `http` server won't support custom methods
// referenc: https://github.com/nodejs/http-parser/blob/master/http_parser.h#L163
// - Node's `http` server won't parse body for GET method.
server = net.createServer(function (socket) {
socket.on('data', function (chunk) {
if (this.data === undefined) {
this.data = '';
setTimeout(() => {
// Status Line
socket.write('HTTP/1.1 200 ok\r\n');
// Response Headers
socket.write('connection: close\r\n');
socket.write('content-type: text/plain\r\n');
socket.write('raw-request: ' + JSON.stringify(this.data) + '\r\n');
// CRLF
socket.write('\r\n');
// Respond with raw request message.
//
// @note http-parser will blow up if body is sent for HEAD request.
// RFC-7231: The HEAD method is identical to GET except that the
// server MUST NOT send a message body in the response.
if (!this.data.startsWith('HEAD / HTTP/1.1')) {
socket.write(this.data);
}
socket.end();
}, 1000);
}
this.data += chunk.toString();
});
});
server.on('listening', function () {
server.port = this.address().port;
server.url = 'http://localhost:' + server.port;
});
enableServerDestroy(server);
return server;
}
/**
* Simple SSL server for tests that emit events with the name of request url path.
*
* @param {Object} [opts] - Options for https.createServer()
*
* @example
* var s = createSSLServer();
* s.on('/foo', function (req, res) {
* res.writeHead(200, {'Content-Type': 'text/plain'});
* res.end('Hello World');
* });
* s.listen(3000, 'localhost');
*/
function createSSLServer (opts) {
var server,
certDataPath = path.join(__dirname, 'certificates'),
options = {
'key': path.join(certDataPath, 'server-key.pem'),
'cert': path.join(certDataPath, 'server-crt.pem'),
'ca': path.join(certDataPath, 'ca.pem')
},
optionsWithFilePath = ['key', 'cert', 'ca', 'pfx'];
if (opts) {
options = Object.assign(options, opts);
}
optionsWithFilePath.forEach(function (option) {
if (!options[option]) { return; }
options[option] = fs.readFileSync(options[option]);
});
server = https.createServer(options, function (req, res) {
server.emit(req.url, req, res);
});
server.on('listening', function () {
server.port = this.address().port;
server.url = 'https://localhost:' + server.port;
});
enableServerDestroy(server);
return server;
}
/**
* Simple redirect server for tests that emit hit events on each request captured.
* Use the URL format: /<urlPath>/<numberOfRedirects>/<responseCode>
* The final redirect in redirect chain will happen at /<urlPath>
*
* @example
* var s = createRedirectServer();
* s.on('hit', function (req, res) {
* console.log(req.location);
* });
* s.on('/foo', function (req, res)) {
* // this is called when there is no redirect.
* }
* s.listen(3000, callback);
*/
function createRedirectServer () {
var server = http.createServer(function (req, res) {
var urlTokens,
numberOfRedirects,
responseCode,
redirectURL;
server.emit('hit', req, res);
// /<urlPath>/<numberOfRedirects>/<responseCode>
if ((/\/\d+\/\d{3}$/).test(req.url)) {
urlTokens = req.url.split('/');
numberOfRedirects = parseInt(urlTokens[urlTokens.length - 2], 10);
responseCode = parseInt(urlTokens[urlTokens.length - 1], 10);
// redirect until all hops are covered
if (numberOfRedirects > 1) {
redirectURL = urlTokens.slice(0, -2).join('/') + `/${(numberOfRedirects - 1)}/${responseCode}`;
}
else {
redirectURL = urlTokens.slice(0, -2).join('/') + '/';
}
res.writeHead(responseCode, {location: redirectURL});
return res.end();
}
// emit event if this is not a redirect request
server.emit(req.url, req, res);
});
enableServerDestroy(server);
return server;
}
/**
* Wrapper for HTTP server that emit events with the name of request url path.
*
* @example
* var s = createHTTPServer();
* s.on('/foo', function (req, res)) {
* res.writeHead(200, {'content-type': 'text/plain'});
* res.end('Hello world!');
* }
* s.listen(3000, callback);
*/
function createHTTPServer () {
var server = http.createServer(function (req, res) {
server.emit(req.url, req, res);
});
server.on('listening', function () {
server.port = this.address().port;
server.url = 'http://localhost:' + server.port;
});
enableServerDestroy(server);
return server;
}
/**
* Simple HTTP proxy server
*
* @param {Object} [options] - Additional options to configure proxy server
* @param {Object} [options.auth] - Proxy authentication, Basic auth
* @param {String} [options.agent] - Agent used for http(s).request
*
* @example
* var s = createProxyServer({
* headers: { proxy: 'true' },
* auth: { username: 'user', password: 'pass' }
* });
* s.listen(3000, callback);
*/
function createProxyServer (options) {
!options && (options = {});
var agent = options.agent === 'https' ? https : http,
server = createHTTPServer(),
proxyAuthHeader;
// pre calculate proxy-authorization header value
if (options.auth) {
proxyAuthHeader = 'Basic ' + Buffer.from(
`${options.auth.username}:${options.auth.password}`
).toString('base64');
}
// listen on every incoming request
server.on('request', function (req, res) {
// verify proxy authentication if auth is set
if (options.auth && req.headers['proxy-authorization'] !== proxyAuthHeader) {
res.writeHead(407);
return res.end('Proxy Authentication Required');
}
// avoid compressed response, ease to respond
delete req.headers['accept-encoding'];
// merge headers set in options
req.headers = Object.assign(req.headers, options.headers || {});
// forward request to the origin and pipe the response
var fwd = agent.request({
host: req.headers.host,
path: req.url,
method: req.method.toLowerCase(),
headers: req.headers
}, function (resp) {
resp.pipe(res);
});
req.pipe(fwd);
});
return server;
}
function createGraphQLServer (options) {
!options && (options = {});
if (options.schema) {
options.schema = GraphQL.buildSchema(options.schema);
}
var server = createHTTPServer();
function badRequest (res, request, error) {
res.writeHead(400, {
'content-type': 'application/json'
});
res.end(JSON.stringify({
request: request,
error: error
}));
}
function responseHandler (req, res, body) {
var stringBody = body && body.toString && body.toString(),
request = {
headers: req.headers,
body: stringBody
},
jsonBody;
try {
jsonBody = JSON.parse(body.toString());
}
catch (e) {
return badRequest(res, request, 'Invalid JSON body');
}
GraphQL.graphql(
options.schema,
jsonBody.query,
options.root,
options.context,
jsonBody.variables,
jsonBody.operationName)
.then(function (data) {
if (data.errors) {
return badRequest(res, request, data.errors);
}
res.writeHead(200, {
'content-type': 'application/json'
});
res.end(JSON.stringify({
request: request,
result: data
}));
})
.catch(function (err) {
badRequest(res, request, err);
});
}
server.on('request', function (req, res) {
req.on('data', function (chunk) {
!this.chunks && (this.chunks = []);
this.chunks.push(chunk);
});
req.on('end', function () {
responseHandler(req, res, this.chunks && Buffer.concat(this.chunks));
});
});
return server;
}
module.exports = {
createSSLServer,
createHTTPServer,
createProxyServer,
createRawEchoServer,
createGraphQLServer,
createRedirectServer
};