@datanimbus/postman-request 3.0.1

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/lib/helpers.js ADDED
@@ -0,0 +1,91 @@
1
+ 'use strict'
2
+
3
+ var jsonSafeStringify = require('json-stringify-safe')
4
+ var crypto = require('crypto')
5
+ var Buffer = require('safe-buffer').Buffer
6
+ var { Transform } = require('stream')
7
+
8
+ var defer = typeof setImmediate === 'undefined'
9
+ ? process.nextTick
10
+ : setImmediate
11
+
12
+ // Reference: https://github.com/postmanlabs/postman-request/pull/23
13
+ //
14
+ // function paramsHaveRequestBody (params) {
15
+ // return (
16
+ // params.body ||
17
+ // params.requestBodyStream ||
18
+ // (params.json && typeof params.json !== 'boolean') ||
19
+ // params.multipart
20
+ // )
21
+ // }
22
+
23
+ function safeStringify (obj, replacer) {
24
+ var ret
25
+ try {
26
+ ret = JSON.stringify(obj, replacer)
27
+ } catch (e) {
28
+ ret = jsonSafeStringify(obj, replacer)
29
+ }
30
+ return ret
31
+ }
32
+
33
+ function md5 (str) {
34
+ return crypto.createHash('md5').update(str).digest('hex')
35
+ }
36
+
37
+ function isReadStream (rs) {
38
+ return rs.readable && rs.path && rs.mode
39
+ }
40
+
41
+ function toBase64 (str) {
42
+ return Buffer.from(str || '', 'utf8').toString('base64')
43
+ }
44
+
45
+ function copy (obj) {
46
+ var o = {}
47
+ Object.keys(obj).forEach(function (i) {
48
+ o[i] = obj[i]
49
+ })
50
+ return o
51
+ }
52
+
53
+ function version () {
54
+ var numbers = process.version.replace('v', '').split('.')
55
+ return {
56
+ major: parseInt(numbers[0], 10),
57
+ minor: parseInt(numbers[1], 10),
58
+ patch: parseInt(numbers[2], 10)
59
+ }
60
+ }
61
+
62
+ function now () {
63
+ return performance.now(); // eslint-disable-line
64
+ }
65
+
66
+ class SizeTrackerStream extends Transform {
67
+ constructor (options) {
68
+ super(options)
69
+ this.size = 0
70
+ }
71
+
72
+ _transform (chunk, encoding, callback) {
73
+ this.size += chunk.length
74
+ this.push(chunk)
75
+ callback()
76
+ }
77
+
78
+ _flush (callback) {
79
+ callback()
80
+ }
81
+ }
82
+
83
+ exports.safeStringify = safeStringify
84
+ exports.md5 = md5
85
+ exports.isReadStream = isReadStream
86
+ exports.toBase64 = toBase64
87
+ exports.copy = copy
88
+ exports.version = version
89
+ exports.defer = defer
90
+ exports.SizeTrackerStream = SizeTrackerStream
91
+ exports.now = now
@@ -0,0 +1,92 @@
1
+ const { EventEmitter } = require('events')
2
+ const http2 = require('http2')
3
+ const { getName: getConnectionName } = require('../autohttp/requestName')
4
+
5
+ class Http2Agent extends EventEmitter {
6
+ constructor (options) {
7
+ super()
8
+ this.options = options
9
+ this.connections = {}
10
+ }
11
+
12
+ createConnection (req, uri, options, socket) {
13
+ const _options = {
14
+ ...options,
15
+ ...this.options
16
+ }
17
+
18
+ const name = getConnectionName(_options)
19
+ let connection = this.connections[name]
20
+
21
+ // Force create a new connection if the connection is destroyed or closed or a new socket object is supplied
22
+ if (!connection || connection.destroyed || connection.closed || socket) {
23
+ const connectionOptions = {
24
+ ..._options,
25
+ port: _options.port || 443,
26
+ settings: {
27
+ enablePush: false
28
+ }
29
+ }
30
+
31
+ // check if a socket is supplied
32
+ if (socket) {
33
+ connectionOptions.createConnection = () => socket
34
+ }
35
+
36
+ connection = http2.connect(uri, connectionOptions)
37
+ // Connection is created in an unreferenced state and is referenced when a stream is created
38
+ // This is to prevent the connection from keeping the event loop alive
39
+ connection.unref()
40
+
41
+ // Counting semaphore, but since node is single-threaded, this is just a counter
42
+ // Multiple streams can be active on a connection
43
+ // Each stream refs the connection at the start, and unrefs it on end
44
+ // The connection should terminate if no streams are active on it
45
+ // Could be refactored into something prettier
46
+ const oldRef = connection.ref
47
+ const oldUnref = connection.unref
48
+
49
+ const timeoutHandler = () => {
50
+ delete connectionsMap[name]
51
+ connection.close()
52
+ }
53
+
54
+ connection.refCount = 0
55
+ connection.ref = function () {
56
+ this.refCount++
57
+ oldRef.call(this)
58
+ connection.off('timeout', timeoutHandler)
59
+ connection.setTimeout(0)
60
+ }
61
+ const connectionsMap = this.connections
62
+ connection.unref = function () {
63
+ this.refCount--
64
+ if (this.refCount === 0) {
65
+ oldUnref.call(this)
66
+ if (_options.timeout) {
67
+ connection.setTimeout(_options.timeout, timeoutHandler)
68
+ }
69
+ }
70
+ }
71
+
72
+ // Add a default error listener to HTTP2 session object to transparently swallow errors incase no streams are active
73
+ // Remove the connection from the connections map if the connection has errored out
74
+ connection.on('error', () => {
75
+ delete this.connections[name]
76
+ })
77
+
78
+ connection.once('close', () => {
79
+ delete this.connections[name]
80
+ })
81
+
82
+ this.connections[name] = connection
83
+ }
84
+
85
+ return connection
86
+ }
87
+ }
88
+
89
+ module.exports = {
90
+ Http2Agent,
91
+ globalAgent: new Http2Agent({})
92
+ }
@@ -0,0 +1,8 @@
1
+ const { Http2Agent, globalAgent } = require('./agent')
2
+ const { request } = require('./request')
3
+
4
+ module.exports = {
5
+ Agent: Http2Agent,
6
+ request,
7
+ globalAgent
8
+ }
@@ -0,0 +1,369 @@
1
+ const url = require('url')
2
+ const http2 = require('http2')
3
+ const { EventEmitter } = require('events')
4
+ const { globalAgent } = require('./agent')
5
+ const { validateRequestHeaders } = require('../autohttp/headerValidations')
6
+
7
+ const kHeadersFlushed = Symbol('kHeadersFlushed')
8
+ // Connection headers that should not be set by the user. Ref; https://datatracker.ietf.org/doc/html/rfc9113#name-connection-specific-header-
9
+ const connectionHeaders = ['connection', 'host', 'proxy-connection', 'keep-alive', 'transfer-encoding', 'upgrade']
10
+
11
+ // HTTP/2 error codes. Moving to a separate variable to prevent browser builds from breaking
12
+ const http2Constants = http2.constants || {}
13
+ const rstErrorCodesMap = {
14
+ [http2Constants.NGHTTP2_NO_ERROR]: 'NGHTTP2_NO_ERROR',
15
+ [http2Constants.NGHTTP2_PROTOCOL_ERROR]: 'NGHTTP2_PROTOCOL_ERROR',
16
+ [http2Constants.NGHTTP2_INTERNAL_ERROR]: 'NGHTTP2_INTERNAL_ERROR',
17
+ [http2Constants.NGHTTP2_FLOW_CONTROL_ERROR]: 'NGHTTP2_FLOW_CONTROL_ERROR',
18
+ [http2Constants.NGHTTP2_SETTINGS_TIMEOUT]: 'NGHTTP2_SETTINGS_TIMEOUT',
19
+ [http2Constants.NGHTTP2_STREAM_CLOSED]: 'NGHTTP2_STREAM_CLOSED',
20
+ [http2Constants.NGHTTP2_FRAME_SIZE_ERROR]: 'NGHTTP2_FRAME_SIZE_ERROR',
21
+ [http2Constants.NGHTTP2_REFUSED_STREAM]: 'NGHTTP2_REFUSED_STREAM',
22
+ [http2Constants.NGHTTP2_CANCEL]: 'NGHTTP2_CANCEL',
23
+ [http2Constants.NGHTTP2_COMPRESSION_ERROR]: 'NGHTTP2_COMPRESSION_ERROR',
24
+ [http2Constants.NGHTTP2_CONNECT_ERROR]: 'NGHTTP2_CONNECT_ERROR',
25
+ [http2Constants.NGHTTP2_ENHANCE_YOUR_CALM]: 'NGHTTP2_ENHANCE_YOUR_CALM',
26
+ [http2Constants.NGHTTP2_INADEQUATE_SECURITY]: 'NGHTTP2_INADEQUATE_SECURITY',
27
+ [http2Constants.NGHTTP2_HTTP_1_1_REQUIRED]: 'NGHTTP2_HTTP_1_1_REQUIRED'
28
+ }
29
+
30
+ function httpOptionsToUri (options) {
31
+ return url.format({
32
+ protocol: 'https',
33
+ host: options.host || 'localhost'
34
+ })
35
+ }
36
+
37
+ class Http2Request extends EventEmitter {
38
+ constructor (options) {
39
+ super()
40
+ this.onError = this.onError.bind(this)
41
+ this.onDrain = this.onDrain.bind(this)
42
+ this.onClose = this.onClose.bind(this)
43
+ this.onResponse = this.onResponse.bind(this)
44
+ this.onEnd = this.onEnd.bind(this)
45
+ this.onTimeout = this.onTimeout.bind(this)
46
+
47
+ this.registerListeners = this.registerListeners.bind(this)
48
+ this._flushHeaders = this._flushHeaders.bind(this)
49
+ this[kHeadersFlushed] = false
50
+
51
+ const uri = httpOptionsToUri(options)
52
+ const _options = {
53
+ ...options,
54
+ port: Number(options.port || 443),
55
+ path: undefined,
56
+ host: options.hostname || options.host || 'localhost'
57
+ }
58
+
59
+ if (options.socketPath) {
60
+ _options.path = options.socketPath
61
+ }
62
+
63
+ const agent = options.agent || globalAgent
64
+
65
+ this._client = agent.createConnection(this, uri, _options)
66
+
67
+ const headers = options.headers || {}
68
+
69
+ this.requestHeaders = {
70
+ ...headers,
71
+ [http2.constants.HTTP2_HEADER_PATH]: options.path || '/',
72
+ [http2.constants.HTTP2_HEADER_METHOD]: _options.method,
73
+ [http2.constants.HTTP2_HEADER_AUTHORITY]: _options.host + (_options.port !== 443 ? ':' + options.port : '')
74
+ }
75
+
76
+ if (options.uri.isUnix || headers['host'] === 'unix' || _options.host === 'unix') {
77
+ // The authority field needs to be set to 'localhost' when using unix sockets.
78
+ // The default URL parser supplies the isUnix flag when the host is 'unix'. Added other checks incase using a different parser like WHATWG URL (new URL()).
79
+ // See: https://github.com/nodejs/node/issues/32326
80
+ this.requestHeaders = {
81
+ ...this.requestHeaders,
82
+ [http2.constants.HTTP2_HEADER_AUTHORITY]: 'localhost'
83
+ }
84
+ }
85
+
86
+ this.socket = this._client.socket
87
+ this._client.once('error', this.onError)
88
+ }
89
+
90
+ get _header () {
91
+ return '\r\n' + Object.entries(this.stream.sentHeaders)
92
+ .map(([key, value]) => `${key}: ${value}`)
93
+ .join('\r\n') + '\r\n\r\n'
94
+ }
95
+
96
+ get httpVersion () {
97
+ return '2.0'
98
+ }
99
+
100
+ registerListeners () {
101
+ this.stream.on('drain', this.onDrain)
102
+ this.stream.on('error', this.onError)
103
+ this.stream.on('close', this.onClose)
104
+ this.stream.on('response', this.onResponse)
105
+ this.stream.on('end', this.onEnd)
106
+ this.stream.on('timeout', this.onTimeout)
107
+ }
108
+
109
+ onDrain (...args) {
110
+ this.emit('drain', ...args)
111
+ }
112
+
113
+ onError (e) {
114
+ this.emit('error', e)
115
+ }
116
+
117
+ onResponse (response) {
118
+ this.emit('response', new ResponseProxy(response, this.stream))
119
+ }
120
+
121
+ onEnd () {
122
+ this.emit('end')
123
+ }
124
+
125
+ onTimeout () {
126
+ this.stream.close()
127
+ }
128
+
129
+ onClose (...args) {
130
+ if (this.stream.rstCode) {
131
+ // Emit error message in case of abnormal stream closure
132
+ // It is fine if the error is emitted multiple times, since the callback has checks to prevent multiple invocations
133
+ this.onError(new Error(`HTTP/2 Stream closed with error code ${rstErrorCodesMap[this.stream.rstCode]}`))
134
+ }
135
+
136
+ this.emit('close', ...args)
137
+
138
+ this._client.off('error', this.onError)
139
+ this.stream.off('drain', this.onDrain)
140
+ this.stream.off('error', this.onError)
141
+ this.stream.off('response', this.onResponse)
142
+ this.stream.off('end', this.onEnd)
143
+ this.stream.off('close', this.onClose)
144
+ this.stream.off('timeout', this.onTimeout)
145
+
146
+ this.removeAllListeners()
147
+ }
148
+
149
+ setDefaultEncoding (encoding) {
150
+ if (!this[kHeadersFlushed]) {
151
+ this._flushHeaders()
152
+ }
153
+
154
+ this.stream.setDefaultEncoding(encoding)
155
+ return this
156
+ }
157
+
158
+ setEncoding (encoding) {
159
+ if (!this[kHeadersFlushed]) {
160
+ this._flushHeaders()
161
+ }
162
+
163
+ this.stream.setEncoding(encoding)
164
+ return this
165
+ }
166
+
167
+ write (chunk) {
168
+ if (!this[kHeadersFlushed]) {
169
+ this._flushHeaders()
170
+ }
171
+
172
+ return this.stream.write(chunk)
173
+ }
174
+
175
+ _flushHeaders (endStream = false) {
176
+ if (this[kHeadersFlushed]) {
177
+ throw new Error('Headers already flushed')
178
+ }
179
+
180
+ this.requestHeaders = Object.fromEntries(
181
+ Object.entries(this.requestHeaders)
182
+ .filter(([key]) => !connectionHeaders.includes(key.toLowerCase()))
183
+ )
184
+
185
+ // The client was created in an unreferenced state and is referenced when a stream is created
186
+ this._client.ref()
187
+ this.stream = this._client.request(this.requestHeaders, {endStream})
188
+
189
+ const unreferenceFn = () => {
190
+ this._client.unref()
191
+ this.stream.off('close', unreferenceFn)
192
+ }
193
+
194
+ this.stream.on('close', unreferenceFn)
195
+
196
+ this.registerListeners()
197
+
198
+ this[kHeadersFlushed] = true
199
+ }
200
+
201
+ pipe (dest) {
202
+ if (!this[kHeadersFlushed]) {
203
+ this._flushHeaders()
204
+ }
205
+ this.stream.pipe(dest)
206
+
207
+ return dest
208
+ }
209
+
210
+ on (eventName, listener) {
211
+ if (eventName === 'socket') {
212
+ listener(this.socket)
213
+ return this
214
+ }
215
+
216
+ return super.on(eventName, listener)
217
+ }
218
+
219
+ abort () {
220
+ if (!this[kHeadersFlushed]) {
221
+ this._flushHeaders()
222
+ }
223
+ this.stream.destroy()
224
+
225
+ return this
226
+ }
227
+
228
+ end () {
229
+ if (!this[kHeadersFlushed]) {
230
+ this._flushHeaders(true)
231
+ }
232
+ this.stream.end()
233
+
234
+ return this
235
+ }
236
+
237
+ setTimeout (timeout, cb) {
238
+ if (!this[kHeadersFlushed]) {
239
+ this._flushHeaders()
240
+ }
241
+ this.stream.setTimeout(timeout, cb)
242
+
243
+ return this
244
+ }
245
+
246
+ removeHeader (headerKey) {
247
+ if (this[kHeadersFlushed]) {
248
+ throw new Error('Headers already flushed. Cannot remove header')
249
+ }
250
+
251
+ if (headerKey.startsWith(':')) {
252
+ return
253
+ }
254
+
255
+ delete this.requestHeaders[headerKey]
256
+
257
+ return this
258
+ }
259
+
260
+ setHeader (headerKey, headerValue) {
261
+ if (this[kHeadersFlushed]) {
262
+ throw new Error('Headers already flushed. Cannot set header')
263
+ }
264
+
265
+ if (headerKey.startsWith(':')) {
266
+ return
267
+ }
268
+
269
+ this.requestHeaders[headerKey] = headerValue
270
+
271
+ return this
272
+ }
273
+ }
274
+
275
+ function request (options) {
276
+ // HTTP/2 internal implementation sucks. In case of an invalid HTTP/2 header, it destroys the entire session and
277
+ // emits an error asynchronously, instead of throwing it synchronously. Hence, it makes more sense to perform all
278
+ // validations before sending the request.
279
+ validateRequestHeaders(options.headers)
280
+
281
+ return new Http2Request(options)
282
+ }
283
+
284
+ class ResponseProxy extends EventEmitter {
285
+ constructor (response, stream) {
286
+ super()
287
+ this.httpVersion = '2.0'
288
+ this.reqStream = stream
289
+ this.response = response
290
+ this.on = this.on.bind(this)
291
+ this.registerRequestListeners()
292
+ this.socket = this.reqStream.session.socket
293
+ }
294
+
295
+ registerRequestListeners () {
296
+ this.reqStream.on('error', (e) => this.emit('error', e))
297
+ this.reqStream.on('close', () => {
298
+ this.emit('close')
299
+ })
300
+ }
301
+
302
+ on (eventName, listener) {
303
+ super.on(eventName, listener)
304
+ if (eventName === 'data') {
305
+ // Attach the data listener to the request stream only when there is a listener.
306
+ // This is because the data event is emitted by the request stream and the response stream is a proxy
307
+ // that forwards the data event to the response object.
308
+ // If there is no listener attached and we use the event forwarding pattern above, the data event will still be emitted
309
+ // but with no listeners attached to it, thus causing data loss.
310
+ this.reqStream.on('data', (chunk) => {
311
+ this.emit('data', chunk)
312
+ })
313
+ }
314
+
315
+ if (eventName === 'end') {
316
+ // Incase of bodies with no data, the end event is emitted immediately after the response event. In such cases, the consumer might not have attached the end listener yet. (eg: postman-echo.com/gets)
317
+ // Thus, when the end event is emitted, we check if the request stream has already ended. If it has, we emit the end event immediately.
318
+ // Otherwise, we wait for the request stream to end and then emit the end event.
319
+ if (this.reqStream.readableEnded) {
320
+ process.nextTick(listener)
321
+ } else {
322
+ this.reqStream.on('end', listener)
323
+ }
324
+ }
325
+ return this
326
+ }
327
+
328
+ get statusCode () {
329
+ return this.response[http2.constants.HTTP2_HEADER_STATUS]
330
+ }
331
+
332
+ get rawHeaders () {
333
+ return Object.entries(this.response).flat()
334
+ }
335
+
336
+ get headers () {
337
+ return Object.fromEntries(Object.entries(this.response))
338
+ }
339
+
340
+ pause () {
341
+ this.reqStream.pause()
342
+ return this
343
+ }
344
+
345
+ resume () {
346
+ this.reqStream.resume()
347
+ return this
348
+ }
349
+
350
+ pipe (dest) {
351
+ this.reqStream.pipe(dest)
352
+ return dest
353
+ }
354
+
355
+ setEncoding (encoding) {
356
+ this.reqStream.setEncoding(encoding)
357
+ return this
358
+ }
359
+
360
+ destroy () {
361
+ this.reqStream.destroy()
362
+ return this
363
+ }
364
+ }
365
+
366
+ module.exports = {
367
+ request,
368
+ Http2Request
369
+ }
package/lib/inflate.js ADDED
@@ -0,0 +1,64 @@
1
+ 'use strict'
2
+
3
+ var zlib = require('zlib')
4
+ var stream = require('stream')
5
+ var inherit = require('util').inherits
6
+ var Buffer = require('safe-buffer').Buffer
7
+ var Inflate
8
+
9
+ Inflate = function (options) {
10
+ this.options = options
11
+ this._stream = null
12
+ stream.Transform.call(this)
13
+ }
14
+
15
+ inherit(Inflate, stream.Transform)
16
+
17
+ Inflate.prototype._transform = function (chunk, encoding, callback) {
18
+ var self = this
19
+ if (!self._stream) {
20
+ // If the response stream does not have a valid deflate header, use `InflateRaw`
21
+ if ((Buffer.from(chunk, encoding)[0] & 0x0F) === 0x08) {
22
+ self._stream = zlib.createInflate(self.options)
23
+ } else {
24
+ self._stream = zlib.createInflateRaw(self.options)
25
+ }
26
+
27
+ self._stream.on('error', function (error) {
28
+ self.emit('error', error)
29
+ })
30
+
31
+ self._stream.on('data', function (chunk) {
32
+ self.push(chunk)
33
+ })
34
+
35
+ self._stream.once('end', function () {
36
+ self._ended = true
37
+ self.push(null)
38
+ })
39
+ }
40
+
41
+ self._stream.write(chunk, encoding, callback)
42
+ }
43
+
44
+ Inflate.prototype._flush = function (callback) {
45
+ if (this._stream && !this._ended) {
46
+ this._stream.once('end', callback)
47
+ this._stream.end()
48
+ } else {
49
+ callback()
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Creates an intelligent inflate stream, that can handle deflate responses from older servers,
55
+ * which do not send the correct GZip headers in the response. See http://stackoverflow.com/a/37528114
56
+ * for details on why this is needed.
57
+ *
58
+ * @param {Object=} options - Are passed to the underlying `Inflate` or `InflateRaw` constructor.
59
+ *
60
+ * @returns {*}
61
+ */
62
+ module.exports.createInflate = function (options) {
63
+ return new Inflate(options)
64
+ }