1 | var http = require('http')
|
2 | var https = require('https')
|
3 | var urlUtils = require('url')
|
4 | var _ = require('underscore')
|
5 | var merge = require('./merge')
|
6 | var querystringLite = require('./querystring-lite')
|
7 | var utils = require('./middlewareUtils')
|
8 | var createDebug = require('debug')
|
9 | var debug = createDebug('httpism')
|
10 | var debugResponse = createDebug('httpism:response')
|
11 | var debugRequest = createDebug('httpism:request')
|
12 | var HttpsProxyAgent = require('https-proxy-agent')
|
13 | var obfuscateUrlPassword = require('./obfuscateUrlPassword')
|
14 | var mimeTypes = require('mime-types')
|
15 | var proxyForUrl = require('proxy-from-env').getProxyForUrl
|
16 |
|
17 | function middleware (name, fn) {
|
18 | exports[name] = fn
|
19 | fn.httpismMiddleware = {
|
20 | name: name
|
21 | }
|
22 | }
|
23 |
|
24 | middleware('exception', utils.exception)
|
25 | middleware('querystring', utils.querystring)
|
26 | middleware('expandUrl', utils.expandUrl)
|
27 |
|
28 | exports.streamToString = function (s) {
|
29 | return new Promise(function (resolve, reject) {
|
30 | s.setEncoding('utf-8')
|
31 | var strings = []
|
32 |
|
33 | s.on('data', function (d) {
|
34 | strings.push(d)
|
35 | })
|
36 |
|
37 | s.on('end', function () {
|
38 | resolve(strings.join(''))
|
39 | })
|
40 |
|
41 | s.on('error', function (e) {
|
42 | reject(e)
|
43 | })
|
44 | })
|
45 | }
|
46 |
|
47 | exports.consumeStream = function (s) {
|
48 | return new Promise(function (resolve, reject) {
|
49 | s.on('end', function () {
|
50 | resolve()
|
51 | })
|
52 |
|
53 | s.on('error', function (e) {
|
54 | reject(e)
|
55 | })
|
56 |
|
57 | s.resume()
|
58 | })
|
59 | }
|
60 |
|
61 | function isStream (body) {
|
62 | return body !== undefined && typeof body.pipe === 'function'
|
63 | }
|
64 |
|
65 | middleware('json', function (request, next) {
|
66 | if (request.body instanceof Object && !isStream(request.body)) {
|
67 | setBodyToString(request, JSON.stringify(request.body))
|
68 | utils.setHeaderTo(request, 'content-type', 'application/json')
|
69 | }
|
70 |
|
71 | utils.setHeaderTo(request, 'accept', 'application/json')
|
72 |
|
73 | return next().then(function (response) {
|
74 | if (utils.shouldParseAs(response, 'json', request)) {
|
75 | return exports.streamToString(response.body).then(function (jsonString) {
|
76 | response.body = JSON.parse(jsonString, request.options.jsonReviver)
|
77 | return response
|
78 | })
|
79 | } else {
|
80 | return response
|
81 | }
|
82 | })
|
83 | })
|
84 |
|
85 | function setBodyToString (r, s) {
|
86 | r.body = stringToStream(s)
|
87 | r.headers['content-length'] = Buffer.byteLength(s, 'utf-8')
|
88 | r.stringBody = s
|
89 | }
|
90 |
|
91 | function stringToStream (s) {
|
92 | return {
|
93 | pipe: function (stream) {
|
94 | stream.write(s)
|
95 | stream.end()
|
96 | }
|
97 | }
|
98 | }
|
99 |
|
100 | exports.stringToStream = stringToStream
|
101 |
|
102 | function nodeRequest (request, options, protocol, withResponse) {
|
103 | if (protocol === 'https:') {
|
104 | return https.request(merge(request, options.https), withResponse)
|
105 | } else {
|
106 | return http.request(merge(request, options.http), withResponse)
|
107 | }
|
108 | }
|
109 |
|
110 | function proxyUrl (request, proxy) {
|
111 | var url = urlUtils.parse(request.url)
|
112 | var proxyUrl = urlUtils.parse(proxy)
|
113 |
|
114 | request.headers.host = url.hostname
|
115 |
|
116 | if (url.protocol === 'https:') {
|
117 | url.agent = new HttpsProxyAgent(proxy)
|
118 | return url
|
119 | } else {
|
120 | if (proxyUrl.auth) {
|
121 | request.headers['proxy-authorization'] = encodeBasicAuthorizationHeader(proxyUrl.auth)
|
122 | }
|
123 |
|
124 | return {
|
125 | hostname: proxyUrl.hostname,
|
126 | port: proxyUrl.port,
|
127 | path: request.url
|
128 | }
|
129 | }
|
130 | }
|
131 |
|
132 | function parseUrl (request) {
|
133 | var proxy = proxyForUrl(request.url) || request.options.proxy
|
134 |
|
135 | if (proxy) {
|
136 | return proxyUrl(request, proxy)
|
137 | } else {
|
138 | return urlUtils.parse(request.url)
|
139 | }
|
140 | }
|
141 |
|
142 | middleware('http', function (request) {
|
143 | return new Promise(function (resolve, reject) {
|
144 | var url = parseUrl(request)
|
145 |
|
146 | var req = nodeRequest(
|
147 | {
|
148 | hostname: url.hostname,
|
149 | port: url.port,
|
150 | method: request.method,
|
151 | path: url.path,
|
152 | headers: request.headers,
|
153 | agent: url.agent
|
154 | },
|
155 | request.options,
|
156 | url.protocol,
|
157 | function (res) {
|
158 | return resolve({
|
159 | statusCode: res.statusCode,
|
160 | statusText: http.STATUS_CODES[res.statusCode],
|
161 | url: request.url,
|
162 | headers: res.headers,
|
163 | body: res
|
164 | })
|
165 | }
|
166 | )
|
167 |
|
168 | req.on('error', function (e) {
|
169 | reject(e)
|
170 | })
|
171 |
|
172 | if (request.body) {
|
173 | request.body.pipe(req)
|
174 | } else {
|
175 | req.end()
|
176 | }
|
177 | })
|
178 | })
|
179 |
|
180 | function removeUndefined (obj) {
|
181 | Object.keys(obj).map(function (key) {
|
182 | if (typeof obj[key] === 'undefined') {
|
183 | delete obj[key]
|
184 | }
|
185 | })
|
186 |
|
187 | return obj
|
188 | }
|
189 |
|
190 | function prepareForLogging (r) {
|
191 | return removeUndefined({
|
192 | method: r.method,
|
193 | url: r.url && obfuscateUrlPassword(r.url),
|
194 | headers: r.headers,
|
195 | body: isStream(r.body) ? '[Stream]' : r.body,
|
196 | statusCode: r.statusCode,
|
197 | statusText: r.statusText
|
198 | })
|
199 | }
|
200 |
|
201 | function logRequest (request) {
|
202 | if (debugRequest.enabled) {
|
203 | debugRequest(prepareForLogging(request))
|
204 | }
|
205 | }
|
206 |
|
207 | middleware('log', function (request, next) {
|
208 | logRequest(request)
|
209 |
|
210 | var promise = next()
|
211 |
|
212 | if (debugResponse.enabled) {
|
213 | return promise.then(function (response) {
|
214 | logResponse(response)
|
215 | return response
|
216 | }, function (e) {
|
217 | var res = _.extend({}, e)
|
218 | logResponse(res)
|
219 | throw e
|
220 | })
|
221 | } else {
|
222 | return promise
|
223 | }
|
224 | })
|
225 |
|
226 | middleware('debugLog', function (request, next) {
|
227 | if (debug.enabled) {
|
228 | var startTime = Date.now()
|
229 | return next().then(function (response) {
|
230 | var headerTime = Date.now() - startTime
|
231 | response.body.on('end', function () {
|
232 | var bodyTime = Date.now() - startTime
|
233 | debug(request.method.toUpperCase() + ' ' + obfuscateUrlPassword(request.url) + ' => ' + response.statusCode + ' (' + headerTime + 'ms, ' + bodyTime + 'ms)')
|
234 | })
|
235 | return response
|
236 | }, function (error) {
|
237 | var headerTime = Date.now() - startTime
|
238 | debug(request.method.toUpperCase() + ' ' + obfuscateUrlPassword(request.url) + ' => ' + error.message + ' (' + headerTime + 'ms)')
|
239 | throw error
|
240 | })
|
241 | } else {
|
242 | return next()
|
243 | }
|
244 | })
|
245 |
|
246 | function logResponse (response) {
|
247 | if (!response.redirectResponse) {
|
248 | debugResponse(prepareForLogging(response))
|
249 | }
|
250 | }
|
251 |
|
252 | middleware('redirect', function (request, next, client) {
|
253 | return next().then(function (response) {
|
254 | var statusCode = response.statusCode
|
255 | var location = response.headers.location
|
256 |
|
257 | if (request.options.redirect !== false && location && (statusCode === 300 || statusCode === 301 || statusCode === 302 || statusCode === 303 || statusCode === 307)) {
|
258 | return exports.consumeStream(response.body).then(function () {
|
259 | logResponse(response)
|
260 | return client.get(urlUtils.resolve(request.url, location), request.options).then(function (redirectResponse) {
|
261 | var error = new Error('redirect')
|
262 | error.redirectResponse = redirectResponse
|
263 | throw error
|
264 | })
|
265 | })
|
266 | } else {
|
267 | return response
|
268 | }
|
269 | })
|
270 | })
|
271 |
|
272 | function loadCookies (cookies, url) {
|
273 | return cookies.getCookieStringSync(url)
|
274 | }
|
275 |
|
276 | function storeCookies (cookies, url, header) {
|
277 | if (header) {
|
278 | var headers =
|
279 | header instanceof Array
|
280 | ? header
|
281 | : [header]
|
282 |
|
283 | headers.forEach(function (setCookieHeader) {
|
284 | cookies.setCookieSync(setCookieHeader, url)
|
285 | })
|
286 | }
|
287 | }
|
288 |
|
289 | middleware('cookies', function (request, next, client) {
|
290 | var cookies
|
291 |
|
292 | if (client._options.cookies === true) {
|
293 | var toughCookie = require('tough-cookie')
|
294 | cookies = request.options.cookies = client._options.cookies = new toughCookie.CookieJar()
|
295 | } else {
|
296 | cookies = request.options.cookies
|
297 | }
|
298 |
|
299 | if (cookies) {
|
300 | request.headers.cookie = loadCookies(cookies, request.url)
|
301 | return next().then(function (response) {
|
302 | storeCookies(cookies, response.url, response.headers['set-cookie'])
|
303 | return response
|
304 | })
|
305 | } else {
|
306 | return next()
|
307 | }
|
308 | })
|
309 |
|
310 | middleware('text', function (request, next) {
|
311 | if (typeof request.body === 'string') {
|
312 | setBodyToString(request, request.body)
|
313 | utils.setHeaderTo(request, 'content-type', 'text/plain')
|
314 | }
|
315 |
|
316 | return next().then(function (response) {
|
317 | if (utils.shouldParseAs(response, 'text', request)) {
|
318 | return exports.streamToString(response.body).then(function (body) {
|
319 | response.body = body
|
320 | return response
|
321 | })
|
322 | } else {
|
323 | return response
|
324 | }
|
325 | })
|
326 | })
|
327 |
|
328 | middleware('form', function (request, next) {
|
329 | if (request.options.form && request.body instanceof Object && !isStream(request.body)) {
|
330 | var querystring = request.options.qs || querystringLite
|
331 | setBodyToString(request, querystring.stringify(request.body))
|
332 | utils.setHeaderTo(request, 'content-type', 'application/x-www-form-urlencoded')
|
333 | }
|
334 |
|
335 | return next().then(function (response) {
|
336 | if (utils.shouldParseAs(response, 'form', request)) {
|
337 | return exports.streamToString(response.body).then(function (body) {
|
338 | var querystring = request.options.qs || querystringLite
|
339 | response.body = querystring.parse(body)
|
340 | return response
|
341 | })
|
342 | } else {
|
343 | return response
|
344 | }
|
345 | })
|
346 | })
|
347 |
|
348 | function contentTypeOfStream (stream) {
|
349 | if (typeof stream.getHeaders === 'function') {
|
350 | return stream.getHeaders()['content-type']
|
351 | } else if (stream.path) {
|
352 | return mimeTypes.lookup(stream.path)
|
353 | }
|
354 | }
|
355 |
|
356 | middleware('streamContentType', function (request, next) {
|
357 | if (isStream(request.body) && !request.headers['content-type']) {
|
358 | request.headers['content-type'] = contentTypeOfStream(request.body)
|
359 | }
|
360 | return next()
|
361 | })
|
362 |
|
363 | function encodeBasicAuthorizationHeader (s) {
|
364 | return 'Basic ' + Buffer.from(s).toString('base64')
|
365 | }
|
366 |
|
367 | middleware('basicAuth', function (request, next) {
|
368 | function basicAuthorizationHeader () {
|
369 | if (request.options.basicAuth) {
|
370 | return encodeBasicAuthorizationHeader(request.options.basicAuth.username.replace(/:/g, '') + ':' + request.options.basicAuth.password)
|
371 | } else {
|
372 | var url = urlUtils.parse(request.url)
|
373 | if (url.auth) {
|
374 | return encodeBasicAuthorizationHeader(url.auth)
|
375 | }
|
376 | }
|
377 | }
|
378 |
|
379 | var header = basicAuthorizationHeader()
|
380 | if (header) {
|
381 | request.headers.authorization = header
|
382 | }
|
383 |
|
384 | return next()
|
385 | })
|