Simon Priet e69a613a37 feat: Created a mini nodeJS server with NewMan for testing without PostMan GUI.
This will mimic a run in a CD/CI environment or docker container.
2021-09-08 14:01:19 +02:00

787 lines
24 KiB
JavaScript

// Browser Request
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/* eslint-disable */
var _ = require('lodash');
var parseHeadersString = require('postman-collection').Header.parse;
request.log = {
'trace': noop, 'debug': noop, 'info': noop, 'warn': noop, 'error': noop
}
var CORS_ERROR_CODE = 'ERR_PM_CORS'; // Custom error code for CORS errors
var MIXED_CONTENT_ERROR_CODE = 'ERR_PM_MIXED_CONTENT'; // Custom error code for mixed content error
var DEFAULT_TIMEOUT = 3 * 60 * 1000 // 3 minutes
// The body is ignored if the request method is GET or HEAD.
// Refer: https://xhr.spec.whatwg.org/#the-send()-method
var METHODS_WITHOUT_BODY = {
'GET': true,
'HEAD': true
};
// Refer: https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name
var FORBIDDEN_HEADERS = {
'accept-charset': true,
'accept-encoding': true,
'access-control-request-headers': true,
'access-control-request-method': true,
connection: true,
'content-length': true,
cookie: true,
cookie2: true,
date: true,
dnt: true,
expect: true,
'feature-policy': true,
host: true,
'keep-alive': true,
origin: true,
referer: true,
te: true,
trailer: true,
'transfer-encoding': true,
upgrade: true,
via: true
};
var IS_LOCALHOST = {
'localhost': true,
'127.0.0.1': true,
'127.1': true,
'[::1]': true
};
function forEachAsync (items, fn, cb) {
!cb && (cb = function () { /* (ಠ_ಠ) */ })
if (!(Array.isArray(items) && fn)) { return cb() }
var index = 0
var totalItems = items.length
function next (err) {
if (err || index >= totalItems) {
return cb(err)
}
try {
fn.call(items, items[index++], next)
} catch (error) {
return cb(error)
}
}
if (!totalItems) { return cb() }
next()
}
//
// request
//
function request(originalRequest, options, onStart, callback) {
var options_onResponse = options.onResponse; // Save this for later.
var XHR = _.get(options, ['agents', options.url && options.url.protocol.slice(0, -1), 'agentClass']) || XMLHttpRequest;
if(typeof options === 'string')
options = {'uri':options};
else
options = _.clone(options); // Use a duplicate for mutating.
options.onResponse = options_onResponse // And put it back.
if (options.verbose) request.log = getLogger();
if(options.url) {
options.uri = options.url && options.url.href || options.url;
delete options.url;
}
if(!options.uri && options.uri !== "")
return callback(new Error("options.uri is a required argument"));
if(typeof options.uri != "string")
return callback(new Error("options.uri must be a string"));
options.onStart = onStart
options.callback = callback
options.method = options.method || 'GET';
options.headers = _.reduce(options.headers || {}, function (accumulator, value, key) {
if (!XHR._allowForbiddenHeaders && isForbiddenHeader(key)) {
// mutate original request and options as these will be passed in the
// request and response callbacks.
originalRequest.headers.remove(key);
delete options.headers[key];
return accumulator;
}
accumulator[key] = value;
return accumulator;
}, {});
options.body = options.body || null
options.timeout = options.timeout || request.DEFAULT_TIMEOUT
if(options.headers.host)
console.warn("Request: Options.headers.host is not supported");
if(options.json) {
options.headers.accept = options.headers.accept || 'application/json'
if(options.method !== 'GET')
options.headers['content-type'] = 'application/json'
if(typeof options.json !== 'boolean')
options.body = JSON.stringify(options.json)
else if(typeof options.body !== 'string')
options.body = JSON.stringify(options.body)
}
//BEGIN QS Hack
var serialize = function(obj) {
var str = [];
for(var p in obj)
if (obj.hasOwnProperty(p)) {
if (_.isArray(obj[p])) {
_.forEach(obj[p], function (value) {
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(value));
});
}
else {
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
}
}
return str.join("&");
}
if(options.qs){
var qs = (typeof options.qs == 'string')? options.qs : serialize(options.qs);
if(options.uri.indexOf('?') !== -1){ //no get params
options.uri = options.uri+'&'+qs;
}else{ //existing get params
options.uri = options.uri+'?'+qs;
}
}
//END QS Hack
//BEGIN FORM Hack
var multipart = function (data) {
if (!Array.isArray(data)) { return; }
var i,
ii,
formParam,
formData = new FormData();
for (i = 0, ii = data.length; i < ii; i++) {
if (!(formParam = data[i])) { continue; }
if (Array.isArray(formParam.value)) {
formParam.value.forEach(function (value) {
formData.append(formParam.key, value);
});
}
else {
formData.append(formParam.key, formParam.value);
}
}
return {
body: formData
};
};
if(options.form){
if(typeof options.form == 'string') {
console.warn('form name unsupported');
}
if(XHR._allowBodyInGET || !METHODS_WITHOUT_BODY[options.method]) {
var encoding = (options.encoding || 'application/x-www-form-urlencoded').toLowerCase();
if (!options.headers['content-type'] && !options.headers['Content-Type']) {
options.headers['content-type'] = encoding;
}
switch(encoding){
case 'application/x-www-form-urlencoded':
options.body = serialize(options.form).replace(/%20/g, "+");
break;
case 'multipart/form-data':
var multi = multipart(options.form);
//options.headers['content-length'] = multi.length;
options.body = multi.body;
options.headers['content-type'] = multi.type;
break;
default : console.warn('unsupported encoding:'+encoding);
}
}
}
if (options.formData && (XHR._allowBodyInGET || !METHODS_WITHOUT_BODY[options.method])) {
var multipartBody = multipart(options.formData);
//options.headers['content-length'] = multipartBody.length;
options.body = multipartBody.body;
multipartBody.type && (options.headers['content-type'] = multipartBody.type);
}
//END FORM Hack
// If onResponse is boolean true, call back immediately when the response is known,
// not when the full request is complete.
options.onResponse = options.onResponse || noop
if(options.onResponse === true) {
options.onResponse = callback
options.callback = noop
}
// XXX Browsers do not like this.
//if(options.body)
// options.headers['content-length'] = options.body.length;
// HTTP basic authentication
if(!options.headers.authorization && options.auth)
options.headers.authorization = 'Basic ' + b64_enc(options.auth.username + ':' + options.auth.password);
// Query cookie jar if available
if ((typeof (options.jar && options.jar.getCookieString) === 'function')) {
options.jar.getCookieString(options.uri, function (_, cookies) {
if (cookies && cookies.length) {
options.cookiesFromJar = cookies;
}
run_xhr(XHR, originalRequest, options)
})
}
else {
return run_xhr(XHR, originalRequest, options)
}
}
var req_seq = 0
function run_xhr(XHR, originalRequest, options) {
var xhr = new XHR(options)
, timed_out = false
, is_cors = is_crossDomain(options.uri)
, supports_cors = ('withCredentials' in xhr)
req_seq += 1
xhr.seq_id = req_seq
xhr.id = req_seq + ': ' + options.method + ' ' + options.uri
xhr._id = xhr.id // I know I will type "_id" from habit all the time.
if(is_cors && !supports_cors) {
// This should never happen in our app
var cors_err = new Error('Browser does not support cross-origin request: ' + options.uri);
cors_err.code = CORS_ERROR_CODE;
cors_err.cors = 'unsupported';
options.callback(cors_err, xhr);
return xhr;
}
xhr.timeoutTimer = setTimeout(too_late, options.timeout)
function too_late() {
timed_out = true
var er = new Error('ETIMEDOUT')
er.code = 'ETIMEDOUT'
er.duration = options.timeout
request.log.error('Timeout', { 'id':xhr._id, 'milliseconds':options.timeout })
return options.callback(er, xhr)
}
// Some states can be skipped over, so remember what is still incomplete.
var did = {'response':false, 'loading':false, 'end':false, 'onStart': false}
xhr.onreadystatechange = on_state_change
xhr.open(options.method, options.uri, true) // asynchronous
if (is_cors) {
xhr.withCredentials = !! options.withCredentials
}
(options.encoding === null) && (xhr.responseType = "arraybuffer");
xhr.send(options.body)
return xhr
function on_state_change(event) {
if(timed_out)
return request.log.debug('Ignoring timed out state change', {'state':xhr.readyState, 'id':xhr.id})
request.log.debug('State change', {'state':xhr.readyState, 'id':xhr.id, 'timed_out':timed_out})
if(xhr.readyState === XHR.OPENED) {
request.log.debug('Request started', { 'id': xhr.id });
var cookies = [],
onInvalidHeader = function (key, error) {
error = new Error(`Header "${key}" contains invalid characters`);
// Do not process this request further.
did.response = true
did.loading = true
did.end = true
options.callback(error, xhr)
};
for (var key in options.headers) {
if (!options.headers.hasOwnProperty(key)) {
continue;
}
// Save all the cookies and add at the end because
if (String(key).toLowerCase() === 'cookie') {
cookies.push(options.headers[key]);
continue;
}
try {
if (Array.isArray(options.headers[key])) {
_.forEach(options.headers[key], function (eachValue) {
xhr.setRequestHeader(key, eachValue);
});
}
else {
xhr.setRequestHeader(key, options.headers[key]);
}
} catch (error) {
onInvalidHeader(key, error)
}
}
// Add `Cookie` header if cookies are present
if (cookies.length || options.cookiesFromJar) {
try {
var cookieString = cookies.join('; ') + (options.cookiesFromJar || '');
xhr.setRequestHeader('Cookie', cookieString);
// Also add update the original request header for console logs
originalRequest.headers.upsert({
key: 'Cookie',
value: cookieString
});
} catch (error) {
onInvalidHeader('Cookie', error)
}
}
}
else if(xhr.readyState === XHR.HEADERS_RECEIVED)
on_response()
else if(xhr.readyState === XHR.LOADING) {
on_response()
on_loading()
}
else if(xhr.readyState === XHR.DONE) {
on_response()
on_loading()
on_end()
}
}
function on_response() {
if(did.response)
return
did.response = true
request.log.debug('Got response', {'id':xhr.id, 'status':xhr.status})
clearTimeout(xhr.timeoutTimer)
xhr.statusCode = xhr.status // Node request compatibility
// Construct postman-request compatible debug object
!xhr.request && (xhr.request = {});
xhr.request._debug = xhr._debugData || [{
request: {
method: options.method,
href: options.uri,
headers: originalRequest.headers.toJSON(),
httpVersion: '1.1'
},
response: {
statusCode: xhr.statusCode,
headers: parseHeadersString(xhr.getAllResponseHeaders()),
httpVersion: '1.1'
}
}];
if (xhr.statusCode === 0 && xhr._error) {
// Do not process this request further.
did.loading = true
did.end = true
return options.callback(xhr._error, xhr);
}
// Detect mixed content failure
if (xhr.statusCode === 0 && is_mixedContent(options.uri)) {
var mixedContent_err = new Error('Mixed Content request rejected: ' + options.uri);
mixedContent_err.code = MIXED_CONTENT_ERROR_CODE;
// Do not process this request further.
did.loading = true
did.end = true
return options.callback(mixedContent_err, xhr)
}
// Detect failed CORS requests.
if(is_cors && xhr.statusCode == 0) {
var cors_err = new Error('CORS request rejected: ' + options.uri);
cors_err.code = CORS_ERROR_CODE;
cors_err.cors = 'rejected';
// Do not process this request further.
did.loading = true
did.end = true
return options.callback(cors_err, xhr)
}
function done () {
// Trigger onStart before callback
did.onStart = true
options.onStart(xhr)
options.onResponse(null, xhr)
// Due to the weird dependency of `onStart` and `callback` order,
// we ensure that callback is not called before onStart.
// This happens only if we are waiting for cookies to be added into the cookie jar.
typeof did.callback === 'function' && did.callback();
}
// We are all done here if the cookie jar is not available
if (!(typeof (options.jar && options.jar.setCookie) === 'function')) {
return done();
}
// Add cookies into the jar
var addCookie = function (cookie, cb) {
options.jar.setCookie(cookie, options.uri, {ignoreError: true}, function () {
cb()
})
},
getSetCookieHeaders = function (headersString) {
var cookies = [];
(parseHeadersString(headersString) || []).filter(function (header) {
if (String(header && header.key).toLowerCase() === 'set-cookie') {
cookies.push(header.value);
}
});
return cookies;
},
cookies = getSetCookieHeaders(xhr.getAllResponseHeaders());
if (!(cookies && cookies.length)) {
return done();
}
forEachAsync(cookies, addCookie, function () {
done()
})
}
function on_loading() {
if(did.loading)
return
did.loading = true
request.log.debug('Response body loading', {'id':xhr.id})
// TODO: Maybe simulate "data" events by watching xhr.responseText
}
function on_end() {
if(did.end)
return
did.end = true
request.log.debug('Request done', {'id':xhr.id})
xhr.body = (options.encoding === null) ? xhr.response : xhr.responseText;
if(options.json) {
try {
xhr.body = (xhr.responseText) ? JSON.parse(xhr.responseText) : xhr.responseText;
}
catch (er) {
return options.callback(er, xhr)
}
}
// Call the final callback if `onStart` is already called
if (did.onStart) {
options.callback(null, xhr, xhr.body, xhr.request && xhr.request._debug)
}
// otherwise, save the callback which will be triggered later in the `done` function
else {
did.callback = options.callback.bind(this, null, xhr, xhr.body, xhr.request && xhr.request._debug)
}
}
} // request
request.withCredentials = false;
request.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
var shortcuts = [
'get',
'post',
'put',
'head',
'del',
'options',
'trace',
'copy',
'lock',
'mkcol',
'move',
'purge',
'propfind',
'proppatch',
'unlock',
'report',
'mkactivity',
'checkout',
'merge',
'm-search',
'notify',
'subscribe',
'unsubscribe',
'patch',
'search'
];
var shortcutsToMethods = {
'del': 'delete'
};
//
// defaults
//
request.defaults = function(options, requester) {
var def = function (method) {
var d = function (params, callback) {
if(typeof params === 'string')
params = {'uri': params};
else {
params = JSON.parse(JSON.stringify(params));
}
for (var i in options) {
if (params[i] === undefined) params[i] = options[i]
}
return method(params, callback)
}
return d
}
var de = def(request)
shortcuts.forEach(function (method) {
de[method] = def(request[method])
})
return de
}
//
// HTTP method shortcuts
//
shortcuts.forEach(function(shortcut) {
var method = shortcutsToMethods[shortcut] || shortcut;
method = method.toUpperCase();
var func = shortcut.toLowerCase();
request[func] = function(opts) {
if(typeof opts === 'string')
opts = {'method':method, 'uri':opts};
else {
opts = JSON.parse(JSON.stringify(opts));
opts.method = method;
}
var args = [opts].concat(Array.prototype.slice.apply(arguments, [1]));
return request.apply(this, args);
}
})
//
// CouchDB shortcut
//
request.couch = function(options, callback) {
if(typeof options === 'string')
options = {'uri':options}
// Just use the request API to do JSON.
options.json = true
if(options.body)
options.json = options.body
delete options.body
callback = callback || noop
var xhr = request(options, couch_handler)
return xhr
function couch_handler(er, resp, body) {
if(er)
return callback(er, resp, body)
if((resp.statusCode < 200 || resp.statusCode > 299) && body.error) {
// The body is a Couch JSON object indicating the error.
er = new Error('CouchDB error: ' + (body.error.reason || body.error.error))
for (var key in body)
er[key] = body[key]
return callback(er, resp, body);
}
return callback(er, resp, body);
}
}
//
// Utility
//
function noop() {}
function getLogger() {
var logger = {}
, levels = ['trace', 'debug', 'info', 'warn', 'error']
, level, i
for(i = 0; i < levels.length; i++) {
level = levels[i]
logger[level] = noop
if(typeof console !== 'undefined' && console && console[level])
logger[level] = formatted(console, level)
}
return logger
}
function formatted(obj, method) {
return formatted_logger
function formatted_logger(str, context) {
if(typeof context === 'object')
str += ' ' + JSON.stringify(context)
return obj[method].call(obj, str)
}
}
function window_location () {
// jQuery #8138, IE may throw an exception when accessing
// a field from window.location if document.domain has been set
var ajaxLocation
try { ajaxLocation = location.href }
catch (e) {
// Use the href attribute of an A element since IE will modify it given document.location
ajaxLocation = document.createElement( "a" );
ajaxLocation.href = "";
ajaxLocation = ajaxLocation.href;
}
return ajaxLocation
}
// Return whether a URL is a cross-domain request.
function is_crossDomain(url) {
var rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/
, ajaxLocation = window_location()
, ajaxLocParts = rurl.exec(ajaxLocation.toLowerCase()) || []
, parts = rurl.exec(url.toLowerCase() )
var result = !!(
parts &&
( parts[1] != ajaxLocParts[1]
|| parts[2] != ajaxLocParts[2]
|| (parts[3] || (parts[1] === "http:" ? 80 : 443)) != (ajaxLocParts[3] || (ajaxLocParts[1] === "http:" ? 80 : 443))
)
)
//console.debug('is_crossDomain('+url+') -> ' + result)
return result
}
function is_mixedContent (url) {
var rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/
, ajaxLocation = window_location()
, ajaxLocParts = rurl.exec(ajaxLocation.toLowerCase()) || []
, parts = rurl.exec(url.toLowerCase() )
return parts[1] != ajaxLocParts[1] && !IS_LOCALHOST[parts[2]]
}
// MIT License from http://phpjs.org/functions/base64_encode:358
function b64_enc (data) {
// Encodes string using MIME base64 algorithm
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, enc="", tmp_arr = [];
if (!data) {
return data;
}
// assume utf8 data
// data = this.utf8_encode(data+'');
do { // pack three octets into four hexets
o1 = data.charCodeAt(i++);
o2 = data.charCodeAt(i++);
o3 = data.charCodeAt(i++);
bits = o1<<16 | o2<<8 | o3;
h1 = bits>>18 & 0x3f;
h2 = bits>>12 & 0x3f;
h3 = bits>>6 & 0x3f;
h4 = bits & 0x3f;
// use hexets to index into b64, and append result to encoded string
tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4);
} while (i < data.length);
enc = tmp_arr.join('');
switch (data.length % 3) {
case 1:
enc = enc.slice(0, -2) + '==';
break;
case 2:
enc = enc.slice(0, -1) + '=';
break;
}
return enc;
}
// Check if given header name is forbidden i.e, cannot be modified programmatically.
// Refer: https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name
// @note The User-Agent header is no longer forbidden. However,
// Chrome will silently drop the header: https://bugs.chromium.org/p/chromium/issues/detail?id=571722
function isForbiddenHeader (headerName) {
headerName = String(headerName).toLowerCase();
return FORBIDDEN_HEADERS[headerName] ||
headerName.startsWith('proxy-') ||
headerName.startsWith('sec-');
}
// ensure that the .jar() function is available
request.jar = _.noop;
module.exports = request;