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.
This commit is contained in:
786
node_modules/postman-runtime/lib/requester/browser/request.js
generated
vendored
Normal file
786
node_modules/postman-runtime/lib/requester/browser/request.js
generated
vendored
Normal file
@@ -0,0 +1,786 @@
|
||||
// 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;
|
303
node_modules/postman-runtime/lib/requester/core-body-builder.js
generated
vendored
Normal file
303
node_modules/postman-runtime/lib/requester/core-body-builder.js
generated
vendored
Normal file
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* @fileOverview
|
||||
*
|
||||
* This module consists all request body transformer functions based on the request body modes supported
|
||||
* Ideally, this should one day move to a function in SDK something like request.getNodeRequestOptions()
|
||||
*
|
||||
*
|
||||
* _
|
||||
* ( ) ,,,,,
|
||||
* \\ . . ,
|
||||
* \\ | - D ,
|
||||
* (._) \__- | ,
|
||||
* | |..
|
||||
* \\|_ , ,---- _ |----.
|
||||
* \__ ( ( / ) _
|
||||
* | \/ \. ' _.| \ ( )
|
||||
* | \ /( / /\_ \ //
|
||||
* \ / ( / / ) //
|
||||
* ( , / / , (_.)
|
||||
* |......\ | \,
|
||||
* / / ) \---
|
||||
* /___/___^//
|
||||
*/
|
||||
var _ = require('lodash'),
|
||||
|
||||
CONTENT_TYPE_HEADER_KEY = 'Content-Type',
|
||||
|
||||
/**
|
||||
* Map content-type to respective body language.
|
||||
*
|
||||
* @private
|
||||
* @type {Object}
|
||||
*/
|
||||
CONTENT_TYPE_LANGUAGE = {
|
||||
'html': 'text/html',
|
||||
'text': 'text/plain',
|
||||
'json': 'application/json',
|
||||
'javascript': 'application/javascript',
|
||||
'xml': 'application/xml'
|
||||
},
|
||||
|
||||
STRING = 'string',
|
||||
E = '',
|
||||
|
||||
oneNormalizedHeader,
|
||||
|
||||
// the following two are reducer functions. we keep it defined here to avoid redefinition upon each parse
|
||||
urlEncodedBodyReducer,
|
||||
formDataBodyReducer;
|
||||
|
||||
/**
|
||||
* Find the enabled header with the given name.
|
||||
*
|
||||
* @todo Add this helper in Collection SDK.
|
||||
*
|
||||
* @private
|
||||
* @param {HeaderList} headers
|
||||
* @param {String} name
|
||||
* @returns {Header|undefined}
|
||||
*/
|
||||
oneNormalizedHeader = function oneNormalizedHeader (headers, name) {
|
||||
var i,
|
||||
header;
|
||||
|
||||
// get all headers with `name`
|
||||
headers = headers.reference[name.toLowerCase()];
|
||||
|
||||
if (Array.isArray(headers)) {
|
||||
// traverse the headers list in reverse direction in order to find the last enabled
|
||||
for (i = headers.length - 1; i >= 0; i--) {
|
||||
header = headers[i];
|
||||
|
||||
if (header && !header.disabled) {
|
||||
return header;
|
||||
}
|
||||
}
|
||||
|
||||
// bail out if no enabled header was found
|
||||
return;
|
||||
}
|
||||
|
||||
// return the single enabled header
|
||||
if (headers && !headers.disabled) {
|
||||
return headers;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reduces postman SDK url encoded form definition (flattened to array) into Node compatible body options
|
||||
*
|
||||
* @param {Object} form - url encoded form params accumulator
|
||||
* @param {Object} param - url encoded form param
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
urlEncodedBodyReducer = function (form, param) {
|
||||
if (!param || param.disabled) {
|
||||
return form;
|
||||
}
|
||||
|
||||
var key = param.key,
|
||||
value = param.value;
|
||||
|
||||
// add the parameter to the form while accounting for duplicate values
|
||||
if (!form.hasOwnProperty(key)) {
|
||||
form[key] = value;
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
// at this point, we know that form has duplicate, so we need to accumulate it in an array
|
||||
if (!Array.isArray(form[key])) {
|
||||
form[key] = [form[key]];
|
||||
}
|
||||
|
||||
form[key].push(value); // finally push the duplicate and return
|
||||
|
||||
return form;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reduces postman SDK multi-part form definition (flattened to array) into Node compatible body options
|
||||
*
|
||||
* @param {Array} data - multi-part form params accumulator
|
||||
* @param {Object} param - multi-part form param
|
||||
*
|
||||
* @returns {Array}
|
||||
*/
|
||||
formDataBodyReducer = function (data, param) {
|
||||
if (!param || param.disabled) {
|
||||
return data;
|
||||
}
|
||||
|
||||
var formParam = {
|
||||
key: param.key,
|
||||
value: param.value
|
||||
},
|
||||
options; // we keep the default blank and then set to object wherever needed. saves doing object keyLength
|
||||
|
||||
// make sure that value is either string or read stream otherwise it'll cause error in postman-request
|
||||
if (param.type !== 'file' && typeof formParam.value !== STRING) {
|
||||
try {
|
||||
formParam.value = JSON.stringify(formParam.value);
|
||||
}
|
||||
catch (err) {
|
||||
formParam.value = E;
|
||||
}
|
||||
}
|
||||
|
||||
// make sure `filename` param is sent for every file without `value`
|
||||
// so that `filename=""` is added to content-disposition header in form data
|
||||
if (param.type === 'file' && !formParam.value && typeof param.fileName !== 'string') {
|
||||
param.fileName = E;
|
||||
formParam.value = E; // make sure value is not null/undefined ever
|
||||
}
|
||||
|
||||
// if data has a truthy content type, we mutate the value to take the options. we are assuming that
|
||||
// blank string will not be considered as an accepted content type.
|
||||
if (param.contentType && typeof param.contentType === STRING) {
|
||||
(options || (options = {})).contentType = param.contentType;
|
||||
}
|
||||
|
||||
// additionally parse the file name and length if sent
|
||||
// @note: Add support for fileName & fileLength option in Schema & SDK.
|
||||
// The filepath property overrides filename and may contain a relative path.
|
||||
if (typeof param.fileName === STRING) { (options || (options = {})).filename = param.fileName; }
|
||||
if (typeof param.fileLength === 'number') { (options || (options = {})).knownLength = param.fileLength; }
|
||||
|
||||
|
||||
// if options were set, add them to formParam
|
||||
options && (formParam.options = options);
|
||||
|
||||
data.push(formParam);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* This module exposes functions that are named corresponding to Postman collection body modes. It accepts the body
|
||||
* definition, usually like `request.body.raw` where mode is `raw` and returns its equivalent structure that needs to be
|
||||
* sent to node request module
|
||||
*/
|
||||
module.exports = {
|
||||
/**
|
||||
* @param {Object} content - request body content
|
||||
* @param {Request} [request] - request object
|
||||
* @returns {Object}
|
||||
*/
|
||||
raw: function (content, request) {
|
||||
var contentLanguage = _.get(request, 'body.options.raw.language', 'text');
|
||||
|
||||
// Add `Content-Type` header from body options if not set already
|
||||
if (request && !oneNormalizedHeader(request.headers, CONTENT_TYPE_HEADER_KEY)) {
|
||||
request.headers.add({
|
||||
key: CONTENT_TYPE_HEADER_KEY,
|
||||
value: CONTENT_TYPE_LANGUAGE[contentLanguage] || CONTENT_TYPE_LANGUAGE.text,
|
||||
system: true
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof content !== STRING) {
|
||||
content = JSON.stringify(content);
|
||||
}
|
||||
|
||||
return {
|
||||
body: content
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} content - request body content
|
||||
* @returns {Object}
|
||||
*/
|
||||
urlencoded: function (content) {
|
||||
if (content && _.isFunction(content.all)) { content = content.all(); } // flatten the body content
|
||||
|
||||
return {
|
||||
form: _.reduce(content, urlEncodedBodyReducer, {})
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} content - request body content
|
||||
* @returns {Object}
|
||||
*/
|
||||
formdata: function (content) {
|
||||
if (content && _.isFunction(content.all)) { content = content.all(); } // flatten the body content
|
||||
|
||||
return {
|
||||
formData: _.reduce(content, formDataBodyReducer, [])
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} content - request body content
|
||||
* @returns {Object}
|
||||
*/
|
||||
file: function (content) {
|
||||
return {
|
||||
body: content && content.content
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} content - request body content
|
||||
* @param {Request} [request] - Request object
|
||||
* @returns {Object}
|
||||
*/
|
||||
graphql: function (content, request) {
|
||||
var body;
|
||||
|
||||
// implicitly add `Content-Type` header if not set already
|
||||
if (request && !oneNormalizedHeader(request.headers, CONTENT_TYPE_HEADER_KEY)) {
|
||||
request.headers.add({
|
||||
key: CONTENT_TYPE_HEADER_KEY,
|
||||
value: CONTENT_TYPE_LANGUAGE.json,
|
||||
system: true
|
||||
});
|
||||
}
|
||||
|
||||
// if `variables` is an object, just stringify the entire content
|
||||
if (content && typeof content.variables !== STRING) {
|
||||
// if any property of graphql is undefined, it will not get stringified
|
||||
// as a result, if no content object's properties are present then the
|
||||
// result will be a blank object being sent.
|
||||
// note that this behavior has to be imitated later when we are
|
||||
// receiving variables as string
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
query: content.query,
|
||||
operationName: content.operationName,
|
||||
variables: content.variables
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
// otherwise, traverse the graphql properties and generate the
|
||||
// stringified content. This avoids parsing the variables.
|
||||
body = [];
|
||||
|
||||
if (content.hasOwnProperty('query') && (typeof content.query === STRING)) {
|
||||
body.push('"query":' + JSON.stringify(content.query));
|
||||
}
|
||||
|
||||
if (content.hasOwnProperty('operationName') && (typeof content.operationName === STRING)) {
|
||||
body.push('"operationName":' + JSON.stringify(content.operationName));
|
||||
}
|
||||
|
||||
if (content.hasOwnProperty('variables') && (typeof content.variables === STRING) &&
|
||||
// even though users are free to send even malformed json string, the case of empty string has to be
|
||||
// specially disallowed since in most default cases if a text editor is used to accept this data, it will
|
||||
// send a blank string for an empty text-editor state and that would be an error flow. That implies majority
|
||||
// default use case will become error flow and handling for the same has to be also coded in every other
|
||||
// place where runtime is used.
|
||||
(content.variables !== E)) {
|
||||
body.push('"variables":' + content.variables); // already a stringified JSON
|
||||
}
|
||||
|
||||
return {
|
||||
body: '{' + body.join(',') + '}' // note that [] body = {} ¯\_(ツ)_/¯
|
||||
};
|
||||
}
|
||||
};
|
769
node_modules/postman-runtime/lib/requester/core.js
generated
vendored
Normal file
769
node_modules/postman-runtime/lib/requester/core.js
generated
vendored
Normal file
@@ -0,0 +1,769 @@
|
||||
var dns = require('dns'),
|
||||
constants = require('constants'),
|
||||
|
||||
_ = require('lodash'),
|
||||
uuid = require('uuid/v4'),
|
||||
sdk = require('postman-collection'),
|
||||
urlEncoder = require('postman-url-encoder'),
|
||||
|
||||
Socket = require('net').Socket,
|
||||
|
||||
requestBodyBuilders = require('./core-body-builder'),
|
||||
version = require('../../package.json').version,
|
||||
|
||||
LOCAL_IPV6 = '::1',
|
||||
LOCAL_IPV4 = '127.0.0.1',
|
||||
LOCALHOST = 'localhost',
|
||||
SOCKET_TIMEOUT = 500,
|
||||
|
||||
COLON = ':',
|
||||
STRING = 'string',
|
||||
HOSTS_TYPE = {
|
||||
HOST_IP_MAP: 'hostIpMap'
|
||||
},
|
||||
HTTPS = 'https',
|
||||
HTTPS_DEFAULT_PORT = 443,
|
||||
HTTP_DEFAULT_PORT = 80,
|
||||
|
||||
S_CONNECT = 'connect',
|
||||
S_ERROR = 'error',
|
||||
S_TIMEOUT = 'timeout',
|
||||
|
||||
SSL_OP_NO = 'SSL_OP_NO_',
|
||||
|
||||
ERROR_ADDRESS_RESOLVE = 'NETERR: getaddrinfo ENOTFOUND ',
|
||||
|
||||
/**
|
||||
* List of request methods without body.
|
||||
*
|
||||
* @private
|
||||
* @type {Object}
|
||||
*
|
||||
* @note hash is used to reduce the lookup cost
|
||||
* these methods are picked from the app, which don't support body.
|
||||
* @todo move this list to SDK for parity.
|
||||
*/
|
||||
METHODS_WITHOUT_BODY = {
|
||||
get: true,
|
||||
copy: true,
|
||||
head: true,
|
||||
purge: true,
|
||||
unlock: true
|
||||
},
|
||||
|
||||
/**
|
||||
* List of request options with their corresponding protocol profile behavior property name;
|
||||
*
|
||||
* @private
|
||||
* @type {Object}
|
||||
*/
|
||||
PPB_OPTS = {
|
||||
// enable or disable certificate verification
|
||||
strictSSL: 'strictSSL',
|
||||
|
||||
// maximum number of redirects to follow (default: 10)
|
||||
maxRedirects: 'maxRedirects',
|
||||
|
||||
// controls redirect behavior
|
||||
// keeping the same convention as Newman
|
||||
followRedirect: 'followRedirects',
|
||||
followAllRedirects: 'followRedirects',
|
||||
|
||||
// retain `authorization` header when a redirect happens to a different hostname
|
||||
followAuthorizationHeader: 'followAuthorizationHeader',
|
||||
|
||||
// redirect with the original HTTP method (default: redirects with GET)
|
||||
followOriginalHttpMethod: 'followOriginalHttpMethod',
|
||||
|
||||
// removes the `referer` header when a redirect happens (default: false)
|
||||
// @note `referer` header set in the initial request will be preserved during redirect chain
|
||||
removeRefererHeader: 'removeRefererHeaderOnRedirect'
|
||||
},
|
||||
|
||||
/**
|
||||
* System headers which can be removed before sending the request if set
|
||||
* in disabledSystemHeaders protocol profile behavior.
|
||||
*
|
||||
*
|
||||
* @private
|
||||
* @type {Array}
|
||||
*/
|
||||
ALLOWED_BLACKLIST_HEADERS = ['content-type', 'content-length', 'accept-encoding', 'connection'],
|
||||
|
||||
/**
|
||||
* Find the enabled header with the given name.
|
||||
*
|
||||
* @todo Add this helper in Collection SDK.
|
||||
*
|
||||
* @private
|
||||
* @param {HeaderList} headers
|
||||
* @param {String} name
|
||||
* @returns {Header|undefined}
|
||||
*/
|
||||
oneNormalizedHeader = function oneNormalizedHeader (headers, name) {
|
||||
var i,
|
||||
header;
|
||||
|
||||
// get all headers with `name`
|
||||
headers = headers.reference[name.toLowerCase()];
|
||||
|
||||
if (Array.isArray(headers)) {
|
||||
// traverse the headers list in reverse direction in order to find the last enabled
|
||||
for (i = headers.length - 1; i >= 0; i--) {
|
||||
header = headers[i];
|
||||
|
||||
if (header && !header.disabled) {
|
||||
return header;
|
||||
}
|
||||
}
|
||||
|
||||
// bail out if no enabled header was found
|
||||
return;
|
||||
}
|
||||
|
||||
// return the single enabled header
|
||||
if (headers && !headers.disabled) {
|
||||
return headers;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add static system headers if they are not disable using `disabledSystemHeaders`
|
||||
* protocol profile behavior.
|
||||
* Add the system headers provided as requester configuration.
|
||||
*
|
||||
* @note Don't traverse the user provided `disabledSystemHeaders` object
|
||||
* to ensure runtime allowed headers and also for security reasons.
|
||||
*
|
||||
* @private
|
||||
* @param {Request} request
|
||||
* @param {Object} options
|
||||
* @param {Object} disabledHeaders
|
||||
* @param {Object} systemHeaders
|
||||
*/
|
||||
addSystemHeaders = function (request, options, disabledHeaders, systemHeaders) {
|
||||
var key,
|
||||
headers = request.headers;
|
||||
|
||||
[
|
||||
{key: 'User-Agent', value: `PostmanRuntime/${version}`},
|
||||
{key: 'Accept', value: '*/*'},
|
||||
{key: 'Cache-Control', value: 'no-cache'},
|
||||
{key: 'Postman-Token', value: uuid()},
|
||||
{key: 'Host', value: options.url && options.url.host},
|
||||
{key: 'Accept-Encoding', value: 'gzip, deflate, br'},
|
||||
{key: 'Connection', value: 'keep-alive'}
|
||||
].forEach(function (header) {
|
||||
key = header.key.toLowerCase();
|
||||
|
||||
// add system header only if,
|
||||
// 1. there's no user added header
|
||||
// 2. not disabled using disabledSystemHeaders
|
||||
!disabledHeaders[key] && !oneNormalizedHeader(headers, key) &&
|
||||
headers.add({
|
||||
key: header.key,
|
||||
value: header.value,
|
||||
system: true
|
||||
});
|
||||
});
|
||||
|
||||
for (key in systemHeaders) {
|
||||
if (systemHeaders.hasOwnProperty(key)) {
|
||||
// upsert instead of add to replace user-defined headers also
|
||||
headers.upsert({
|
||||
key: key,
|
||||
value: systemHeaders[key],
|
||||
system: true
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper function to extract top level domain for the given hostname
|
||||
*
|
||||
* @private
|
||||
*
|
||||
* @param {String} hostname
|
||||
* @returns {String}
|
||||
*/
|
||||
getTLD = function (hostname) {
|
||||
if (!hostname) {
|
||||
return '';
|
||||
}
|
||||
|
||||
hostname = String(hostname);
|
||||
|
||||
return hostname.substring(hostname.lastIndexOf('.') + 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Abstracts out the logic for domain resolution
|
||||
*
|
||||
* @param options
|
||||
* @param hostLookup
|
||||
* @param hostLookup.type
|
||||
* @param hostLookup.hostIpMap
|
||||
* @param hostname
|
||||
* @param callback
|
||||
*/
|
||||
_lookup = function (options, hostLookup, hostname, callback) {
|
||||
var hostIpMap,
|
||||
resolvedFamily = 4,
|
||||
resolvedAddr;
|
||||
|
||||
// first we try to resolve the hostname using hosts file configuration
|
||||
hostLookup && hostLookup.type === HOSTS_TYPE.HOST_IP_MAP &&
|
||||
(hostIpMap = hostLookup[HOSTS_TYPE.HOST_IP_MAP]) && (resolvedAddr = hostIpMap[hostname]);
|
||||
|
||||
if (resolvedAddr) {
|
||||
// since we only get an string for the resolved ip address, we manually find it's family (4 or 6)
|
||||
// there will be at-least one `:` in an IPv6 (https://en.wikipedia.org/wiki/IPv6_address#Representation)
|
||||
resolvedAddr.indexOf(COLON) !== -1 && (resolvedFamily = 6); // eslint-disable-line lodash/prefer-includes
|
||||
|
||||
// returning error synchronously causes uncaught error because listeners are not attached to error events
|
||||
// on socket yet
|
||||
return setImmediate(function () {
|
||||
callback(null, resolvedAddr, resolvedFamily);
|
||||
});
|
||||
}
|
||||
|
||||
// no hosts file configuration provided or no match found. Falling back to normal dns lookup
|
||||
return dns.lookup(hostname, options, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Tries to make a TCP connection to the given host and port. If successful, the connection is immediately
|
||||
* destroyed.
|
||||
*
|
||||
* @param host
|
||||
* @param port
|
||||
* @param callback
|
||||
*/
|
||||
connect = function (host, port, callback) {
|
||||
var socket = new Socket(),
|
||||
called,
|
||||
|
||||
done = function (type) {
|
||||
if (!called) {
|
||||
callback(type === S_CONNECT ? null : true); // eslint-disable-line callback-return
|
||||
called = true;
|
||||
this.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
socket.setTimeout(SOCKET_TIMEOUT, done.bind(socket, S_TIMEOUT));
|
||||
socket.once('connect', done.bind(socket, S_CONNECT));
|
||||
socket.once('error', done.bind(socket, S_ERROR));
|
||||
socket.connect(port, host);
|
||||
socket = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Override DNS lookups in Node, to handle localhost as a special case.
|
||||
* Chrome tries connecting to IPv6 by default, so we try the same thing.
|
||||
*
|
||||
* @param lookupOptions
|
||||
* @param lookupOptions.port
|
||||
* @param lookupOptions.network
|
||||
* @param lookupOptions.network.restrictedAddresses
|
||||
* @param lookupOptions.network.hostLookup
|
||||
* @param lookupOptions.network.hostLookup.type
|
||||
* @param lookupOptions.network.hostLookup.hostIpMap
|
||||
* @param hostname
|
||||
* @param options
|
||||
* @param callback
|
||||
*/
|
||||
lookup = function (lookupOptions, hostname, options, callback) {
|
||||
var self = this,
|
||||
lowercaseHost = hostname && hostname.toLowerCase(),
|
||||
networkOpts = lookupOptions.network || {},
|
||||
hostLookup = networkOpts.hostLookup;
|
||||
|
||||
// do dns.lookup if hostname is not one of:
|
||||
// - localhost
|
||||
// - *.localhost
|
||||
if (getTLD(lowercaseHost) !== LOCALHOST) {
|
||||
return _lookup(options, hostLookup, lowercaseHost, function (err, addr, family) {
|
||||
if (err) { return callback(err); }
|
||||
|
||||
return callback(self.isAddressRestricted(addr, networkOpts) ?
|
||||
new Error(ERROR_ADDRESS_RESOLVE + hostname) : null, addr, family);
|
||||
});
|
||||
}
|
||||
|
||||
// Try checking if we can connect to IPv6 localhost ('::1')
|
||||
connect(LOCAL_IPV6, lookupOptions.port, function (err) {
|
||||
// use IPv4 if we cannot connect to IPv6
|
||||
if (err) { return callback(null, LOCAL_IPV4, 4); }
|
||||
|
||||
callback(null, LOCAL_IPV6, 6);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper function to return postman-request compatible URL parser which
|
||||
* respects the `disableUrlEncoding` protocol profile behavior.
|
||||
*
|
||||
* @private
|
||||
* @param {Boolean} disableUrlEncoding
|
||||
* @returns {Object}
|
||||
*/
|
||||
urlParser = function (disableUrlEncoding) {
|
||||
return {
|
||||
parse: function (urlToParse) {
|
||||
return urlEncoder.toNodeUrl(urlToParse, disableUrlEncoding);
|
||||
},
|
||||
|
||||
resolve: function (base, relative) {
|
||||
if (typeof base === STRING) {
|
||||
// @note we parse base URL here to respect `disableUrlEncoding`
|
||||
// option even though resolveNodeUrl() accepts it as a string
|
||||
base = urlEncoder.toNodeUrl(base, disableUrlEncoding);
|
||||
}
|
||||
|
||||
return urlEncoder.resolveNodeUrl(base, relative);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Resolves given property with protocol profile behavior.
|
||||
* Returns protocolProfileBehavior value if the given property is present.
|
||||
* Else, returns value defined in default options.
|
||||
*
|
||||
* @param {String} property - Property name to look for
|
||||
* @param {Object} defaultOpts - Default request options
|
||||
* @param {Object} protocolProfileBehavior - Protocol profile behaviors
|
||||
* @returns {*} - Resolved request option value
|
||||
*/
|
||||
resolveWithProtocolProfileBehavior = function (property, defaultOpts, protocolProfileBehavior) {
|
||||
// bail out if property or defaultOpts is not defined
|
||||
if (!(property && defaultOpts)) { return; }
|
||||
|
||||
if (protocolProfileBehavior && protocolProfileBehavior.hasOwnProperty(property)) {
|
||||
return protocolProfileBehavior[property];
|
||||
}
|
||||
|
||||
return defaultOpts[property];
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
/**
|
||||
* Creates a node request compatible options object from a request.
|
||||
*
|
||||
* @param request
|
||||
* @param defaultOpts
|
||||
* @param defaultOpts.agents
|
||||
* @param defaultOpts.network
|
||||
* @param defaultOpts.keepAlive
|
||||
* @param defaultOpts.timeout
|
||||
* @param defaultOpts.strictSSL
|
||||
* @param defaultOpts.cookieJar The cookie jar to use (if any).
|
||||
* @param defaultOpts.followRedirects
|
||||
* @param defaultOpts.followOriginalHttpMethod
|
||||
* @param defaultOpts.maxRedirects
|
||||
* @param defaultOpts.maxResponseSize
|
||||
* @param defaultOpts.implicitCacheControl
|
||||
* @param defaultOpts.implicitTraceHeader
|
||||
* @param defaultOpts.removeRefererHeaderOnRedirect
|
||||
* @param defaultOpts.timings
|
||||
* @param protocolProfileBehavior
|
||||
* @returns {{}}
|
||||
*/
|
||||
getRequestOptions: function (request, defaultOpts, protocolProfileBehavior) {
|
||||
!defaultOpts && (defaultOpts = {});
|
||||
!protocolProfileBehavior && (protocolProfileBehavior = {});
|
||||
|
||||
var options = {},
|
||||
networkOptions = defaultOpts.network || {},
|
||||
self = this,
|
||||
bodyParams,
|
||||
useWhatWGUrlParser = defaultOpts.useWhatWGUrlParser,
|
||||
disableUrlEncoding = protocolProfileBehavior.disableUrlEncoding,
|
||||
disabledSystemHeaders = protocolProfileBehavior.disabledSystemHeaders || {},
|
||||
// the system headers provided in requester configuration
|
||||
systemHeaders = defaultOpts.systemHeaders || {},
|
||||
url = useWhatWGUrlParser ? urlEncoder.toNodeUrl(request.url, disableUrlEncoding) :
|
||||
urlEncoder.toLegacyNodeUrl(request.url.toString(true)),
|
||||
isSSL = _.startsWith(url.protocol, HTTPS),
|
||||
isTunnelingProxy = request.proxy && (request.proxy.tunnel || isSSL),
|
||||
header,
|
||||
reqOption,
|
||||
portNumber,
|
||||
behaviorName,
|
||||
port = url && url.port,
|
||||
hostname = url && url.hostname && url.hostname.toLowerCase(),
|
||||
proxyHostname = request.proxy && request.proxy.host;
|
||||
|
||||
// resolve all *.localhost to localhost itself
|
||||
// RFC: 6761 section 6.3 (https://tools.ietf.org/html/rfc6761#section-6.3)
|
||||
if (getTLD(hostname) === LOCALHOST) {
|
||||
// @note setting hostname to localhost ensures that we override lookup function
|
||||
hostname = LOCALHOST;
|
||||
}
|
||||
|
||||
if (getTLD(proxyHostname) === LOCALHOST) {
|
||||
proxyHostname = LOCALHOST;
|
||||
}
|
||||
|
||||
options.url = url;
|
||||
options.method = request.method;
|
||||
options.timeout = defaultOpts.timeout;
|
||||
options.gzip = true;
|
||||
options.brotli = true;
|
||||
options.time = defaultOpts.timings;
|
||||
options.verbose = defaultOpts.verbose;
|
||||
options.agents = defaultOpts.agents;
|
||||
options.extraCA = defaultOpts.extendedRootCA;
|
||||
options.ignoreProxyEnvironmentVariables = defaultOpts.ignoreProxyEnvironmentVariables;
|
||||
|
||||
// Disable encoding of URL in postman-request in order to use pre-encoded URL object returned from
|
||||
// toNodeUrl() function of postman-url-encoder
|
||||
options.disableUrlEncoding = true;
|
||||
|
||||
// Ensures that "request" creates URL encoded formdata or querystring as
|
||||
// foo=bar&foo=baz instead of foo[0]=bar&foo[1]=baz
|
||||
options.useQuerystring = true;
|
||||
|
||||
// set encoding to null so that the response is a stream
|
||||
options.encoding = null;
|
||||
|
||||
// Re-encode status message using `utf8` character encoding in postman-request.
|
||||
// This is done to correctly represent status messages with characters that lie outside
|
||||
// the range of `latin1` encoding (which is the default encoding in which status message is returned)
|
||||
options.statusMessageEncoding = 'utf8';
|
||||
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (reqOption in PPB_OPTS) {
|
||||
behaviorName = PPB_OPTS[reqOption];
|
||||
options[reqOption] = resolveWithProtocolProfileBehavior(behaviorName, defaultOpts, protocolProfileBehavior);
|
||||
}
|
||||
|
||||
// set cookie jar if not disabled
|
||||
if (!protocolProfileBehavior.disableCookies) {
|
||||
options.jar = defaultOpts.cookieJar || true;
|
||||
}
|
||||
|
||||
// use the server's cipher suite order instead of the client's during negotiation
|
||||
if (protocolProfileBehavior.tlsPreferServerCiphers) {
|
||||
options.honorCipherOrder = true;
|
||||
}
|
||||
|
||||
// the SSL and TLS protocol versions to disabled during negotiation
|
||||
if (Array.isArray(protocolProfileBehavior.tlsDisabledProtocols)) {
|
||||
protocolProfileBehavior.tlsDisabledProtocols.forEach(function (protocol) {
|
||||
// since secure options doesn't support TLSv1.3 before Node 14
|
||||
// @todo remove the if condition when we drop support for Node 12
|
||||
if (protocol === 'TLSv1_3' && !constants[SSL_OP_NO + protocol]) {
|
||||
options.maxVersion = 'TLSv1.2';
|
||||
}
|
||||
else {
|
||||
options.secureOptions |= constants[SSL_OP_NO + protocol];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// order of cipher suites that the SSL server profile uses to establish a secure connection
|
||||
if (Array.isArray(protocolProfileBehavior.tlsCipherSelection)) {
|
||||
options.ciphers = protocolProfileBehavior.tlsCipherSelection.join(':');
|
||||
}
|
||||
|
||||
if (typeof defaultOpts.maxResponseSize === 'number') {
|
||||
options.maxResponseSize = defaultOpts.maxResponseSize;
|
||||
}
|
||||
|
||||
// Request body may return different options depending on the type of the body.
|
||||
// @note getRequestBody may add system headers based on intent
|
||||
bodyParams = self.getRequestBody(request, protocolProfileBehavior);
|
||||
|
||||
// Disable 'Cache-Control' and 'Postman-Token' based on global options
|
||||
// @note this also make 'cache-control' and 'postman-token' part of `disabledSystemHeaders`
|
||||
!defaultOpts.implicitCacheControl && (disabledSystemHeaders['cache-control'] = true);
|
||||
!defaultOpts.implicitTraceHeader && (disabledSystemHeaders['postman-token'] = true);
|
||||
|
||||
// Add additional system headers to the request instance
|
||||
addSystemHeaders(request, options, disabledSystemHeaders, systemHeaders);
|
||||
|
||||
|
||||
// Don't add `Host` header if disabled using disabledSystemHeaders
|
||||
// @note This can't be part of `blacklistHeaders` option as `setHost` is
|
||||
// a Node.js http.request option to specifies whether or not to
|
||||
// automatically add the Host header or not.
|
||||
if (disabledSystemHeaders.host) {
|
||||
header = oneNormalizedHeader(request.headers, 'host');
|
||||
|
||||
// only possible with AWS auth
|
||||
header && header.system && (header.disabled = true);
|
||||
|
||||
// set `setHost` to false if there's no host header defined by the user
|
||||
// or, the present host is added by the system.
|
||||
(!header || header.system) && (options.setHost = false);
|
||||
}
|
||||
|
||||
// Set `allowContentTypeOverride` if content-type header is disabled,
|
||||
// this allows overriding (if invalid) the content-type for form-data
|
||||
// and urlencoded request body.
|
||||
if (disabledSystemHeaders['content-type']) {
|
||||
options.allowContentTypeOverride = true;
|
||||
}
|
||||
|
||||
options.blacklistHeaders = [];
|
||||
ALLOWED_BLACKLIST_HEADERS.forEach(function (headerKey) {
|
||||
if (!disabledSystemHeaders[headerKey]) { return; } // not disabled
|
||||
|
||||
header = oneNormalizedHeader(request.headers, headerKey);
|
||||
|
||||
// content-type added by body helper
|
||||
header && header.system && (header.disabled = true);
|
||||
|
||||
// blacklist only if it's missing or part of system added headers
|
||||
(!header || header.system) && options.blacklistHeaders.push(headerKey);
|
||||
|
||||
// @note for non-GET requests if no 'content-length' is set, it
|
||||
// it assumes to be chucked request body and add 'transfer-encoding'
|
||||
// here, we ensure blacklisting 'content-length' will also blacklist
|
||||
// 'transfer-encoding' header.
|
||||
if (headerKey === 'content-length') {
|
||||
header = oneNormalizedHeader(request.headers, 'transfer-encoding');
|
||||
(!header || header.system) && options.blacklistHeaders.push('transfer-encoding');
|
||||
}
|
||||
});
|
||||
|
||||
// Finally, get headers object
|
||||
options.headers = request.getHeaders({enabled: true, sanitizeKeys: true});
|
||||
|
||||
// override URL parser to WhatWG URL parser
|
||||
if (useWhatWGUrlParser) {
|
||||
options.urlParser = urlParser(disableUrlEncoding);
|
||||
}
|
||||
|
||||
// override DNS lookup
|
||||
if (networkOptions.restrictedAddresses || hostname === LOCALHOST ||
|
||||
(!isTunnelingProxy && proxyHostname === LOCALHOST) || networkOptions.hostLookup) {
|
||||
// Use proxy port for localhost resolution in case of non-tunneling proxy
|
||||
// because the request will be sent to proxy server by postman-request
|
||||
if (request.proxy && !isTunnelingProxy) {
|
||||
portNumber = Number(request.proxy.port);
|
||||
}
|
||||
// Otherwise, use request's port
|
||||
else {
|
||||
portNumber = Number(port) || (isSSL ? HTTPS_DEFAULT_PORT : HTTP_DEFAULT_PORT);
|
||||
}
|
||||
|
||||
_.isFinite(portNumber) && (options.lookup = lookup.bind(this, {
|
||||
port: portNumber,
|
||||
network: networkOptions
|
||||
}));
|
||||
}
|
||||
|
||||
_.assign(options, bodyParams, {
|
||||
// @note these common agent options can be overridden by specifying
|
||||
// custom http/https agents using requester option `agents`
|
||||
agentOptions: {
|
||||
keepAlive: defaultOpts.keepAlive
|
||||
}
|
||||
});
|
||||
|
||||
return options;
|
||||
},
|
||||
|
||||
/**
|
||||
* Processes a request body and puts it in a format compatible with
|
||||
* the "request" library.
|
||||
*
|
||||
* @todo: Move this to the SDK.
|
||||
* @param request - Request object
|
||||
* @param protocolProfileBehavior - Protocol profile behaviors
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
getRequestBody: function (request, protocolProfileBehavior) {
|
||||
if (!(request && request.body)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var i,
|
||||
property,
|
||||
requestBody = request.body,
|
||||
requestBodyType = requestBody.mode,
|
||||
requestMethod = (typeof request.method === STRING) ? request.method.toLowerCase() : undefined,
|
||||
bodyIsEmpty = requestBody.isEmpty(),
|
||||
bodyIsDisabled = requestBody.disabled,
|
||||
bodyContent = requestBody[requestBodyType],
|
||||
|
||||
// flag to decide body pruning for METHODS_WITHOUT_BODY
|
||||
// @note this will be `true` even if protocolProfileBehavior is undefined
|
||||
pruneBody = protocolProfileBehavior ? !protocolProfileBehavior.disableBodyPruning : true;
|
||||
|
||||
// early bailout for empty or disabled body (this area has some legacy shenanigans)
|
||||
if (bodyIsEmpty || bodyIsDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// body is empty if all the params in urlencoded and formdata body are disabled
|
||||
// @todo update Collection SDK isEmpty method to account for this
|
||||
if (sdk.PropertyList.isPropertyList(bodyContent)) {
|
||||
bodyIsEmpty = true;
|
||||
|
||||
for (i = bodyContent.members.length - 1; i >= 0; i--) {
|
||||
property = bodyContent.members[i];
|
||||
// bail out if a single enabled property is present
|
||||
if (property && !property.disabled) {
|
||||
bodyIsEmpty = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// bail out if body is empty
|
||||
if (bodyIsEmpty) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// bail out if request method doesn't support body and pruneBody is true.
|
||||
if (METHODS_WITHOUT_BODY[requestMethod] && pruneBody) {
|
||||
return;
|
||||
}
|
||||
|
||||
// even if body is not empty, but the body type is not known, we do not know how to parse the same
|
||||
//
|
||||
// @note if you'd like to support additional body types beyond formdata, url-encoding, etc, add the same to
|
||||
// the builder module
|
||||
if (!requestBodyBuilders.hasOwnProperty(requestBodyType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return requestBodyBuilders[requestBodyType](bodyContent, request);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a JSON compatible with the Node's request library. (Also contains the original request)
|
||||
*
|
||||
* @param rawResponse Can be an XHR response or a Node request compatible response.
|
||||
* about the actual request that was sent.
|
||||
* @param requestOptions Options that were used to send the request.
|
||||
* @param responseBody Body as a string.
|
||||
*/
|
||||
jsonifyResponse: function (rawResponse, requestOptions, responseBody) {
|
||||
if (!rawResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
var responseJSON;
|
||||
|
||||
if (rawResponse.toJSON) {
|
||||
responseJSON = rawResponse.toJSON();
|
||||
responseJSON.request && _.assign(responseJSON.request, {
|
||||
data: requestOptions.form || requestOptions.formData || requestOptions.body || {},
|
||||
uri: { // @todo remove this
|
||||
href: requestOptions.url && requestOptions.url.href || requestOptions.url
|
||||
},
|
||||
url: requestOptions.url && requestOptions.url.href || requestOptions.url
|
||||
});
|
||||
|
||||
rawResponse.rawHeaders &&
|
||||
(responseJSON.headers = this.arrayPairsToObject(rawResponse.rawHeaders) || responseJSON.headers);
|
||||
|
||||
return responseJSON;
|
||||
}
|
||||
|
||||
responseBody = responseBody || '';
|
||||
|
||||
// @todo drop support or isolate XHR requester in v8
|
||||
// XHR :/
|
||||
return {
|
||||
statusCode: rawResponse.status,
|
||||
body: responseBody,
|
||||
headers: _.transform(sdk.Header.parse(rawResponse.getAllResponseHeaders()), function (acc, header) {
|
||||
if (acc[header.key]) {
|
||||
!Array.isArray(acc[header.key]) && (acc[header.key] = [acc[header.key]]);
|
||||
acc[header.key].push(header.value);
|
||||
}
|
||||
else {
|
||||
acc[header.key] = header.value;
|
||||
}
|
||||
}, {}),
|
||||
request: {
|
||||
method: requestOptions.method || 'GET',
|
||||
headers: requestOptions.headers,
|
||||
uri: { // @todo remove this
|
||||
href: requestOptions.url && requestOptions.url.href || requestOptions.url
|
||||
},
|
||||
url: requestOptions.url && requestOptions.url.href || requestOptions.url,
|
||||
data: requestOptions.form || requestOptions.formData || requestOptions.body || {}
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* ArrayBuffer to String
|
||||
*
|
||||
* @param {ArrayBuffer} buffer
|
||||
* @returns {String}
|
||||
*/
|
||||
arrayBufferToString: function (buffer) {
|
||||
var str = '',
|
||||
uArrayVal = new Uint8Array(buffer),
|
||||
|
||||
i,
|
||||
ii;
|
||||
|
||||
for (i = 0, ii = uArrayVal.length; i < ii; i++) {
|
||||
str += String.fromCharCode(uArrayVal[i]);
|
||||
}
|
||||
|
||||
return str;
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts an array of sequential pairs to an object.
|
||||
*
|
||||
* @param arr
|
||||
* @returns {{}}
|
||||
*
|
||||
* @example
|
||||
* ['a', 'b', 'c', 'd'] ====> {a: 'b', c: 'd' }
|
||||
*/
|
||||
arrayPairsToObject: function (arr) {
|
||||
if (!_.isArray(arr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var obj = {},
|
||||
key,
|
||||
val,
|
||||
i,
|
||||
ii;
|
||||
|
||||
for (i = 0, ii = arr.length; i < ii; i += 2) {
|
||||
key = arr[i];
|
||||
val = arr[i + 1];
|
||||
|
||||
if (_.has(obj, key)) {
|
||||
!_.isArray(obj[key]) && (obj[key] = [obj[key]]);
|
||||
obj[key].push(val);
|
||||
}
|
||||
else {
|
||||
obj[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if a given host or IP is has been restricted in the options.
|
||||
*
|
||||
* @param {String} host
|
||||
* @param {Object} networkOptions
|
||||
* @param {Array<String>} networkOptions.restrictedAddresses
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isAddressRestricted: function (host, networkOptions) {
|
||||
return networkOptions.restrictedAddresses &&
|
||||
networkOptions.restrictedAddresses[(host && host.toLowerCase())];
|
||||
}
|
||||
};
|
334
node_modules/postman-runtime/lib/requester/dry-run.js
generated
vendored
Normal file
334
node_modules/postman-runtime/lib/requester/dry-run.js
generated
vendored
Normal file
@@ -0,0 +1,334 @@
|
||||
/* istanbul ignore file */
|
||||
// @todo
|
||||
// 1. Return with annotations like (overridden headers, auth headers etc.)
|
||||
// 2. Utilize requester (core.js) methods for dryRun
|
||||
// 3. Add tests
|
||||
const _ = require('lodash'),
|
||||
async = require('async'),
|
||||
mime = require('mime-types'),
|
||||
urlEncoder = require('postman-url-encoder'),
|
||||
Request = require('postman-collection').Request,
|
||||
authorizeRequest = require('../authorizer').authorizeRequest,
|
||||
authHandlers = require('../authorizer').AuthLoader.handlers,
|
||||
version = require('../../package.json').version,
|
||||
|
||||
CALCULATED_AT_RUNTIME = '<calculated when request is sent>',
|
||||
COOKIE = 'Cookie',
|
||||
FUNCTION = 'function',
|
||||
CONTENT_TYPE = 'Content-Type',
|
||||
DEFAULT_MIME_TYPE = 'application/octet-stream',
|
||||
CONTENT_TYPE_URLENCODED = 'application/x-www-form-urlencoded',
|
||||
CONTENT_TYPE_FORMDATA = 'multipart/form-data; boundary=' + CALCULATED_AT_RUNTIME,
|
||||
|
||||
CONTENT_TYPE_LANGUAGE = {
|
||||
'html': 'text/html',
|
||||
'text': 'text/plain',
|
||||
'json': 'application/json',
|
||||
'javascript': 'application/javascript',
|
||||
'xml': 'application/xml'
|
||||
},
|
||||
|
||||
BODY_MODE = {
|
||||
raw: 'raw',
|
||||
file: 'file',
|
||||
graphql: 'graphql',
|
||||
formdata: 'formdata',
|
||||
urlencoded: 'urlencoded'
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if request body is empty and also handles disabled params for urlencoded
|
||||
* and formdata bodies.
|
||||
*
|
||||
* @todo Update Collection SDK isEmpty method to account for this.
|
||||
*
|
||||
* @private
|
||||
* @param {RequestBody} body
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
function bodyIsEmpty (body) {
|
||||
if (!body || body.disabled || body.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var i,
|
||||
param,
|
||||
mode = body.mode;
|
||||
|
||||
if (!(mode === BODY_MODE.formdata || mode === BODY_MODE.urlencoded)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (i = body[mode].members.length - 1; i >= 0; i--) {
|
||||
param = body[mode].members[i];
|
||||
// bail out if a single enabled param is present
|
||||
if (param && !param.disabled) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new System header.
|
||||
*
|
||||
* @param {object} headers
|
||||
* @param {String} key
|
||||
* @param {String} value
|
||||
*/
|
||||
function addSystemHeader (headers, key, value) {
|
||||
headers.add({
|
||||
key: key,
|
||||
value: value,
|
||||
system: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorize the given request.
|
||||
*
|
||||
* @private
|
||||
* @param {Request} request
|
||||
* @param {Function} callback
|
||||
*/
|
||||
function setAuthorization (request, callback) {
|
||||
authorizeRequest(request, function (err, clonedRequest) {
|
||||
// @note authorizeRequest returns a cloned request.
|
||||
!clonedRequest && (clonedRequest = new Request(request.toJSON()));
|
||||
|
||||
if (err) {
|
||||
return callback(null, clonedRequest);
|
||||
}
|
||||
|
||||
var auth = request.auth,
|
||||
authType = auth && auth.type,
|
||||
manifest = _.get(authHandlers, [authType, 'manifest']),
|
||||
headers = _.get(clonedRequest, 'headers.reference') || {},
|
||||
queryParams = _.get(clonedRequest, 'url.query.reference') || {},
|
||||
bodyParams = _.get(clonedRequest, 'body.urlencoded.reference') || {},
|
||||
propertyList,
|
||||
propertyKey,
|
||||
property;
|
||||
|
||||
if (authType === 'apikey' && (auth = auth.apikey)) {
|
||||
propertyKey = String(auth.get('key')).toLowerCase();
|
||||
propertyList = auth.get('in') === 'query' ? queryParams : headers;
|
||||
|
||||
if ((property = propertyList[propertyKey])) {
|
||||
Array.isArray(property) && (property = property[property.length - 1]);
|
||||
property.auth = true;
|
||||
}
|
||||
|
||||
return callback(null, clonedRequest);
|
||||
}
|
||||
|
||||
if (!(manifest && manifest.updates)) {
|
||||
return callback(null, clonedRequest);
|
||||
}
|
||||
|
||||
manifest.updates.forEach(function (update) {
|
||||
propertyKey = update.property;
|
||||
|
||||
switch (update.type) {
|
||||
case 'header':
|
||||
propertyKey = propertyKey.toLowerCase();
|
||||
propertyList = headers;
|
||||
break;
|
||||
case 'url.param':
|
||||
propertyList = queryParams;
|
||||
break;
|
||||
case 'body.urlencoded':
|
||||
propertyList = bodyParams;
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
if ((property = propertyList[propertyKey])) {
|
||||
Array.isArray(property) && (property = property[property.length - 1]);
|
||||
property.auth = true;
|
||||
}
|
||||
});
|
||||
|
||||
callback(null, clonedRequest);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Content-Type header based on selected request body.
|
||||
*
|
||||
* @private
|
||||
* @param {Request} request
|
||||
* @param {Function} callback
|
||||
*/
|
||||
function setContentType (request, callback) {
|
||||
// bail out if body is empty
|
||||
if (bodyIsEmpty(request.body)) {
|
||||
return callback(null, request);
|
||||
}
|
||||
|
||||
var headers = request.headers,
|
||||
contentLanguage;
|
||||
|
||||
switch (request.body.mode) {
|
||||
case BODY_MODE.raw:
|
||||
contentLanguage = _.get(request, 'body.options.raw.language', 'text');
|
||||
addSystemHeader(headers, CONTENT_TYPE, CONTENT_TYPE_LANGUAGE[contentLanguage] ||
|
||||
CONTENT_TYPE_LANGUAGE.text);
|
||||
break;
|
||||
case BODY_MODE.urlencoded:
|
||||
addSystemHeader(headers, CONTENT_TYPE, CONTENT_TYPE_URLENCODED);
|
||||
break;
|
||||
case BODY_MODE.formdata:
|
||||
addSystemHeader(headers, CONTENT_TYPE, CONTENT_TYPE_FORMDATA);
|
||||
break;
|
||||
case BODY_MODE.graphql:
|
||||
addSystemHeader(headers, CONTENT_TYPE, CONTENT_TYPE_LANGUAGE.json);
|
||||
break;
|
||||
case BODY_MODE.file:
|
||||
addSystemHeader(headers, CONTENT_TYPE,
|
||||
mime.lookup(request.body.file && request.body.file.src) || DEFAULT_MIME_TYPE);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
addSystemHeader(headers, 'Content-Length', CALCULATED_AT_RUNTIME);
|
||||
|
||||
callback(null, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Cookie header for the given request url.
|
||||
*
|
||||
* @private
|
||||
* @param {Request} request
|
||||
* @param {Object} cookieJar
|
||||
* @param {Function} callback
|
||||
*/
|
||||
function setCookie (request, cookieJar, callback) {
|
||||
// bail out if not a valid instance of CookieJar
|
||||
if (!(cookieJar && cookieJar.getCookieString)) {
|
||||
return callback(null, request);
|
||||
}
|
||||
|
||||
// @note don't pass request.url instance to force re-parsing of the URL
|
||||
cookieJar.getCookieString(urlEncoder.toNodeUrl(request.url.toString()), function (err, cookies) {
|
||||
if (err) {
|
||||
return callback(null, request);
|
||||
}
|
||||
|
||||
if (cookies && cookies.length) {
|
||||
addSystemHeader(request.headers, COOKIE, cookies);
|
||||
}
|
||||
|
||||
callback(null, request);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method to dry run the given request instance.
|
||||
* It returns the cloned request instance with the system added properties.
|
||||
*
|
||||
* @param {Request} request
|
||||
* @param {Object} options
|
||||
* @param {Object} options.cookieJar
|
||||
* @param {Object} options.protocolProfileBehavior
|
||||
* @param {Function} done
|
||||
*/
|
||||
function dryRun (request, options, done) {
|
||||
if (!done && typeof options === FUNCTION) {
|
||||
done = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
if (!Request.isRequest(request)) {
|
||||
return done(new Error('Invalid Request instance'));
|
||||
}
|
||||
|
||||
!options && (options = {});
|
||||
|
||||
var cookieJar = options.cookieJar,
|
||||
implicitCacheControl = options.implicitCacheControl,
|
||||
implicitTraceHeader = options.implicitTraceHeader,
|
||||
disabledSystemHeaders = _.get(options.protocolProfileBehavior, 'disabledSystemHeaders') || {},
|
||||
disableCookies = _.get(options.protocolProfileBehavior, 'disableCookies');
|
||||
|
||||
async.waterfall([
|
||||
function setAuthorizationHeaders (next) {
|
||||
setAuthorization(request, next);
|
||||
},
|
||||
function setContentTypeHeader (request, next) {
|
||||
setContentType(request, next);
|
||||
},
|
||||
function setContentLength (request, next) {
|
||||
var headers = request.headers,
|
||||
header = headers.one('content-length');
|
||||
|
||||
// bail out if header added by body helper
|
||||
if (header && header.system) {
|
||||
return next(null, request);
|
||||
}
|
||||
|
||||
switch (String(request.method).toUpperCase()) {
|
||||
case 'GET':
|
||||
case 'HEAD':
|
||||
case 'TRACE':
|
||||
case 'DELETE':
|
||||
case 'CONNECT':
|
||||
case 'OPTIONS':
|
||||
break;
|
||||
default:
|
||||
addSystemHeader(headers, 'Content-Length', '0');
|
||||
break;
|
||||
}
|
||||
|
||||
next(null, request);
|
||||
},
|
||||
function setCookieHeader (request, next) {
|
||||
if (disableCookies || !cookieJar) {
|
||||
return next(null, request);
|
||||
}
|
||||
|
||||
setCookie(request, cookieJar, next);
|
||||
},
|
||||
function setStaticHeaders (request, next) {
|
||||
var headers = request.headers;
|
||||
|
||||
// remove header added by auth helpers
|
||||
headers.remove(function (header) {
|
||||
return header.system && header.key.toLowerCase() === 'host';
|
||||
});
|
||||
|
||||
addSystemHeader(headers, 'User-Agent', 'PostmanRuntime/' + version);
|
||||
addSystemHeader(headers, 'Accept', '*/*');
|
||||
addSystemHeader(headers, 'Accept-Encoding', 'gzip, deflate, br');
|
||||
addSystemHeader(headers, 'Host', CALCULATED_AT_RUNTIME);
|
||||
addSystemHeader(headers, 'Connection', 'keep-alive');
|
||||
implicitCacheControl && addSystemHeader(headers, 'Cache-Control', 'no-cache');
|
||||
implicitTraceHeader && addSystemHeader(headers, 'Postman-Token', CALCULATED_AT_RUNTIME);
|
||||
|
||||
next(null, request);
|
||||
},
|
||||
function disableSystemHeaders (request, next) {
|
||||
var headersReference = request.headers.reference,
|
||||
header;
|
||||
|
||||
_.forEach(disabledSystemHeaders, function (disabled, headerKey) {
|
||||
if (!disabled) { return; }
|
||||
|
||||
if ((header = headersReference[headerKey.toLowerCase()])) {
|
||||
Array.isArray(header) && (header = header[header.length - 1]);
|
||||
header.system && (header.disabled = true);
|
||||
}
|
||||
});
|
||||
|
||||
next(null, request);
|
||||
}
|
||||
], function (err, request) {
|
||||
if (err) { return done(err); }
|
||||
|
||||
done(null, request);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = dryRun;
|
4
node_modules/postman-runtime/lib/requester/index.js
generated
vendored
Normal file
4
node_modules/postman-runtime/lib/requester/index.js
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
Requester: require('./requester').Requester,
|
||||
RequesterPool: require('./requester-pool').RequesterPool
|
||||
};
|
94
node_modules/postman-runtime/lib/requester/request-wrapper.js
generated
vendored
Normal file
94
node_modules/postman-runtime/lib/requester/request-wrapper.js
generated
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
var _ = require('lodash'),
|
||||
async = require('async'),
|
||||
requests = require('postman-request'),
|
||||
|
||||
/**
|
||||
* Sets the Proxy and tunnel to the options
|
||||
*
|
||||
* @param request
|
||||
* @param options
|
||||
* @param cb
|
||||
*/
|
||||
setProxy = function (request, options, cb) {
|
||||
var proxyConfig;
|
||||
|
||||
if ((proxyConfig = _.get(request, 'proxy'))) {
|
||||
options.proxy = proxyConfig.getProxyUrl();
|
||||
// TODO: Use tri-state var for tunnel in SDK and update here
|
||||
// for now determine the tunnel value from the URL unless explicitly set to true
|
||||
options.tunnel = proxyConfig.tunnel ? true : request.url.protocol === 'https';
|
||||
}
|
||||
|
||||
// if proxy is not set, postman-request implicitly fallbacks to proxy
|
||||
// environment variables. To opt-out of this, set `ignoreProxyEnvironmentVariables`
|
||||
// requester option.
|
||||
// Setting proxy to `false` opt out of the implicit proxy configuration
|
||||
// of the other environment variables.
|
||||
if (!options.proxy && options.ignoreProxyEnvironmentVariables) {
|
||||
options.proxy = false;
|
||||
}
|
||||
|
||||
cb(null, request, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the certificate from the options.certificate
|
||||
* And appends it with the options provided
|
||||
*
|
||||
* @param request
|
||||
* @param options
|
||||
* @param cb
|
||||
*/
|
||||
setCertificate = function (request, options, cb) {
|
||||
var certificate,
|
||||
isSSL = request.url.protocol === 'https',
|
||||
hasCertificate = request.certificate;
|
||||
|
||||
// exit if protocol is not https
|
||||
// or both certificateManager and certificateList are missing
|
||||
if (!isSSL || !hasCertificate) {
|
||||
return cb(null, options);
|
||||
}
|
||||
|
||||
certificate = request.certificate;
|
||||
|
||||
if (!certificate) { return cb(null, options); }
|
||||
|
||||
_.assign(options, {
|
||||
pfx: _.get(certificate, 'pfx.value'),
|
||||
key: _.get(certificate, 'key.value'),
|
||||
cert: _.get(certificate, 'cert.value'),
|
||||
passphrase: certificate.passphrase
|
||||
});
|
||||
cb(null, options);
|
||||
};
|
||||
|
||||
// Enable support for extending root CAs.
|
||||
// Refer: https://github.com/postmanlabs/postman-request/pull/35
|
||||
// @todo trigger console warning (using callback) if not enabled.
|
||||
requests.enableNodeExtraCACerts();
|
||||
|
||||
module.exports = function (request, options, onStart, callback) {
|
||||
var req = {};
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
setProxy(request, options, next);
|
||||
},
|
||||
function (request, options, next) {
|
||||
setCertificate(request, options, next);
|
||||
}
|
||||
], function (err, options) {
|
||||
if (err) { return callback(err); }
|
||||
|
||||
var request = requests(options, callback);
|
||||
|
||||
// todo: this is a hack to ensure that we can abort requests from the app before they're complete.
|
||||
req.abort = request.abort.bind(request);
|
||||
|
||||
// emit responseStart event
|
||||
request.on('response', onStart);
|
||||
});
|
||||
|
||||
return req;
|
||||
};
|
69
node_modules/postman-runtime/lib/requester/requester-pool.js
generated
vendored
Normal file
69
node_modules/postman-runtime/lib/requester/requester-pool.js
generated
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
var _ = require('lodash'),
|
||||
Requester = require('./requester').Requester,
|
||||
RequestCookieJar = require('postman-request').jar,
|
||||
|
||||
STRING = 'string',
|
||||
FUNCTION = 'function',
|
||||
|
||||
RequesterPool; // fn
|
||||
|
||||
RequesterPool = function (options, callback) {
|
||||
var self = this,
|
||||
extendedRootCA,
|
||||
fileResolver = options && options.fileResolver;
|
||||
|
||||
_.assign((self.options = {}), {
|
||||
timeout: _.min([
|
||||
_.get(options, 'timeout.request'),
|
||||
_.get(options, 'timeout.global')
|
||||
]), // validated later inside requester
|
||||
timings: _.get(options, 'requester.timings', true),
|
||||
verbose: _.get(options, 'requester.verbose', false),
|
||||
keepAlive: _.get(options, 'requester.keepAlive', true),
|
||||
agents: _.get(options, 'requester.agents'), // http(s).Agent instances
|
||||
cookieJar: _.get(options, 'requester.cookieJar'), // default set later in this constructor
|
||||
strictSSL: _.get(options, 'requester.strictSSL'),
|
||||
maxResponseSize: _.get(options, 'requester.maxResponseSize'),
|
||||
// @todo drop support in v8
|
||||
useWhatWGUrlParser: _.get(options, 'requester.useWhatWGUrlParser', false),
|
||||
followRedirects: _.get(options, 'requester.followRedirects', true),
|
||||
followOriginalHttpMethod: _.get(options, 'requester.followOriginalHttpMethod'),
|
||||
maxRedirects: _.get(options, 'requester.maxRedirects'),
|
||||
implicitCacheControl: _.get(options, 'requester.implicitCacheControl', true),
|
||||
implicitTraceHeader: _.get(options, 'requester.implicitTraceHeader', true),
|
||||
systemHeaders: _.get(options, 'requester.systemHeaders', {}),
|
||||
removeRefererHeaderOnRedirect: _.get(options, 'requester.removeRefererHeaderOnRedirect'),
|
||||
ignoreProxyEnvironmentVariables: _.get(options, 'ignoreProxyEnvironmentVariables'),
|
||||
network: _.get(options, 'network', {})
|
||||
});
|
||||
|
||||
// create a cookie jar if one is not provided
|
||||
if (!self.options.cookieJar) {
|
||||
self.options.cookieJar = RequestCookieJar();
|
||||
}
|
||||
|
||||
if (fileResolver && typeof fileResolver.readFile === FUNCTION &&
|
||||
typeof (extendedRootCA = _.get(options, 'requester.extendedRootCA')) === STRING) {
|
||||
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
||||
fileResolver.readFile(extendedRootCA, function (err, caCerts) {
|
||||
if (err) {
|
||||
// @todo trigger console error
|
||||
}
|
||||
else {
|
||||
// set extendedRootCA option
|
||||
self.options.extendedRootCA = caCerts;
|
||||
}
|
||||
|
||||
return callback();
|
||||
});
|
||||
}
|
||||
else {
|
||||
return callback();
|
||||
}
|
||||
};
|
||||
|
||||
RequesterPool.prototype.create = function (trace, callback) {
|
||||
return Requester.create(trace, this.options, callback);
|
||||
};
|
||||
|
||||
module.exports.RequesterPool = RequesterPool;
|
494
node_modules/postman-runtime/lib/requester/requester.js
generated
vendored
Normal file
494
node_modules/postman-runtime/lib/requester/requester.js
generated
vendored
Normal file
@@ -0,0 +1,494 @@
|
||||
var _ = require('lodash'),
|
||||
core = require('./core'),
|
||||
Emitter = require('events'),
|
||||
inherits = require('inherits'),
|
||||
now = require('performance-now'),
|
||||
sdk = require('postman-collection'),
|
||||
requests = require('./request-wrapper'),
|
||||
dryRun = require('./dry-run'),
|
||||
|
||||
RESPONSE_START_EVENT_BASE = 'response.start.',
|
||||
RESPONSE_END_EVENT_BASE = 'response.end.',
|
||||
|
||||
RESPONSE_START = 'responseStart',
|
||||
RESPONSE_END = 'response',
|
||||
|
||||
ERROR_RESTRICTED_ADDRESS = 'NETERR: getaddrinfo ENOTFOUND ',
|
||||
|
||||
/**
|
||||
* Headers which get overwritten by the requester.
|
||||
*
|
||||
* @private
|
||||
* @const
|
||||
* @type {Object}
|
||||
*/
|
||||
OVERWRITTEN_HEADERS = {
|
||||
cookie: true, // cookies get appended with `;`
|
||||
'content-length': true
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a sdk compatible cookie from a tough-cookie compatible cookie.
|
||||
*
|
||||
* @param cookie
|
||||
* @returns {Object}
|
||||
*/
|
||||
toPostmanCookie = function (cookie) {
|
||||
var expires = cookie.expiryTime();
|
||||
|
||||
cookie.toJSON && (cookie = cookie.toJSON());
|
||||
|
||||
return new sdk.Cookie({
|
||||
name: cookie.key,
|
||||
value: cookie.value,
|
||||
expires: Number.isFinite(expires) ? new Date(expires) : null,
|
||||
maxAge: cookie.maxAge,
|
||||
domain: cookie.domain,
|
||||
path: cookie.path,
|
||||
secure: cookie.secure,
|
||||
httpOnly: cookie.httpOnly,
|
||||
hostOnly: cookie.hostOnly,
|
||||
extensions: cookie.extensions
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* This method is used in conjunction with _.transform method to convert multi-value headers to multiple single
|
||||
* value headers
|
||||
*
|
||||
* @param {Array} acc
|
||||
* @param {Array|String} val
|
||||
* @param {String} key
|
||||
* @return {Object}
|
||||
*/
|
||||
transformMultiValueHeaders = function (acc, val, key) {
|
||||
var i, ii;
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
for (i = 0, ii = val.length; i < ii; i++) {
|
||||
acc.push({
|
||||
key: key,
|
||||
value: val[i]
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
acc.push({
|
||||
key: key,
|
||||
value: val
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculate request timings offset by adding runtime overhead which
|
||||
* helps to determine request prepare and process time taken.
|
||||
*
|
||||
* @param {Number} runtimeTimer - Runtime request start HR time
|
||||
* @param {Number} requestTimer - Request start HR time
|
||||
* @param {Object} timings - Request timings offset
|
||||
* @returns {Object}
|
||||
*/
|
||||
calcTimingsOffset = function (runtimeTimer, requestTimer, timings) {
|
||||
if (!(runtimeTimer && requestTimer && timings)) { return; }
|
||||
|
||||
// runtime + postman-request initialization time
|
||||
var initTime = requestTimer - runtimeTimer,
|
||||
offset = {
|
||||
request: initTime
|
||||
};
|
||||
|
||||
// add initialization overhead to request offsets
|
||||
_.forOwn(timings, function (value, key) {
|
||||
offset[key] = value + initTime;
|
||||
});
|
||||
|
||||
// total time taken by runtime to get the response
|
||||
// @note if offset.end is missing, that means request is not complete.
|
||||
// this is used to calculate timings on responseStart.
|
||||
if (offset.end) {
|
||||
offset.done = now() - runtimeTimer;
|
||||
}
|
||||
|
||||
return offset;
|
||||
},
|
||||
|
||||
Requester;
|
||||
|
||||
/**
|
||||
* Creates a new Requester, which is used to make HTTP(s) requests.
|
||||
*
|
||||
* @param trace
|
||||
* @param options
|
||||
* @param {Boolean} [options.keepAlive=true] Optimizes HTTP connections by keeping them alive, so that new requests
|
||||
* to the same host are made over the same underlying TCP connection.
|
||||
* @param {CookieJar} [options.cookieJar] A cookie jar to use with Node requests.
|
||||
* @param {Boolean} [options.strictSSL]
|
||||
* @param {Boolean} [options.followRedirects=true] If false, returns a 301/302 as the response code
|
||||
* instead of following the redirect
|
||||
* @note `options.keepAlive` is only supported in Node.
|
||||
* @note `options.cookieJar` is only supported in Node.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
inherits(Requester = function (trace, options) {
|
||||
this.options = options || {};
|
||||
|
||||
// protect the timeout value from being non-numeric or infinite
|
||||
if (!_.isFinite(this.options.timeout)) {
|
||||
this.options.timeout = undefined;
|
||||
}
|
||||
|
||||
this.trace = trace;
|
||||
Requester.super_.call(this);
|
||||
}, Emitter);
|
||||
|
||||
_.assign(Requester.prototype, /** @lends Requester.prototype */ {
|
||||
|
||||
/**
|
||||
* Perform an HTTP request.
|
||||
*
|
||||
* @param {String} id
|
||||
* @param {Request} request
|
||||
* @param {Object} protocolProfileBehavior
|
||||
* @param {Function} callback
|
||||
*/
|
||||
request: function (id, request, protocolProfileBehavior, callback) {
|
||||
var self = this,
|
||||
hostname,
|
||||
cookieJar,
|
||||
requestOptions,
|
||||
networkOptions = self.options.network || {},
|
||||
startTime = Date.now(),
|
||||
startTimer = now(), // high-resolution time
|
||||
cookies = [],
|
||||
responseHeaders = [],
|
||||
responseJSON = {},
|
||||
|
||||
// keep track of `responseStart` and `response` triggers
|
||||
_responseStarted = false,
|
||||
_responseEnded = false,
|
||||
_responseData = {},
|
||||
|
||||
// Refer: https://github.com/postmanlabs/postman-runtime/blob/v7.14.0/docs/history.md
|
||||
getExecutionHistory = function (debugInfo) {
|
||||
var history = {
|
||||
execution: {
|
||||
verbose: Boolean(requestOptions.verbose),
|
||||
sessions: {},
|
||||
data: []
|
||||
}
|
||||
},
|
||||
executionData = [],
|
||||
requestSessions = {};
|
||||
|
||||
if (!Array.isArray(debugInfo)) {
|
||||
return history;
|
||||
}
|
||||
|
||||
// prepare history from request debug data
|
||||
debugInfo.forEach(function (debugData) {
|
||||
if (!debugData) { return; }
|
||||
|
||||
// @todo cache connection sessions and fetch reused session
|
||||
// from the requester pool.
|
||||
if (debugData.session && !requestSessions[debugData.session.id]) {
|
||||
requestSessions[debugData.session.id] = debugData.session.data;
|
||||
}
|
||||
|
||||
executionData.push({
|
||||
request: debugData.request,
|
||||
response: debugData.response,
|
||||
timings: debugData.timings && {
|
||||
// runtime start time
|
||||
start: startTime,
|
||||
// request start time
|
||||
requestStart: debugData.timingStart,
|
||||
// offsets calculated are relative to runtime start time
|
||||
offset: calcTimingsOffset(startTimer, debugData.timingStartTimer, debugData.timings)
|
||||
},
|
||||
session: debugData.session && {
|
||||
id: debugData.session.id,
|
||||
// is connection socket reused
|
||||
reused: debugData.session.reused
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// update history object
|
||||
history.execution.data = executionData;
|
||||
history.execution.sessions = requestSessions;
|
||||
|
||||
return history;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add the missing/system headers in the request object
|
||||
*
|
||||
* @param {Object[]} headers
|
||||
*/
|
||||
addMissingRequestHeaders = function (headers) {
|
||||
_.forEach(headers, function (header) {
|
||||
var lowerCasedKey = header.key.toLowerCase();
|
||||
|
||||
// update headers which gets overwritten by the requester
|
||||
if (OVERWRITTEN_HEADERS[lowerCasedKey]) {
|
||||
if (Array.isArray(_.get(request.headers, ['reference', lowerCasedKey]))) {
|
||||
request.headers.remove(header.key);
|
||||
}
|
||||
|
||||
request.headers.upsert({
|
||||
key: header.key,
|
||||
value: header.value,
|
||||
system: true
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper function to trigger `callback` and complete the request function
|
||||
*
|
||||
* @param {Error} error - error while requesting
|
||||
* @param {Response} response - SDK Response instance
|
||||
* @param {Object} history - Request-Response History
|
||||
*/
|
||||
onEnd = function (error, response, history) {
|
||||
self.emit(RESPONSE_END_EVENT_BASE + id, error, self.trace.cursor,
|
||||
self.trace, response, request, cookies, history);
|
||||
|
||||
return callback(error, response, request, cookies, history);
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper function to keep track of `responseStart` and `response`
|
||||
* triggers to make they are emitted in correct order.
|
||||
*
|
||||
* @todo fix requester control flow to remove this hack!
|
||||
* this is required because CookieJar.getCookies is async method
|
||||
* and by that time postman-request ends the request, which affects
|
||||
* request post-send helpers because `response.start` event is not
|
||||
* emitted on time and shared variables `cookies`, `responseJSON`,
|
||||
* and, `responseHeaders` are initialized in onStart function.
|
||||
*
|
||||
* @param {String} trigger - trigger name
|
||||
* @param {Response} response - SDK Response instance
|
||||
* @param {Object} history - Request-Response History
|
||||
*/
|
||||
onComplete = function (trigger, response, history) {
|
||||
if (trigger === RESPONSE_START) {
|
||||
// set flag for responseStart callback
|
||||
_responseStarted = true;
|
||||
|
||||
// if response is ended, end the response using cached data
|
||||
if (_responseEnded) {
|
||||
onEnd(null, _responseData.response, _responseData.history);
|
||||
}
|
||||
|
||||
// bail out and wait for response end if not ended already
|
||||
return;
|
||||
}
|
||||
|
||||
// if response started, don't wait and end the response
|
||||
if (_responseStarted) {
|
||||
onEnd(null, response, history);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// wait for responseStart and cache response callback data
|
||||
_responseEnded = true;
|
||||
_responseData = {
|
||||
response: response,
|
||||
history: history
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper function to trigger `responseStart` callback and
|
||||
* - transform postman-request response instance to SDK Response
|
||||
* - filter cookies
|
||||
* - filter response headers
|
||||
* - add missing request headers
|
||||
*
|
||||
* @param {Object} response - Postman-Request response instance
|
||||
*/
|
||||
onStart = function (response) {
|
||||
var responseStartEventName = RESPONSE_START_EVENT_BASE + id,
|
||||
executionData,
|
||||
initialRequest,
|
||||
finalRequest,
|
||||
sdkResponse,
|
||||
history,
|
||||
done = function () {
|
||||
// emit the response.start event which eventually
|
||||
// triggers responseStart callback
|
||||
self.emit(responseStartEventName, null, sdkResponse, request, cookies, history);
|
||||
|
||||
// trigger completion of responseStart
|
||||
onComplete(RESPONSE_START);
|
||||
};
|
||||
|
||||
// @todo get rid of jsonifyResponse
|
||||
responseJSON = core.jsonifyResponse(response, requestOptions);
|
||||
|
||||
// transform response headers to SDK compatible HeaderList
|
||||
responseHeaders = _.transform(responseJSON.headers, transformMultiValueHeaders, []);
|
||||
|
||||
// initialize SDK Response instance
|
||||
sdkResponse = new sdk.Response({
|
||||
status: response && response.statusMessage,
|
||||
code: responseJSON.statusCode,
|
||||
header: responseHeaders
|
||||
});
|
||||
|
||||
// prepare history from request debug data
|
||||
history = getExecutionHistory(_.get(response, 'request._debug'));
|
||||
|
||||
// get the initial and final (on redirect) request from history
|
||||
executionData = _.get(history, 'execution.data') || [];
|
||||
initialRequest = _.get(executionData, '[0].request') || {};
|
||||
finalRequest = executionData.length > 1 ?
|
||||
// get final redirect
|
||||
_.get(executionData, [executionData.length - 1, 'request']) :
|
||||
// no redirects
|
||||
initialRequest;
|
||||
|
||||
// add missing request headers so that they get bubbled up into the UI
|
||||
addMissingRequestHeaders(initialRequest.headers);
|
||||
|
||||
// pull out cookies from the cookie jar, and make them chrome compatible.
|
||||
if (cookieJar && _.isFunction(cookieJar.getCookies)) {
|
||||
// get cookies set for the final request URL
|
||||
cookieJar.getCookies(finalRequest.href, function (err, cookiesFromJar) {
|
||||
if (err) {
|
||||
return done();
|
||||
}
|
||||
|
||||
cookies = _.transform(cookiesFromJar, function (acc, cookie) {
|
||||
acc.push(toPostmanCookie(cookie));
|
||||
}, []);
|
||||
|
||||
cookies = new sdk.CookieList(null, cookies);
|
||||
|
||||
done();
|
||||
});
|
||||
}
|
||||
else {
|
||||
cookies = new sdk.CookieList(null, []);
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
||||
// at this point the request could have come from collection, auth or sandbox
|
||||
// we can't trust the integrity of this request
|
||||
// bail out if request url is empty
|
||||
if (!(request && request.url && request.url.toString && request.url.toString())) {
|
||||
return onEnd(new Error('runtime:extensions~request: request url is empty'));
|
||||
}
|
||||
|
||||
cookieJar = self.options.cookieJar;
|
||||
requestOptions = core.getRequestOptions(request, self.options, protocolProfileBehavior);
|
||||
|
||||
// update url with the final encoded url
|
||||
// @note this mutates the request object which will be passed in request
|
||||
// and response callbacks
|
||||
request.url.update(requestOptions.url.href);
|
||||
hostname = request.url.getHost();
|
||||
|
||||
// check if host is on the `restrictedAddresses`
|
||||
if (networkOptions.restrictedAddresses && core.isAddressRestricted(hostname, networkOptions)) {
|
||||
return onEnd(new Error(ERROR_RESTRICTED_ADDRESS + hostname));
|
||||
}
|
||||
|
||||
return requests(request, requestOptions, onStart, function (err, res, resBody, debug) {
|
||||
// prepare history from request debug data
|
||||
var history = getExecutionHistory(debug),
|
||||
responseTime,
|
||||
response;
|
||||
|
||||
if (err) {
|
||||
// bubble up http errors
|
||||
// @todo - Should we send an empty sdk Response here?
|
||||
//
|
||||
// Sending `history` object even in case of error
|
||||
return onEnd(err, undefined, history);
|
||||
}
|
||||
|
||||
|
||||
// Calculate the time taken for us to get the response.
|
||||
responseTime = Date.now() - startTime;
|
||||
|
||||
if (res && res.timings) {
|
||||
// update response time to actual response end time
|
||||
// of the final request in the redirect chain.
|
||||
responseTime = Math.ceil(res.timings.end);
|
||||
}
|
||||
|
||||
if (resBody && resBody instanceof ArrayBuffer) {
|
||||
resBody = Buffer.from(resBody);
|
||||
}
|
||||
|
||||
// Response in the SDK format
|
||||
// @todo reuse same response instance used for responseStart callback
|
||||
response = new sdk.Response({
|
||||
code: responseJSON.statusCode,
|
||||
status: res && res.statusMessage,
|
||||
header: responseHeaders,
|
||||
stream: resBody,
|
||||
responseTime: responseTime
|
||||
});
|
||||
|
||||
onComplete(RESPONSE_END, response, history);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes all current event listeners on the requester, and makes it ready for garbage collection :).
|
||||
*
|
||||
* @param {Function=} cb - Optional callback to be called on disposal
|
||||
*
|
||||
* @todo - In the future, when the requester manages its own connections etc, close them all here.
|
||||
*/
|
||||
dispose: function (cb) {
|
||||
// This is safe for us, because we do not use wait on events. (i.e, no part of Runtime ever waits on
|
||||
// any event to occur). We rely on callbacks for that, only choosing to use events as a way of streaming
|
||||
// information outside runtime.
|
||||
this.removeAllListeners();
|
||||
|
||||
_.isFunction(cb) && cb();
|
||||
}
|
||||
});
|
||||
|
||||
_.assign(Requester, /** @lends Requester */ {
|
||||
/**
|
||||
* Asynchronously create a new requester.
|
||||
*
|
||||
* @param trace
|
||||
* @param trace.type - type of requester to return (for now, just http)
|
||||
* @param trace.source - information about who needs this requester, e.g Auth, etc.
|
||||
* @param trace.cursor - the cursor
|
||||
* @param options
|
||||
* @param callback
|
||||
* @returns {*}
|
||||
*/
|
||||
create: function (trace, options, callback) {
|
||||
return callback(null, new Requester(trace, options));
|
||||
},
|
||||
|
||||
/**
|
||||
* A helper method to dry run the given request instance.
|
||||
* It returns the cloned request instance with the system added properties.
|
||||
*
|
||||
* @param {Request} request
|
||||
* @param {Object} options
|
||||
* @param {Object} options.cookieJar
|
||||
* @param {Object} options.protocolProfileBehavior
|
||||
* @param {Object} options.implicitCacheControl
|
||||
* @param {Object} options.implicitTraceHeader
|
||||
* @param {Function} done
|
||||
*/
|
||||
dryRun
|
||||
});
|
||||
|
||||
module.exports.Requester = Requester;
|
Reference in New Issue
Block a user