postman-runtime 7.20.1 → 7.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc CHANGED
@@ -60,7 +60,7 @@
60
60
  "block-scoped-var": "error",
61
61
  "class-methods-use-this": "error",
62
62
  "complexity": "off",
63
- "consistent-return": "warn",
63
+ "consistent-return": "off",
64
64
  "curly": "error",
65
65
  "default-case": "error",
66
66
  "dot-location": [
package/CHANGELOG.yaml CHANGED
@@ -1,3 +1,29 @@
1
+ 7.21.0:
2
+ date: 2019-12-02
3
+ new features:
4
+ - >-
5
+ GH-937 Added ability to use OAuth2 tokens with unknown types as Bearer
6
+ token in OAuth2 authentication
7
+ - GH-934 Added ability to pull domain name from username for NTLM auth
8
+ fixed bugs:
9
+ - >-
10
+ GH-939 Fixed a bug where IPv6 localhost request through IPv4 localhost
11
+ proxy was failing.
12
+ - >-
13
+ GH-928 Fixed a bug where Basic Auth was failing if credentials had
14
+ non-ASCII characters
15
+ - >-
16
+ GH-929 Fixed a bug where error was thrown when `username` or `password`
17
+ fields were empty for NTLM auth
18
+ - >-
19
+ GH-933 Fixed a bug where NTLM could not complete authentication if
20
+ multiple `www-authenticate` headers were sent
21
+ chores:
22
+ - >-
23
+ GH-942 Convert response into a JSON serializable object before executing
24
+ the script
25
+ - Updated dependencies
26
+
1
27
  7.20.1:
2
28
  date: 2019-11-13
3
29
  chores:
@@ -1,5 +1,3 @@
1
- var btoa = require('btoa');
2
-
3
1
  /**
4
2
  * @implements {AuthHandlerInterface}
5
3
  */
@@ -70,7 +68,7 @@ module.exports = {
70
68
  request.removeHeader('Authorization', {ignoreCase: true});
71
69
  request.addHeader({
72
70
  key: 'Authorization',
73
- value: 'Basic ' + btoa(username + ':' + password),
71
+ value: 'Basic ' + Buffer.from(`${username}:${password}`, 'utf8').toString('base64'),
74
72
  system: true
75
73
  });
76
74
 
@@ -31,6 +31,58 @@ var ntlmUtil = require('httpntlm').ntlm,
31
31
  T3_MSG_CREATED: 'T3_MSG_CREATED'
32
32
  };
33
33
 
34
+ /**
35
+ * Parses the username to separate username and domain. It can handle two formats:
36
+ * - Down-Level Logon name format `DOMAIN\USERNAME`
37
+ * - User Principal Name format `USERNAME@DOMAIN`
38
+ *
39
+ * @param {String} username - Username string to parse from
40
+ * @return {Object} - An object with `username` and `domain` fields, which are `strings`.
41
+ */
42
+ function parseParametersFromUsername (username) {
43
+ var dllParams,
44
+ upnParams;
45
+
46
+ if (!(username && typeof username === 'string')) {
47
+ return {
48
+ username: EMPTY,
49
+ domain: EMPTY
50
+ };
51
+ }
52
+
53
+ dllParams = username.split('\\');
54
+ upnParams = username.split('@');
55
+
56
+ // username should be either of the two formats, not both
57
+ if (dllParams.length > 1 && upnParams.length > 1) {
58
+ return {
59
+ username,
60
+ domain: EMPTY
61
+ };
62
+ }
63
+
64
+ // try to parse from "down level logon" format
65
+ if (dllParams.length === 2 && dllParams[0] && dllParams[1]) {
66
+ return {
67
+ username: dllParams[1],
68
+ domain: dllParams[0]
69
+ };
70
+ }
71
+
72
+ // try to parse from "user principal name" format
73
+ if (upnParams.length === 2 && upnParams[0] && upnParams[1]) {
74
+ return {
75
+ username: upnParams[0],
76
+ domain: upnParams[1]
77
+ };
78
+ }
79
+
80
+ return {
81
+ username,
82
+ domain: EMPTY
83
+ };
84
+ }
85
+
34
86
  /**
35
87
  * NTLM auth while authenticating requires negotiateMessage (type 1) and authenticateMessage (type 3) to be stored.
36
88
  * Also it needs to know which stage is it in (INITIALIZED, T1_MSG_CREATED and T3_MSG_CREATED).
@@ -96,16 +148,26 @@ module.exports = {
96
148
  var state = auth.get(STATE),
97
149
  domain = auth.get(NTLM_PARAMETERS.DOMAIN) || EMPTY,
98
150
  workstation = auth.get(NTLM_PARAMETERS.WORKSTATION) || EMPTY,
99
- username = auth.get(NTLM_PARAMETERS.USERNAME),
100
- password = auth.get(NTLM_PARAMETERS.PASSWORD),
151
+ username = auth.get(NTLM_PARAMETERS.USERNAME) || EMPTY,
152
+ password = auth.get(NTLM_PARAMETERS.PASSWORD) || EMPTY,
101
153
  negotiateMessage, // type 1
102
154
  challengeMessage, // type 2
103
- authenticateMessage; // type 3
155
+ authenticateMessage, // type 3
156
+ ntlmType2Header,
157
+ parsedParameters;
104
158
 
105
159
  if (response.code !== 401 && response.code !== 403) {
106
160
  return done(null, true);
107
161
  }
108
162
 
163
+ // we try to extract domain from username if not specified.
164
+ if (!domain) {
165
+ parsedParameters = parseParametersFromUsername(username) || {};
166
+
167
+ username = parsedParameters.username;
168
+ domain = parsedParameters.domain;
169
+ }
170
+
109
171
  if (state === STATES.INITIALIZED) {
110
172
  // Nothing to do if the server does not ask us for auth in the first place.
111
173
  if (!(response.headers.has(WWW_AUTHENTICATE, NTLM) ||
@@ -130,7 +192,20 @@ module.exports = {
130
192
  }
131
193
  else if (state === STATES.T1_MSG_CREATED) {
132
194
  // At this point, we can assume that the type 1 message was sent to the server
133
- challengeMessage = ntlmUtil.parseType2Message(response.headers.get(WWW_AUTHENTICATE) || EMPTY, _.noop);
195
+
196
+ // there can be multiple headers present with key `www-authenticate`.
197
+ // iterate to get the one which has the NTLM hash. if multiple
198
+ // headers have the NTLM hash, use the first one.
199
+ ntlmType2Header = response.headers.find(function (header) {
200
+ return String(header.key).toLowerCase() === WWW_AUTHENTICATE &&
201
+ header.valueOf().startsWith('NTLM ');
202
+ });
203
+
204
+ if (!ntlmType2Header) {
205
+ return done(new Error('ntlm: server did not send NTLM type 2 message'));
206
+ }
207
+
208
+ challengeMessage = ntlmUtil.parseType2Message(ntlmType2Header.valueOf(), _.noop);
134
209
 
135
210
  if (!challengeMessage) {
136
211
  return done(new Error('ntlm: server did not correctly process authentication request'));
@@ -3,6 +3,7 @@ var _ = require('lodash'),
3
3
  HEADER = 'header',
4
4
  QUERY_PARAMS = 'queryParams',
5
5
  BEARER = 'bearer',
6
+ MAC = 'mac',
6
7
  AUTHORIZATION = 'Authorization',
7
8
  ACCESS_TOKEN = 'access_token',
8
9
  AUTHORIZATION_PREFIX = 'Bearer ',
@@ -94,27 +95,31 @@ module.exports = {
94
95
  tokenType = _.toLower(params.tokenType);
95
96
 
96
97
  // @TODO Add support for HMAC
97
- if (tokenType === BEARER) {
98
- // clean conflicting headers and query params
99
- // @todo: we should be able to get conflicting params from auth manifest
100
- // and clear them before the sign step for any auth
101
- request.removeHeader(AUTHORIZATION, {ignoreCase: true});
102
- request.removeQueryParams([ACCESS_TOKEN]);
98
+ if (tokenType === MAC) {
99
+ return done();
100
+ }
103
101
 
104
- if (params.addTokenTo === QUERY_PARAMS) {
105
- request.addQueryParams({
106
- key: ACCESS_TOKEN,
107
- value: params.accessToken,
108
- system: true
109
- });
110
- }
111
- else if (params.addTokenTo === HEADER) {
112
- request.addHeader({
113
- key: AUTHORIZATION,
114
- value: AUTHORIZATION_PREFIX + params.accessToken,
115
- system: true
116
- });
117
- }
102
+ // treat every token types (other than MAC) as bearer token
103
+
104
+ // clean conflicting headers and query params
105
+ // @todo: we should be able to get conflicting params from auth manifest
106
+ // and clear them before the sign step for any auth
107
+ request.removeHeader(AUTHORIZATION, {ignoreCase: true});
108
+ request.removeQueryParams([ACCESS_TOKEN]);
109
+
110
+ if (params.addTokenTo === QUERY_PARAMS) {
111
+ request.addQueryParams({
112
+ key: ACCESS_TOKEN,
113
+ value: params.accessToken,
114
+ system: true
115
+ });
116
+ }
117
+ else if (params.addTokenTo === HEADER) {
118
+ request.addHeader({
119
+ key: AUTHORIZATION,
120
+ value: AUTHORIZATION_PREFIX + params.accessToken,
121
+ system: true
122
+ });
118
123
  }
119
124
 
120
125
  return done();
@@ -251,12 +251,14 @@ module.exports = {
251
251
  self = this,
252
252
  bodyParams,
253
253
  url = request.url && urlEncoder.toNodeUrl(request.url.toString(true)),
254
- isSSL,
254
+ isSSL = _.startsWith(url.protocol, HTTPS),
255
+ isTunnelingProxy = request.proxy && (request.proxy.tunnel || isSSL),
255
256
  reqOption,
256
257
  portNumber,
257
258
  behaviorName,
258
259
  port = url && url.port,
259
- hostname = url && url.hostname && url.hostname.toLowerCase();
260
+ hostname = url && url.hostname && url.hostname.toLowerCase(),
261
+ proxyHostname = request.proxy && request.proxy.host;
260
262
 
261
263
  !defaultOpts && (defaultOpts = {});
262
264
  !protocolProfileBehavior && (protocolProfileBehavior = {});
@@ -268,6 +270,10 @@ module.exports = {
268
270
  hostname = LOCALHOST;
269
271
  }
270
272
 
273
+ if (getTLD(proxyHostname) === LOCALHOST) {
274
+ proxyHostname = LOCALHOST;
275
+ }
276
+
271
277
  options.url = url;
272
278
  options.method = request.method;
273
279
  options.jar = defaultOpts.cookieJar || true;
@@ -334,9 +340,17 @@ module.exports = {
334
340
  self.ensureHeaderExists(options.headers, 'Host', url.host);
335
341
 
336
342
  // override DNS lookup
337
- if (networkOptions.restrictedAddresses || hostname === LOCALHOST || networkOptions.hostLookup) {
338
- isSSL = _.startsWith(request.url.protocol, HTTPS);
339
- portNumber = Number(port) || (isSSL ? HTTPS_DEFAULT_PORT : HTTP_DEFAULT_PORT);
343
+ if (networkOptions.restrictedAddresses || hostname === LOCALHOST ||
344
+ (!isTunnelingProxy && proxyHostname === LOCALHOST) || networkOptions.hostLookup) {
345
+ // Use proxy port for localhost resolution in case of non-tunneling proxy
346
+ // because the request will be sent to proxy server by postman-request
347
+ if (request.proxy && !isTunnelingProxy) {
348
+ portNumber = Number(request.proxy.port);
349
+ }
350
+ // Otherwise, use request's port
351
+ else {
352
+ portNumber = Number(port) || (isSSL ? HTTPS_DEFAULT_PORT : HTTP_DEFAULT_PORT);
353
+ }
340
354
 
341
355
  _.isFinite(portNumber) && (options.lookup = lookup.bind(this, {
342
356
  port: portNumber,
@@ -490,7 +490,13 @@ module.exports = {
490
490
  result && result.collectionVariables &&
491
491
  (result.collectionVariables = new sdk.VariableScope(result.collectionVariables));
492
492
  result && result.request && (result.request = new sdk.Request(result.request));
493
- result && result.response && (result.response = new sdk.Response(result.response));
493
+
494
+ // @note Since postman-sandbox@3.5.2, response object is not included in the execution result.
495
+ // Refer: https://github.com/postmanlabs/postman-sandbox/pull/512
496
+ // Adding back here to avoid breaking change in `script` callback.
497
+ // @todo revisit script callback args in runtime v8.
498
+ payload.context && payload.context.response &&
499
+ (result.response = new sdk.Response(payload.context.response));
494
500
 
495
501
  // persist the pm.variables for the next script
496
502
  result && result._variables &&
@@ -1,9 +1,10 @@
1
1
  var _ = require('lodash'),
2
2
  uuid = require('uuid'),
3
+ Response = require('postman-collection').Response,
3
4
  visualizer = require('../../visualizer'),
4
5
 
5
6
  /**
6
- * List of request properties which can be mutated via prerequest
7
+ * List of request properties which can be mutated via pre-request
7
8
  *
8
9
  * @private
9
10
  * @const
@@ -11,7 +12,8 @@ var _ = require('lodash'),
11
12
  */
12
13
  ALLOWED_REQUEST_MUTATIONS = ['url', 'method', 'headers'],
13
14
 
14
- extractVisualizerData;
15
+ extractVisualizerData,
16
+ getResponseJSON;
15
17
 
16
18
  /**
17
19
  * Returns visualizer data from the latest execution result.
@@ -36,7 +38,7 @@ extractVisualizerData = function (prereqExecutions, testExecutions) {
36
38
  }
37
39
 
38
40
  if (_.isArray(prereqExecutions)) {
39
- // extraxt visualizer data from pre-request script results if it is not found earlier
41
+ // extract visualizer data from pre-request script results if it is not found earlier
40
42
  for (i = prereqExecutions.length - 1; i >= 0; i--) {
41
43
  visualizerData = _.get(prereqExecutions[i], 'result.return.visualizer');
42
44
 
@@ -47,6 +49,31 @@ extractVisualizerData = function (prereqExecutions, testExecutions) {
47
49
  }
48
50
  };
49
51
 
52
+ /**
53
+ * Convert response into a JSON serializable object.
54
+ * The stream property is converted to base64 string for performance reasons.
55
+ *
56
+ * @param {Object} response - SDK Response instance
57
+ * @returns {Object}
58
+ */
59
+ getResponseJSON = function (response) {
60
+ if (!Response.isResponse(response)) {
61
+ return;
62
+ }
63
+
64
+ return {
65
+ id: response.id,
66
+ code: response.code,
67
+ status: response.status,
68
+ header: response.headers && response.headers.toJSON(),
69
+ stream: response.stream && {
70
+ type: 'Base64',
71
+ data: response.stream.toString('base64')
72
+ },
73
+ responseTime: response.responseTime
74
+ };
75
+ };
76
+
50
77
  /**
51
78
  * Add options
52
79
  * stopOnError:Boolean
@@ -173,7 +200,15 @@ module.exports = {
173
200
 
174
201
  // also the test object requires the updated request object (since auth helpers may modify it)
175
202
  request && (ctxTemplate.request = request);
176
- response && (ctxTemplate.response = response);
203
+
204
+ // @note convert response instance to plain object.
205
+ // we want to avoid calling Response.toJSON() which triggers toJSON on Response.stream buffer.
206
+ // Because that increases the size of stringified object by 3 times.
207
+ // Also, that increases the total number of tokens (buffer.data) whereas Buffer.toString
208
+ // generates a single string that is easier to stringify and sent over the UVM bridge.
209
+ response && (ctxTemplate.response = getResponseJSON(response));
210
+
211
+ // set cookies for this transaction
177
212
  cookies && (ctxTemplate.cookies = cookies);
178
213
 
179
214
  // the context template also has a test object to store assertions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postman-runtime",
3
- "version": "7.20.1",
3
+ "version": "7.21.0",
4
4
  "description": "Underlying library of executing Postman Collections (used by Newman)",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -30,20 +30,19 @@
30
30
  "license": "Apache-2.0",
31
31
  "dependencies": {
32
32
  "async": "2.6.2",
33
- "aws4": "1.8.0",
34
- "btoa": "1.2.1",
33
+ "aws4": "1.9.0",
35
34
  "crypto-js": "3.1.9-1",
36
35
  "eventemitter3": "4.0.0",
37
- "handlebars": "4.5.1",
36
+ "handlebars": "4.5.3",
38
37
  "http-reasons": "0.1.0",
39
38
  "httpntlm": "1.7.6",
40
39
  "inherits": "2.0.4",
41
40
  "lodash": "4.17.15",
42
41
  "node-oauth1": "1.2.2",
43
42
  "performance-now": "2.1.0",
44
- "postman-collection": "3.5.4",
43
+ "postman-collection": "3.5.5",
45
44
  "postman-request": "2.88.1-postman.16",
46
- "postman-sandbox": "3.5.1",
45
+ "postman-sandbox": "3.5.2",
47
46
  "postman-url-encoder": "1.0.3",
48
47
  "resolve-from": "5.0.0",
49
48
  "serialised-error": "1.1.3",
@@ -58,14 +57,14 @@
58
57
  "eslint": "5.16.0",
59
58
  "eslint-plugin-jsdoc": "8.7.0",
60
59
  "eslint-plugin-lodash": "5.1.0",
61
- "eslint-plugin-mocha": "6.2.1",
60
+ "eslint-plugin-mocha": "6.2.2",
62
61
  "eslint-plugin-security": "1.4.0",
63
62
  "graphql": "14.5.8",
64
63
  "http-proxy": "1.18.0",
65
64
  "istanbul": "0.4.5",
66
65
  "js-yaml": "3.13.1",
67
66
  "jsdoc": "3.6.3",
68
- "jsdoc-to-markdown": "5.0.2",
67
+ "jsdoc-to-markdown": "5.0.3",
69
68
  "mocha": "6.2.2",
70
69
  "parse-gitignore": "0.5.1",
71
70
  "postman-jsdoc-theme": "0.0.3",
@@ -1,11 +1,14 @@
1
1
  const fs = require('fs'),
2
2
  net = require('net'),
3
+ url = require('url'),
4
+ dns = require('dns'),
3
5
  _ = require('lodash'),
4
6
  path = require('path'),
5
7
  http = require('http'),
6
8
  https = require('https'),
7
9
  crypto = require('crypto'),
8
10
  GraphQL = require('graphql'),
11
+ ntlmUtils = require('httpntlm').ntlm,
9
12
  enableServerDestroy = require('server-destroy');
10
13
 
11
14
  /**
@@ -206,6 +209,7 @@ function createHTTPServer () {
206
209
  * @param {Object} [options] - Additional options to configure proxy server
207
210
  * @param {Object} [options.auth] - Proxy authentication, Basic auth
208
211
  * @param {String} [options.agent] - Agent used for http(s).request
212
+ * @param {Boolean} [options.useIPv6] - If true, force using IPv6 address while forwarding request.
209
213
  *
210
214
  * @example
211
215
  * var s = createProxyServer({
@@ -244,14 +248,22 @@ function createProxyServer (options) {
244
248
  req.headers = Object.assign(req.headers, options.headers || {});
245
249
 
246
250
  // forward request to the origin and pipe the response
247
- var fwd = agent.request({
248
- host: req.headers.host,
249
- path: req.url,
250
- method: req.method.toLowerCase(),
251
- headers: req.headers
252
- }, function (resp) {
253
- resp.pipe(res);
254
- });
251
+ var requestUrl = url.parse(req.url),
252
+ fwd = agent.request({
253
+ host: requestUrl.hostname,
254
+ path: requestUrl.path,
255
+ port: requestUrl.port,
256
+ method: req.method.toLowerCase(),
257
+ headers: req.headers,
258
+ lookup: options.useIPv6 && function (hostname, options, callback) {
259
+ !options && (options = {});
260
+ options.family = 6;
261
+
262
+ return dns.lookup(hostname, options, callback);
263
+ }
264
+ }, function (resp) {
265
+ resp.pipe(res);
266
+ });
255
267
 
256
268
  req.pipe(fwd);
257
269
  });
@@ -497,6 +509,106 @@ function createEdgeGridAuthServer (options) {
497
509
  return server;
498
510
  }
499
511
 
512
+ /**
513
+ * Creates an NTLM server.
514
+ *
515
+ * @param {Object} options - The options for the server
516
+ * @param {String} options.username - Username for authentication
517
+ * @param {String} options.password - Password for authentication
518
+ * @param {String} options.domain - Domain name for authentication
519
+ * @param {String} options.workstation - Workstation for authentication
520
+ * @param {Boolean} options.debug - Enable logging of requests
521
+ *
522
+ * @return {Object} - http server
523
+ */
524
+ function createNTLMServer (options) {
525
+ options = options || {};
526
+
527
+ var type2Message = 'NTLM ' +
528
+ 'TlRMTVNTUAACAAAAHgAeADgAAAAFgoqiBevywvJykjAAAAAAAAAAAJgAmABWAAAA' +
529
+ 'CgC6RwAAAA9EAEUAUwBLAFQATwBQAC0ASgBTADQAVQBKAFQARAACAB4ARABFAFMA' +
530
+ 'SwBUAE8AUAAtAEoAUwA0AFUASgBUAEQAAQAeAEQARQBTAEsAVABPAFAALQBKAFMA' +
531
+ 'NABVAEoAVABEAAQAHgBEAEUAUwBLAFQATwBQAC0ASgBTADQAVQBKAFQARAADAB4A' +
532
+ 'RABFAFMASwBUAE8AUAAtAEoAUwA0AFUASgBUAEQABwAIADmguzCHn9UBAAAAAA==',
533
+ parsedType2Message = ntlmUtils.parseType2Message(type2Message, _.noop),
534
+
535
+ username = options.username || 'username',
536
+ password = options.password || 'password',
537
+ domain = options.domain || '',
538
+ workstation = options.workstation || '',
539
+
540
+ type1Message = ntlmUtils.createType1Message({
541
+ domain,
542
+ workstation
543
+ }),
544
+ type3Message = ntlmUtils.createType3Message(parsedType2Message, {
545
+ domain,
546
+ workstation,
547
+ username,
548
+ password
549
+ }),
550
+
551
+ handler = function (req, res) {
552
+ var authHeaders = req.headers.authorization;
553
+
554
+ // send type2 message and ask for type3 message
555
+ if (authHeaders && authHeaders.startsWith(type1Message.slice(0, 20))) {
556
+ res.writeHead(401, {
557
+
558
+ // @note we're sending a 'Negotiate' header here to make
559
+ // sure that runtime can handle it.
560
+ 'www-authenticate': [type2Message, 'Negotiate']
561
+ });
562
+
563
+ options.debug && console.info('401: got type1 message');
564
+ }
565
+
566
+ // successful auth
567
+ // @note we don't check if the username and password are correct
568
+ // because I don't know how.
569
+ else if (authHeaders && authHeaders.startsWith(type3Message.slice(0, 100))) {
570
+ res.writeHead(200);
571
+
572
+ options.debug && console.info('200: got type3 message');
573
+ }
574
+
575
+ // no valid auth headers, ask for type1 message
576
+ else {
577
+ res.writeHead(401, {
578
+ 'www-authenticate': ['NTLM', 'Negotiate']
579
+ });
580
+
581
+ options.debug && console.info('401: got no authorization header');
582
+ }
583
+
584
+ res.end();
585
+ },
586
+ server = http.createServer(handler);
587
+
588
+ enableServerDestroy(server);
589
+
590
+ return server;
591
+ }
592
+
593
+ /**
594
+ * Custom junk bytes response server.
595
+ *
596
+ * `/${bytes}` returns binary response of given bytes size.
597
+ */
598
+ function createBytesServer () {
599
+ var server = http.createServer(function (req, res) {
600
+ var bytes = Number(req.url.substr(1)) || 0; // remove leading /
601
+
602
+ res.writeHead(200);
603
+ res.write(Buffer.alloc(bytes));
604
+ res.end();
605
+ });
606
+
607
+ enableServerDestroy(server);
608
+
609
+ return server;
610
+ }
611
+
500
612
  module.exports = {
501
613
  createSSLServer,
502
614
  createHTTPServer,
@@ -504,5 +616,7 @@ module.exports = {
504
616
  createRawEchoServer,
505
617
  createGraphQLServer,
506
618
  createRedirectServer,
507
- createEdgeGridAuthServer
619
+ createEdgeGridAuthServer,
620
+ createNTLMServer,
621
+ createBytesServer
508
622
  };
@@ -1,20 +1,24 @@
1
1
  var expect = require('chai').expect,
2
- _ = require('lodash');
2
+ _ = require('lodash'),
3
3
 
4
- describe.skip('NTLM', function () {
4
+ server = require('../../fixtures/server');
5
+
6
+ describe('NTLM', function () {
5
7
  // @todo Add '/ntlm' endpoint in echo server
6
- var ntlmServerIP = '34.214.154.175',
8
+ var PORT = 2000,
7
9
  USERNAME = 'postman',
8
10
  PASSWORD = 'NTLM@123',
9
- DOMAIN = '',
10
- WORKSTATION = '',
11
+ DOMAIN = 'domain',
12
+ WORKSTATION = 'workstation',
13
+ ntlmServerURL = 'http://localhost:' + PORT,
14
+ ntlmServer,
11
15
  testrun,
12
16
  runOptions = {
13
17
  collection: {
14
18
  item: {
15
19
  name: 'NTLM Sample Request',
16
20
  request: {
17
- url: ntlmServerIP,
21
+ url: ntlmServerURL,
18
22
  auth: {
19
23
  type: 'ntlm',
20
24
  ntlm: {
@@ -29,6 +33,20 @@ describe.skip('NTLM', function () {
29
33
  }
30
34
  };
31
35
 
36
+ before(function (done) {
37
+ ntlmServer = server.createNTLMServer({
38
+ // debug: true,
39
+ username: USERNAME,
40
+ password: PASSWORD,
41
+ domain: DOMAIN,
42
+ workstation: WORKSTATION
43
+ }).listen(PORT, done);
44
+ });
45
+
46
+ after(function (done) {
47
+ ntlmServer.destroy(done);
48
+ });
49
+
32
50
  describe('with request server not supporting NTLM', function () {
33
51
  before(function (done) {
34
52
  var clonedRunOptions = _.merge({}, runOptions, {
@@ -87,6 +105,58 @@ describe.skip('NTLM', function () {
87
105
  });
88
106
  });
89
107
 
108
+ describe('with empty details', function () {
109
+ before(function (done) {
110
+ // creating local copy of collection because we don't want to send
111
+ // any parameters for NTLM auth
112
+ var localRunOptions = {
113
+ collection: {
114
+ item: {
115
+ name: 'NTLM Sample Request',
116
+ request: {
117
+ url: ntlmServerURL,
118
+ auth: {
119
+ type: 'ntlm'
120
+ }
121
+ }
122
+ }
123
+ }
124
+ };
125
+
126
+ // perform the collection run
127
+ this.run(localRunOptions, function (err, results) {
128
+ testrun = results;
129
+ done(err);
130
+ });
131
+ });
132
+
133
+ it('should have completed the run', function () {
134
+ expect(testrun).to.be.ok;
135
+ expect(testrun).to.nested.include({
136
+ 'done.callCount': 1
137
+ });
138
+
139
+ var err = testrun.request.firstCall.args[0];
140
+
141
+ err && console.error(err.stack);
142
+ expect(err).to.be.null;
143
+
144
+ expect(testrun).to.nested.include({
145
+ 'start.callCount': 1
146
+ });
147
+ });
148
+
149
+ it('should have sent the request thrice', function () {
150
+ expect(testrun).to.nested.include({
151
+ 'request.callCount': 3
152
+ });
153
+
154
+ var response = testrun.request.firstCall.args[2];
155
+
156
+ expect(response).to.have.property('code', 401);
157
+ });
158
+ });
159
+
90
160
  describe('with in-correct details', function () {
91
161
  before(function (done) {
92
162
  var clonedRunOptions = _.merge({}, runOptions, {
@@ -253,4 +323,160 @@ describe.skip('NTLM', function () {
253
323
  expect(response).to.have.property('code', 200);
254
324
  });
255
325
  });
326
+
327
+ describe('with username in down-level logon format', function () {
328
+ before(function (done) {
329
+ var clonedRunOptions = _.merge({}, runOptions, {
330
+ environment: {
331
+ values: [{
332
+ key: 'uname',
333
+ value: DOMAIN + '\\' + USERNAME
334
+ }, {
335
+ key: 'pass',
336
+ value: PASSWORD
337
+ }, {
338
+ key: 'domain',
339
+ value: ''
340
+ }, {
341
+ key: 'workstation',
342
+ value: WORKSTATION
343
+ }]
344
+ }
345
+ }, runOptions);
346
+
347
+ // perform the collection run
348
+ this.run(clonedRunOptions, function (err, results) {
349
+ testrun = results;
350
+ done(err);
351
+ });
352
+ });
353
+
354
+ it('should have completed the run successfully', function () {
355
+ expect(testrun).to.be.ok;
356
+ expect(testrun).to.nested.include({
357
+ 'done.callCount': 1
358
+ });
359
+ testrun.done.getCall(0).args[0] && console.error(testrun.done.getCall(0).args[0].stack);
360
+ expect(testrun.done.getCall(0).args[0]).to.be.null;
361
+ expect(testrun).to.nested.include({
362
+ 'start.callCount': 1
363
+ });
364
+ });
365
+
366
+ it('should have sent the request thrice', function () {
367
+ expect(testrun).to.nested.include({
368
+ 'request.callCount': 3
369
+ });
370
+
371
+ var err = testrun.request.thirdCall.args[0],
372
+ response = testrun.request.thirdCall.args[2];
373
+
374
+ expect(err).to.be.null;
375
+ expect(response).to.have.property('code', 200);
376
+ });
377
+ });
378
+
379
+ describe('with username in user principal name format', function () {
380
+ before(function (done) {
381
+ var clonedRunOptions = _.merge({}, runOptions, {
382
+ environment: {
383
+ values: [{
384
+ key: 'uname',
385
+ value: USERNAME + '@' + DOMAIN
386
+ }, {
387
+ key: 'pass',
388
+ value: PASSWORD
389
+ }, {
390
+ key: 'domain',
391
+ value: ''
392
+ }, {
393
+ key: 'workstation',
394
+ value: WORKSTATION
395
+ }]
396
+ }
397
+ }, runOptions);
398
+
399
+ // perform the collection run
400
+ this.run(clonedRunOptions, function (err, results) {
401
+ testrun = results;
402
+ done(err);
403
+ });
404
+ });
405
+
406
+ it('should have completed the run successfully', function () {
407
+ expect(testrun).to.be.ok;
408
+ expect(testrun).to.nested.include({
409
+ 'done.callCount': 1
410
+ });
411
+ testrun.done.getCall(0).args[0] && console.error(testrun.done.getCall(0).args[0].stack);
412
+ expect(testrun.done.getCall(0).args[0]).to.be.null;
413
+ expect(testrun).to.nested.include({
414
+ 'start.callCount': 1
415
+ });
416
+ });
417
+
418
+ it('should have sent the request thrice', function () {
419
+ expect(testrun).to.nested.include({
420
+ 'request.callCount': 3
421
+ });
422
+
423
+ var err = testrun.request.thirdCall.args[0],
424
+ response = testrun.request.thirdCall.args[2];
425
+
426
+ expect(err).to.be.null;
427
+ expect(response).to.have.property('code', 200);
428
+ });
429
+ });
430
+
431
+ describe('with username in both formats', function () {
432
+ before(function (done) {
433
+ var clonedRunOptions = _.merge({}, runOptions, {
434
+ environment: {
435
+ values: [{
436
+ key: 'uname',
437
+ value: DOMAIN + '\\' + USERNAME + '@' + DOMAIN
438
+ }, {
439
+ key: 'pass',
440
+ value: PASSWORD
441
+ }, {
442
+ key: 'domain',
443
+ value: ''
444
+ }, {
445
+ key: 'workstation',
446
+ value: WORKSTATION
447
+ }]
448
+ }
449
+ }, runOptions);
450
+
451
+ // perform the collection run
452
+ this.run(clonedRunOptions, function (err, results) {
453
+ testrun = results;
454
+ done(err);
455
+ });
456
+ });
457
+
458
+ it('should have completed the run successfully', function () {
459
+ expect(testrun).to.be.ok;
460
+ expect(testrun).to.nested.include({
461
+ 'done.callCount': 1
462
+ });
463
+ testrun.done.getCall(0).args[0] && console.error(testrun.done.getCall(0).args[0].stack);
464
+ expect(testrun.done.getCall(0).args[0]).to.be.null;
465
+ expect(testrun).to.nested.include({
466
+ 'start.callCount': 1
467
+ });
468
+ });
469
+
470
+ it('should have sent the request thrice with failed authentication', function () {
471
+ expect(testrun).to.nested.include({
472
+ 'request.callCount': 3
473
+ });
474
+
475
+ var err = testrun.request.thirdCall.args[0],
476
+ response = testrun.request.thirdCall.args[2];
477
+
478
+ expect(err).to.be.null;
479
+ expect(response).to.have.property('code', 401);
480
+ });
481
+ });
256
482
  });
@@ -0,0 +1,79 @@
1
+ var sinon = require('sinon'),
2
+ expect = require('chai').expect,
3
+ createBytesServer = require('../../fixtures/server').createBytesServer;
4
+
5
+ // @todo move to bipbip
6
+ describe('Benchmark: large response', function () {
7
+ var testrun,
8
+ PORT = 5050,
9
+ URL = 'http://localhost:' + PORT,
10
+ server = createBytesServer();
11
+
12
+ before(function (done) {
13
+ server.listen(PORT, done);
14
+ });
15
+
16
+ after(function (done) {
17
+ server.destroy(done);
18
+ });
19
+
20
+ // @todo increase to 100 MB once we drop support for Node v6
21
+ describe('50 MB response with test script', function () {
22
+ const RESPONSE_SIZE = 50 * 1024 * 1024; // 50 MB
23
+
24
+ before(function (done) {
25
+ this.run({
26
+ timeout: {
27
+ global: 20000 // 20s
28
+ },
29
+ collection: {
30
+ item: [{
31
+ request: URL + '/' + (RESPONSE_SIZE),
32
+ event: [{
33
+ listen: 'test',
34
+ script: {
35
+ type: 'text/javascript',
36
+ exec: `
37
+ "use sandbox2";
38
+ pm.test("response size", function () {
39
+ pm.response.to.be.ok;
40
+ pm.response.to.have.statusCode(200);
41
+ pm.expect(pm.response.size().body).to.equal(${RESPONSE_SIZE});
42
+ });
43
+ `
44
+ }
45
+ }]
46
+ }]
47
+ }
48
+ }, function (err, results) {
49
+ testrun = results;
50
+ done(err);
51
+ });
52
+ });
53
+
54
+ it('should complete the run', function () {
55
+ expect(testrun).to.be.ok;
56
+ sinon.assert.calledOnce(testrun.start);
57
+ sinon.assert.calledOnce(testrun.done);
58
+ sinon.assert.calledWith(testrun.done.getCall(0), null);
59
+ });
60
+
61
+ it('should run the test script successfully', function () {
62
+ sinon.assert.calledOnce(testrun.request);
63
+ sinon.assert.calledWith(testrun.request.getCall(0), null);
64
+
65
+ sinon.assert.calledOnce(testrun.response);
66
+ sinon.assert.calledWith(testrun.response.getCall(0), null);
67
+
68
+ sinon.assert.calledOnce(testrun.assertion);
69
+
70
+ expect(testrun.assertion.getCall(0).args[1][0]).to.include({
71
+ error: null,
72
+ index: 0,
73
+ passed: true,
74
+ skipped: false,
75
+ name: 'response size'
76
+ });
77
+ });
78
+ });
79
+ });
@@ -5,6 +5,61 @@ var expect = require('chai').expect,
5
5
  describe('sandbox library - pm api', function () {
6
6
  var testrun;
7
7
 
8
+ describe('sanity', function () {
9
+ before(function (done) {
10
+ this.run({
11
+ collection: {
12
+ item: [{
13
+ request: 'https://postman-echo.com/get',
14
+ event: [{
15
+ listen: 'test',
16
+ script: {
17
+ type: 'text/javascript',
18
+ exec: `
19
+ console.log(pm.request.toJSON());
20
+ console.log(pm.response.toJSON());
21
+ `
22
+ }
23
+ }]
24
+ }]
25
+ }
26
+ }, function (err, results) {
27
+ testrun = results;
28
+ done(err);
29
+ });
30
+ });
31
+
32
+ it('should complete the run', function () {
33
+ expect(testrun).to.be.ok;
34
+ sinon.assert.calledOnce(testrun.start);
35
+ sinon.assert.calledOnce(testrun.done);
36
+ sinon.assert.calledWith(testrun.done.getCall(0), null);
37
+
38
+ sinon.assert.calledOnce(testrun.request);
39
+ sinon.assert.calledWith(testrun.request.getCall(0), null);
40
+
41
+ sinon.assert.calledOnce(testrun.response);
42
+ sinon.assert.calledWith(testrun.response.getCall(0), null);
43
+ });
44
+
45
+ it('should run the test script successfully', function () {
46
+ var request = testrun.response.getCall(0).args[3],
47
+ response = testrun.response.getCall(0).args[2];
48
+
49
+ sinon.assert.calledOnce(testrun.script);
50
+ sinon.assert.calledWith(testrun.script.getCall(0), null);
51
+
52
+ sinon.assert.calledOnce(testrun.test);
53
+ sinon.assert.calledWith(testrun.script.getCall(0), null);
54
+
55
+ sinon.assert.calledTwice(testrun.console);
56
+
57
+ // validate pm.request and pm.response
58
+ expect(testrun.console.getCall(0).args[2]).to.eql(request.toJSON());
59
+ expect(testrun.console.getCall(1).args[2]).to.eql(response.toJSON());
60
+ });
61
+ });
62
+
8
63
  describe('chai', function () {
9
64
  before(function (done) {
10
65
  this.run({
@@ -184,4 +184,73 @@ describe('proxy', function () {
184
184
  proxyServer.destroy();
185
185
  });
186
186
  });
187
+
188
+ // issue: https://github.com/postmanlabs/postman-app-support/issues/5626
189
+ // Skip in TRAVIS because IPv6 is disabled there
190
+ // eslint-disable-next-line no-process-env
191
+ (process.env.TRAVIS ? describe.skip : describe)('IPv6 request through IPv4 proxy', function () {
192
+ var proxyList = new ProxyConfigList({}, [{
193
+ host: proxyHost,
194
+ port: port
195
+ }]),
196
+ requestPort = 9091,
197
+ requestServer;
198
+
199
+ before(function (done) {
200
+ proxyServer = server.createProxyServer({
201
+ useIPv6: true,
202
+ headers: {'x-postman-proxy': 'true'}
203
+ });
204
+
205
+ // listening on IPv4
206
+ proxyServer.listen(port, '127.0.0.1');
207
+
208
+ requestServer = server.createHTTPServer();
209
+ requestServer.on('/foo', function (req, res) {
210
+ var proxyHeader = Boolean(req.headers['x-postman-proxy']);
211
+
212
+ res.writeHead(200, {'content-type': 'text/plain'});
213
+ res.end(`Hello Postman!!\nproxy-header:${proxyHeader}`);
214
+ });
215
+
216
+ // listening on IPv6
217
+ requestServer.listen(requestPort, '::1');
218
+
219
+ this.run({
220
+ collection: {
221
+ item: {
222
+ request: `http://localhost:${requestPort}/foo`
223
+ }
224
+ },
225
+ proxies: proxyList
226
+ }, function (err, results) {
227
+ testrun = results;
228
+ done(err);
229
+ });
230
+ });
231
+
232
+ it('should have started and completed the test run', function () {
233
+ expect(testrun).to.be.ok;
234
+ expect(testrun).to.nested.include({
235
+ 'done.calledOnce': true,
236
+ 'start.calledOnce': true
237
+ });
238
+ });
239
+
240
+ it('should receive response from the proxy', function () {
241
+ var response = testrun.request.getCall(0).args[2],
242
+ request = testrun.request.getCall(0).args[3];
243
+
244
+ expect(testrun.request.calledOnce).to.be.ok;
245
+ expect(request.proxy.getProxyUrl()).to.eql(proxyUrlForHttpRequest);
246
+ expect(response.reason()).to.eql('OK');
247
+ expect(response.text()).to.include('Hello Postman!!');
248
+ expect(response.text()).to.include('proxy-header:true');
249
+ });
250
+
251
+ after(function () {
252
+ proxyServer.destroy();
253
+ requestServer.destroy();
254
+ });
255
+ });
187
256
  });
@@ -1,6 +1,5 @@
1
1
  var _ = require('lodash'),
2
2
  expect = require('chai').expect,
3
- btoa = require('btoa'),
4
3
  aws4 = require('aws4'),
5
4
  sdk = require('postman-collection'),
6
5
  AuthLoader = require('../../lib/authorizer').AuthLoader,
@@ -91,7 +90,8 @@ describe('Auth Handler:', function () {
91
90
  authInterface = createAuthInterface(auth),
92
91
  username = rawRequests.basic.auth.basic.username,
93
92
  password = rawRequests.basic.auth.basic.password,
94
- expectedAuthHeader = 'Authorization: Basic ' + btoa(username + ':' + password),
93
+ expectedAuthHeader = 'Authorization: Basic ' +
94
+ Buffer.from(`${username}:${password}`, 'utf8').toString('base64'),
95
95
  handler = AuthLoader.getHandler(auth.type),
96
96
  headers,
97
97
  authHeader;
@@ -106,6 +106,27 @@ describe('Auth Handler:', function () {
106
106
  expect(authHeader.system).to.be.true;
107
107
  });
108
108
 
109
+ it('should generate correct header for parameters with unicode characters', function () {
110
+ var rawBasicReq = _.cloneDeep(rawRequests.basic),
111
+ request,
112
+ authInterface,
113
+ handler;
114
+
115
+ rawBasicReq.auth.basic = {username: '中文', password: '文中'};
116
+ request = new Request(rawBasicReq);
117
+ authInterface = createAuthInterface(request.auth);
118
+ handler = AuthLoader.getHandler(request.auth.type);
119
+ handler.sign(authInterface, request, _.noop);
120
+
121
+ expect(request.headers.toJSON()).to.eql([
122
+ {
123
+ key: 'Authorization',
124
+ value: 'Basic ' + Buffer.from('中文:文中', 'utf8').toString('base64'),
125
+ system: true
126
+ }
127
+ ]);
128
+ });
129
+
109
130
  it('should use default values for the missing parameters', function () {
110
131
  var rawBasicReq = _.cloneDeep(rawRequests.basic),
111
132
  request,
@@ -119,7 +140,11 @@ describe('Auth Handler:', function () {
119
140
  handler.sign(authInterface, request, _.noop);
120
141
 
121
142
  expect(request.headers.toJSON()).to.eql([
122
- {key: 'Authorization', value: 'Basic ' + btoa('foo:'), system: true}
143
+ {
144
+ key: 'Authorization',
145
+ value: 'Basic ' + Buffer.from('foo:', 'utf8').toString('base64'),
146
+ system: true
147
+ }
123
148
  ]);
124
149
 
125
150
  rawBasicReq.auth.basic = {password: 'foo'}; // no username present
@@ -129,7 +154,11 @@ describe('Auth Handler:', function () {
129
154
  handler.sign(authInterface, request, _.noop);
130
155
 
131
156
  expect(request.headers.toJSON()).to.eql([
132
- {key: 'Authorization', value: 'Basic ' + btoa(':foo'), system: true}
157
+ {
158
+ key: 'Authorization',
159
+ value: 'Basic ' + Buffer.from(':foo', 'utf8').toString('base64'),
160
+ system: true
161
+ }
133
162
  ]);
134
163
 
135
164
  rawBasicReq.auth.basic = {}; // no username and no password present
@@ -139,7 +168,11 @@ describe('Auth Handler:', function () {
139
168
  handler.sign(authInterface, request, _.noop);
140
169
 
141
170
  expect(request.headers.toJSON()).to.eql([
142
- {key: 'Authorization', value: 'Basic ' + btoa(':'), system: true}
171
+ {
172
+ key: 'Authorization',
173
+ value: 'Basic ' + Buffer.from(':', 'utf8').toString('base64'),
174
+ system: true
175
+ }
143
176
  ]);
144
177
  });
145
178
  });
@@ -698,7 +731,7 @@ describe('Auth Handler:', function () {
698
731
  });
699
732
  });
700
733
 
701
- it('should return when token type is not known', function () {
734
+ it('should treat unknown token type as "Bearer"', function () {
702
735
  var clonedRequestObj,
703
736
  request,
704
737
  auth,
@@ -715,6 +748,31 @@ describe('Auth Handler:', function () {
715
748
 
716
749
  handler.sign(authInterface, request, _.noop);
717
750
 
751
+ expect(request.headers.all()).to.be.an('array').that.has.lengthOf(1);
752
+ expect(request.headers.toJSON()[0]).to.eql({
753
+ key: 'Authorization',
754
+ value: 'Bearer ' + requestObj.auth.oauth2.accessToken,
755
+ system: true
756
+ });
757
+ });
758
+
759
+ it('should return when token type is MAC', function () {
760
+ var clonedRequestObj,
761
+ request,
762
+ auth,
763
+ authInterface,
764
+ handler;
765
+
766
+ clonedRequestObj = _.cloneDeep(requestObj);
767
+ clonedRequestObj.auth.oauth2.tokenType = 'mac';
768
+
769
+ request = new Request(clonedRequestObj);
770
+ auth = request.auth;
771
+ authInterface = createAuthInterface(auth);
772
+ handler = AuthLoader.getHandler(auth.type);
773
+
774
+ handler.sign(authInterface, request, _.noop);
775
+
718
776
  expect(request.headers.all()).to.be.an('array').that.is.empty;
719
777
  expect(request.url.query.all()).to.be.an('array').that.is.empty;
720
778
  });
@@ -827,37 +885,6 @@ describe('Auth Handler:', function () {
827
885
  query: [{key: 'access_token', value: 'old-token'}],
828
886
  variable: []
829
887
  });
830
-
831
- // invalid token type
832
- requestWithAuthHeader = _.defaults({
833
- auth: {
834
- type: 'oauth2',
835
- oauth2: {
836
- accessToken: '123456789abcdefghi',
837
- addTokenTo: 'queryParams',
838
- tokenType: 'micdrop'
839
- }
840
- },
841
- header: [{key: 'Authorization', value: 'Old-Header'}],
842
- url: 'https://postman-echo.com/get?access_token=old-token'
843
- }, requestObj);
844
-
845
- request = new Request(requestWithAuthHeader);
846
- auth = request.auth;
847
- authInterface = createAuthInterface(auth);
848
- handler = AuthLoader.getHandler(auth.type);
849
-
850
- handler.sign(authInterface, request, _.noop);
851
-
852
- expect(request.headers.toJSON()).to.eql([{key: 'Authorization', value: 'Old-Header'}]);
853
-
854
- expect(request.url.toJSON()).to.eql({
855
- protocol: 'https',
856
- path: ['get'],
857
- host: ['postman-echo', 'com'],
858
- query: [{key: 'access_token', value: 'old-token'}],
859
- variable: []
860
- });
861
888
  });
862
889
  });
863
890