diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 000000000..f036dc9a5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,56 @@
+
+
+### Summary
+
+
+### Simplest Example to Reproduce
+
+
+```js
+request({
+ method: 'GET',
+ url: 'http://example.com', // a public URL that we can hit to reproduce, if possible
+ more: { 'options': 'here' }
+},
+```
+
+### Expected Behavior
+
+
+
+
+### Current Behavior
+
+
+
+### Possible Solution
+
+
+
+### Context
+
+
+
+### Your Environment
+
+
+| software | version
+| ---------------- | -------
+| request |
+| node |
+| npm |
+| Operating System |
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 000000000..0cb35f040
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,13 @@
+## PR Checklist:
+- [ ] I have run `npm test` locally and all tests are passing.
+- [ ] I have added/updated tests for any new behavior.
+
+- [ ] If this is a significant change, an issue has already been created where the problem / solution was discussed: [N/A, or add link to issue here]
+
+
+
+## PR Description
+
diff --git a/.github/stale.yml b/.github/stale.yml
new file mode 100644
index 000000000..ad26df134
--- /dev/null
+++ b/.github/stale.yml
@@ -0,0 +1,19 @@
+# Number of days of inactivity before an issue becomes stale
+daysUntilStale: 365
+# Number of days of inactivity before a stale issue is closed
+daysUntilClose: 7
+# Issues with these labels will never be considered stale
+exemptLabels:
+ - "Up for consideration"
+ - greenkeeper
+ - neverstale
+ - bug
+# Label to use when marking an issue as stale
+staleLabel: stale
+# Comment to post when marking an issue as stale. Set to `false` to disable
+markComment: >
+ This issue has been automatically marked as stale because it has not had
+ recent activity. It will be closed if no further activity occurs. Thank you
+ for your contributions.
+# Comment to post when closing a stale issue. Set to `false` to disable
+closeComment: false
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..9a254ab59
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+coverage
+.idea
+npm-debug.log
+package-lock.json
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..9c9940a2b
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,21 @@
+
+language: node_js
+
+node_js:
+ - node
+ - 10
+ - 8
+ - 6
+
+after_script:
+ - npm run test-cov
+ - codecov
+ - cat ./coverage/lcov.info | coveralls
+
+webhooks:
+ urls: https://webhooks.gitter.im/e/237280ed4796c19cc626
+ on_success: change # options: [always|never|change] default: always
+ on_failure: always # options: [always|never|change] default: always
+ on_start: false # default: false
+
+sudo: false
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..d3ffcd00d
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,717 @@
+## Change Log
+
+### v2.88.0 (2018/08/10)
+- [#2996](https://github.com/request/request/pull/2996) fix(uuid): import versioned uuid (@kwonoj)
+- [#2994](https://github.com/request/request/pull/2994) Update to oauth-sign 0.9.0 (@dlecocq)
+- [#2993](https://github.com/request/request/pull/2993) Fix header tests (@simov)
+- [#2904](https://github.com/request/request/pull/2904) #515, #2894 Strip port suffix from Host header if the protocol is known. (#2904) (@paambaati)
+- [#2791](https://github.com/request/request/pull/2791) Improve AWS SigV4 support. (#2791) (@vikhyat)
+- [#2977](https://github.com/request/request/pull/2977) Update test certificates (@simov)
+
+### v2.87.0 (2018/05/21)
+- [#2943](https://github.com/request/request/pull/2943) Replace hawk dependency with a local implemenation (#2943) (@hueniverse)
+
+### v2.86.0 (2018/05/15)
+- [#2885](https://github.com/request/request/pull/2885) Remove redundant code (for Node.js 0.9.4 and below) and dependency (@ChALkeR)
+- [#2942](https://github.com/request/request/pull/2942) Make Test GREEN Again! (@simov)
+- [#2923](https://github.com/request/request/pull/2923) Alterations for failing CI tests (@gareth-robinson)
+
+### v2.85.0 (2018/03/12)
+- [#2880](https://github.com/request/request/pull/2880) Revert "Update hawk to 7.0.7 (#2880)" (@simov)
+
+### v2.84.0 (2018/03/12)
+- [#2793](https://github.com/request/request/pull/2793) Fixed calculation of oauth_body_hash, issue #2792 (@dvishniakov)
+- [#2880](https://github.com/request/request/pull/2880) Update hawk to 7.0.7 (#2880) (@kornel-kedzierski)
+
+### v2.83.0 (2017/09/27)
+- [#2776](https://github.com/request/request/pull/2776) Updating tough-cookie due to security fix. (#2776) (@karlnorling)
+
+### v2.82.0 (2017/09/19)
+- [#2703](https://github.com/request/request/pull/2703) Add Node.js v8 to Travis CI (@ryysud)
+- [#2751](https://github.com/request/request/pull/2751) Update of hawk and qs to latest version (#2751) (@Olivier-Moreau)
+- [#2658](https://github.com/request/request/pull/2658) Fixed some text in README.md (#2658) (@Marketionist)
+- [#2635](https://github.com/request/request/pull/2635) chore(package): update aws-sign2 to version 0.7.0 (#2635) (@greenkeeperio-bot)
+- [#2641](https://github.com/request/request/pull/2641) Update README to simplify & update convenience methods (#2641) (@FredKSchott)
+- [#2541](https://github.com/request/request/pull/2541) Add convenience method for HTTP OPTIONS (#2541) (@jamesseanwright)
+- [#2605](https://github.com/request/request/pull/2605) Add promise support section to README (#2605) (@FredKSchott)
+- [#2579](https://github.com/request/request/pull/2579) refactor(lint): replace eslint with standard (#2579) (@ahmadnassri)
+- [#2598](https://github.com/request/request/pull/2598) Update codecov to version 2.0.2 🚀 (@greenkeeperio-bot)
+- [#2590](https://github.com/request/request/pull/2590) Adds test-timing keepAlive test (@nicjansma)
+- [#2589](https://github.com/request/request/pull/2589) fix tabulation on request example README.MD (@odykyi)
+- [#2594](https://github.com/request/request/pull/2594) chore(dependencies): har-validator to 5.x [removes babel dep] (@ahmadnassri)
+
+### v2.81.0 (2017/03/09)
+- [#2584](https://github.com/request/request/pull/2584) Security issue: Upgrade qs to version 6.4.0 (@sergejmueller)
+- [#2578](https://github.com/request/request/pull/2578) safe-buffer doesn't zero-fill by default, its just a polyfill. (#2578) (@mikeal)
+- [#2566](https://github.com/request/request/pull/2566) Timings: Tracks 'lookup', adds 'wait' time, fixes connection re-use (#2566) (@nicjansma)
+- [#2574](https://github.com/request/request/pull/2574) Migrating to safe-buffer for improved security. (@mikeal)
+- [#2573](https://github.com/request/request/pull/2573) fixes #2572 (@ahmadnassri)
+
+### v2.80.0 (2017/03/04)
+- [#2571](https://github.com/request/request/pull/2571) Correctly format the Host header for IPv6 addresses (@JamesMGreene)
+- [#2558](https://github.com/request/request/pull/2558) Update README.md example snippet (@FredKSchott)
+- [#2221](https://github.com/request/request/pull/2221) Adding a simple Response object reference in argument specification (@calamarico)
+- [#2452](https://github.com/request/request/pull/2452) Adds .timings array with DNC, TCP, request and response times (@nicjansma)
+- [#2553](https://github.com/request/request/pull/2553) add ISSUE_TEMPLATE, move PR template (@FredKSchott)
+- [#2539](https://github.com/request/request/pull/2539) Create PULL_REQUEST_TEMPLATE.md (@FredKSchott)
+- [#2524](https://github.com/request/request/pull/2524) Update caseless to version 0.12.0 🚀 (@greenkeeperio-bot)
+- [#2460](https://github.com/request/request/pull/2460) Fix wrong MIME type in example (@OwnageIsMagic)
+- [#2514](https://github.com/request/request/pull/2514) Change tags to keywords in package.json (@humphd)
+- [#2492](https://github.com/request/request/pull/2492) More lenient gzip decompression (@addaleax)
+
+### v2.79.0 (2016/11/18)
+- [#2368](https://github.com/request/request/pull/2368) Fix typeof check in test-pool.js (@forivall)
+- [#2394](https://github.com/request/request/pull/2394) Use `files` in package.json (@SimenB)
+- [#2463](https://github.com/request/request/pull/2463) AWS support for session tokens for temporary credentials (@simov)
+- [#2467](https://github.com/request/request/pull/2467) Migrate to uuid (@simov, @antialias)
+- [#2459](https://github.com/request/request/pull/2459) Update taper to version 0.5.0 🚀 (@greenkeeperio-bot)
+- [#2448](https://github.com/request/request/pull/2448) Make other connect timeout test more reliable too (@mscdex)
+
+### v2.78.0 (2016/11/03)
+- [#2447](https://github.com/request/request/pull/2447) Always set request timeout on keep-alive connections (@mscdex)
+
+### v2.77.0 (2016/11/03)
+- [#2439](https://github.com/request/request/pull/2439) Fix socket 'connect' listener handling (@mscdex)
+- [#2442](https://github.com/request/request/pull/2442) 👻😱 Node.js 0.10 is unmaintained 😱👻 (@greenkeeperio-bot)
+- [#2435](https://github.com/request/request/pull/2435) Add followOriginalHttpMethod to redirect to original HTTP method (@kirrg001)
+- [#2414](https://github.com/request/request/pull/2414) Improve test-timeout reliability (@mscdex)
+
+### v2.76.0 (2016/10/25)
+- [#2424](https://github.com/request/request/pull/2424) Handle buffers directly instead of using "bl" (@zertosh)
+- [#2415](https://github.com/request/request/pull/2415) Re-enable timeout tests on Travis + other fixes (@mscdex)
+- [#2431](https://github.com/request/request/pull/2431) Improve timeouts accuracy and node v6.8.0+ compatibility (@mscdex, @greenkeeperio-bot)
+- [#2428](https://github.com/request/request/pull/2428) Update qs to version 6.3.0 🚀 (@greenkeeperio-bot)
+- [#2420](https://github.com/request/request/pull/2420) change .on to .once, remove possible memory leaks (@duereg)
+- [#2426](https://github.com/request/request/pull/2426) Remove "isFunction" helper in favor of "typeof" check (@zertosh)
+- [#2425](https://github.com/request/request/pull/2425) Simplify "defer" helper creation (@zertosh)
+- [#2402](https://github.com/request/request/pull/2402) form-data@2.1.1 breaks build 🚨 (@greenkeeperio-bot)
+- [#2393](https://github.com/request/request/pull/2393) Update form-data to version 2.1.0 🚀 (@greenkeeperio-bot)
+
+### v2.75.0 (2016/09/17)
+- [#2381](https://github.com/request/request/pull/2381) Drop support for Node 0.10 (@simov)
+- [#2377](https://github.com/request/request/pull/2377) Update form-data to version 2.0.0 🚀 (@greenkeeperio-bot)
+- [#2353](https://github.com/request/request/pull/2353) Add greenkeeper ignored packages (@simov)
+- [#2351](https://github.com/request/request/pull/2351) Update karma-tap to version 3.0.1 🚀 (@greenkeeperio-bot)
+- [#2348](https://github.com/request/request/pull/2348) form-data@1.0.1 breaks build 🚨 (@greenkeeperio-bot)
+- [#2349](https://github.com/request/request/pull/2349) Check error type instead of string (@scotttrinh)
+
+### v2.74.0 (2016/07/22)
+- [#2295](https://github.com/request/request/pull/2295) Update tough-cookie to 2.3.0 (@stash-sfdc)
+- [#2280](https://github.com/request/request/pull/2280) Update karma-tap to version 2.0.1 🚀 (@greenkeeperio-bot)
+
+### v2.73.0 (2016/07/09)
+- [#2240](https://github.com/request/request/pull/2240) Remove connectionErrorHandler to fix #1903 (@zarenner)
+- [#2251](https://github.com/request/request/pull/2251) tape@4.6.0 breaks build 🚨 (@greenkeeperio-bot)
+- [#2225](https://github.com/request/request/pull/2225) Update docs (@ArtskydJ)
+- [#2203](https://github.com/request/request/pull/2203) Update browserify to version 13.0.1 🚀 (@greenkeeperio-bot)
+- [#2275](https://github.com/request/request/pull/2275) Update karma to version 1.1.1 🚀 (@greenkeeperio-bot)
+- [#2204](https://github.com/request/request/pull/2204) Add codecov.yml and disable PR comments (@simov)
+- [#2212](https://github.com/request/request/pull/2212) Fix link to http.IncomingMessage documentation (@nazieb)
+- [#2208](https://github.com/request/request/pull/2208) Update to form-data RC4 and pass null values to it (@simov)
+- [#2207](https://github.com/request/request/pull/2207) Move aws4 require statement to the top (@simov)
+- [#2199](https://github.com/request/request/pull/2199) Update karma-coverage to version 1.0.0 🚀 (@greenkeeperio-bot)
+- [#2206](https://github.com/request/request/pull/2206) Update qs to version 6.2.0 🚀 (@greenkeeperio-bot)
+- [#2205](https://github.com/request/request/pull/2205) Use server-destory to close hanging sockets in tests (@simov)
+- [#2200](https://github.com/request/request/pull/2200) Update karma-cli to version 1.0.0 🚀 (@greenkeeperio-bot)
+
+### v2.72.0 (2016/04/17)
+- [#2176](https://github.com/request/request/pull/2176) Do not try to pipe Gzip responses with no body (@simov)
+- [#2175](https://github.com/request/request/pull/2175) Add 'delete' alias for the 'del' API method (@simov, @MuhanZou)
+- [#2172](https://github.com/request/request/pull/2172) Add support for deflate content encoding (@czardoz)
+- [#2169](https://github.com/request/request/pull/2169) Add callback option (@simov)
+- [#2165](https://github.com/request/request/pull/2165) Check for self.req existence inside the write method (@simov)
+- [#2167](https://github.com/request/request/pull/2167) Fix TravisCI badge reference master branch (@a0viedo)
+
+### v2.71.0 (2016/04/12)
+- [#2164](https://github.com/request/request/pull/2164) Catch errors from the underlying http module (@simov)
+
+### v2.70.0 (2016/04/05)
+- [#2147](https://github.com/request/request/pull/2147) Update eslint to version 2.5.3 🚀 (@simov, @greenkeeperio-bot)
+- [#2009](https://github.com/request/request/pull/2009) Support JSON stringify replacer argument. (@elyobo)
+- [#2142](https://github.com/request/request/pull/2142) Update eslint to version 2.5.1 🚀 (@greenkeeperio-bot)
+- [#2128](https://github.com/request/request/pull/2128) Update browserify-istanbul to version 2.0.0 🚀 (@greenkeeperio-bot)
+- [#2115](https://github.com/request/request/pull/2115) Update eslint to version 2.3.0 🚀 (@simov, @greenkeeperio-bot)
+- [#2089](https://github.com/request/request/pull/2089) Fix badges (@simov)
+- [#2092](https://github.com/request/request/pull/2092) Update browserify-istanbul to version 1.0.0 🚀 (@greenkeeperio-bot)
+- [#2079](https://github.com/request/request/pull/2079) Accept read stream as body option (@simov)
+- [#2070](https://github.com/request/request/pull/2070) Update bl to version 1.1.2 🚀 (@greenkeeperio-bot)
+- [#2063](https://github.com/request/request/pull/2063) Up bluebird and oauth-sign (@simov)
+- [#2058](https://github.com/request/request/pull/2058) Karma fixes for latest versions (@eiriksm)
+- [#2057](https://github.com/request/request/pull/2057) Update contributing guidelines (@simov)
+- [#2054](https://github.com/request/request/pull/2054) Update qs to version 6.1.0 🚀 (@greenkeeperio-bot)
+
+### v2.69.0 (2016/01/27)
+- [#2041](https://github.com/request/request/pull/2041) restore aws4 as regular dependency (@rmg)
+
+### v2.68.0 (2016/01/27)
+- [#2036](https://github.com/request/request/pull/2036) Add AWS Signature Version 4 (@simov, @mirkods)
+- [#2022](https://github.com/request/request/pull/2022) Convert numeric multipart bodies to string (@simov, @feross)
+- [#2024](https://github.com/request/request/pull/2024) Update har-validator dependency for nsp advisory #76 (@TylerDixon)
+- [#2016](https://github.com/request/request/pull/2016) Update qs to version 6.0.2 🚀 (@greenkeeperio-bot)
+- [#2007](https://github.com/request/request/pull/2007) Use the `extend` module instead of util._extend (@simov)
+- [#2003](https://github.com/request/request/pull/2003) Update browserify to version 13.0.0 🚀 (@greenkeeperio-bot)
+- [#1989](https://github.com/request/request/pull/1989) Update buffer-equal to version 1.0.0 🚀 (@greenkeeperio-bot)
+- [#1956](https://github.com/request/request/pull/1956) Check form-data content-length value before setting up the header (@jongyoonlee)
+- [#1958](https://github.com/request/request/pull/1958) Use IncomingMessage.destroy method (@simov)
+- [#1952](https://github.com/request/request/pull/1952) Adds example for Tor proxy (@prometheansacrifice)
+- [#1943](https://github.com/request/request/pull/1943) Update eslint to version 1.10.3 🚀 (@simov, @greenkeeperio-bot)
+- [#1924](https://github.com/request/request/pull/1924) Update eslint to version 1.10.1 🚀 (@greenkeeperio-bot)
+- [#1915](https://github.com/request/request/pull/1915) Remove content-length and transfer-encoding headers from defaultProxyHeaderWhiteList (@yaxia)
+
+### v2.67.0 (2015/11/19)
+- [#1913](https://github.com/request/request/pull/1913) Update http-signature to version 1.1.0 🚀 (@greenkeeperio-bot)
+
+### v2.66.0 (2015/11/18)
+- [#1906](https://github.com/request/request/pull/1906) Update README URLs based on HTTP redirects (@ReadmeCritic)
+- [#1905](https://github.com/request/request/pull/1905) Convert typed arrays into regular buffers (@simov)
+- [#1902](https://github.com/request/request/pull/1902) node-uuid@1.4.7 breaks build 🚨 (@greenkeeperio-bot)
+- [#1894](https://github.com/request/request/pull/1894) Fix tunneling after redirection from https (Original: #1881) (@simov, @falms)
+- [#1893](https://github.com/request/request/pull/1893) Update eslint to version 1.9.0 🚀 (@greenkeeperio-bot)
+- [#1852](https://github.com/request/request/pull/1852) Update eslint to version 1.7.3 🚀 (@simov, @greenkeeperio-bot, @paulomcnally, @michelsalib, @arbaaz, @nsklkn, @LoicMahieu, @JoshWillik, @jzaefferer, @ryanwholey, @djchie, @thisconnect, @mgenereu, @acroca, @Sebmaster, @KoltesDigital)
+- [#1876](https://github.com/request/request/pull/1876) Implement loose matching for har mime types (@simov)
+- [#1875](https://github.com/request/request/pull/1875) Update bluebird to version 3.0.2 🚀 (@simov, @greenkeeperio-bot)
+- [#1871](https://github.com/request/request/pull/1871) Update browserify to version 12.0.1 🚀 (@greenkeeperio-bot)
+- [#1866](https://github.com/request/request/pull/1866) Add missing quotes on x-token property in README (@miguelmota)
+- [#1874](https://github.com/request/request/pull/1874) Fix typo in README.md (@gswalden)
+- [#1860](https://github.com/request/request/pull/1860) Improve referer header tests and docs (@simov)
+- [#1861](https://github.com/request/request/pull/1861) Remove redundant call to Stream constructor (@watson)
+- [#1857](https://github.com/request/request/pull/1857) Fix Referer header to point to the original host name (@simov)
+- [#1850](https://github.com/request/request/pull/1850) Update karma-coverage to version 0.5.3 🚀 (@greenkeeperio-bot)
+- [#1847](https://github.com/request/request/pull/1847) Use node's latest version when building (@simov)
+- [#1836](https://github.com/request/request/pull/1836) Tunnel: fix wrong property name (@KoltesDigital)
+- [#1820](https://github.com/request/request/pull/1820) Set href as request.js uses it (@mgenereu)
+- [#1840](https://github.com/request/request/pull/1840) Update http-signature to version 1.0.2 🚀 (@greenkeeperio-bot)
+- [#1845](https://github.com/request/request/pull/1845) Update istanbul to version 0.4.0 🚀 (@greenkeeperio-bot)
+
+### v2.65.0 (2015/10/11)
+- [#1833](https://github.com/request/request/pull/1833) Update aws-sign2 to version 0.6.0 🚀 (@greenkeeperio-bot)
+- [#1811](https://github.com/request/request/pull/1811) Enable loose cookie parsing in tough-cookie (@Sebmaster)
+- [#1830](https://github.com/request/request/pull/1830) Bring back tilde ranges for all dependencies (@simov)
+- [#1821](https://github.com/request/request/pull/1821) Implement support for RFC 2617 MD5-sess algorithm. (@BigDSK)
+- [#1828](https://github.com/request/request/pull/1828) Updated qs dependency to 5.2.0 (@acroca)
+- [#1818](https://github.com/request/request/pull/1818) Extract `readResponseBody` method out of `onRequestResponse` (@pvoisin)
+- [#1819](https://github.com/request/request/pull/1819) Run stringify once (@mgenereu)
+- [#1814](https://github.com/request/request/pull/1814) Updated har-validator to version 2.0.2 (@greenkeeperio-bot)
+- [#1807](https://github.com/request/request/pull/1807) Updated tough-cookie to version 2.1.0 (@greenkeeperio-bot)
+- [#1800](https://github.com/request/request/pull/1800) Add caret ranges for devDependencies, except eslint (@simov)
+- [#1799](https://github.com/request/request/pull/1799) Updated karma-browserify to version 4.4.0 (@greenkeeperio-bot)
+- [#1797](https://github.com/request/request/pull/1797) Updated tape to version 4.2.0 (@greenkeeperio-bot)
+- [#1788](https://github.com/request/request/pull/1788) Pinned all dependencies (@greenkeeperio-bot)
+
+### v2.64.0 (2015/09/25)
+- [#1787](https://github.com/request/request/pull/1787) npm ignore examples, release.sh and disabled.appveyor.yml (@thisconnect)
+- [#1775](https://github.com/request/request/pull/1775) Fix typo in README.md (@djchie)
+- [#1776](https://github.com/request/request/pull/1776) Changed word 'conjuction' to read 'conjunction' in README.md (@ryanwholey)
+- [#1785](https://github.com/request/request/pull/1785) Revert: Set default application/json content-type when using json option #1772 (@simov)
+
+### v2.63.0 (2015/09/21)
+- [#1772](https://github.com/request/request/pull/1772) Set default application/json content-type when using json option (@jzaefferer)
+
+### v2.62.0 (2015/09/15)
+- [#1768](https://github.com/request/request/pull/1768) Add node 4.0 to the list of build targets (@simov)
+- [#1767](https://github.com/request/request/pull/1767) Query strings now cooperate with unix sockets (@JoshWillik)
+- [#1750](https://github.com/request/request/pull/1750) Revert doc about installation of tough-cookie added in #884 (@LoicMahieu)
+- [#1746](https://github.com/request/request/pull/1746) Missed comma in Readme (@nsklkn)
+- [#1743](https://github.com/request/request/pull/1743) Fix options not being initialized in defaults method (@simov)
+
+### v2.61.0 (2015/08/19)
+- [#1721](https://github.com/request/request/pull/1721) Minor fix in README.md (@arbaaz)
+- [#1733](https://github.com/request/request/pull/1733) Avoid useless Buffer transformation (@michelsalib)
+- [#1726](https://github.com/request/request/pull/1726) Update README.md (@paulomcnally)
+- [#1715](https://github.com/request/request/pull/1715) Fix forever option in node > 0.10 #1709 (@calibr)
+- [#1716](https://github.com/request/request/pull/1716) Do not create Buffer from Object in setContentLength(iojs v3.0 issue) (@calibr)
+- [#1711](https://github.com/request/request/pull/1711) Add ability to detect connect timeouts (@kevinburke)
+- [#1712](https://github.com/request/request/pull/1712) Set certificate expiration to August 2, 2018 (@kevinburke)
+- [#1700](https://github.com/request/request/pull/1700) debug() when JSON.parse() on a response body fails (@phillipj)
+
+### v2.60.0 (2015/07/21)
+- [#1687](https://github.com/request/request/pull/1687) Fix caseless bug - content-type not being set for multipart/form-data (@simov, @garymathews)
+
+### v2.59.0 (2015/07/20)
+- [#1671](https://github.com/request/request/pull/1671) Add tests and docs for using the agent, agentClass, agentOptions and forever options.
+ Forever option defaults to using http(s).Agent in node 0.12+ (@simov)
+- [#1679](https://github.com/request/request/pull/1679) Fix - do not remove OAuth param when using OAuth realm (@simov, @jhalickman)
+- [#1668](https://github.com/request/request/pull/1668) updated dependencies (@deamme)
+- [#1656](https://github.com/request/request/pull/1656) Fix form method (@simov)
+- [#1651](https://github.com/request/request/pull/1651) Preserve HEAD method when using followAllRedirects (@simov)
+- [#1652](https://github.com/request/request/pull/1652) Update `encoding` option documentation in README.md (@daniel347x)
+- [#1650](https://github.com/request/request/pull/1650) Allow content-type overriding when using the `form` option (@simov)
+- [#1646](https://github.com/request/request/pull/1646) Clarify the nature of setting `ca` in `agentOptions` (@jeffcharles)
+
+### v2.58.0 (2015/06/16)
+- [#1638](https://github.com/request/request/pull/1638) Use the `extend` module to deep extend in the defaults method (@simov)
+- [#1631](https://github.com/request/request/pull/1631) Move tunnel logic into separate module (@simov)
+- [#1634](https://github.com/request/request/pull/1634) Fix OAuth query transport_method (@simov)
+- [#1603](https://github.com/request/request/pull/1603) Add codecov (@simov)
+
+### v2.57.0 (2015/05/31)
+- [#1615](https://github.com/request/request/pull/1615) Replace '.client' with '.socket' as the former was deprecated in 2.2.0. (@ChALkeR)
+
+### v2.56.0 (2015/05/28)
+- [#1610](https://github.com/request/request/pull/1610) Bump module dependencies (@simov)
+- [#1600](https://github.com/request/request/pull/1600) Extract the querystring logic into separate module (@simov)
+- [#1607](https://github.com/request/request/pull/1607) Re-generate certificates (@simov)
+- [#1599](https://github.com/request/request/pull/1599) Move getProxyFromURI logic below the check for Invaild URI (#1595) (@simov)
+- [#1598](https://github.com/request/request/pull/1598) Fix the way http verbs are defined in order to please intellisense IDEs (@simov, @flannelJesus)
+- [#1591](https://github.com/request/request/pull/1591) A few minor fixes: (@simov)
+- [#1584](https://github.com/request/request/pull/1584) Refactor test-default tests (according to comments in #1430) (@simov)
+- [#1585](https://github.com/request/request/pull/1585) Fixing documentation regarding TLS options (#1583) (@mainakae)
+- [#1574](https://github.com/request/request/pull/1574) Refresh the oauth_nonce on redirect (#1573) (@simov)
+- [#1570](https://github.com/request/request/pull/1570) Discovered tests that weren't properly running (@seanstrom)
+- [#1569](https://github.com/request/request/pull/1569) Fix pause before response arrives (@kevinoid)
+- [#1558](https://github.com/request/request/pull/1558) Emit error instead of throw (@simov)
+- [#1568](https://github.com/request/request/pull/1568) Fix stall when piping gzipped response (@kevinoid)
+- [#1560](https://github.com/request/request/pull/1560) Update combined-stream (@apechimp)
+- [#1543](https://github.com/request/request/pull/1543) Initial support for oauth_body_hash on json payloads (@simov, @aesopwolf)
+- [#1541](https://github.com/request/request/pull/1541) Fix coveralls (@simov)
+- [#1540](https://github.com/request/request/pull/1540) Fix recursive defaults for convenience methods (@simov)
+- [#1536](https://github.com/request/request/pull/1536) More eslint style rules (@froatsnook)
+- [#1533](https://github.com/request/request/pull/1533) Adding dependency status bar to README.md (@YasharF)
+- [#1539](https://github.com/request/request/pull/1539) ensure the latest version of har-validator is included (@ahmadnassri)
+- [#1516](https://github.com/request/request/pull/1516) forever+pool test (@devTristan)
+
+### v2.55.0 (2015/04/05)
+- [#1520](https://github.com/request/request/pull/1520) Refactor defaults (@simov)
+- [#1525](https://github.com/request/request/pull/1525) Delete request headers with undefined value. (@froatsnook)
+- [#1521](https://github.com/request/request/pull/1521) Add promise tests (@simov)
+- [#1518](https://github.com/request/request/pull/1518) Fix defaults (@simov)
+- [#1515](https://github.com/request/request/pull/1515) Allow static invoking of convenience methods (@simov)
+- [#1505](https://github.com/request/request/pull/1505) Fix multipart boundary extraction regexp (@simov)
+- [#1510](https://github.com/request/request/pull/1510) Fix basic auth form data (@simov)
+
+### v2.54.0 (2015/03/24)
+- [#1501](https://github.com/request/request/pull/1501) HTTP Archive 1.2 support (@ahmadnassri)
+- [#1486](https://github.com/request/request/pull/1486) Add a test for the forever agent (@akshayp)
+- [#1500](https://github.com/request/request/pull/1500) Adding handling for no auth method and null bearer (@philberg)
+- [#1498](https://github.com/request/request/pull/1498) Add table of contents in readme (@simov)
+- [#1477](https://github.com/request/request/pull/1477) Add support for qs options via qsOptions key (@simov)
+- [#1496](https://github.com/request/request/pull/1496) Parameters encoded to base 64 should be decoded as UTF-8, not ASCII. (@albanm)
+- [#1494](https://github.com/request/request/pull/1494) Update eslint (@froatsnook)
+- [#1474](https://github.com/request/request/pull/1474) Require Colon in Basic Auth (@erykwalder)
+- [#1481](https://github.com/request/request/pull/1481) Fix baseUrl and redirections. (@burningtree)
+- [#1469](https://github.com/request/request/pull/1469) Feature/base url (@froatsnook)
+- [#1459](https://github.com/request/request/pull/1459) Add option to time request/response cycle (including rollup of redirects) (@aaron-em)
+- [#1468](https://github.com/request/request/pull/1468) Re-enable io.js/node 0.12 build (@simov, @mikeal, @BBB)
+- [#1442](https://github.com/request/request/pull/1442) Fixed the issue with strictSSL tests on 0.12 & io.js by explicitly setting a cipher that matches the cert. (@BBB, @nickmccurdy, @demohi, @simov, @0x4139)
+- [#1460](https://github.com/request/request/pull/1460) localAddress or proxy config is lost when redirecting (@simov, @0x4139)
+- [#1453](https://github.com/request/request/pull/1453) Test on Node.js 0.12 and io.js with allowed failures (@nickmccurdy, @demohi)
+- [#1426](https://github.com/request/request/pull/1426) Fixing tests to pass on io.js and node 0.12 (only test-https.js stiff failing) (@mikeal)
+- [#1446](https://github.com/request/request/pull/1446) Missing HTTP referer header with redirects Fixes #1038 (@simov, @guimon)
+- [#1428](https://github.com/request/request/pull/1428) Deprecate Node v0.8.x (@nylen)
+- [#1436](https://github.com/request/request/pull/1436) Add ability to set a requester without setting default options (@tikotzky)
+- [#1435](https://github.com/request/request/pull/1435) dry up verb methods (@sethpollack)
+- [#1423](https://github.com/request/request/pull/1423) Allow fully qualified multipart content-type header (@simov)
+- [#1430](https://github.com/request/request/pull/1430) Fix recursive requester (@tikotzky)
+- [#1429](https://github.com/request/request/pull/1429) Throw error when making HEAD request with a body (@tikotzky)
+- [#1419](https://github.com/request/request/pull/1419) Add note that the project is broken in 0.12.x (@nylen)
+- [#1413](https://github.com/request/request/pull/1413) Fix basic auth (@simov)
+- [#1397](https://github.com/request/request/pull/1397) Improve pipe-from-file tests (@nylen)
+
+### v2.53.0 (2015/02/02)
+- [#1396](https://github.com/request/request/pull/1396) Do not rfc3986 escape JSON bodies (@nylen, @simov)
+- [#1392](https://github.com/request/request/pull/1392) Improve `timeout` option description (@watson)
+
+### v2.52.0 (2015/02/02)
+- [#1383](https://github.com/request/request/pull/1383) Add missing HTTPS options that were not being passed to tunnel (@brichard19) (@nylen)
+- [#1388](https://github.com/request/request/pull/1388) Upgrade mime-types package version (@roderickhsiao)
+- [#1389](https://github.com/request/request/pull/1389) Revise Setup Tunnel Function (@seanstrom)
+- [#1374](https://github.com/request/request/pull/1374) Allow explicitly disabling tunneling for proxied https destinations (@nylen)
+- [#1376](https://github.com/request/request/pull/1376) Use karma-browserify for tests. Add browser test coverage reporter. (@eiriksm)
+- [#1366](https://github.com/request/request/pull/1366) Refactor OAuth into separate module (@simov)
+- [#1373](https://github.com/request/request/pull/1373) Rewrite tunnel test to be pure Node.js (@nylen)
+- [#1371](https://github.com/request/request/pull/1371) Upgrade test reporter (@nylen)
+- [#1360](https://github.com/request/request/pull/1360) Refactor basic, bearer, digest auth logic into separate class (@simov)
+- [#1354](https://github.com/request/request/pull/1354) Remove circular dependency from debugging code (@nylen)
+- [#1351](https://github.com/request/request/pull/1351) Move digest auth into private prototype method (@simov)
+- [#1352](https://github.com/request/request/pull/1352) Update hawk dependency to ~2.3.0 (@mridgway)
+- [#1353](https://github.com/request/request/pull/1353) Correct travis-ci badge (@dogancelik)
+- [#1349](https://github.com/request/request/pull/1349) Make sure we return on errored browser requests. (@eiriksm)
+- [#1346](https://github.com/request/request/pull/1346) getProxyFromURI Extraction Refactor (@seanstrom)
+- [#1337](https://github.com/request/request/pull/1337) Standardize test ports on 6767 (@nylen)
+- [#1341](https://github.com/request/request/pull/1341) Emit FormData error events as Request error events (@nylen, @rwky)
+- [#1343](https://github.com/request/request/pull/1343) Clean up readme badges, and add Travis and Coveralls badges (@nylen)
+- [#1345](https://github.com/request/request/pull/1345) Update README.md (@Aaron-Hartwig)
+- [#1338](https://github.com/request/request/pull/1338) Always wait for server.close() callback in tests (@nylen)
+- [#1342](https://github.com/request/request/pull/1342) Add mock https server and redo start of browser tests for this purpose. (@eiriksm)
+- [#1339](https://github.com/request/request/pull/1339) Improve auth docs (@nylen)
+- [#1335](https://github.com/request/request/pull/1335) Add support for OAuth plaintext signature method (@simov)
+- [#1332](https://github.com/request/request/pull/1332) Add clean script to remove test-browser.js after the tests run (@seanstrom)
+- [#1327](https://github.com/request/request/pull/1327) Fix errors generating coverage reports. (@nylen)
+- [#1330](https://github.com/request/request/pull/1330) Return empty buffer upon empty response body and encoding is set to null (@seanstrom)
+- [#1326](https://github.com/request/request/pull/1326) Use faster container-based infrastructure on Travis (@nylen)
+- [#1315](https://github.com/request/request/pull/1315) Implement rfc3986 option (@simov, @nylen, @apoco, @DullReferenceException, @mmalecki, @oliamb, @cliffcrosland, @LewisJEllis, @eiriksm, @poislagarde)
+- [#1314](https://github.com/request/request/pull/1314) Detect urlencoded form data header via regex (@simov)
+- [#1317](https://github.com/request/request/pull/1317) Improve OAuth1.0 server side flow example (@simov)
+
+### v2.51.0 (2014/12/10)
+- [#1310](https://github.com/request/request/pull/1310) Revert changes introduced in https://github.com/request/request/pull/1282 (@simov)
+
+### v2.50.0 (2014/12/09)
+- [#1308](https://github.com/request/request/pull/1308) Add browser test to keep track of browserify compability. (@eiriksm)
+- [#1299](https://github.com/request/request/pull/1299) Add optional support for jsonReviver (@poislagarde)
+- [#1277](https://github.com/request/request/pull/1277) Add Coveralls configuration (@simov)
+- [#1307](https://github.com/request/request/pull/1307) Upgrade form-data, add back browserify compability. Fixes #455. (@eiriksm)
+- [#1305](https://github.com/request/request/pull/1305) Fix typo in README.md (@LewisJEllis)
+- [#1288](https://github.com/request/request/pull/1288) Update README.md to explain custom file use case (@cliffcrosland)
+
+### v2.49.0 (2014/11/28)
+- [#1295](https://github.com/request/request/pull/1295) fix(proxy): no-proxy false positive (@oliamb)
+- [#1292](https://github.com/request/request/pull/1292) Upgrade `caseless` to 0.8.1 (@mmalecki)
+- [#1276](https://github.com/request/request/pull/1276) Set transfer encoding for multipart/related to chunked by default (@simov)
+- [#1275](https://github.com/request/request/pull/1275) Fix multipart content-type headers detection (@simov)
+- [#1269](https://github.com/request/request/pull/1269) adds streams example for review (@tbuchok)
+- [#1238](https://github.com/request/request/pull/1238) Add examples README.md (@simov)
+
+### v2.48.0 (2014/11/12)
+- [#1263](https://github.com/request/request/pull/1263) Fixed a syntax error / typo in README.md (@xna2)
+- [#1253](https://github.com/request/request/pull/1253) Add multipart chunked flag (@simov, @nylen)
+- [#1251](https://github.com/request/request/pull/1251) Clarify that defaults() does not modify global defaults (@nylen)
+- [#1250](https://github.com/request/request/pull/1250) Improve documentation for pool and maxSockets options (@nylen)
+- [#1237](https://github.com/request/request/pull/1237) Documenting error handling when using streams (@vmattos)
+- [#1244](https://github.com/request/request/pull/1244) Finalize changelog command (@nylen)
+- [#1241](https://github.com/request/request/pull/1241) Fix typo (@alexanderGugel)
+- [#1223](https://github.com/request/request/pull/1223) Show latest version number instead of "upcoming" in changelog (@nylen)
+- [#1236](https://github.com/request/request/pull/1236) Document how to use custom CA in README (#1229) (@hypesystem)
+- [#1228](https://github.com/request/request/pull/1228) Support for oauth with RSA-SHA1 signing (@nylen)
+- [#1216](https://github.com/request/request/pull/1216) Made json and multipart options coexist (@nylen, @simov)
+- [#1225](https://github.com/request/request/pull/1225) Allow header white/exclusive lists in any case. (@RReverser)
+
+### v2.47.0 (2014/10/26)
+- [#1222](https://github.com/request/request/pull/1222) Move from mikeal/request to request/request (@nylen)
+- [#1220](https://github.com/request/request/pull/1220) update qs dependency to 2.3.1 (@FredKSchott)
+- [#1212](https://github.com/request/request/pull/1212) Improve tests/test-timeout.js (@nylen)
+- [#1219](https://github.com/request/request/pull/1219) remove old globalAgent workaround for node 0.4 (@request)
+- [#1214](https://github.com/request/request/pull/1214) Remove cruft left over from optional dependencies (@nylen)
+- [#1215](https://github.com/request/request/pull/1215) Add proxyHeaderExclusiveList option for proxy-only headers. (@RReverser)
+- [#1211](https://github.com/request/request/pull/1211) Allow 'Host' header instead of 'host' and remember case across redirects (@nylen)
+- [#1208](https://github.com/request/request/pull/1208) Improve release script (@nylen)
+- [#1213](https://github.com/request/request/pull/1213) Support for custom cookie store (@nylen, @mitsuru)
+- [#1197](https://github.com/request/request/pull/1197) Clean up some code around setting the agent (@FredKSchott)
+- [#1209](https://github.com/request/request/pull/1209) Improve multipart form append test (@simov)
+- [#1207](https://github.com/request/request/pull/1207) Update changelog (@nylen)
+- [#1185](https://github.com/request/request/pull/1185) Stream multipart/related bodies (@simov)
+
+### v2.46.0 (2014/10/23)
+- [#1198](https://github.com/request/request/pull/1198) doc for TLS/SSL protocol options (@shawnzhu)
+- [#1200](https://github.com/request/request/pull/1200) Add a Gitter chat badge to README.md (@gitter-badger)
+- [#1196](https://github.com/request/request/pull/1196) Upgrade taper test reporter to v0.3.0 (@nylen)
+- [#1199](https://github.com/request/request/pull/1199) Fix lint error: undeclared var i (@nylen)
+- [#1191](https://github.com/request/request/pull/1191) Move self.proxy decision logic out of init and into a helper (@FredKSchott)
+- [#1190](https://github.com/request/request/pull/1190) Move _buildRequest() logic back into init (@FredKSchott)
+- [#1186](https://github.com/request/request/pull/1186) Support Smarter Unix URL Scheme (@FredKSchott)
+- [#1178](https://github.com/request/request/pull/1178) update form documentation for new usage (@FredKSchott)
+- [#1180](https://github.com/request/request/pull/1180) Enable no-mixed-requires linting rule (@nylen)
+- [#1184](https://github.com/request/request/pull/1184) Don't forward authorization header across redirects to different hosts (@nylen)
+- [#1183](https://github.com/request/request/pull/1183) Correct README about pre and postamble CRLF using multipart and not mult... (@netpoetica)
+- [#1179](https://github.com/request/request/pull/1179) Lint tests directory (@nylen)
+- [#1169](https://github.com/request/request/pull/1169) add metadata for form-data file field (@dotcypress)
+- [#1173](https://github.com/request/request/pull/1173) remove optional dependencies (@seanstrom)
+- [#1165](https://github.com/request/request/pull/1165) Cleanup event listeners and remove function creation from init (@FredKSchott)
+- [#1174](https://github.com/request/request/pull/1174) update the request.cookie docs to have a valid cookie example (@seanstrom)
+- [#1168](https://github.com/request/request/pull/1168) create a detach helper and use detach helper in replace of nextTick (@seanstrom)
+- [#1171](https://github.com/request/request/pull/1171) in post can send form data and use callback (@MiroRadenovic)
+- [#1159](https://github.com/request/request/pull/1159) accept charset for x-www-form-urlencoded content-type (@seanstrom)
+- [#1157](https://github.com/request/request/pull/1157) Update README.md: body with json=true (@Rob--W)
+- [#1164](https://github.com/request/request/pull/1164) Disable tests/test-timeout.js on Travis (@nylen)
+- [#1153](https://github.com/request/request/pull/1153) Document how to run a single test (@nylen)
+- [#1144](https://github.com/request/request/pull/1144) adds documentation for the "response" event within the streaming section (@tbuchok)
+- [#1162](https://github.com/request/request/pull/1162) Update eslintrc file to no longer allow past errors (@FredKSchott)
+- [#1155](https://github.com/request/request/pull/1155) Support/use self everywhere (@seanstrom)
+- [#1161](https://github.com/request/request/pull/1161) fix no-use-before-define lint warnings (@emkay)
+- [#1156](https://github.com/request/request/pull/1156) adding curly brackets to get rid of lint errors (@emkay)
+- [#1151](https://github.com/request/request/pull/1151) Fix localAddress test on OS X (@nylen)
+- [#1145](https://github.com/request/request/pull/1145) documentation: fix outdated reference to setCookieSync old name in README (@FredKSchott)
+- [#1131](https://github.com/request/request/pull/1131) Update pool documentation (@FredKSchott)
+- [#1143](https://github.com/request/request/pull/1143) Rewrite all tests to use tape (@nylen)
+- [#1137](https://github.com/request/request/pull/1137) Add ability to specifiy querystring lib in options. (@jgrund)
+- [#1138](https://github.com/request/request/pull/1138) allow hostname and port in place of host on uri (@cappslock)
+- [#1134](https://github.com/request/request/pull/1134) Fix multiple redirects and `self.followRedirect` (@blakeembrey)
+- [#1130](https://github.com/request/request/pull/1130) documentation fix: add note about npm test for contributing (@FredKSchott)
+- [#1120](https://github.com/request/request/pull/1120) Support/refactor request setup tunnel (@seanstrom)
+- [#1129](https://github.com/request/request/pull/1129) linting fix: convert double quote strings to use single quotes (@FredKSchott)
+- [#1124](https://github.com/request/request/pull/1124) linting fix: remove unneccesary semi-colons (@FredKSchott)
+
+### v2.45.0 (2014/10/06)
+- [#1128](https://github.com/request/request/pull/1128) Add test for setCookie regression (@nylen)
+- [#1127](https://github.com/request/request/pull/1127) added tests around using objects as values in a query string (@bcoe)
+- [#1103](https://github.com/request/request/pull/1103) Support/refactor request constructor (@nylen, @seanstrom)
+- [#1119](https://github.com/request/request/pull/1119) add basic linting to request library (@FredKSchott)
+- [#1121](https://github.com/request/request/pull/1121) Revert "Explicitly use sync versions of cookie functions" (@nylen)
+- [#1118](https://github.com/request/request/pull/1118) linting fix: Restructure bad empty if statement (@FredKSchott)
+- [#1117](https://github.com/request/request/pull/1117) Fix a bad check for valid URIs (@FredKSchott)
+- [#1113](https://github.com/request/request/pull/1113) linting fix: space out operators (@FredKSchott)
+- [#1116](https://github.com/request/request/pull/1116) Fix typo in `noProxyHost` definition (@FredKSchott)
+- [#1114](https://github.com/request/request/pull/1114) linting fix: Added a `new` operator that was missing when creating and throwing a new error (@FredKSchott)
+- [#1096](https://github.com/request/request/pull/1096) No_proxy support (@samcday)
+- [#1107](https://github.com/request/request/pull/1107) linting-fix: remove unused variables (@FredKSchott)
+- [#1112](https://github.com/request/request/pull/1112) linting fix: Make return values consistent and more straitforward (@FredKSchott)
+- [#1111](https://github.com/request/request/pull/1111) linting fix: authPieces was getting redeclared (@FredKSchott)
+- [#1105](https://github.com/request/request/pull/1105) Use strict mode in request (@FredKSchott)
+- [#1110](https://github.com/request/request/pull/1110) linting fix: replace lazy '==' with more strict '===' (@FredKSchott)
+- [#1109](https://github.com/request/request/pull/1109) linting fix: remove function call from if-else conditional statement (@FredKSchott)
+- [#1102](https://github.com/request/request/pull/1102) Fix to allow setting a `requester` on recursive calls to `request.defaults` (@tikotzky)
+- [#1095](https://github.com/request/request/pull/1095) Tweaking engines in package.json (@pdehaan)
+- [#1082](https://github.com/request/request/pull/1082) Forward the socket event from the httpModule request (@seanstrom)
+- [#972](https://github.com/request/request/pull/972) Clarify gzip handling in the README (@kevinoid)
+- [#1089](https://github.com/request/request/pull/1089) Mention that encoding defaults to utf8, not Buffer (@stuartpb)
+- [#1088](https://github.com/request/request/pull/1088) Fix cookie example in README.md and make it more clear (@pipi32167)
+- [#1027](https://github.com/request/request/pull/1027) Add support for multipart form data in request options. (@crocket)
+- [#1076](https://github.com/request/request/pull/1076) use Request.abort() to abort the request when the request has timed-out (@seanstrom)
+- [#1068](https://github.com/request/request/pull/1068) add optional postamble required by .NET multipart requests (@netpoetica)
+
+### v2.43.0 (2014/09/18)
+- [#1057](https://github.com/request/request/pull/1057) Defaults should not overwrite defined options (@davidwood)
+- [#1046](https://github.com/request/request/pull/1046) Propagate datastream errors, useful in case gzip fails. (@ZJONSSON, @Janpot)
+- [#1063](https://github.com/request/request/pull/1063) copy the input headers object #1060 (@finnp)
+- [#1031](https://github.com/request/request/pull/1031) Explicitly use sync versions of cookie functions (@ZJONSSON)
+- [#1056](https://github.com/request/request/pull/1056) Fix redirects when passing url.parse(x) as URL to convenience method (@nylen)
+
+### v2.42.0 (2014/09/04)
+- [#1053](https://github.com/request/request/pull/1053) Fix #1051 Parse auth properly when using non-tunneling proxy (@isaacs)
+
+### v2.41.0 (2014/09/04)
+- [#1050](https://github.com/request/request/pull/1050) Pass whitelisted headers to tunneling proxy. Organize all tunneling logic. (@isaacs, @Feldhacker)
+- [#1035](https://github.com/request/request/pull/1035) souped up nodei.co badge (@rvagg)
+- [#1048](https://github.com/request/request/pull/1048) Aws is now possible over a proxy (@steven-aerts)
+- [#1039](https://github.com/request/request/pull/1039) extract out helper functions to a helper file (@seanstrom)
+- [#1021](https://github.com/request/request/pull/1021) Support/refactor indexjs (@seanstrom)
+- [#1033](https://github.com/request/request/pull/1033) Improve and document debug options (@nylen)
+- [#1034](https://github.com/request/request/pull/1034) Fix readme headings (@nylen)
+- [#1030](https://github.com/request/request/pull/1030) Allow recursive request.defaults (@tikotzky)
+- [#1029](https://github.com/request/request/pull/1029) Fix a couple of typos (@nylen)
+- [#675](https://github.com/request/request/pull/675) Checking for SSL fault on connection before reading SSL properties (@VRMink)
+- [#989](https://github.com/request/request/pull/989) Added allowRedirect function. Should return true if redirect is allowed or false otherwise (@doronin)
+- [#1025](https://github.com/request/request/pull/1025) [fixes #1023] Set self._ended to true once response has ended (@mridgway)
+- [#1020](https://github.com/request/request/pull/1020) Add back removed debug metadata (@FredKSchott)
+- [#1008](https://github.com/request/request/pull/1008) Moving to module instead of cutomer buffer concatenation. (@mikeal)
+- [#770](https://github.com/request/request/pull/770) Added dependency badge for README file; (@timgluz, @mafintosh, @lalitkapoor, @stash, @bobyrizov)
+- [#1016](https://github.com/request/request/pull/1016) toJSON no longer results in an infinite loop, returns simple objects (@FredKSchott)
+- [#1018](https://github.com/request/request/pull/1018) Remove pre-0.4.4 HTTPS fix (@mmalecki)
+- [#1006](https://github.com/request/request/pull/1006) Migrate to caseless, fixes #1001 (@mikeal)
+- [#995](https://github.com/request/request/pull/995) Fix parsing array of objects (@sjonnet19)
+- [#999](https://github.com/request/request/pull/999) Fix fallback for browserify for optional modules. (@eiriksm)
+- [#996](https://github.com/request/request/pull/996) Wrong oauth signature when multiple same param keys exist [updated] (@bengl)
+
+### v2.40.0 (2014/08/06)
+- [#992](https://github.com/request/request/pull/992) Fix security vulnerability. Update qs (@poeticninja)
+- [#988](https://github.com/request/request/pull/988) “--” -> “—” (@upisfree)
+- [#987](https://github.com/request/request/pull/987) Show optional modules as being loaded by the module that reqeusted them (@iarna)
+
+### v2.39.0 (2014/07/24)
+- [#976](https://github.com/request/request/pull/976) Update README.md (@pvoznenko)
+
+### v2.38.0 (2014/07/22)
+- [#952](https://github.com/request/request/pull/952) Adding support to client certificate with proxy use case (@ofirshaked)
+- [#884](https://github.com/request/request/pull/884) Documented tough-cookie installation. (@wbyoung)
+- [#935](https://github.com/request/request/pull/935) Correct repository url (@fritx)
+- [#963](https://github.com/request/request/pull/963) Update changelog (@nylen)
+- [#960](https://github.com/request/request/pull/960) Support gzip with encoding on node pre-v0.9.4 (@kevinoid)
+- [#953](https://github.com/request/request/pull/953) Add async Content-Length computation when using form-data (@LoicMahieu)
+- [#844](https://github.com/request/request/pull/844) Add support for HTTP[S]_PROXY environment variables. Fixes #595. (@jvmccarthy)
+- [#946](https://github.com/request/request/pull/946) defaults: merge headers (@aj0strow)
+
+### v2.37.0 (2014/07/07)
+- [#957](https://github.com/request/request/pull/957) Silence EventEmitter memory leak warning #311 (@watson)
+- [#955](https://github.com/request/request/pull/955) check for content-length header before setting it in nextTick (@camilleanne)
+- [#951](https://github.com/request/request/pull/951) Add support for gzip content decoding (@kevinoid)
+- [#949](https://github.com/request/request/pull/949) Manually enter querystring in form option (@charlespwd)
+- [#944](https://github.com/request/request/pull/944) Make request work with browserify (@eiriksm)
+- [#943](https://github.com/request/request/pull/943) New mime module (@eiriksm)
+- [#927](https://github.com/request/request/pull/927) Bump version of hawk dep. (@samccone)
+- [#907](https://github.com/request/request/pull/907) append secureOptions to poolKey (@medovob)
+
+### v2.35.0 (2014/05/17)
+- [#901](https://github.com/request/request/pull/901) Fixes #555 (@pigulla)
+- [#897](https://github.com/request/request/pull/897) merge with default options (@vohof)
+- [#891](https://github.com/request/request/pull/891) fixes 857 - options object is mutated by calling request (@lalitkapoor)
+- [#869](https://github.com/request/request/pull/869) Pipefilter test (@tgohn)
+- [#866](https://github.com/request/request/pull/866) Fix typo (@dandv)
+- [#861](https://github.com/request/request/pull/861) Add support for RFC 6750 Bearer Tokens (@phedny)
+- [#809](https://github.com/request/request/pull/809) upgrade tunnel-proxy to 0.4.0 (@ksato9700)
+- [#850](https://github.com/request/request/pull/850) Fix word consistency in readme (@0xNobody)
+- [#810](https://github.com/request/request/pull/810) add some exposition to mpu example in README.md (@mikermcneil)
+- [#840](https://github.com/request/request/pull/840) improve error reporting for invalid protocols (@FND)
+- [#821](https://github.com/request/request/pull/821) added secureOptions back (@nw)
+- [#815](https://github.com/request/request/pull/815) Create changelog based on pull requests (@lalitkapoor)
+
+### v2.34.0 (2014/02/18)
+- [#516](https://github.com/request/request/pull/516) UNIX Socket URL Support (@lyuzashi)
+- [#801](https://github.com/request/request/pull/801) 794 ignore cookie parsing and domain errors (@lalitkapoor)
+- [#802](https://github.com/request/request/pull/802) Added the Apache license to the package.json. (@keskival)
+- [#793](https://github.com/request/request/pull/793) Adds content-length calculation when submitting forms using form-data li... (@Juul)
+- [#785](https://github.com/request/request/pull/785) Provide ability to override content-type when `json` option used (@vvo)
+- [#781](https://github.com/request/request/pull/781) simpler isReadStream function (@joaojeronimo)
+
+### v2.32.0 (2014/01/16)
+- [#767](https://github.com/request/request/pull/767) Use tough-cookie CookieJar sync API (@stash)
+- [#764](https://github.com/request/request/pull/764) Case-insensitive authentication scheme (@bobyrizov)
+- [#763](https://github.com/request/request/pull/763) Upgrade tough-cookie to 0.10.0 (@stash)
+- [#744](https://github.com/request/request/pull/744) Use Cookie.parse (@lalitkapoor)
+- [#757](https://github.com/request/request/pull/757) require aws-sign2 (@mafintosh)
+
+### v2.31.0 (2014/01/08)
+- [#645](https://github.com/request/request/pull/645) update twitter api url to v1.1 (@mick)
+- [#746](https://github.com/request/request/pull/746) README: Markdown code highlight (@weakish)
+- [#745](https://github.com/request/request/pull/745) updating setCookie example to make it clear that the callback is required (@emkay)
+- [#742](https://github.com/request/request/pull/742) Add note about JSON output body type (@iansltx)
+- [#741](https://github.com/request/request/pull/741) README example is using old cookie jar api (@emkay)
+- [#736](https://github.com/request/request/pull/736) Fix callback arguments documentation (@mmalecki)
+- [#732](https://github.com/request/request/pull/732) JSHINT: Creating global 'for' variable. Should be 'for (var ...'. (@Fritz-Lium)
+- [#730](https://github.com/request/request/pull/730) better HTTP DIGEST support (@dai-shi)
+- [#728](https://github.com/request/request/pull/728) Fix TypeError when calling request.cookie (@scarletmeow)
+- [#727](https://github.com/request/request/pull/727) fix requester bug (@jchris)
+- [#724](https://github.com/request/request/pull/724) README.md: add custom HTTP Headers example. (@tcort)
+- [#719](https://github.com/request/request/pull/719) Made a comment gender neutral. (@unsetbit)
+- [#715](https://github.com/request/request/pull/715) Request.multipart no longer crashes when header 'Content-type' present (@pastaclub)
+- [#710](https://github.com/request/request/pull/710) Fixing listing in callback part of docs. (@lukasz-zak)
+- [#696](https://github.com/request/request/pull/696) Edited README.md for formatting and clarity of phrasing (@Zearin)
+- [#694](https://github.com/request/request/pull/694) Typo in README (@VRMink)
+- [#690](https://github.com/request/request/pull/690) Handle blank password in basic auth. (@diversario)
+- [#682](https://github.com/request/request/pull/682) Optional dependencies (@Turbo87)
+- [#683](https://github.com/request/request/pull/683) Travis CI support (@Turbo87)
+- [#674](https://github.com/request/request/pull/674) change cookie module,to tough-cookie.please check it . (@sxyizhiren)
+- [#666](https://github.com/request/request/pull/666) make `ciphers` and `secureProtocol` to work in https request (@richarddong)
+- [#656](https://github.com/request/request/pull/656) Test case for #304. (@diversario)
+- [#662](https://github.com/request/request/pull/662) option.tunnel to explicitly disable tunneling (@seanmonstar)
+- [#659](https://github.com/request/request/pull/659) fix failure when running with NODE_DEBUG=request, and a test for that (@jrgm)
+- [#630](https://github.com/request/request/pull/630) Send random cnonce for HTTP Digest requests (@wprl)
+- [#619](https://github.com/request/request/pull/619) decouple things a bit (@joaojeronimo)
+- [#613](https://github.com/request/request/pull/613) Fixes #583, moved initialization of self.uri.pathname (@lexander)
+- [#605](https://github.com/request/request/pull/605) Only include ":" + pass in Basic Auth if it's defined (fixes #602) (@bendrucker)
+- [#596](https://github.com/request/request/pull/596) Global agent is being used when pool is specified (@Cauldrath)
+- [#594](https://github.com/request/request/pull/594) Emit complete event when there is no callback (@RomainLK)
+- [#601](https://github.com/request/request/pull/601) Fixed a small typo (@michalstanko)
+- [#589](https://github.com/request/request/pull/589) Prevent setting headers after they are sent (@geek)
+- [#587](https://github.com/request/request/pull/587) Global cookie jar disabled by default (@threepointone)
+- [#544](https://github.com/request/request/pull/544) Update http-signature version. (@davidlehn)
+- [#581](https://github.com/request/request/pull/581) Fix spelling of "ignoring." (@bigeasy)
+- [#568](https://github.com/request/request/pull/568) use agentOptions to create agent when specified in request (@SamPlacette)
+- [#564](https://github.com/request/request/pull/564) Fix redirections (@criloz)
+- [#541](https://github.com/request/request/pull/541) The exported request function doesn't have an auth method (@tschaub)
+- [#542](https://github.com/request/request/pull/542) Expose Request class (@regality)
+- [#536](https://github.com/request/request/pull/536) Allow explicitly empty user field for basic authentication. (@mikeando)
+- [#532](https://github.com/request/request/pull/532) fix typo (@fredericosilva)
+- [#497](https://github.com/request/request/pull/497) Added redirect event (@Cauldrath)
+- [#503](https://github.com/request/request/pull/503) Fix basic auth for passwords that contain colons (@tonistiigi)
+- [#521](https://github.com/request/request/pull/521) Improving test-localAddress.js (@noway)
+- [#529](https://github.com/request/request/pull/529) dependencies versions bump (@jodaka)
+- [#523](https://github.com/request/request/pull/523) Updating dependencies (@noway)
+- [#520](https://github.com/request/request/pull/520) Fixing test-tunnel.js (@noway)
+- [#519](https://github.com/request/request/pull/519) Update internal path state on post-creation QS changes (@jblebrun)
+- [#510](https://github.com/request/request/pull/510) Add HTTP Signature support. (@davidlehn)
+- [#502](https://github.com/request/request/pull/502) Fix POST (and probably other) requests that are retried after 401 Unauthorized (@nylen)
+- [#508](https://github.com/request/request/pull/508) Honor the .strictSSL option when using proxies (tunnel-agent) (@jhs)
+- [#512](https://github.com/request/request/pull/512) Make password optional to support the format: http://username@hostname/ (@pajato1)
+- [#513](https://github.com/request/request/pull/513) add 'localAddress' support (@yyfrankyy)
+- [#498](https://github.com/request/request/pull/498) Moving response emit above setHeaders on destination streams (@kenperkins)
+- [#490](https://github.com/request/request/pull/490) Empty response body (3-rd argument) must be passed to callback as an empty string (@Olegas)
+- [#479](https://github.com/request/request/pull/479) Changing so if Accept header is explicitly set, sending json does not ov... (@RoryH)
+- [#475](https://github.com/request/request/pull/475) Use `unescape` from `querystring` (@shimaore)
+- [#473](https://github.com/request/request/pull/473) V0.10 compat (@isaacs)
+- [#471](https://github.com/request/request/pull/471) Using querystring library from visionmedia (@kbackowski)
+- [#461](https://github.com/request/request/pull/461) Strip the UTF8 BOM from a UTF encoded response (@kppullin)
+- [#460](https://github.com/request/request/pull/460) hawk 0.10.0 (@hueniverse)
+- [#462](https://github.com/request/request/pull/462) if query params are empty, then request path shouldn't end with a '?' (merges cleanly now) (@jaipandya)
+- [#456](https://github.com/request/request/pull/456) hawk 0.9.0 (@hueniverse)
+- [#429](https://github.com/request/request/pull/429) Copy options before adding callback. (@nrn, @nfriedly, @youurayy, @jplock, @kapetan, @landeiro, @othiym23, @mmalecki)
+- [#454](https://github.com/request/request/pull/454) Destroy the response if present when destroying the request (clean merge) (@mafintosh)
+- [#310](https://github.com/request/request/pull/310) Twitter Oauth Stuff Out of Date; Now Updated (@joemccann, @isaacs, @mscdex)
+- [#413](https://github.com/request/request/pull/413) rename googledoodle.png to .jpg (@nfriedly, @youurayy, @jplock, @kapetan, @landeiro, @othiym23, @mmalecki)
+- [#448](https://github.com/request/request/pull/448) Convenience method for PATCH (@mloar)
+- [#444](https://github.com/request/request/pull/444) protect against double callbacks on error path (@spollack)
+- [#433](https://github.com/request/request/pull/433) Added support for HTTPS cert & key (@mmalecki)
+- [#430](https://github.com/request/request/pull/430) Respect specified {Host,host} headers, not just {host} (@andrewschaaf)
+- [#415](https://github.com/request/request/pull/415) Fixed a typo. (@jerem)
+- [#338](https://github.com/request/request/pull/338) Add more auth options, including digest support (@nylen)
+- [#403](https://github.com/request/request/pull/403) Optimize environment lookup to happen once only (@mmalecki)
+- [#398](https://github.com/request/request/pull/398) Add more reporting to tests (@mmalecki)
+- [#388](https://github.com/request/request/pull/388) Ensure "safe" toJSON doesn't break EventEmitters (@othiym23)
+- [#381](https://github.com/request/request/pull/381) Resolving "Invalid signature. Expected signature base string: " (@landeiro)
+- [#380](https://github.com/request/request/pull/380) Fixes missing host header on retried request when using forever agent (@mac-)
+- [#376](https://github.com/request/request/pull/376) Headers lost on redirect (@kapetan)
+- [#375](https://github.com/request/request/pull/375) Fix for missing oauth_timestamp parameter (@jplock)
+- [#374](https://github.com/request/request/pull/374) Correct Host header for proxy tunnel CONNECT (@youurayy)
+- [#370](https://github.com/request/request/pull/370) Twitter reverse auth uses x_auth_mode not x_auth_type (@drudge)
+- [#369](https://github.com/request/request/pull/369) Don't remove x_auth_mode for Twitter reverse auth (@drudge)
+- [#344](https://github.com/request/request/pull/344) Make AWS auth signing find headers correctly (@nlf)
+- [#363](https://github.com/request/request/pull/363) rfc3986 on base_uri, now passes tests (@jeffmarshall)
+- [#362](https://github.com/request/request/pull/362) Running `rfc3986` on `base_uri` in `oauth.hmacsign` instead of just `encodeURIComponent` (@jeffmarshall)
+- [#361](https://github.com/request/request/pull/361) Don't create a Content-Length header if we already have it set (@danjenkins)
+- [#360](https://github.com/request/request/pull/360) Delete self._form along with everything else on redirect (@jgautier)
+- [#355](https://github.com/request/request/pull/355) stop sending erroneous headers on redirected requests (@azylman)
+- [#332](https://github.com/request/request/pull/332) Fix #296 - Only set Content-Type if body exists (@Marsup)
+- [#343](https://github.com/request/request/pull/343) Allow AWS to work in more situations, added a note in the README on its usage (@nlf)
+- [#320](https://github.com/request/request/pull/320) request.defaults() doesn't need to wrap jar() (@StuartHarris)
+- [#322](https://github.com/request/request/pull/322) Fix + test for piped into request bumped into redirect. #321 (@alexindigo)
+- [#326](https://github.com/request/request/pull/326) Do not try to remove listener from an undefined connection (@CartoDB)
+- [#318](https://github.com/request/request/pull/318) Pass servername to tunneling secure socket creation (@isaacs)
+- [#317](https://github.com/request/request/pull/317) Workaround for #313 (@isaacs)
+- [#293](https://github.com/request/request/pull/293) Allow parser errors to bubble up to request (@mscdex)
+- [#290](https://github.com/request/request/pull/290) A test for #289 (@isaacs)
+- [#280](https://github.com/request/request/pull/280) Like in node.js print options if NODE_DEBUG contains the word request (@Filirom1)
+- [#207](https://github.com/request/request/pull/207) Fix #206 Change HTTP/HTTPS agent when redirecting between protocols (@isaacs)
+- [#214](https://github.com/request/request/pull/214) documenting additional behavior of json option (@jphaas, @vpulim)
+- [#272](https://github.com/request/request/pull/272) Boundary begins with CRLF? (@elspoono, @timshadel, @naholyr, @nanodocumet, @TehShrike)
+- [#284](https://github.com/request/request/pull/284) Remove stray `console.log()` call in multipart generator. (@bcherry)
+- [#241](https://github.com/request/request/pull/241) Composability updates suggested by issue #239 (@polotek)
+- [#282](https://github.com/request/request/pull/282) OAuth Authorization header contains non-"oauth_" parameters (@jplock)
+- [#279](https://github.com/request/request/pull/279) fix tests with boundary by injecting boundry from header (@benatkin)
+- [#273](https://github.com/request/request/pull/273) Pipe back pressure issue (@mafintosh)
+- [#268](https://github.com/request/request/pull/268) I'm not OCD seriously (@TehShrike)
+- [#263](https://github.com/request/request/pull/263) Bug in OAuth key generation for sha1 (@nanodocumet)
+- [#265](https://github.com/request/request/pull/265) uncaughtException when redirected to invalid URI (@naholyr)
+- [#262](https://github.com/request/request/pull/262) JSON test should check for equality (@timshadel)
+- [#261](https://github.com/request/request/pull/261) Setting 'pool' to 'false' does NOT disable Agent pooling (@timshadel)
+- [#249](https://github.com/request/request/pull/249) Fix for the fix of your (closed) issue #89 where self.headers[content-length] is set to 0 for all methods (@sethbridges, @polotek, @zephrax, @jeromegn)
+- [#255](https://github.com/request/request/pull/255) multipart allow body === '' ( the empty string ) (@Filirom1)
+- [#260](https://github.com/request/request/pull/260) fixed just another leak of 'i' (@sreuter)
+- [#246](https://github.com/request/request/pull/246) Fixing the set-cookie header (@jeromegn)
+- [#243](https://github.com/request/request/pull/243) Dynamic boundary (@zephrax)
+- [#240](https://github.com/request/request/pull/240) don't error when null is passed for options (@polotek)
+- [#211](https://github.com/request/request/pull/211) Replace all occurrences of special chars in RFC3986 (@chriso, @vpulim)
+- [#224](https://github.com/request/request/pull/224) Multipart content-type change (@janjongboom)
+- [#217](https://github.com/request/request/pull/217) need to use Authorization (titlecase) header with Tumblr OAuth (@visnup)
+- [#203](https://github.com/request/request/pull/203) Fix cookie and redirect bugs and add auth support for HTTPS tunnel (@vpulim)
+- [#199](https://github.com/request/request/pull/199) Tunnel (@isaacs)
+- [#198](https://github.com/request/request/pull/198) Bugfix on forever usage of util.inherits (@isaacs)
+- [#197](https://github.com/request/request/pull/197) Make ForeverAgent work with HTTPS (@isaacs)
+- [#193](https://github.com/request/request/pull/193) Fixes GH-119 (@goatslacker)
+- [#188](https://github.com/request/request/pull/188) Add abort support to the returned request (@itay)
+- [#176](https://github.com/request/request/pull/176) Querystring option (@csainty)
+- [#182](https://github.com/request/request/pull/182) Fix request.defaults to support (uri, options, callback) api (@twilson63)
+- [#180](https://github.com/request/request/pull/180) Modified the post, put, head and del shortcuts to support uri optional param (@twilson63)
+- [#179](https://github.com/request/request/pull/179) fix to add opts in .pipe(stream, opts) (@substack)
+- [#177](https://github.com/request/request/pull/177) Issue #173 Support uri as first and optional config as second argument (@twilson63)
+- [#170](https://github.com/request/request/pull/170) can't create a cookie in a wrapped request (defaults) (@fabianonunes)
+- [#168](https://github.com/request/request/pull/168) Picking off an EasyFix by adding some missing mimetypes. (@serby)
+- [#161](https://github.com/request/request/pull/161) Fix cookie jar/headers.cookie collision (#125) (@papandreou)
+- [#162](https://github.com/request/request/pull/162) Fix issue #159 (@dpetukhov)
+- [#90](https://github.com/request/request/pull/90) add option followAllRedirects to follow post/put redirects (@jroes)
+- [#148](https://github.com/request/request/pull/148) Retry Agent (@thejh)
+- [#146](https://github.com/request/request/pull/146) Multipart should respect content-type if previously set (@apeace)
+- [#144](https://github.com/request/request/pull/144) added "form" option to readme (@petejkim)
+- [#133](https://github.com/request/request/pull/133) Fixed cookies parsing (@afanasy)
+- [#135](https://github.com/request/request/pull/135) host vs hostname (@iangreenleaf)
+- [#132](https://github.com/request/request/pull/132) return the body as a Buffer when encoding is set to null (@jahewson)
+- [#112](https://github.com/request/request/pull/112) Support using a custom http-like module (@jhs)
+- [#104](https://github.com/request/request/pull/104) Cookie handling contains bugs (@janjongboom)
+- [#121](https://github.com/request/request/pull/121) Another patch for cookie handling regression (@jhurliman)
+- [#117](https://github.com/request/request/pull/117) Remove the global `i` (@3rd-Eden)
+- [#110](https://github.com/request/request/pull/110) Update to Iris Couch URL (@jhs)
+- [#86](https://github.com/request/request/pull/86) Can't post binary to multipart requests (@kkaefer)
+- [#105](https://github.com/request/request/pull/105) added test for proxy option. (@dominictarr)
+- [#102](https://github.com/request/request/pull/102) Implemented cookies - closes issue 82: https://github.com/mikeal/request/issues/82 (@alessioalex)
+- [#97](https://github.com/request/request/pull/97) Typo in previous pull causes TypeError in non-0.5.11 versions (@isaacs)
+- [#96](https://github.com/request/request/pull/96) Authless parsed url host support (@isaacs)
+- [#81](https://github.com/request/request/pull/81) Enhance redirect handling (@danmactough)
+- [#78](https://github.com/request/request/pull/78) Don't try to do strictSSL for non-ssl connections (@isaacs)
+- [#76](https://github.com/request/request/pull/76) Bug when a request fails and a timeout is set (@Marsup)
+- [#70](https://github.com/request/request/pull/70) add test script to package.json (@isaacs, @aheckmann)
+- [#73](https://github.com/request/request/pull/73) Fix #71 Respect the strictSSL flag (@isaacs)
+- [#69](https://github.com/request/request/pull/69) Flatten chunked requests properly (@isaacs)
+- [#67](https://github.com/request/request/pull/67) fixed global variable leaks (@aheckmann)
+- [#66](https://github.com/request/request/pull/66) Do not overwrite established content-type headers for read stream deliver (@voodootikigod)
+- [#53](https://github.com/request/request/pull/53) Parse json: Issue #51 (@benatkin)
+- [#45](https://github.com/request/request/pull/45) Added timeout option (@mbrevoort)
+- [#35](https://github.com/request/request/pull/35) The "end" event isn't emitted for some responses (@voxpelli)
+- [#31](https://github.com/request/request/pull/31) Error on piping a request to a destination (@tobowers)
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..8aa6999ac
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,81 @@
+
+# Contributing to Request
+
+:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
+
+The following is a set of guidelines for contributing to Request and its packages, which are hosted in the [Request Organization](https://github.com/request) on GitHub.
+These are just guidelines, not rules, use your best judgment and feel free to propose changes to this document in a pull request.
+
+
+## Submitting an Issue
+
+1. Provide a small self **sufficient** code example to **reproduce** the issue.
+2. Run your test code using [request-debug](https://github.com/request/request-debug) and copy/paste the results inside the issue.
+3. You should **always** use fenced code blocks when submitting code examples or any other formatted output:
+
+ ```js
+ put your javascript code here
+ ```
+
+ ```
+ put any other formatted output here,
+ like for example the one returned from using request-debug
+ ```
+
+
+If the problem cannot be reliably reproduced, the issue will be marked as `Not enough info (see CONTRIBUTING.md)`.
+
+If the problem is not related to request the issue will be marked as `Help (please use Stackoverflow)`.
+
+
+## Submitting a Pull Request
+
+1. In almost all of the cases your PR **needs tests**. Make sure you have any.
+2. Run `npm test` locally. Fix any errors before pushing to GitHub.
+3. After submitting the PR a build will be triggered on TravisCI. Wait for it to ends and make sure all jobs are passing.
+
+
+-----------------------------------------
+
+
+## Becoming a Contributor
+
+Individuals making significant and valuable contributions are given
+commit-access to the project to contribute as they see fit. This project is
+more like an open wiki than a standard guarded open source project.
+
+
+## Rules
+
+There are a few basic ground-rules for contributors:
+
+1. **No `--force` pushes** or modifying the Git history in any way.
+1. **Non-master branches** ought to be used for ongoing work.
+1. **Any** change should be added through Pull Request.
+1. **External API changes and significant modifications** ought to be subject
+ to an **internal pull-request** to solicit feedback from other contributors.
+1. Internal pull-requests to solicit feedback are *encouraged* for any other
+ non-trivial contribution but left to the discretion of the contributor.
+1. For significant changes wait a full 24 hours before merging so that active
+ contributors who are distributed throughout the world have a chance to weigh
+ in.
+1. Contributors should attempt to adhere to the prevailing code-style.
+1. Run `npm test` locally before submitting your PR, to catch any easy to miss
+ style & testing issues. To diagnose test failures, there are two ways to
+ run a single test file:
+ - `node_modules/.bin/taper tests/test-file.js` - run using the default
+ [`taper`](https://github.com/nylen/taper) test reporter.
+ - `node tests/test-file.js` - view the raw
+ [tap](https://testanything.org/) output.
+
+
+## Releases
+
+Declaring formal releases remains the prerogative of the project maintainer.
+
+
+## Changes to this arrangement
+
+This is an experiment and feedback is welcome! This document may also be
+subject to pull-requests or changes by contributors where you believe you have
+something valuable to add or change.
diff --git a/README.md b/README.md
index 639d1a45a..e89abc6a5 100644
--- a/README.md
+++ b/README.md
@@ -1,88 +1,132 @@
-# Request -- Simplified HTTP request method
-## Install
+# Request - Simplified HTTP client
-
- npm install request
-
+[](https://nodei.co/npm/request/)
-Or from source:
+[](https://travis-ci.org/request/request)
+[](https://codecov.io/github/request/request?branch=master)
+[](https://coveralls.io/r/request/request)
+[](https://david-dm.org/request/request)
+[](https://snyk.io/test/npm/request)
+[](https://gitter.im/request/request?utm_source=badge)
-
- git clone git://github.com/mikeal/request.git
- cd request
- npm link
-
## Super simple to use
-Request is designed to be the simplest way possible to make http calls. It support HTTPS and follows redirects by default.
+Request is designed to be the simplest way possible to make http calls. It supports HTTPS and follows redirects by default.
-```javascript
-var request = require('request');
+```js
+const request = require('request');
request('http://www.google.com', function (error, response, body) {
- if (!error && response.statusCode == 200) {
- console.log(body) // Print the google web page.
- }
-})
+ console.log('error:', error); // Print the error if one occurred
+ console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
+ console.log('body:', body); // Print the HTML for the Google homepage.
+});
```
+
+## Table of contents
+
+- [Streaming](#streaming)
+- [Promises & Async/Await](#promises--asyncawait)
+- [Forms](#forms)
+- [HTTP Authentication](#http-authentication)
+- [Custom HTTP Headers](#custom-http-headers)
+- [OAuth Signing](#oauth-signing)
+- [Proxies](#proxies)
+- [Unix Domain Sockets](#unix-domain-sockets)
+- [TLS/SSL Protocol](#tlsssl-protocol)
+- [Support for HAR 1.2](#support-for-har-12)
+- [**All Available Options**](#requestoptions-callback)
+
+Request also offers [convenience methods](#convenience-methods) like
+`request.defaults` and `request.post`, and there are
+lots of [usage examples](#examples) and several
+[debugging techniques](#debugging).
+
+
+---
+
+
## Streaming
You can stream any response to a file stream.
-```javascript
+```js
request('http://google.com/doodle.png').pipe(fs.createWriteStream('doodle.png'))
```
-You can also stream a file to a PUT or POST request. This method will also check the file extension against a mapping of file extensions to content-types, in this case `application/json`, and use the proper content-type in the PUT request if one is not already provided in the headers.
+You can also stream a file to a PUT or POST request. This method will also check the file extension against a mapping of file extensions to content-types (in this case `application/json`) and use the proper `content-type` in the PUT request (if the headers don’t already provide one).
-```javascript
+```js
fs.createReadStream('file.json').pipe(request.put('http://mysite.com/obj.json'))
```
-Request can also pipe to itself. When doing so the content-type and content-length will be preserved in the PUT headers.
+Request can also `pipe` to itself. When doing so, `content-type` and `content-length` are preserved in the PUT headers.
-```javascript
+```js
request.get('http://google.com/img.png').pipe(request.put('http://mysite.com/img.png'))
```
-Now let's get fancy.
+Request emits a "response" event when a response is received. The `response` argument will be an instance of [http.IncomingMessage](https://nodejs.org/api/http.html#http_class_http_incomingmessage).
+
+```js
+request
+ .get('http://google.com/img.png')
+ .on('response', function(response) {
+ console.log(response.statusCode) // 200
+ console.log(response.headers['content-type']) // 'image/png'
+ })
+ .pipe(request.put('http://mysite.com/img.png'))
+```
+
+To easily handle errors when streaming requests, listen to the `error` event before piping:
+
+```js
+request
+ .get('http://mysite.com/doodle.png')
+ .on('error', function(err) {
+ console.log(err)
+ })
+ .pipe(fs.createWriteStream('doodle.png'))
+```
-```javascript
+Now let’s get fancy.
+
+```js
http.createServer(function (req, resp) {
if (req.url === '/doodle.png') {
if (req.method === 'PUT') {
req.pipe(request.put('http://mysite.com/doodle.png'))
} else if (req.method === 'GET' || req.method === 'HEAD') {
request.get('http://mysite.com/doodle.png').pipe(resp)
- }
+ }
}
})
```
-You can also pipe() from a http.ServerRequest instance and to a http.ServerResponse instance. The HTTP method and headers will be sent as well as the entity-body data. Which means that, if you don't really care about security, you can do:
+You can also `pipe()` from `http.ServerRequest` instances, as well as to `http.ServerResponse` instances. The HTTP method, headers, and entity-body data will be sent. Which means that, if you don't really care about security, you can do:
-```javascript
+```js
http.createServer(function (req, resp) {
if (req.url === '/doodle.png') {
- var x = request('http://mysite.com/doodle.png')
+ const x = request('http://mysite.com/doodle.png')
req.pipe(x)
x.pipe(resp)
}
})
```
-And since pipe() returns the destination stream in node 0.5.x you can do one line proxying :)
+And since `pipe()` returns the destination stream in ≥ Node 0.5.x you can do one line proxying. :)
-```javascript
+```js
req.pipe(request('http://mysite.com/doodle.png')).pipe(resp)
```
Also, none of this new functionality conflicts with requests previous features, it just expands them.
-```javascript
-var r = request.defaults({'proxy':'http://localproxy.com'})
+```js
+const r = request.defaults({'proxy':'http://localproxy.com'})
http.createServer(function (req, resp) {
if (req.url === '/doodle.png') {
@@ -93,11 +137,254 @@ http.createServer(function (req, resp) {
You can still use intermediate proxies, the requests will still follow HTTP forwards, etc.
+[back to top](#table-of-contents)
+
+
+---
+
+
+## Promises & Async/Await
+
+`request` supports both streaming and callback interfaces natively. If you'd like `request` to return a Promise instead, you can use an alternative interface wrapper for `request`. These wrappers can be useful if you prefer to work with Promises, or if you'd like to use `async`/`await` in ES2017.
+
+Several alternative interfaces are provided by the request team, including:
+- [`request-promise`](https://github.com/request/request-promise) (uses [Bluebird](https://github.com/petkaantonov/bluebird) Promises)
+- [`request-promise-native`](https://github.com/request/request-promise-native) (uses native Promises)
+- [`request-promise-any`](https://github.com/request/request-promise-any) (uses [any-promise](https://www.npmjs.com/package/any-promise) Promises)
+
+Also, [`util.promisify`](https://nodejs.org/api/util.html#util_util_promisify_original), which is available from Node.js v8.0 can be used to convert a regular function that takes a callback to return a promise instead.
+
+
+[back to top](#table-of-contents)
+
+
+---
+
+
+## Forms
+
+`request` supports `application/x-www-form-urlencoded` and `multipart/form-data` form uploads. For `multipart/related` refer to the `multipart` API.
+
+
+#### application/x-www-form-urlencoded (URL-Encoded Forms)
+
+URL-encoded forms are simple.
+
+```js
+request.post('http://service.com/upload', {form:{key:'value'}})
+// or
+request.post('http://service.com/upload').form({key:'value'})
+// or
+request.post({url:'http://service.com/upload', form: {key:'value'}}, function(err,httpResponse,body){ /* ... */ })
+```
+
+
+#### multipart/form-data (Multipart Form Uploads)
+
+For `multipart/form-data` we use the [form-data](https://github.com/form-data/form-data) library by [@felixge](https://github.com/felixge). For the most cases, you can pass your upload form data via the `formData` option.
+
+
+```js
+const formData = {
+ // Pass a simple key-value pair
+ my_field: 'my_value',
+ // Pass data via Buffers
+ my_buffer: Buffer.from([1, 2, 3]),
+ // Pass data via Streams
+ my_file: fs.createReadStream(__dirname + '/unicycle.jpg'),
+ // Pass multiple values /w an Array
+ attachments: [
+ fs.createReadStream(__dirname + '/attachment1.jpg'),
+ fs.createReadStream(__dirname + '/attachment2.jpg')
+ ],
+ // Pass optional meta-data with an 'options' object with style: {value: DATA, options: OPTIONS}
+ // Use case: for some types of streams, you'll need to provide "file"-related information manually.
+ // See the `form-data` README for more information about options: https://github.com/form-data/form-data
+ custom_file: {
+ value: fs.createReadStream('/dev/urandom'),
+ options: {
+ filename: 'topsecret.jpg',
+ contentType: 'image/jpeg'
+ }
+ }
+};
+request.post({url:'http://service.com/upload', formData: formData}, function optionalCallback(err, httpResponse, body) {
+ if (err) {
+ return console.error('upload failed:', err);
+ }
+ console.log('Upload successful! Server responded with:', body);
+});
+```
+
+For advanced cases, you can access the form-data object itself via `r.form()`. This can be modified until the request is fired on the next cycle of the event-loop. (Note that this calling `form()` will clear the currently set form data for that request.)
+
+```js
+// NOTE: Advanced use-case, for normal use see 'formData' usage above
+const r = request.post('http://service.com/upload', function optionalCallback(err, httpResponse, body) {...})
+const form = r.form();
+form.append('my_field', 'my_value');
+form.append('my_buffer', Buffer.from([1, 2, 3]));
+form.append('custom_file', fs.createReadStream(__dirname + '/unicycle.jpg'), {filename: 'unicycle.jpg'});
+```
+See the [form-data README](https://github.com/form-data/form-data) for more information & examples.
+
+
+#### multipart/related
+
+Some variations in different HTTP implementations require a newline/CRLF before, after, or both before and after the boundary of a `multipart/related` request (using the multipart option). This has been observed in the .NET WebAPI version 4.0. You can turn on a boundary preambleCRLF or postamble by passing them as `true` to your request options.
+
+```js
+ request({
+ method: 'PUT',
+ preambleCRLF: true,
+ postambleCRLF: true,
+ uri: 'http://service.com/upload',
+ multipart: [
+ {
+ 'content-type': 'application/json',
+ body: JSON.stringify({foo: 'bar', _attachments: {'message.txt': {follows: true, length: 18, 'content_type': 'text/plain' }}})
+ },
+ { body: 'I am an attachment' },
+ { body: fs.createReadStream('image.png') }
+ ],
+ // alternatively pass an object containing additional options
+ multipart: {
+ chunked: false,
+ data: [
+ {
+ 'content-type': 'application/json',
+ body: JSON.stringify({foo: 'bar', _attachments: {'message.txt': {follows: true, length: 18, 'content_type': 'text/plain' }}})
+ },
+ { body: 'I am an attachment' }
+ ]
+ }
+ },
+ function (error, response, body) {
+ if (error) {
+ return console.error('upload failed:', error);
+ }
+ console.log('Upload successful! Server responded with:', body);
+ })
+```
+
+[back to top](#table-of-contents)
+
+
+---
+
+
+## HTTP Authentication
+
+```js
+request.get('http://some.server.com/').auth('username', 'password', false);
+// or
+request.get('http://some.server.com/', {
+ 'auth': {
+ 'user': 'username',
+ 'pass': 'password',
+ 'sendImmediately': false
+ }
+});
+// or
+request.get('http://some.server.com/').auth(null, null, true, 'bearerToken');
+// or
+request.get('http://some.server.com/', {
+ 'auth': {
+ 'bearer': 'bearerToken'
+ }
+});
+```
+
+If passed as an option, `auth` should be a hash containing values:
+
+- `user` || `username`
+- `pass` || `password`
+- `sendImmediately` (optional)
+- `bearer` (optional)
+
+The method form takes parameters
+`auth(username, password, sendImmediately, bearer)`.
+
+`sendImmediately` defaults to `true`, which causes a basic or bearer
+authentication header to be sent. If `sendImmediately` is `false`, then
+`request` will retry with a proper authentication header after receiving a
+`401` response from the server (which must contain a `WWW-Authenticate` header
+indicating the required authentication method).
+
+Note that you can also specify basic authentication using the URL itself, as
+detailed in [RFC 1738](http://www.ietf.org/rfc/rfc1738.txt). Simply pass the
+`user:password` before the host with an `@` sign:
+
+```js
+const username = 'username',
+ password = 'password',
+ url = 'http://' + username + ':' + password + '@some.server.com';
+
+request({url}, function (error, response, body) {
+ // Do more stuff with 'body' here
+});
+```
+
+Digest authentication is supported, but it only works with `sendImmediately`
+set to `false`; otherwise `request` will send basic authentication on the
+initial request, which will probably cause the request to fail.
+
+Bearer authentication is supported, and is activated when the `bearer` value is
+available. The value may be either a `String` or a `Function` returning a
+`String`. Using a function to supply the bearer token is particularly useful if
+used in conjunction with `defaults` to allow a single function to supply the
+last known token at the time of sending a request, or to compute one on the fly.
+
+[back to top](#table-of-contents)
+
+
+---
+
+
+## Custom HTTP Headers
+
+HTTP Headers, such as `User-Agent`, can be set in the `options` object.
+In the example below, we call the github API to find out the number
+of stars and forks for the request repository. This requires a
+custom `User-Agent` header as well as https.
+
+```js
+const request = require('request');
+
+const options = {
+ url: 'https://api.github.com/repos/request/request',
+ headers: {
+ 'User-Agent': 'request'
+ }
+};
+
+function callback(error, response, body) {
+ if (!error && response.statusCode == 200) {
+ const info = JSON.parse(body);
+ console.log(info.stargazers_count + " Stars");
+ console.log(info.forks_count + " Forks");
+ }
+}
+
+request(options, callback);
+```
+
+[back to top](#table-of-contents)
+
+
+---
+
+
## OAuth Signing
-```javascript
-// Twitter OAuth
-var qs = require('querystring')
+[OAuth version 1.0](https://tools.ietf.org/html/rfc5849) is supported. The
+default signing algorithm is
+[HMAC-SHA1](https://tools.ietf.org/html/rfc5849#section-3.4.2):
+
+```js
+// OAuth1.0 - 3-legged server side flow (Twitter example)
+// step 1
+const qs = require('querystring')
, oauth =
{ callback: 'http://mysite.com/callback/'
, consumer_key: CONSUMER_KEY
@@ -106,146 +393,630 @@ var qs = require('querystring')
, url = 'https://api.twitter.com/oauth/request_token'
;
request.post({url:url, oauth:oauth}, function (e, r, body) {
- // Assume by some stretch of magic you aquired the verifier
- var access_token = qs.parse(body)
- , oauth =
+ // Ideally, you would take the body in the response
+ // and construct a URL that a user clicks on (like a sign in button).
+ // The verifier is only available in the response after a user has
+ // verified with twitter that they are authorizing your app.
+
+ // step 2
+ const req_data = qs.parse(body)
+ const uri = 'https://api.twitter.com/oauth/authenticate'
+ + '?' + qs.stringify({oauth_token: req_data.oauth_token})
+ // redirect the user to the authorize uri
+
+ // step 3
+ // after the user is redirected back to your server
+ const auth_data = qs.parse(body)
+ , oauth =
{ consumer_key: CONSUMER_KEY
, consumer_secret: CONSUMER_SECRET
- , token: access_token.oauth_token
- , verifier: VERIFIER
- , token_secret: access_token.oauth_token_secret
+ , token: auth_data.oauth_token
+ , token_secret: req_data.oauth_token_secret
+ , verifier: auth_data.oauth_verifier
}
, url = 'https://api.twitter.com/oauth/access_token'
;
request.post({url:url, oauth:oauth}, function (e, r, body) {
- var perm_token = qs.parse(body)
- , oauth =
+ // ready to make signed requests on behalf of the user
+ const perm_data = qs.parse(body)
+ , oauth =
{ consumer_key: CONSUMER_KEY
, consumer_secret: CONSUMER_SECRET
- , token: perm_token.oauth_token
- , token_secret: perm_token.oauth_token_secret
+ , token: perm_data.oauth_token
+ , token_secret: perm_data.oauth_token_secret
}
- , url = 'https://api.twitter.com/1/users/show.json?'
- , params =
- { screen_name: perm_token.screen_name
- , user_id: perm_token.user_id
+ , url = 'https://api.twitter.com/1.1/users/show.json'
+ , qs =
+ { screen_name: perm_data.screen_name
+ , user_id: perm_data.user_id
}
;
- url += qs.stringify(params)
- request.get({url:url, oauth:oauth, json:true}, function (e, r, user) {
+ request.get({url:url, oauth:oauth, qs:qs, json:true}, function (e, r, user) {
console.log(user)
})
})
})
```
+For [RSA-SHA1 signing](https://tools.ietf.org/html/rfc5849#section-3.4.3), make
+the following changes to the OAuth options object:
+* Pass `signature_method : 'RSA-SHA1'`
+* Instead of `consumer_secret`, specify a `private_key` string in
+ [PEM format](http://how2ssl.com/articles/working_with_pem_files/)
+For [PLAINTEXT signing](http://oauth.net/core/1.0/#anchor22), make
+the following changes to the OAuth options object:
+* Pass `signature_method : 'PLAINTEXT'`
-### request(options, callback)
+To send OAuth parameters via query params or in a post body as described in The
+[Consumer Request Parameters](http://oauth.net/core/1.0/#consumer_req_param)
+section of the oauth1 spec:
+* Pass `transport_method : 'query'` or `transport_method : 'body'` in the OAuth
+ options object.
+* `transport_method` defaults to `'header'`
-The first argument can be either a url or an options object. The only required option is uri, all others are optional.
+To use [Request Body Hash](https://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html) you can either
+* Manually generate the body hash and pass it as a string `body_hash: '...'`
+* Automatically generate the body hash by passing `body_hash: true`
-* `uri` || `url` - fully qualified uri or a parsed url object from url.parse()
-* `qs` - object containing querystring values to be appended to the uri
-* `method` - http method, defaults to GET
-* `headers` - http headers, defaults to {}
-* `body` - entity body for POST and PUT requests. Must be buffer or string.
-* `form` - sets `body` but to querystring representation of value and adds `Content-type: application/x-www-form-urlencoded; charset=utf-8` header.
-* `json` - sets `body` but to JSON representation of value and adds `Content-type: application/json` header.
-* `multipart` - (experimental) array of objects which contains their own headers and `body` attribute. Sends `multipart/related` request. See example below.
-* `followRedirect` - follow HTTP 3xx responses as redirects. defaults to true.
-* `followAllRedirects` - follow non-GET HTTP 3xx responses as redirects. defaults to false.
-* `maxRedirects` - the maximum number of redirects to follow, defaults to 10.
-* `encoding` - Encoding to be used on `setEncoding` of response data. If set to `null`, the body is returned as a Buffer.
-* `pool` - A hash object containing the agents for these requests. If omitted this request will use the global pool which is set to node's default maxSockets.
-* `pool.maxSockets` - Integer containing the maximum amount of sockets in the pool.
-* `timeout` - Integer containing the number of milliseconds to wait for a request to respond before aborting the request
-* `proxy` - An HTTP proxy to be used. Support proxy Auth with Basic Auth the same way it's supported with the `url` parameter by embedding the auth info in the uri.
-* `oauth` - Options for OAuth HMAC-SHA1 signing, see documentation above.
-* `strictSSL` - Set to `true` to require that SSL certificates be valid. Note: to use your own certificate authority, you need to specify an agent that was created with that ca as an option.
-* `jar` - Set to `false` if you don't want cookies to be remembered for future use or define your custom cookie jar (see examples section)
+[back to top](#table-of-contents)
-The callback argument gets 3 arguments. The first is an error when applicable (usually from the http.Client option not the http.ClientRequest object). The second in an http.ClientResponse object. The third is the response body String or Buffer.
+---
-## Convenience methods
-There are also shorthand methods for different HTTP METHODs and some other conveniences.
+## Proxies
+
+If you specify a `proxy` option, then the request (and any subsequent
+redirects) will be sent via a connection to the proxy server.
+
+If your endpoint is an `https` url, and you are using a proxy, then
+request will send a `CONNECT` request to the proxy server *first*, and
+then use the supplied connection to connect to the endpoint.
+
+That is, first it will make a request like:
+
+```
+HTTP/1.1 CONNECT endpoint-server.com:80
+Host: proxy-server.com
+User-Agent: whatever user agent you specify
+```
+
+and then the proxy server make a TCP connection to `endpoint-server`
+on port `80`, and return a response that looks like:
+
+```
+HTTP/1.1 200 OK
+```
+
+At this point, the connection is left open, and the client is
+communicating directly with the `endpoint-server.com` machine.
+
+See [the wikipedia page on HTTP Tunneling](https://en.wikipedia.org/wiki/HTTP_tunnel)
+for more information.
+
+By default, when proxying `http` traffic, request will simply make a
+standard proxied `http` request. This is done by making the `url`
+section of the initial line of the request a fully qualified url to
+the endpoint.
+
+For example, it will make a single request that looks like:
+
+```
+HTTP/1.1 GET http://endpoint-server.com/some-url
+Host: proxy-server.com
+Other-Headers: all go here
+
+request body or whatever
+```
+
+Because a pure "http over http" tunnel offers no additional security
+or other features, it is generally simpler to go with a
+straightforward HTTP proxy in this case. However, if you would like
+to force a tunneling proxy, you may set the `tunnel` option to `true`.
+
+You can also make a standard proxied `http` request by explicitly setting
+`tunnel : false`, but **note that this will allow the proxy to see the traffic
+to/from the destination server**.
+
+If you are using a tunneling proxy, you may set the
+`proxyHeaderWhiteList` to share certain headers with the proxy.
+
+You can also set the `proxyHeaderExclusiveList` to share certain
+headers only with the proxy and not with destination host.
+
+By default, this set is:
+
+```
+accept
+accept-charset
+accept-encoding
+accept-language
+accept-ranges
+cache-control
+content-encoding
+content-language
+content-length
+content-location
+content-md5
+content-range
+content-type
+connection
+date
+expect
+max-forwards
+pragma
+proxy-authorization
+referer
+te
+transfer-encoding
+user-agent
+via
+```
+
+Note that, when using a tunneling proxy, the `proxy-authorization`
+header and any headers from custom `proxyHeaderExclusiveList` are
+*never* sent to the endpoint server, but only to the proxy server.
+
+
+### Controlling proxy behaviour using environment variables
+
+The following environment variables are respected by `request`:
+
+ * `HTTP_PROXY` / `http_proxy`
+ * `HTTPS_PROXY` / `https_proxy`
+ * `NO_PROXY` / `no_proxy`
-### request.defaults(options)
-
-This method returns a wrapper around the normal request API that defaults to whatever options you pass in to it.
+When `HTTP_PROXY` / `http_proxy` are set, they will be used to proxy non-SSL requests that do not have an explicit `proxy` configuration option present. Similarly, `HTTPS_PROXY` / `https_proxy` will be respected for SSL requests that do not have an explicit `proxy` configuration option. It is valid to define a proxy in one of the environment variables, but then override it for a specific request, using the `proxy` configuration option. Furthermore, the `proxy` configuration option can be explicitly set to false / null to opt out of proxying altogether for that request.
-### request.put
+`request` is also aware of the `NO_PROXY`/`no_proxy` environment variables. These variables provide a granular way to opt out of proxying, on a per-host basis. It should contain a comma separated list of hosts to opt out of proxying. It is also possible to opt of proxying when a particular destination port is used. Finally, the variable may be set to `*` to opt out of the implicit proxy configuration of the other environment variables.
-Same as request() but defaults to `method: "PUT"`.
+Here's some examples of valid `no_proxy` values:
-```javascript
-request.put(url)
+ * `google.com` - don't proxy HTTP/HTTPS requests to Google.
+ * `google.com:443` - don't proxy HTTPS requests to Google, but *do* proxy HTTP requests to Google.
+ * `google.com:443, yahoo.com:80` - don't proxy HTTPS requests to Google, and don't proxy HTTP requests to Yahoo!
+ * `*` - ignore `https_proxy`/`http_proxy` environment variables altogether.
+
+[back to top](#table-of-contents)
+
+
+---
+
+
+## UNIX Domain Sockets
+
+`request` supports making requests to [UNIX Domain Sockets](https://en.wikipedia.org/wiki/Unix_domain_socket). To make one, use the following URL scheme:
+
+```js
+/* Pattern */ 'http://unix:SOCKET:PATH'
+/* Example */ request.get('http://unix:/absolute/path/to/unix.socket:/request/path')
```
-### request.post
+Note: The `SOCKET` path is assumed to be absolute to the root of the host file system.
+
+[back to top](#table-of-contents)
+
-Same as request() but defaults to `method: "POST"`.
+---
-```javascript
-request.post(url)
+
+## TLS/SSL Protocol
+
+TLS/SSL Protocol options, such as `cert`, `key` and `passphrase`, can be
+set directly in `options` object, in the `agentOptions` property of the `options` object, or even in `https.globalAgent.options`. Keep in mind that, although `agentOptions` allows for a slightly wider range of configurations, the recommended way is via `options` object directly, as using `agentOptions` or `https.globalAgent.options` would not be applied in the same way in proxied environments (as data travels through a TLS connection instead of an http/https agent).
+
+```js
+const fs = require('fs')
+ , path = require('path')
+ , certFile = path.resolve(__dirname, 'ssl/client.crt')
+ , keyFile = path.resolve(__dirname, 'ssl/client.key')
+ , caFile = path.resolve(__dirname, 'ssl/ca.cert.pem')
+ , request = require('request');
+
+const options = {
+ url: 'https://api.some-server.com/',
+ cert: fs.readFileSync(certFile),
+ key: fs.readFileSync(keyFile),
+ passphrase: 'password',
+ ca: fs.readFileSync(caFile)
+};
+
+request.get(options);
```
-### request.head
+### Using `options.agentOptions`
+
+In the example below, we call an API that requires client side SSL certificate
+(in PEM format) with passphrase protected private key (in PEM format) and disable the SSLv3 protocol:
-Same as request() but defaults to `method: "HEAD"`.
+```js
+const fs = require('fs')
+ , path = require('path')
+ , certFile = path.resolve(__dirname, 'ssl/client.crt')
+ , keyFile = path.resolve(__dirname, 'ssl/client.key')
+ , request = require('request');
+
+const options = {
+ url: 'https://api.some-server.com/',
+ agentOptions: {
+ cert: fs.readFileSync(certFile),
+ key: fs.readFileSync(keyFile),
+ // Or use `pfx` property replacing `cert` and `key` when using private key, certificate and CA certs in PFX or PKCS12 format:
+ // pfx: fs.readFileSync(pfxFilePath),
+ passphrase: 'password',
+ securityOptions: 'SSL_OP_NO_SSLv3'
+ }
+};
-```javascript
-request.head(url)
+request.get(options);
```
-### request.del
+It is able to force using SSLv3 only by specifying `secureProtocol`:
+
+```js
+request.get({
+ url: 'https://api.some-server.com/',
+ agentOptions: {
+ secureProtocol: 'SSLv3_method'
+ }
+});
+```
+
+It is possible to accept other certificates than those signed by generally allowed Certificate Authorities (CAs).
+This can be useful, for example, when using self-signed certificates.
+To require a different root certificate, you can specify the signing CA by adding the contents of the CA's certificate file to the `agentOptions`.
+The certificate the domain presents must be signed by the root certificate specified:
+
+```js
+request.get({
+ url: 'https://api.some-server.com/',
+ agentOptions: {
+ ca: fs.readFileSync('ca.cert.pem')
+ }
+});
+```
+
+The `ca` value can be an array of certificates, in the event you have a private or internal corporate public-key infrastructure hierarchy. For example, if you want to connect to https://api.some-server.com which presents a key chain consisting of:
+1. its own public key, which is signed by:
+2. an intermediate "Corp Issuing Server", that is in turn signed by:
+3. a root CA "Corp Root CA";
+
+you can configure your request as follows:
+
+```js
+request.get({
+ url: 'https://api.some-server.com/',
+ agentOptions: {
+ ca: [
+ fs.readFileSync('Corp Issuing Server.pem'),
+ fs.readFileSync('Corp Root CA.pem')
+ ]
+ }
+});
+```
+
+[back to top](#table-of-contents)
+
+
+---
+
+## Support for HAR 1.2
-Same as request() but defaults to `method: "DELETE"`.
+The `options.har` property will override the values: `url`, `method`, `qs`, `headers`, `form`, `formData`, `body`, `json`, as well as construct multipart data and read files from disk when `request.postData.params[].fileName` is present without a matching `value`.
-```javascript
-request.del(url)
+A validation step will check if the HAR Request format matches the latest spec (v1.2) and will skip parsing if not matching.
+
+```js
+ const request = require('request')
+ request({
+ // will be ignored
+ method: 'GET',
+ uri: 'http://www.google.com',
+
+ // HTTP Archive Request Object
+ har: {
+ url: 'http://www.mockbin.com/har',
+ method: 'POST',
+ headers: [
+ {
+ name: 'content-type',
+ value: 'application/x-www-form-urlencoded'
+ }
+ ],
+ postData: {
+ mimeType: 'application/x-www-form-urlencoded',
+ params: [
+ {
+ name: 'foo',
+ value: 'bar'
+ },
+ {
+ name: 'hello',
+ value: 'world'
+ }
+ ]
+ }
+ }
+ })
+
+ // a POST request will be sent to http://www.mockbin.com
+ // with body an application/x-www-form-urlencoded body:
+ // foo=bar&hello=world
```
-### request.get
+[back to top](#table-of-contents)
+
+
+---
+
+## request(options, callback)
+
+The first argument can be either a `url` or an `options` object. The only required option is `uri`; all others are optional.
+
+- `uri` || `url` - fully qualified uri or a parsed url object from `url.parse()`
+- `baseUrl` - fully qualified uri string used as the base url. Most useful with `request.defaults`, for example when you want to do many requests to the same domain. If `baseUrl` is `https://example.com/api/`, then requesting `/end/point?test=true` will fetch `https://example.com/api/end/point?test=true`. When `baseUrl` is given, `uri` must also be a string.
+- `method` - http method (default: `"GET"`)
+- `headers` - http headers (default: `{}`)
+
+---
+
+- `qs` - object containing querystring values to be appended to the `uri`
+- `qsParseOptions` - object containing options to pass to the [qs.parse](https://github.com/hapijs/qs#parsing-objects) method. Alternatively pass options to the [querystring.parse](https://nodejs.org/docs/v0.12.0/api/querystring.html#querystring_querystring_parse_str_sep_eq_options) method using this format `{sep:';', eq:':', options:{}}`
+- `qsStringifyOptions` - object containing options to pass to the [qs.stringify](https://github.com/hapijs/qs#stringifying) method. Alternatively pass options to the [querystring.stringify](https://nodejs.org/docs/v0.12.0/api/querystring.html#querystring_querystring_stringify_obj_sep_eq_options) method using this format `{sep:';', eq:':', options:{}}`. For example, to change the way arrays are converted to query strings using the `qs` module pass the `arrayFormat` option with one of `indices|brackets|repeat`
+- `useQuerystring` - if true, use `querystring` to stringify and parse
+ querystrings, otherwise use `qs` (default: `false`). Set this option to
+ `true` if you need arrays to be serialized as `foo=bar&foo=baz` instead of the
+ default `foo[0]=bar&foo[1]=baz`.
+
+---
+
+- `body` - entity body for PATCH, POST and PUT requests. Must be a `Buffer`, `String` or `ReadStream`. If `json` is `true`, then `body` must be a JSON-serializable object.
+- `form` - when passed an object or a querystring, this sets `body` to a querystring representation of value, and adds `Content-type: application/x-www-form-urlencoded` header. When passed no options, a `FormData` instance is returned (and is piped to request). See "Forms" section above.
+- `formData` - data to pass for a `multipart/form-data` request. See
+ [Forms](#forms) section above.
+- `multipart` - array of objects which contain their own headers and `body`
+ attributes. Sends a `multipart/related` request. See [Forms](#forms) section
+ above.
+ - Alternatively you can pass in an object `{chunked: false, data: []}` where
+ `chunked` is used to specify whether the request is sent in
+ [chunked transfer encoding](https://en.wikipedia.org/wiki/Chunked_transfer_encoding)
+ In non-chunked requests, data items with body streams are not allowed.
+- `preambleCRLF` - append a newline/CRLF before the boundary of your `multipart/form-data` request.
+- `postambleCRLF` - append a newline/CRLF at the end of the boundary of your `multipart/form-data` request.
+- `json` - sets `body` to JSON representation of value and adds `Content-type: application/json` header. Additionally, parses the response body as JSON.
+- `jsonReviver` - a [reviver function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) that will be passed to `JSON.parse()` when parsing a JSON response body.
+- `jsonReplacer` - a [replacer function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) that will be passed to `JSON.stringify()` when stringifying a JSON request body.
+
+---
+
+- `auth` - a hash containing values `user` || `username`, `pass` || `password`, and `sendImmediately` (optional). See documentation above.
+- `oauth` - options for OAuth HMAC-SHA1 signing. See documentation above.
+- `hawk` - options for [Hawk signing](https://github.com/hueniverse/hawk). The `credentials` key must contain the necessary signing info, [see hawk docs for details](https://github.com/hueniverse/hawk#usage-example).
+- `aws` - `object` containing AWS signing information. Should have the properties `key`, `secret`, and optionally `session` (note that this only works for services that require session as part of the canonical string). Also requires the property `bucket`, unless you’re specifying your `bucket` as part of the path, or the request doesn’t use a bucket (i.e. GET Services). If you want to use AWS sign version 4 use the parameter `sign_version` with value `4` otherwise the default is version 2. If you are using SigV4, you can also include a `service` property that specifies the service name. **Note:** you need to `npm install aws4` first.
+- `httpSignature` - options for the [HTTP Signature Scheme](https://github.com/joyent/node-http-signature/blob/master/http_signing.md) using [Joyent's library](https://github.com/joyent/node-http-signature). The `keyId` and `key` properties must be specified. See the docs for other options.
+
+---
+
+- `followRedirect` - follow HTTP 3xx responses as redirects (default: `true`). This property can also be implemented as function which gets `response` object as a single argument and should return `true` if redirects should continue or `false` otherwise.
+- `followAllRedirects` - follow non-GET HTTP 3xx responses as redirects (default: `false`)
+- `followOriginalHttpMethod` - by default we redirect to HTTP method GET. you can enable this property to redirect to the original HTTP method (default: `false`)
+- `maxRedirects` - the maximum number of redirects to follow (default: `10`)
+- `removeRefererHeader` - removes the referer header when a redirect happens (default: `false`). **Note:** if true, referer header set in the initial request is preserved during redirect chain.
+
+---
+
+- `encoding` - encoding to be used on `setEncoding` of response data. If `null`, the `body` is returned as a `Buffer`. Anything else **(including the default value of `undefined`)** will be passed as the [encoding](http://nodejs.org/api/buffer.html#buffer_buffer) parameter to `toString()` (meaning this is effectively `utf8` by default). (**Note:** if you expect binary data, you should set `encoding: null`.)
+- `gzip` - if `true`, add an `Accept-Encoding` header to request compressed content encodings from the server (if not already present) and decode supported content encodings in the response. **Note:** Automatic decoding of the response content is performed on the body data returned through `request` (both through the `request` stream and passed to the callback function) but is not performed on the `response` stream (available from the `response` event) which is the unmodified `http.IncomingMessage` object which may contain compressed data. See example below.
+- `jar` - if `true`, remember cookies for future use (or define your custom cookie jar; see examples section)
+
+---
+
+- `agent` - `http(s).Agent` instance to use
+- `agentClass` - alternatively specify your agent's class name
+- `agentOptions` - and pass its options. **Note:** for HTTPS see [tls API doc for TLS/SSL options](http://nodejs.org/api/tls.html#tls_tls_connect_options_callback) and the [documentation above](#using-optionsagentoptions).
+- `forever` - set to `true` to use the [forever-agent](https://github.com/request/forever-agent) **Note:** Defaults to `http(s).Agent({keepAlive:true})` in node 0.12+
+- `pool` - an object describing which agents to use for the request. If this option is omitted the request will use the global agent (as long as your options allow for it). Otherwise, request will search the pool for your custom agent. If no custom agent is found, a new agent will be created and added to the pool. **Note:** `pool` is used only when the `agent` option is not specified.
+ - A `maxSockets` property can also be provided on the `pool` object to set the max number of sockets for all agents created (ex: `pool: {maxSockets: Infinity}`).
+ - Note that if you are sending multiple requests in a loop and creating
+ multiple new `pool` objects, `maxSockets` will not work as intended. To
+ work around this, either use [`request.defaults`](#requestdefaultsoptions)
+ with your pool options or create the pool object with the `maxSockets`
+ property outside of the loop.
+- `timeout` - integer containing number of milliseconds, controls two timeouts.
+ - **Read timeout**: Time to wait for a server to send response headers (and start the response body) before aborting the request.
+ - **Connection timeout**: Sets the socket to timeout after `timeout` milliseconds of inactivity. Note that increasing the timeout beyond the OS-wide TCP connection timeout will not have any effect ([the default in Linux can be anywhere from 20-120 seconds][linux-timeout])
+
+[linux-timeout]: http://www.sekuda.com/overriding_the_default_linux_kernel_20_second_tcp_socket_connect_timeout
+
+---
+
+- `localAddress` - local interface to bind for network connections.
+- `proxy` - an HTTP proxy to be used. Supports proxy Auth with Basic Auth, identical to support for the `url` parameter (by embedding the auth info in the `uri`)
+- `strictSSL` - if `true`, requires SSL certificates be valid. **Note:** to use your own certificate authority, you need to specify an agent that was created with that CA as an option.
+- `tunnel` - controls the behavior of
+ [HTTP `CONNECT` tunneling](https://en.wikipedia.org/wiki/HTTP_tunnel#HTTP_CONNECT_tunneling)
+ as follows:
+ - `undefined` (default) - `true` if the destination is `https`, `false` otherwise
+ - `true` - always tunnel to the destination by making a `CONNECT` request to
+ the proxy
+ - `false` - request the destination as a `GET` request.
+- `proxyHeaderWhiteList` - a whitelist of headers to send to a
+ tunneling proxy.
+- `proxyHeaderExclusiveList` - a whitelist of headers to send
+ exclusively to a tunneling proxy and not to destination.
+
+---
+
+- `time` - if `true`, the request-response cycle (including all redirects) is timed at millisecond resolution. When set, the following properties are added to the response object:
+ - `elapsedTime` Duration of the entire request/response in milliseconds (*deprecated*).
+ - `responseStartTime` Timestamp when the response began (in Unix Epoch milliseconds) (*deprecated*).
+ - `timingStart` Timestamp of the start of the request (in Unix Epoch milliseconds).
+ - `timings` Contains event timestamps in millisecond resolution relative to `timingStart`. If there were redirects, the properties reflect the timings of the final request in the redirect chain:
+ - `socket` Relative timestamp when the [`http`](https://nodejs.org/api/http.html#http_event_socket) module's `socket` event fires. This happens when the socket is assigned to the request.
+ - `lookup` Relative timestamp when the [`net`](https://nodejs.org/api/net.html#net_event_lookup) module's `lookup` event fires. This happens when the DNS has been resolved.
+ - `connect`: Relative timestamp when the [`net`](https://nodejs.org/api/net.html#net_event_connect) module's `connect` event fires. This happens when the server acknowledges the TCP connection.
+ - `response`: Relative timestamp when the [`http`](https://nodejs.org/api/http.html#http_event_response) module's `response` event fires. This happens when the first bytes are received from the server.
+ - `end`: Relative timestamp when the last bytes of the response are received.
+ - `timingPhases` Contains the durations of each request phase. If there were redirects, the properties reflect the timings of the final request in the redirect chain:
+ - `wait`: Duration of socket initialization (`timings.socket`)
+ - `dns`: Duration of DNS lookup (`timings.lookup` - `timings.socket`)
+ - `tcp`: Duration of TCP connection (`timings.connect` - `timings.socket`)
+ - `firstByte`: Duration of HTTP server response (`timings.response` - `timings.connect`)
+ - `download`: Duration of HTTP download (`timings.end` - `timings.response`)
+ - `total`: Duration entire HTTP round-trip (`timings.end`)
+
+- `har` - a [HAR 1.2 Request Object](http://www.softwareishard.com/blog/har-12-spec/#request), will be processed from HAR format into options overwriting matching values *(see the [HAR 1.2 section](#support-for-har-12) for details)*
+- `callback` - alternatively pass the request's callback in the options object
+
+The callback argument gets 3 arguments:
+
+1. An `error` when applicable (usually from [`http.ClientRequest`](http://nodejs.org/api/http.html#http_class_http_clientrequest) object)
+2. An [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage) object (Response object)
+3. The third is the `response` body (`String` or `Buffer`, or JSON object if the `json` option is supplied)
+
+[back to top](#table-of-contents)
-Alias to normal request method for uniformity.
-```javascript
-request.get(url)
+---
+
+## Convenience methods
+
+There are also shorthand methods for different HTTP METHODs and some other conveniences.
+
+
+### request.defaults(options)
+
+This method **returns a wrapper** around the normal request API that defaults
+to whatever options you pass to it.
+
+**Note:** `request.defaults()` **does not** modify the global request API;
+instead, it **returns a wrapper** that has your default settings applied to it.
+
+**Note:** You can call `.defaults()` on the wrapper that is returned from
+`request.defaults` to add/override defaults that were previously defaulted.
+
+For example:
+```js
+//requests using baseRequest() will set the 'x-token' header
+const baseRequest = request.defaults({
+ headers: {'x-token': 'my-token'}
+})
+
+//requests using specialRequest() will include the 'x-token' header set in
+//baseRequest and will also include the 'special' header
+const specialRequest = baseRequest.defaults({
+ headers: {special: 'special value'}
+})
```
-### request.cookie
+
+### request.METHOD()
+
+These HTTP method convenience functions act just like `request()` but with a default method already set for you:
+
+- *request.get()*: Defaults to `method: "GET"`.
+- *request.post()*: Defaults to `method: "POST"`.
+- *request.put()*: Defaults to `method: "PUT"`.
+- *request.patch()*: Defaults to `method: "PATCH"`.
+- *request.del() / request.delete()*: Defaults to `method: "DELETE"`.
+- *request.head()*: Defaults to `method: "HEAD"`.
+- *request.options()*: Defaults to `method: "OPTIONS"`.
+
+### request.cookie()
Function that creates a new cookie.
-```javascript
-request.cookie('cookie_string_here')
+```js
+request.cookie('key1=value1')
```
-### request.jar
+### request.jar()
Function that creates a new cookie jar.
-```javascript
+```js
request.jar()
```
+### response.caseless.get('header-name')
+
+Function that returns the specified response header field using a [case-insensitive match](https://tools.ietf.org/html/rfc7230#section-3.2)
+
+```js
+request('http://www.google.com', function (error, response, body) {
+ // print the Content-Type header even if the server returned it as 'content-type' (lowercase)
+ console.log('Content-Type is:', response.caseless.get('Content-Type'));
+});
+```
+
+[back to top](#table-of-contents)
+
+
+---
+
+
+## Debugging
+
+There are at least three ways to debug the operation of `request`:
+
+1. Launch the node process like `NODE_DEBUG=request node script.js`
+ (`lib,request,otherlib` works too).
+
+2. Set `require('request').debug = true` at any time (this does the same thing
+ as #1).
+
+3. Use the [request-debug module](https://github.com/request/request-debug) to
+ view request and response headers and bodies.
+
+[back to top](#table-of-contents)
+
+
+---
+
+## Timeouts
+
+Most requests to external servers should have a timeout attached, in case the
+server is not responding in a timely manner. Without a timeout, your code may
+have a socket open/consume resources for minutes or more.
+
+There are two main types of timeouts: **connection timeouts** and **read
+timeouts**. A connect timeout occurs if the timeout is hit while your client is
+attempting to establish a connection to a remote machine (corresponding to the
+[connect() call][connect] on the socket). A read timeout occurs any time the
+server is too slow to send back a part of the response.
+
+These two situations have widely different implications for what went wrong
+with the request, so it's useful to be able to distinguish them. You can detect
+timeout errors by checking `err.code` for an 'ETIMEDOUT' value. Further, you
+can detect whether the timeout was a connection timeout by checking if the
+`err.connect` property is set to `true`.
+
+```js
+request.get('http://10.255.255.1', {timeout: 1500}, function(err) {
+ console.log(err.code === 'ETIMEDOUT');
+ // Set to `true` if the timeout was a connection timeout, `false` or
+ // `undefined` otherwise.
+ console.log(err.connect === true);
+ process.exit(0);
+});
+```
+
+[connect]: http://linux.die.net/man/2/connect
## Examples:
-```javascript
- var request = require('request')
+```js
+ const request = require('request')
, rand = Math.floor(Math.random()*100000000).toString()
;
request(
{ method: 'PUT'
, uri: 'http://mikeal.iriscouch.com/testjs/' + rand
- , multipart:
+ , multipart:
[ { 'content-type': 'application/json'
, body: JSON.stringify({foo: 'bar', _attachments: {'message.txt': {follows: true, length: 18, 'content_type': 'text/plain' }}})
}
, { body: 'I am an attachment' }
- ]
+ ]
}
, function (error, response, body) {
if(response.statusCode == 201){
@@ -257,31 +1028,100 @@ request.jar()
}
)
```
-Cookies are enabled by default (so they can be used in subsequent requests). To disable cookies set jar to false (either in defaults or in the options sent).
-```javascript
-var request = request.defaults({jar: false})
+For backwards-compatibility, response compression is not supported by default.
+To accept gzip-compressed responses, set the `gzip` option to `true`. Note
+that the body data passed through `request` is automatically decompressed
+while the response object is unmodified and will contain compressed data if
+the server sent a compressed response.
+
+```js
+ const request = require('request')
+ request(
+ { method: 'GET'
+ , uri: 'http://www.google.com'
+ , gzip: true
+ }
+ , function (error, response, body) {
+ // body is the decompressed response body
+ console.log('server encoded the data as: ' + (response.headers['content-encoding'] || 'identity'))
+ console.log('the decoded data is: ' + body)
+ }
+ )
+ .on('data', function(data) {
+ // decompressed data as it is received
+ console.log('decoded chunk: ' + data)
+ })
+ .on('response', function(response) {
+ // unmodified http.IncomingMessage object
+ response.on('data', function(data) {
+ // compressed data as it is received
+ console.log('received ' + data.length + ' bytes of compressed data')
+ })
+ })
+```
+
+Cookies are disabled by default (else, they would be used in subsequent requests). To enable cookies, set `jar` to `true` (either in `defaults` or `options`).
+
+```js
+const request = request.defaults({jar: true})
request('http://www.google.com', function () {
request('http://images.google.com')
})
```
-If you to use a custom cookie jar (instead of letting request use its own global cookie jar) you do so by setting the jar default or by specifying it as an option:
+To use a custom cookie jar (instead of `request`’s global cookie jar), set `jar` to an instance of `request.jar()` (either in `defaults` or `options`)
-```javascript
-var j = request.jar()
-var request = request.defaults({jar:j})
+```js
+const j = request.jar()
+const request = request.defaults({jar:j})
request('http://www.google.com', function () {
request('http://images.google.com')
})
```
+
OR
-```javascript
-var j = request.jar()
-var cookie = request.cookie('your_cookie_here')
-j.add(cookie)
-request({url: 'http://www.google.com', jar: j}, function () {
+```js
+const j = request.jar();
+const cookie = request.cookie('key1=value1');
+const url = 'http://www.google.com';
+j.setCookie(cookie, url);
+request({url: url, jar: j}, function () {
+ request('http://images.google.com')
+})
+```
+
+To use a custom cookie store (such as a
+[`FileCookieStore`](https://github.com/mitsuru/tough-cookie-filestore)
+which supports saving to and restoring from JSON files), pass it as a parameter
+to `request.jar()`:
+
+```js
+const FileCookieStore = require('tough-cookie-filestore');
+// NOTE - currently the 'cookies.json' file must already exist!
+const j = request.jar(new FileCookieStore('cookies.json'));
+request = request.defaults({ jar : j })
+request('http://www.google.com', function() {
request('http://images.google.com')
})
```
+
+The cookie store must be a
+[`tough-cookie`](https://github.com/SalesforceEng/tough-cookie)
+store and it must support synchronous operations; see the
+[`CookieStore` API docs](https://github.com/SalesforceEng/tough-cookie#api)
+for details.
+
+To inspect your cookie jar after a request:
+
+```js
+const j = request.jar()
+request({url: 'http://www.google.com', jar: j}, function () {
+ const cookie_string = j.getCookieString(url); // "key1=value1; key2=value2; ..."
+ const cookies = j.getCookies(url);
+ // [{key: 'key1', value: 'value1', domain: "www.google.com", ...}, ...]
+})
+```
+
+[back to top](#table-of-contents)
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 000000000..acd3f33ce
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,2 @@
+
+comment: false
diff --git a/disabled.appveyor.yml b/disabled.appveyor.yml
new file mode 100644
index 000000000..238f3d695
--- /dev/null
+++ b/disabled.appveyor.yml
@@ -0,0 +1,36 @@
+# http://www.appveyor.com/docs/appveyor-yml
+
+# Fix line endings in Windows. (runs before repo cloning)
+init:
+ - git config --global core.autocrlf input
+
+# Test against these versions of Node.js.
+environment:
+ matrix:
+ - nodejs_version: "0.10"
+ - nodejs_version: "0.8"
+ - nodejs_version: "0.11"
+
+# Allow failing jobs for bleeding-edge Node.js versions.
+matrix:
+ allow_failures:
+ - nodejs_version: "0.11"
+
+# Install scripts. (runs after repo cloning)
+install:
+ # Get the latest stable version of Node 0.STABLE.latest
+ - ps: Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version)
+ # Typical npm stuff.
+ - npm install
+
+# Post-install test scripts.
+test_script:
+ # Output useful info for debugging.
+ - ps: "npm test # PowerShell" # Pass comment to PS for easier debugging
+ - cmd: npm test
+
+# Don't actually build.
+build: off
+
+# Set build version format here instead of in the admin panel.
+version: "{build}"
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 000000000..615a33da5
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,135 @@
+
+# Authentication
+
+## OAuth
+
+### OAuth1.0 Refresh Token
+
+- http://oauth.googlecode.com/svn/spec/ext/session/1.0/drafts/1/spec.html#anchor4
+- https://developer.yahoo.com/oauth/guide/oauth-refreshaccesstoken.html
+
+```js
+request.post('https://api.login.yahoo.com/oauth/v2/get_token', {
+ oauth: {
+ consumer_key: '...',
+ consumer_secret: '...',
+ token: '...',
+ token_secret: '...',
+ session_handle: '...'
+ }
+}, function (err, res, body) {
+ var result = require('querystring').parse(body)
+ // assert.equal(typeof result, 'object')
+})
+```
+
+### OAuth2 Refresh Token
+
+- https://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-6
+
+```js
+request.post('https://accounts.google.com/o/oauth2/token', {
+ form: {
+ grant_type: 'refresh_token',
+ client_id: '...',
+ client_secret: '...',
+ refresh_token: '...'
+ },
+ json: true
+}, function (err, res, body) {
+ // assert.equal(typeof body, 'object')
+})
+```
+
+# Multipart
+
+## multipart/form-data
+
+### Flickr Image Upload
+
+- https://www.flickr.com/services/api/upload.api.html
+
+```js
+request.post('https://up.flickr.com/services/upload', {
+ oauth: {
+ consumer_key: '...',
+ consumer_secret: '...',
+ token: '...',
+ token_secret: '...'
+ },
+ // all meta data should be included here for proper signing
+ qs: {
+ title: 'My cat is awesome',
+ description: 'Sent on ' + new Date(),
+ is_public: 1
+ },
+ // again the same meta data + the actual photo
+ formData: {
+ title: 'My cat is awesome',
+ description: 'Sent on ' + new Date(),
+ is_public: 1,
+ photo:fs.createReadStream('cat.png')
+ },
+ json: true
+}, function (err, res, body) {
+ // assert.equal(typeof body, 'object')
+})
+```
+
+# Streams
+
+## `POST` data
+
+Use Request as a Writable stream to easily `POST` Readable streams (like files, other HTTP requests, or otherwise).
+
+TL;DR: Pipe a Readable Stream onto Request via:
+
+```
+READABLE.pipe(request.post(URL));
+```
+
+A more detailed example:
+
+```js
+var fs = require('fs')
+ , path = require('path')
+ , http = require('http')
+ , request = require('request')
+ , TMP_FILE_PATH = path.join(path.sep, 'tmp', 'foo')
+;
+
+// write a temporary file:
+fs.writeFileSync(TMP_FILE_PATH, 'foo bar baz quk\n');
+
+http.createServer(function(req, res) {
+ console.log('the server is receiving data!\n');
+ req
+ .on('end', res.end.bind(res))
+ .pipe(process.stdout)
+ ;
+}).listen(3000).unref();
+
+fs.createReadStream(TMP_FILE_PATH)
+ .pipe(request.post('http://127.0.0.1:3000'))
+;
+```
+
+# Proxys
+
+Run tor on the terminal and try the following. (Needs `socks5-http-client` to connect to tor)
+
+```js
+var request = require('../index.js');
+var Agent = require('socks5-http-client/lib/Agent');
+
+request.get({
+ url: 'http://www.tenreads.io',
+ agentClass: Agent,
+ agentOptions: {
+ socksHost: 'localhost', // Defaults to 'localhost'.
+ socksPort: 9050 // Defaults to 1080.
+ }
+}, function (err, res) {
+ console.log(res.body);
+});
+```
diff --git a/forever.js b/forever.js
deleted file mode 100644
index ac853c0d2..000000000
--- a/forever.js
+++ /dev/null
@@ -1,103 +0,0 @@
-module.exports = ForeverAgent
-ForeverAgent.SSL = ForeverAgentSSL
-
-var util = require('util')
- , Agent = require('http').Agent
- , net = require('net')
- , tls = require('tls')
- , AgentSSL = require('https').Agent
-
-function ForeverAgent(options) {
- var self = this
- self.options = options || {}
- self.requests = {}
- self.sockets = {}
- self.freeSockets = {}
- self.maxSockets = self.options.maxSockets || Agent.defaultMaxSockets
- self.minSockets = self.options.minSockets || ForeverAgent.defaultMinSockets
- self.on('free', function(socket, host, port) {
- var name = host + ':' + port
- if (self.requests[name] && self.requests[name].length) {
- self.requests[name].shift().onSocket(socket)
- } else if (self.sockets[name].length < self.minSockets) {
- if (!self.freeSockets[name]) self.freeSockets[name] = []
- self.freeSockets[name].push(socket)
-
- // if an error happens while we don't use the socket anyway, meh, throw the socket away
- function onIdleError() {
- socket.destroy()
- }
- socket._onIdleError = onIdleError
- socket.on('error', onIdleError)
- } else {
- // If there are no pending requests just destroy the
- // socket and it will get removed from the pool. This
- // gets us out of timeout issues and allows us to
- // default to Connection:keep-alive.
- socket.destroy();
- }
- })
-
-}
-util.inherits(ForeverAgent, Agent)
-
-ForeverAgent.defaultMinSockets = 5
-
-
-ForeverAgent.prototype.createConnection = net.createConnection
-ForeverAgent.prototype.addRequestNoreuse = Agent.prototype.addRequest
-ForeverAgent.prototype.addRequest = function(req, host, port) {
- var name = host + ':' + port
- if (this.freeSockets[name] && this.freeSockets[name].length > 0 && !req.useChunkedEncodingByDefault) {
- var idleSocket = this.freeSockets[name].pop()
- idleSocket.removeListener('error', idleSocket._onIdleError)
- delete idleSocket._onIdleError
- req._reusedSocket = true
- req.onSocket(idleSocket)
- } else {
- this.addRequestNoreuse(req, host, port)
- }
-}
-
-ForeverAgent.prototype.removeSocket = function(s, name, host, port) {
- if (this.sockets[name]) {
- var index = this.sockets[name].indexOf(s);
- if (index !== -1) {
- this.sockets[name].splice(index, 1);
- }
- } else if (this.sockets[name] && this.sockets[name].length === 0) {
- // don't leak
- delete this.sockets[name];
- delete this.requests[name];
- }
-
- if (this.freeSockets[name]) {
- var index = this.freeSockets[name].indexOf(s)
- if (index !== -1) {
- this.freeSockets[name].splice(index, 1)
- if (this.freeSockets[name].length === 0) {
- delete this.freeSockets[name]
- }
- }
- }
-
- if (this.requests[name] && this.requests[name].length) {
- // If we have pending requests and a socket gets closed a new one
- // needs to be created to take over in the pool for the one that closed.
- this.createSocket(name, host, port).emit('free');
- }
-}
-
-function ForeverAgentSSL (options) {
- ForeverAgent.call(this, options)
-}
-util.inherits(ForeverAgentSSL, ForeverAgent)
-
-ForeverAgentSSL.prototype.createConnection = createConnectionSSL
-ForeverAgentSSL.prototype.addRequestNoreuse = AgentSSL.prototype.addRequest
-
-function createConnectionSSL (port, host, options) {
- options.port = port
- options.host = host
- return tls.connect(options)
-}
diff --git a/index.js b/index.js
new file mode 100755
index 000000000..d50f9917b
--- /dev/null
+++ b/index.js
@@ -0,0 +1,155 @@
+// Copyright 2010-2012 Mikeal Rogers
+//
+// 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.
+
+'use strict'
+
+var extend = require('extend')
+var cookies = require('./lib/cookies')
+var helpers = require('./lib/helpers')
+
+var paramsHaveRequestBody = helpers.paramsHaveRequestBody
+
+// organize params for patch, post, put, head, del
+function initParams (uri, options, callback) {
+ if (typeof options === 'function') {
+ callback = options
+ }
+
+ var params = {}
+ if (options !== null && typeof options === 'object') {
+ extend(params, options, {uri: uri})
+ } else if (typeof uri === 'string') {
+ extend(params, {uri: uri})
+ } else {
+ extend(params, uri)
+ }
+
+ params.callback = callback || params.callback
+ return params
+}
+
+function request (uri, options, callback) {
+ if (typeof uri === 'undefined') {
+ throw new Error('undefined is not a valid uri or options object.')
+ }
+
+ var params = initParams(uri, options, callback)
+
+ if (params.method === 'HEAD' && paramsHaveRequestBody(params)) {
+ throw new Error('HTTP HEAD requests MUST NOT include a request body.')
+ }
+
+ return new request.Request(params)
+}
+
+function verbFunc (verb) {
+ var method = verb.toUpperCase()
+ return function (uri, options, callback) {
+ var params = initParams(uri, options, callback)
+ params.method = method
+ return request(params, params.callback)
+ }
+}
+
+// define like this to please codeintel/intellisense IDEs
+request.get = verbFunc('get')
+request.head = verbFunc('head')
+request.options = verbFunc('options')
+request.post = verbFunc('post')
+request.put = verbFunc('put')
+request.patch = verbFunc('patch')
+request.del = verbFunc('delete')
+request['delete'] = verbFunc('delete')
+
+request.jar = function (store) {
+ return cookies.jar(store)
+}
+
+request.cookie = function (str) {
+ return cookies.parse(str)
+}
+
+function wrapRequestMethod (method, options, requester, verb) {
+ return function (uri, opts, callback) {
+ var params = initParams(uri, opts, callback)
+
+ var target = {}
+ extend(true, target, options, params)
+
+ target.pool = params.pool || options.pool
+
+ if (verb) {
+ target.method = verb.toUpperCase()
+ }
+
+ if (typeof requester === 'function') {
+ method = requester
+ }
+
+ return method(target, target.callback)
+ }
+}
+
+request.defaults = function (options, requester) {
+ var self = this
+
+ options = options || {}
+
+ if (typeof options === 'function') {
+ requester = options
+ options = {}
+ }
+
+ var defaults = wrapRequestMethod(self, options, requester)
+
+ var verbs = ['get', 'head', 'post', 'put', 'patch', 'del', 'delete']
+ verbs.forEach(function (verb) {
+ defaults[verb] = wrapRequestMethod(self[verb], options, requester, verb)
+ })
+
+ defaults.cookie = wrapRequestMethod(self.cookie, options, requester)
+ defaults.jar = self.jar
+ defaults.defaults = self.defaults
+ return defaults
+}
+
+request.forever = function (agentOptions, optionsArg) {
+ var options = {}
+ if (optionsArg) {
+ extend(options, optionsArg)
+ }
+ if (agentOptions) {
+ options.agentOptions = agentOptions
+ }
+
+ options.forever = true
+ return request.defaults(options)
+}
+
+// Exports
+
+module.exports = request
+request.Request = require('./request')
+request.initParams = initParams
+
+// Backwards compatibility for request.debug
+Object.defineProperty(request, 'debug', {
+ enumerable: true,
+ get: function () {
+ return request.Request.debug
+ },
+ set: function (debug) {
+ request.Request.debug = debug
+ }
+})
diff --git a/lib/auth.js b/lib/auth.js
new file mode 100644
index 000000000..02f203869
--- /dev/null
+++ b/lib/auth.js
@@ -0,0 +1,167 @@
+'use strict'
+
+var caseless = require('caseless')
+var uuid = require('uuid/v4')
+var helpers = require('./helpers')
+
+var md5 = helpers.md5
+var toBase64 = helpers.toBase64
+
+function Auth (request) {
+ // define all public properties here
+ this.request = request
+ this.hasAuth = false
+ this.sentAuth = false
+ this.bearerToken = null
+ this.user = null
+ this.pass = null
+}
+
+Auth.prototype.basic = function (user, pass, sendImmediately) {
+ var self = this
+ if (typeof user !== 'string' || (pass !== undefined && typeof pass !== 'string')) {
+ self.request.emit('error', new Error('auth() received invalid user or password'))
+ }
+ self.user = user
+ self.pass = pass
+ self.hasAuth = true
+ var header = user + ':' + (pass || '')
+ if (sendImmediately || typeof sendImmediately === 'undefined') {
+ var authHeader = 'Basic ' + toBase64(header)
+ self.sentAuth = true
+ return authHeader
+ }
+}
+
+Auth.prototype.bearer = function (bearer, sendImmediately) {
+ var self = this
+ self.bearerToken = bearer
+ self.hasAuth = true
+ if (sendImmediately || typeof sendImmediately === 'undefined') {
+ if (typeof bearer === 'function') {
+ bearer = bearer()
+ }
+ var authHeader = 'Bearer ' + (bearer || '')
+ self.sentAuth = true
+ return authHeader
+ }
+}
+
+Auth.prototype.digest = function (method, path, authHeader) {
+ // TODO: More complete implementation of RFC 2617.
+ // - handle challenge.domain
+ // - support qop="auth-int" only
+ // - handle Authentication-Info (not necessarily?)
+ // - check challenge.stale (not necessarily?)
+ // - increase nc (not necessarily?)
+ // For reference:
+ // http://tools.ietf.org/html/rfc2617#section-3
+ // https://github.com/bagder/curl/blob/master/lib/http_digest.c
+
+ var self = this
+
+ var challenge = {}
+ var re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi
+ while (true) {
+ var match = re.exec(authHeader)
+ if (!match) {
+ break
+ }
+ challenge[match[1]] = match[2] || match[3]
+ }
+
+ /**
+ * RFC 2617: handle both MD5 and MD5-sess algorithms.
+ *
+ * If the algorithm directive's value is "MD5" or unspecified, then HA1 is
+ * HA1=MD5(username:realm:password)
+ * If the algorithm directive's value is "MD5-sess", then HA1 is
+ * HA1=MD5(MD5(username:realm:password):nonce:cnonce)
+ */
+ var ha1Compute = function (algorithm, user, realm, pass, nonce, cnonce) {
+ var ha1 = md5(user + ':' + realm + ':' + pass)
+ if (algorithm && algorithm.toLowerCase() === 'md5-sess') {
+ return md5(ha1 + ':' + nonce + ':' + cnonce)
+ } else {
+ return ha1
+ }
+ }
+
+ var qop = /(^|,)\s*auth\s*($|,)/.test(challenge.qop) && 'auth'
+ var nc = qop && '00000001'
+ var cnonce = qop && uuid().replace(/-/g, '')
+ var ha1 = ha1Compute(challenge.algorithm, self.user, challenge.realm, self.pass, challenge.nonce, cnonce)
+ var ha2 = md5(method + ':' + path)
+ var digestResponse = qop
+ ? md5(ha1 + ':' + challenge.nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2)
+ : md5(ha1 + ':' + challenge.nonce + ':' + ha2)
+ var authValues = {
+ username: self.user,
+ realm: challenge.realm,
+ nonce: challenge.nonce,
+ uri: path,
+ qop: qop,
+ response: digestResponse,
+ nc: nc,
+ cnonce: cnonce,
+ algorithm: challenge.algorithm,
+ opaque: challenge.opaque
+ }
+
+ authHeader = []
+ for (var k in authValues) {
+ if (authValues[k]) {
+ if (k === 'qop' || k === 'nc' || k === 'algorithm') {
+ authHeader.push(k + '=' + authValues[k])
+ } else {
+ authHeader.push(k + '="' + authValues[k] + '"')
+ }
+ }
+ }
+ authHeader = 'Digest ' + authHeader.join(', ')
+ self.sentAuth = true
+ return authHeader
+}
+
+Auth.prototype.onRequest = function (user, pass, sendImmediately, bearer) {
+ var self = this
+ var request = self.request
+
+ var authHeader
+ if (bearer === undefined && user === undefined) {
+ self.request.emit('error', new Error('no auth mechanism defined'))
+ } else if (bearer !== undefined) {
+ authHeader = self.bearer(bearer, sendImmediately)
+ } else {
+ authHeader = self.basic(user, pass, sendImmediately)
+ }
+ if (authHeader) {
+ request.setHeader('authorization', authHeader)
+ }
+}
+
+Auth.prototype.onResponse = function (response) {
+ var self = this
+ var request = self.request
+
+ if (!self.hasAuth || self.sentAuth) { return null }
+
+ var c = caseless(response.headers)
+
+ var authHeader = c.get('www-authenticate')
+ var authVerb = authHeader && authHeader.split(' ')[0].toLowerCase()
+ request.debug('reauth', authVerb)
+
+ switch (authVerb) {
+ case 'basic':
+ return self.basic(self.user, self.pass, true)
+
+ case 'bearer':
+ return self.bearer(self.bearerToken, true)
+
+ case 'digest':
+ return self.digest(request.method, request.path, authHeader)
+ }
+}
+
+exports.Auth = Auth
diff --git a/lib/cookies.js b/lib/cookies.js
new file mode 100644
index 000000000..bd5d46bea
--- /dev/null
+++ b/lib/cookies.js
@@ -0,0 +1,38 @@
+'use strict'
+
+var tough = require('tough-cookie')
+
+var Cookie = tough.Cookie
+var CookieJar = tough.CookieJar
+
+exports.parse = function (str) {
+ if (str && str.uri) {
+ str = str.uri
+ }
+ if (typeof str !== 'string') {
+ throw new Error('The cookie function only accepts STRING as param')
+ }
+ return Cookie.parse(str, {loose: true})
+}
+
+// Adapt the sometimes-Async api of tough.CookieJar to our requirements
+function RequestJar (store) {
+ var self = this
+ self._jar = new CookieJar(store, {looseMode: true})
+}
+RequestJar.prototype.setCookie = function (cookieOrStr, uri, options) {
+ var self = this
+ return self._jar.setCookieSync(cookieOrStr, uri, options || {})
+}
+RequestJar.prototype.getCookieString = function (uri) {
+ var self = this
+ return self._jar.getCookieStringSync(uri)
+}
+RequestJar.prototype.getCookies = function (uri) {
+ var self = this
+ return self._jar.getCookiesSync(uri)
+}
+
+exports.jar = function (store) {
+ return new RequestJar(store)
+}
diff --git a/lib/getProxyFromURI.js b/lib/getProxyFromURI.js
new file mode 100644
index 000000000..0b9b18e5a
--- /dev/null
+++ b/lib/getProxyFromURI.js
@@ -0,0 +1,79 @@
+'use strict'
+
+function formatHostname (hostname) {
+ // canonicalize the hostname, so that 'oogle.com' won't match 'google.com'
+ return hostname.replace(/^\.*/, '.').toLowerCase()
+}
+
+function parseNoProxyZone (zone) {
+ zone = zone.trim().toLowerCase()
+
+ var zoneParts = zone.split(':', 2)
+ var zoneHost = formatHostname(zoneParts[0])
+ var zonePort = zoneParts[1]
+ var hasPort = zone.indexOf(':') > -1
+
+ return {hostname: zoneHost, port: zonePort, hasPort: hasPort}
+}
+
+function uriInNoProxy (uri, noProxy) {
+ var port = uri.port || (uri.protocol === 'https:' ? '443' : '80')
+ var hostname = formatHostname(uri.hostname)
+ var noProxyList = noProxy.split(',')
+
+ // iterate through the noProxyList until it finds a match.
+ return noProxyList.map(parseNoProxyZone).some(function (noProxyZone) {
+ var isMatchedAt = hostname.indexOf(noProxyZone.hostname)
+ var hostnameMatched = (
+ isMatchedAt > -1 &&
+ (isMatchedAt === hostname.length - noProxyZone.hostname.length)
+ )
+
+ if (noProxyZone.hasPort) {
+ return (port === noProxyZone.port) && hostnameMatched
+ }
+
+ return hostnameMatched
+ })
+}
+
+function getProxyFromURI (uri) {
+ // Decide the proper request proxy to use based on the request URI object and the
+ // environmental variables (NO_PROXY, HTTP_PROXY, etc.)
+ // respect NO_PROXY environment variables (see: https://lynx.invisible-island.net/lynx2.8.7/breakout/lynx_help/keystrokes/environments.html)
+
+ var noProxy = process.env.NO_PROXY || process.env.no_proxy || ''
+
+ // if the noProxy is a wildcard then return null
+
+ if (noProxy === '*') {
+ return null
+ }
+
+ // if the noProxy is not empty and the uri is found return null
+
+ if (noProxy !== '' && uriInNoProxy(uri, noProxy)) {
+ return null
+ }
+
+ // Check for HTTP or HTTPS Proxy in environment Else default to null
+
+ if (uri.protocol === 'http:') {
+ return process.env.HTTP_PROXY ||
+ process.env.http_proxy || null
+ }
+
+ if (uri.protocol === 'https:') {
+ return process.env.HTTPS_PROXY ||
+ process.env.https_proxy ||
+ process.env.HTTP_PROXY ||
+ process.env.http_proxy || null
+ }
+
+ // if none of that works, return null
+ // (What uri protocol are you using then?)
+
+ return null
+}
+
+module.exports = getProxyFromURI
diff --git a/lib/har.js b/lib/har.js
new file mode 100644
index 000000000..0dedee444
--- /dev/null
+++ b/lib/har.js
@@ -0,0 +1,205 @@
+'use strict'
+
+var fs = require('fs')
+var qs = require('querystring')
+var validate = require('har-validator')
+var extend = require('extend')
+
+function Har (request) {
+ this.request = request
+}
+
+Har.prototype.reducer = function (obj, pair) {
+ // new property ?
+ if (obj[pair.name] === undefined) {
+ obj[pair.name] = pair.value
+ return obj
+ }
+
+ // existing? convert to array
+ var arr = [
+ obj[pair.name],
+ pair.value
+ ]
+
+ obj[pair.name] = arr
+
+ return obj
+}
+
+Har.prototype.prep = function (data) {
+ // construct utility properties
+ data.queryObj = {}
+ data.headersObj = {}
+ data.postData.jsonObj = false
+ data.postData.paramsObj = false
+
+ // construct query objects
+ if (data.queryString && data.queryString.length) {
+ data.queryObj = data.queryString.reduce(this.reducer, {})
+ }
+
+ // construct headers objects
+ if (data.headers && data.headers.length) {
+ // loweCase header keys
+ data.headersObj = data.headers.reduceRight(function (headers, header) {
+ headers[header.name] = header.value
+ return headers
+ }, {})
+ }
+
+ // construct Cookie header
+ if (data.cookies && data.cookies.length) {
+ var cookies = data.cookies.map(function (cookie) {
+ return cookie.name + '=' + cookie.value
+ })
+
+ if (cookies.length) {
+ data.headersObj.cookie = cookies.join('; ')
+ }
+ }
+
+ // prep body
+ function some (arr) {
+ return arr.some(function (type) {
+ return data.postData.mimeType.indexOf(type) === 0
+ })
+ }
+
+ if (some([
+ 'multipart/mixed',
+ 'multipart/related',
+ 'multipart/form-data',
+ 'multipart/alternative'])) {
+ // reset values
+ data.postData.mimeType = 'multipart/form-data'
+ } else if (some([
+ 'application/x-www-form-urlencoded'])) {
+ if (!data.postData.params) {
+ data.postData.text = ''
+ } else {
+ data.postData.paramsObj = data.postData.params.reduce(this.reducer, {})
+
+ // always overwrite
+ data.postData.text = qs.stringify(data.postData.paramsObj)
+ }
+ } else if (some([
+ 'text/json',
+ 'text/x-json',
+ 'application/json',
+ 'application/x-json'])) {
+ data.postData.mimeType = 'application/json'
+
+ if (data.postData.text) {
+ try {
+ data.postData.jsonObj = JSON.parse(data.postData.text)
+ } catch (e) {
+ this.request.debug(e)
+
+ // force back to text/plain
+ data.postData.mimeType = 'text/plain'
+ }
+ }
+ }
+
+ return data
+}
+
+Har.prototype.options = function (options) {
+ // skip if no har property defined
+ if (!options.har) {
+ return options
+ }
+
+ var har = {}
+ extend(har, options.har)
+
+ // only process the first entry
+ if (har.log && har.log.entries) {
+ har = har.log.entries[0]
+ }
+
+ // add optional properties to make validation successful
+ har.url = har.url || options.url || options.uri || options.baseUrl || '/'
+ har.httpVersion = har.httpVersion || 'HTTP/1.1'
+ har.queryString = har.queryString || []
+ har.headers = har.headers || []
+ har.cookies = har.cookies || []
+ har.postData = har.postData || {}
+ har.postData.mimeType = har.postData.mimeType || 'application/octet-stream'
+
+ har.bodySize = 0
+ har.headersSize = 0
+ har.postData.size = 0
+
+ if (!validate.request(har)) {
+ return options
+ }
+
+ // clean up and get some utility properties
+ var req = this.prep(har)
+
+ // construct new options
+ if (req.url) {
+ options.url = req.url
+ }
+
+ if (req.method) {
+ options.method = req.method
+ }
+
+ if (Object.keys(req.queryObj).length) {
+ options.qs = req.queryObj
+ }
+
+ if (Object.keys(req.headersObj).length) {
+ options.headers = req.headersObj
+ }
+
+ function test (type) {
+ return req.postData.mimeType.indexOf(type) === 0
+ }
+ if (test('application/x-www-form-urlencoded')) {
+ options.form = req.postData.paramsObj
+ } else if (test('application/json')) {
+ if (req.postData.jsonObj) {
+ options.body = req.postData.jsonObj
+ options.json = true
+ }
+ } else if (test('multipart/form-data')) {
+ options.formData = {}
+
+ req.postData.params.forEach(function (param) {
+ var attachment = {}
+
+ if (!param.fileName && !param.contentType) {
+ options.formData[param.name] = param.value
+ return
+ }
+
+ // attempt to read from disk!
+ if (param.fileName && !param.value) {
+ attachment.value = fs.createReadStream(param.fileName)
+ } else if (param.value) {
+ attachment.value = param.value
+ }
+
+ if (param.fileName) {
+ attachment.options = {
+ filename: param.fileName,
+ contentType: param.contentType ? param.contentType : null
+ }
+ }
+
+ options.formData[param.name] = attachment
+ })
+ } else {
+ if (req.postData.text) {
+ options.body = req.postData.text
+ }
+ }
+
+ return options
+}
+
+exports.Har = Har
diff --git a/lib/hawk.js b/lib/hawk.js
new file mode 100644
index 000000000..de48a9851
--- /dev/null
+++ b/lib/hawk.js
@@ -0,0 +1,89 @@
+'use strict'
+
+var crypto = require('crypto')
+
+function randomString (size) {
+ var bits = (size + 1) * 6
+ var buffer = crypto.randomBytes(Math.ceil(bits / 8))
+ var string = buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
+ return string.slice(0, size)
+}
+
+function calculatePayloadHash (payload, algorithm, contentType) {
+ var hash = crypto.createHash(algorithm)
+ hash.update('hawk.1.payload\n')
+ hash.update((contentType ? contentType.split(';')[0].trim().toLowerCase() : '') + '\n')
+ hash.update(payload || '')
+ hash.update('\n')
+ return hash.digest('base64')
+}
+
+exports.calculateMac = function (credentials, opts) {
+ var normalized = 'hawk.1.header\n' +
+ opts.ts + '\n' +
+ opts.nonce + '\n' +
+ (opts.method || '').toUpperCase() + '\n' +
+ opts.resource + '\n' +
+ opts.host.toLowerCase() + '\n' +
+ opts.port + '\n' +
+ (opts.hash || '') + '\n'
+
+ if (opts.ext) {
+ normalized = normalized + opts.ext.replace('\\', '\\\\').replace('\n', '\\n')
+ }
+
+ normalized = normalized + '\n'
+
+ if (opts.app) {
+ normalized = normalized + opts.app + '\n' + (opts.dlg || '') + '\n'
+ }
+
+ var hmac = crypto.createHmac(credentials.algorithm, credentials.key).update(normalized)
+ var digest = hmac.digest('base64')
+ return digest
+}
+
+exports.header = function (uri, method, opts) {
+ var timestamp = opts.timestamp || Math.floor((Date.now() + (opts.localtimeOffsetMsec || 0)) / 1000)
+ var credentials = opts.credentials
+ if (!credentials || !credentials.id || !credentials.key || !credentials.algorithm) {
+ return ''
+ }
+
+ if (['sha1', 'sha256'].indexOf(credentials.algorithm) === -1) {
+ return ''
+ }
+
+ var artifacts = {
+ ts: timestamp,
+ nonce: opts.nonce || randomString(6),
+ method: method,
+ resource: uri.pathname + (uri.search || ''),
+ host: uri.hostname,
+ port: uri.port || (uri.protocol === 'http:' ? 80 : 443),
+ hash: opts.hash,
+ ext: opts.ext,
+ app: opts.app,
+ dlg: opts.dlg
+ }
+
+ if (!artifacts.hash && (opts.payload || opts.payload === '')) {
+ artifacts.hash = calculatePayloadHash(opts.payload, credentials.algorithm, opts.contentType)
+ }
+
+ var mac = exports.calculateMac(credentials, artifacts)
+
+ var hasExt = artifacts.ext !== null && artifacts.ext !== undefined && artifacts.ext !== ''
+ var header = 'Hawk id="' + credentials.id +
+ '", ts="' + artifacts.ts +
+ '", nonce="' + artifacts.nonce +
+ (artifacts.hash ? '", hash="' + artifacts.hash : '') +
+ (hasExt ? '", ext="' + artifacts.ext.replace(/\\/g, '\\\\').replace(/"/g, '\\"') : '') +
+ '", mac="' + mac + '"'
+
+ if (artifacts.app) {
+ header = header + ', app="' + artifacts.app + (artifacts.dlg ? '", dlg="' + artifacts.dlg : '') + '"'
+ }
+
+ return header
+}
diff --git a/lib/helpers.js b/lib/helpers.js
new file mode 100644
index 000000000..8b2a7e6eb
--- /dev/null
+++ b/lib/helpers.js
@@ -0,0 +1,66 @@
+'use strict'
+
+var jsonSafeStringify = require('json-stringify-safe')
+var crypto = require('crypto')
+var Buffer = require('safe-buffer').Buffer
+
+var defer = typeof setImmediate === 'undefined'
+ ? process.nextTick
+ : setImmediate
+
+function paramsHaveRequestBody (params) {
+ return (
+ params.body ||
+ params.requestBodyStream ||
+ (params.json && typeof params.json !== 'boolean') ||
+ params.multipart
+ )
+}
+
+function safeStringify (obj, replacer) {
+ var ret
+ try {
+ ret = JSON.stringify(obj, replacer)
+ } catch (e) {
+ ret = jsonSafeStringify(obj, replacer)
+ }
+ return ret
+}
+
+function md5 (str) {
+ return crypto.createHash('md5').update(str).digest('hex')
+}
+
+function isReadStream (rs) {
+ return rs.readable && rs.path && rs.mode
+}
+
+function toBase64 (str) {
+ return Buffer.from(str || '', 'utf8').toString('base64')
+}
+
+function copy (obj) {
+ var o = {}
+ Object.keys(obj).forEach(function (i) {
+ o[i] = obj[i]
+ })
+ return o
+}
+
+function version () {
+ var numbers = process.version.replace('v', '').split('.')
+ return {
+ major: parseInt(numbers[0], 10),
+ minor: parseInt(numbers[1], 10),
+ patch: parseInt(numbers[2], 10)
+ }
+}
+
+exports.paramsHaveRequestBody = paramsHaveRequestBody
+exports.safeStringify = safeStringify
+exports.md5 = md5
+exports.isReadStream = isReadStream
+exports.toBase64 = toBase64
+exports.copy = copy
+exports.version = version
+exports.defer = defer
diff --git a/lib/multipart.js b/lib/multipart.js
new file mode 100644
index 000000000..6a009bc13
--- /dev/null
+++ b/lib/multipart.js
@@ -0,0 +1,112 @@
+'use strict'
+
+var uuid = require('uuid/v4')
+var CombinedStream = require('combined-stream')
+var isstream = require('isstream')
+var Buffer = require('safe-buffer').Buffer
+
+function Multipart (request) {
+ this.request = request
+ this.boundary = uuid()
+ this.chunked = false
+ this.body = null
+}
+
+Multipart.prototype.isChunked = function (options) {
+ var self = this
+ var chunked = false
+ var parts = options.data || options
+
+ if (!parts.forEach) {
+ self.request.emit('error', new Error('Argument error, options.multipart.'))
+ }
+
+ if (options.chunked !== undefined) {
+ chunked = options.chunked
+ }
+
+ if (self.request.getHeader('transfer-encoding') === 'chunked') {
+ chunked = true
+ }
+
+ if (!chunked) {
+ parts.forEach(function (part) {
+ if (typeof part.body === 'undefined') {
+ self.request.emit('error', new Error('Body attribute missing in multipart.'))
+ }
+ if (isstream(part.body)) {
+ chunked = true
+ }
+ })
+ }
+
+ return chunked
+}
+
+Multipart.prototype.setHeaders = function (chunked) {
+ var self = this
+
+ if (chunked && !self.request.hasHeader('transfer-encoding')) {
+ self.request.setHeader('transfer-encoding', 'chunked')
+ }
+
+ var header = self.request.getHeader('content-type')
+
+ if (!header || header.indexOf('multipart') === -1) {
+ self.request.setHeader('content-type', 'multipart/related; boundary=' + self.boundary)
+ } else {
+ if (header.indexOf('boundary') !== -1) {
+ self.boundary = header.replace(/.*boundary=([^\s;]+).*/, '$1')
+ } else {
+ self.request.setHeader('content-type', header + '; boundary=' + self.boundary)
+ }
+ }
+}
+
+Multipart.prototype.build = function (parts, chunked) {
+ var self = this
+ var body = chunked ? new CombinedStream() : []
+
+ function add (part) {
+ if (typeof part === 'number') {
+ part = part.toString()
+ }
+ return chunked ? body.append(part) : body.push(Buffer.from(part))
+ }
+
+ if (self.request.preambleCRLF) {
+ add('\r\n')
+ }
+
+ parts.forEach(function (part) {
+ var preamble = '--' + self.boundary + '\r\n'
+ Object.keys(part).forEach(function (key) {
+ if (key === 'body') { return }
+ preamble += key + ': ' + part[key] + '\r\n'
+ })
+ preamble += '\r\n'
+ add(preamble)
+ add(part.body)
+ add('\r\n')
+ })
+ add('--' + self.boundary + '--')
+
+ if (self.request.postambleCRLF) {
+ add('\r\n')
+ }
+
+ return body
+}
+
+Multipart.prototype.onRequest = function (options) {
+ var self = this
+
+ var chunked = self.isChunked(options)
+ var parts = options.data || options
+
+ self.setHeaders(chunked)
+ self.chunked = chunked
+ self.body = self.build(parts, chunked)
+}
+
+exports.Multipart = Multipart
diff --git a/lib/oauth.js b/lib/oauth.js
new file mode 100644
index 000000000..96de72b8e
--- /dev/null
+++ b/lib/oauth.js
@@ -0,0 +1,148 @@
+'use strict'
+
+var url = require('url')
+var qs = require('qs')
+var caseless = require('caseless')
+var uuid = require('uuid/v4')
+var oauth = require('oauth-sign')
+var crypto = require('crypto')
+var Buffer = require('safe-buffer').Buffer
+
+function OAuth (request) {
+ this.request = request
+ this.params = null
+}
+
+OAuth.prototype.buildParams = function (_oauth, uri, method, query, form, qsLib) {
+ var oa = {}
+ for (var i in _oauth) {
+ oa['oauth_' + i] = _oauth[i]
+ }
+ if (!oa.oauth_version) {
+ oa.oauth_version = '1.0'
+ }
+ if (!oa.oauth_timestamp) {
+ oa.oauth_timestamp = Math.floor(Date.now() / 1000).toString()
+ }
+ if (!oa.oauth_nonce) {
+ oa.oauth_nonce = uuid().replace(/-/g, '')
+ }
+ if (!oa.oauth_signature_method) {
+ oa.oauth_signature_method = 'HMAC-SHA1'
+ }
+
+ var consumer_secret_or_private_key = oa.oauth_consumer_secret || oa.oauth_private_key // eslint-disable-line camelcase
+ delete oa.oauth_consumer_secret
+ delete oa.oauth_private_key
+
+ var token_secret = oa.oauth_token_secret // eslint-disable-line camelcase
+ delete oa.oauth_token_secret
+
+ var realm = oa.oauth_realm
+ delete oa.oauth_realm
+ delete oa.oauth_transport_method
+
+ var baseurl = uri.protocol + '//' + uri.host + uri.pathname
+ var params = qsLib.parse([].concat(query, form, qsLib.stringify(oa)).join('&'))
+
+ oa.oauth_signature = oauth.sign(
+ oa.oauth_signature_method,
+ method,
+ baseurl,
+ params,
+ consumer_secret_or_private_key, // eslint-disable-line camelcase
+ token_secret // eslint-disable-line camelcase
+ )
+
+ if (realm) {
+ oa.realm = realm
+ }
+
+ return oa
+}
+
+OAuth.prototype.buildBodyHash = function (_oauth, body) {
+ if (['HMAC-SHA1', 'RSA-SHA1'].indexOf(_oauth.signature_method || 'HMAC-SHA1') < 0) {
+ this.request.emit('error', new Error('oauth: ' + _oauth.signature_method +
+ ' signature_method not supported with body_hash signing.'))
+ }
+
+ var shasum = crypto.createHash('sha1')
+ shasum.update(body || '')
+ var sha1 = shasum.digest('hex')
+
+ return Buffer.from(sha1, 'hex').toString('base64')
+}
+
+OAuth.prototype.concatParams = function (oa, sep, wrap) {
+ wrap = wrap || ''
+
+ var params = Object.keys(oa).filter(function (i) {
+ return i !== 'realm' && i !== 'oauth_signature'
+ }).sort()
+
+ if (oa.realm) {
+ params.splice(0, 0, 'realm')
+ }
+ params.push('oauth_signature')
+
+ return params.map(function (i) {
+ return i + '=' + wrap + oauth.rfc3986(oa[i]) + wrap
+ }).join(sep)
+}
+
+OAuth.prototype.onRequest = function (_oauth) {
+ var self = this
+ self.params = _oauth
+
+ var uri = self.request.uri || {}
+ var method = self.request.method || ''
+ var headers = caseless(self.request.headers)
+ var body = self.request.body || ''
+ var qsLib = self.request.qsLib || qs
+
+ var form
+ var query
+ var contentType = headers.get('content-type') || ''
+ var formContentType = 'application/x-www-form-urlencoded'
+ var transport = _oauth.transport_method || 'header'
+
+ if (contentType.slice(0, formContentType.length) === formContentType) {
+ contentType = formContentType
+ form = body
+ }
+ if (uri.query) {
+ query = uri.query
+ }
+ if (transport === 'body' && (method !== 'POST' || contentType !== formContentType)) {
+ self.request.emit('error', new Error('oauth: transport_method of body requires POST ' +
+ 'and content-type ' + formContentType))
+ }
+
+ if (!form && typeof _oauth.body_hash === 'boolean') {
+ _oauth.body_hash = self.buildBodyHash(_oauth, self.request.body.toString())
+ }
+
+ var oa = self.buildParams(_oauth, uri, method, query, form, qsLib)
+
+ switch (transport) {
+ case 'header':
+ self.request.setHeader('Authorization', 'OAuth ' + self.concatParams(oa, ',', '"'))
+ break
+
+ case 'query':
+ var href = self.request.uri.href += (query ? '&' : '?') + self.concatParams(oa, '&')
+ self.request.uri = url.parse(href)
+ self.request.path = self.request.uri.path
+ break
+
+ case 'body':
+ self.request.body = (form ? form + '&' : '') + self.concatParams(oa, '&')
+ break
+
+ default:
+ self.request.emit('error', new Error('oauth: transport_method invalid'))
+ }
+}
+
+exports.OAuth = OAuth
diff --git a/lib/querystring.js b/lib/querystring.js
new file mode 100644
index 000000000..4a32cd149
--- /dev/null
+++ b/lib/querystring.js
@@ -0,0 +1,50 @@
+'use strict'
+
+var qs = require('qs')
+var querystring = require('querystring')
+
+function Querystring (request) {
+ this.request = request
+ this.lib = null
+ this.useQuerystring = null
+ this.parseOptions = null
+ this.stringifyOptions = null
+}
+
+Querystring.prototype.init = function (options) {
+ if (this.lib) { return }
+
+ this.useQuerystring = options.useQuerystring
+ this.lib = (this.useQuerystring ? querystring : qs)
+
+ this.parseOptions = options.qsParseOptions || {}
+ this.stringifyOptions = options.qsStringifyOptions || {}
+}
+
+Querystring.prototype.stringify = function (obj) {
+ return (this.useQuerystring)
+ ? this.rfc3986(this.lib.stringify(obj,
+ this.stringifyOptions.sep || null,
+ this.stringifyOptions.eq || null,
+ this.stringifyOptions))
+ : this.lib.stringify(obj, this.stringifyOptions)
+}
+
+Querystring.prototype.parse = function (str) {
+ return (this.useQuerystring)
+ ? this.lib.parse(str,
+ this.parseOptions.sep || null,
+ this.parseOptions.eq || null,
+ this.parseOptions)
+ : this.lib.parse(str, this.parseOptions)
+}
+
+Querystring.prototype.rfc3986 = function (str) {
+ return str.replace(/[!'()*]/g, function (c) {
+ return '%' + c.charCodeAt(0).toString(16).toUpperCase()
+ })
+}
+
+Querystring.prototype.unescape = querystring.unescape
+
+exports.Querystring = Querystring
diff --git a/lib/redirect.js b/lib/redirect.js
new file mode 100644
index 000000000..b9150e77c
--- /dev/null
+++ b/lib/redirect.js
@@ -0,0 +1,154 @@
+'use strict'
+
+var url = require('url')
+var isUrl = /^https?:/
+
+function Redirect (request) {
+ this.request = request
+ this.followRedirect = true
+ this.followRedirects = true
+ this.followAllRedirects = false
+ this.followOriginalHttpMethod = false
+ this.allowRedirect = function () { return true }
+ this.maxRedirects = 10
+ this.redirects = []
+ this.redirectsFollowed = 0
+ this.removeRefererHeader = false
+}
+
+Redirect.prototype.onRequest = function (options) {
+ var self = this
+
+ if (options.maxRedirects !== undefined) {
+ self.maxRedirects = options.maxRedirects
+ }
+ if (typeof options.followRedirect === 'function') {
+ self.allowRedirect = options.followRedirect
+ }
+ if (options.followRedirect !== undefined) {
+ self.followRedirects = !!options.followRedirect
+ }
+ if (options.followAllRedirects !== undefined) {
+ self.followAllRedirects = options.followAllRedirects
+ }
+ if (self.followRedirects || self.followAllRedirects) {
+ self.redirects = self.redirects || []
+ }
+ if (options.removeRefererHeader !== undefined) {
+ self.removeRefererHeader = options.removeRefererHeader
+ }
+ if (options.followOriginalHttpMethod !== undefined) {
+ self.followOriginalHttpMethod = options.followOriginalHttpMethod
+ }
+}
+
+Redirect.prototype.redirectTo = function (response) {
+ var self = this
+ var request = self.request
+
+ var redirectTo = null
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.caseless.has('location')) {
+ var location = response.caseless.get('location')
+ request.debug('redirect', location)
+
+ if (self.followAllRedirects) {
+ redirectTo = location
+ } else if (self.followRedirects) {
+ switch (request.method) {
+ case 'PATCH':
+ case 'PUT':
+ case 'POST':
+ case 'DELETE':
+ // Do not follow redirects
+ break
+ default:
+ redirectTo = location
+ break
+ }
+ }
+ } else if (response.statusCode === 401) {
+ var authHeader = request._auth.onResponse(response)
+ if (authHeader) {
+ request.setHeader('authorization', authHeader)
+ redirectTo = request.uri
+ }
+ }
+ return redirectTo
+}
+
+Redirect.prototype.onResponse = function (response) {
+ var self = this
+ var request = self.request
+
+ var redirectTo = self.redirectTo(response)
+ if (!redirectTo || !self.allowRedirect.call(request, response)) {
+ return false
+ }
+
+ request.debug('redirect to', redirectTo)
+
+ // ignore any potential response body. it cannot possibly be useful
+ // to us at this point.
+ // response.resume should be defined, but check anyway before calling. Workaround for browserify.
+ if (response.resume) {
+ response.resume()
+ }
+
+ if (self.redirectsFollowed >= self.maxRedirects) {
+ request.emit('error', new Error('Exceeded maxRedirects. Probably stuck in a redirect loop ' + request.uri.href))
+ return false
+ }
+ self.redirectsFollowed += 1
+
+ if (!isUrl.test(redirectTo)) {
+ redirectTo = url.resolve(request.uri.href, redirectTo)
+ }
+
+ var uriPrev = request.uri
+ request.uri = url.parse(redirectTo)
+
+ // handle the case where we change protocol from https to http or vice versa
+ if (request.uri.protocol !== uriPrev.protocol) {
+ delete request.agent
+ }
+
+ self.redirects.push({ statusCode: response.statusCode, redirectUri: redirectTo })
+
+ if (self.followAllRedirects && request.method !== 'HEAD' &&
+ response.statusCode !== 401 && response.statusCode !== 307) {
+ request.method = self.followOriginalHttpMethod ? request.method : 'GET'
+ }
+ // request.method = 'GET' // Force all redirects to use GET || commented out fixes #215
+ delete request.src
+ delete request.req
+ delete request._started
+ if (response.statusCode !== 401 && response.statusCode !== 307) {
+ // Remove parameters from the previous response, unless this is the second request
+ // for a server that requires digest authentication.
+ delete request.body
+ delete request._form
+ if (request.headers) {
+ request.removeHeader('host')
+ request.removeHeader('content-type')
+ request.removeHeader('content-length')
+ if (request.uri.hostname !== request.originalHost.split(':')[0]) {
+ // Remove authorization if changing hostnames (but not if just
+ // changing ports or protocols). This matches the behavior of curl:
+ // https://github.com/bagder/curl/blob/6beb0eee/lib/http.c#L710
+ request.removeHeader('authorization')
+ }
+ }
+ }
+
+ if (!self.removeRefererHeader) {
+ request.setHeader('referer', uriPrev.href)
+ }
+
+ request.emit('redirect')
+
+ request.init()
+
+ return true
+}
+
+exports.Redirect = Redirect
diff --git a/lib/tunnel.js b/lib/tunnel.js
new file mode 100644
index 000000000..4479003f6
--- /dev/null
+++ b/lib/tunnel.js
@@ -0,0 +1,175 @@
+'use strict'
+
+var url = require('url')
+var tunnel = require('tunnel-agent')
+
+var defaultProxyHeaderWhiteList = [
+ 'accept',
+ 'accept-charset',
+ 'accept-encoding',
+ 'accept-language',
+ 'accept-ranges',
+ 'cache-control',
+ 'content-encoding',
+ 'content-language',
+ 'content-location',
+ 'content-md5',
+ 'content-range',
+ 'content-type',
+ 'connection',
+ 'date',
+ 'expect',
+ 'max-forwards',
+ 'pragma',
+ 'referer',
+ 'te',
+ 'user-agent',
+ 'via'
+]
+
+var defaultProxyHeaderExclusiveList = [
+ 'proxy-authorization'
+]
+
+function constructProxyHost (uriObject) {
+ var port = uriObject.port
+ var protocol = uriObject.protocol
+ var proxyHost = uriObject.hostname + ':'
+
+ if (port) {
+ proxyHost += port
+ } else if (protocol === 'https:') {
+ proxyHost += '443'
+ } else {
+ proxyHost += '80'
+ }
+
+ return proxyHost
+}
+
+function constructProxyHeaderWhiteList (headers, proxyHeaderWhiteList) {
+ var whiteList = proxyHeaderWhiteList
+ .reduce(function (set, header) {
+ set[header.toLowerCase()] = true
+ return set
+ }, {})
+
+ return Object.keys(headers)
+ .filter(function (header) {
+ return whiteList[header.toLowerCase()]
+ })
+ .reduce(function (set, header) {
+ set[header] = headers[header]
+ return set
+ }, {})
+}
+
+function constructTunnelOptions (request, proxyHeaders) {
+ var proxy = request.proxy
+
+ var tunnelOptions = {
+ proxy: {
+ host: proxy.hostname,
+ port: +proxy.port,
+ proxyAuth: proxy.auth,
+ headers: proxyHeaders
+ },
+ headers: request.headers,
+ ca: request.ca,
+ cert: request.cert,
+ key: request.key,
+ passphrase: request.passphrase,
+ pfx: request.pfx,
+ ciphers: request.ciphers,
+ rejectUnauthorized: request.rejectUnauthorized,
+ secureOptions: request.secureOptions,
+ secureProtocol: request.secureProtocol
+ }
+
+ return tunnelOptions
+}
+
+function constructTunnelFnName (uri, proxy) {
+ var uriProtocol = (uri.protocol === 'https:' ? 'https' : 'http')
+ var proxyProtocol = (proxy.protocol === 'https:' ? 'Https' : 'Http')
+ return [uriProtocol, proxyProtocol].join('Over')
+}
+
+function getTunnelFn (request) {
+ var uri = request.uri
+ var proxy = request.proxy
+ var tunnelFnName = constructTunnelFnName(uri, proxy)
+ return tunnel[tunnelFnName]
+}
+
+function Tunnel (request) {
+ this.request = request
+ this.proxyHeaderWhiteList = defaultProxyHeaderWhiteList
+ this.proxyHeaderExclusiveList = []
+ if (typeof request.tunnel !== 'undefined') {
+ this.tunnelOverride = request.tunnel
+ }
+}
+
+Tunnel.prototype.isEnabled = function () {
+ var self = this
+ var request = self.request
+ // Tunnel HTTPS by default. Allow the user to override this setting.
+
+ // If self.tunnelOverride is set (the user specified a value), use it.
+ if (typeof self.tunnelOverride !== 'undefined') {
+ return self.tunnelOverride
+ }
+
+ // If the destination is HTTPS, tunnel.
+ if (request.uri.protocol === 'https:') {
+ return true
+ }
+
+ // Otherwise, do not use tunnel.
+ return false
+}
+
+Tunnel.prototype.setup = function (options) {
+ var self = this
+ var request = self.request
+
+ options = options || {}
+
+ if (typeof request.proxy === 'string') {
+ request.proxy = url.parse(request.proxy)
+ }
+
+ if (!request.proxy || !request.tunnel) {
+ return false
+ }
+
+ // Setup Proxy Header Exclusive List and White List
+ if (options.proxyHeaderWhiteList) {
+ self.proxyHeaderWhiteList = options.proxyHeaderWhiteList
+ }
+ if (options.proxyHeaderExclusiveList) {
+ self.proxyHeaderExclusiveList = options.proxyHeaderExclusiveList
+ }
+
+ var proxyHeaderExclusiveList = self.proxyHeaderExclusiveList.concat(defaultProxyHeaderExclusiveList)
+ var proxyHeaderWhiteList = self.proxyHeaderWhiteList.concat(proxyHeaderExclusiveList)
+
+ // Setup Proxy Headers and Proxy Headers Host
+ // Only send the Proxy White Listed Header names
+ var proxyHeaders = constructProxyHeaderWhiteList(request.headers, proxyHeaderWhiteList)
+ proxyHeaders.host = constructProxyHost(request.uri)
+
+ proxyHeaderExclusiveList.forEach(request.removeHeader, request)
+
+ // Set Agent from Tunnel Data
+ var tunnelFn = getTunnelFn(request)
+ var tunnelOptions = constructTunnelOptions(request, proxyHeaders)
+ request.agent = tunnelFn(tunnelOptions)
+
+ return true
+}
+
+Tunnel.defaultProxyHeaderWhiteList = defaultProxyHeaderWhiteList
+Tunnel.defaultProxyHeaderExclusiveList = defaultProxyHeaderExclusiveList
+exports.Tunnel = Tunnel
diff --git a/main.js b/main.js
deleted file mode 100644
index d734b0858..000000000
--- a/main.js
+++ /dev/null
@@ -1,913 +0,0 @@
-// Copyright 2010-2012 Mikeal Rogers
-//
-// 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.
-
-var http = require('http')
- , https = false
- , tls = false
- , url = require('url')
- , util = require('util')
- , stream = require('stream')
- , qs = require('querystring')
- , mimetypes = require('./mimetypes')
- , oauth = require('./oauth')
- , uuid = require('./uuid')
- , ForeverAgent = require('./forever')
- , Cookie = require('./vendor/cookie')
- , CookieJar = require('./vendor/cookie/jar')
- , cookieJar = new CookieJar
- , tunnel = require('./tunnel')
- ;
-
-if (process.logging) {
- var log = process.logging('request')
-}
-
-try {
- https = require('https')
-} catch (e) {}
-
-try {
- tls = require('tls')
-} catch (e) {}
-
-function toBase64 (str) {
- return (new Buffer(str || "", "ascii")).toString("base64")
-}
-
-// Hacky fix for pre-0.4.4 https
-if (https && !https.Agent) {
- https.Agent = function (options) {
- http.Agent.call(this, options)
- }
- util.inherits(https.Agent, http.Agent)
- https.Agent.prototype._getConnection = function(host, port, cb) {
- var s = tls.connect(port, host, this.options, function() {
- // do other checks here?
- if (cb) cb()
- })
- return s
- }
-}
-
-function isReadStream (rs) {
- if (rs.readable && rs.path && rs.mode) {
- return true
- }
-}
-
-function copy (obj) {
- var o = {}
- Object.keys(obj).forEach(function (i) {
- o[i] = obj[i]
- })
- return o
-}
-
-var isUrl = /^https?:/
-
-var globalPool = {}
-
-function Request (options) {
- stream.Stream.call(this)
- this.readable = true
- this.writable = true
-
- if (typeof options === 'string') {
- options = {uri:options}
- }
-
- var reserved = Object.keys(Request.prototype)
- for (var i in options) {
- if (reserved.indexOf(i) === -1) {
- this[i] = options[i]
- } else {
- if (typeof options[i] === 'function') {
- delete options[i]
- }
- }
- }
- options = copy(options)
-
- this.init(options)
-}
-util.inherits(Request, stream.Stream)
-Request.prototype.init = function (options) {
- var self = this
-
- if (!options) options = {}
-
- if (!self.pool) self.pool = globalPool
- self.dests = []
- self.__isRequestRequest = true
-
- // Protect against double callback
- if (!self._callback && self.callback) {
- self._callback = self.callback
- self.callback = function () {
- if (self._callbackCalled) return // Print a warning maybe?
- self._callback.apply(self, arguments)
- self._callbackCalled = true
- }
- self.on('error', self.callback.bind())
- self.on('complete', self.callback.bind(self, null))
- }
-
- if (self.url) {
- // People use this property instead all the time so why not just support it.
- self.uri = self.url
- delete self.url
- }
-
- if (!self.uri) {
- throw new Error("options.uri is a required argument")
- } else {
- if (typeof self.uri == "string") self.uri = url.parse(self.uri)
- }
- if (self.proxy) {
- if (typeof self.proxy == 'string') self.proxy = url.parse(self.proxy)
-
- // do the HTTP CONNECT dance using koichik/node-tunnel
- if (http.globalAgent && self.uri.protocol === "https:") {
- self.tunnel = true
- var tunnelFn = self.proxy.protocol === "http:"
- ? tunnel.httpsOverHttp : tunnel.httpsOverHttps
-
- var tunnelOptions = { proxy: { host: self.proxy.hostname
- , port: +self.proxy.port
- , proxyAuth: self.proxy.auth }
- , ca: this.ca }
-
- self.agent = tunnelFn(tunnelOptions)
- self.tunnel = true
- }
- }
-
- self._redirectsFollowed = self._redirectsFollowed || 0
- self.maxRedirects = (self.maxRedirects !== undefined) ? self.maxRedirects : 10
- self.followRedirect = (self.followRedirect !== undefined) ? self.followRedirect : true
- self.followAllRedirects = (self.followAllRedirects !== undefined) ? self.followAllRedirects : false;
- if (self.followRedirect || self.followAllRedirects)
- self.redirects = self.redirects || []
-
- self.headers = self.headers ? copy(self.headers) : {}
-
- self.setHost = false
- if (!self.headers.host) {
- self.headers.host = self.uri.hostname
- if (self.uri.port) {
- if ( !(self.uri.port === 80 && self.uri.protocol === 'http:') &&
- !(self.uri.port === 443 && self.uri.protocol === 'https:') )
- self.headers.host += (':'+self.uri.port)
- }
- self.setHost = true
- }
-
- self.jar(self._jar || options.jar)
-
- if (!self.uri.pathname) {self.uri.pathname = '/'}
- if (!self.uri.port) {
- if (self.uri.protocol == 'http:') {self.uri.port = 80}
- else if (self.uri.protocol == 'https:') {self.uri.port = 443}
- }
-
- if (self.proxy && !self.tunnel) {
- self.port = self.proxy.port
- self.host = self.proxy.hostname
- } else {
- self.port = self.uri.port
- self.host = self.uri.hostname
- }
-
- self.clientErrorHandler = function (error) {
- if (self._aborted) return
-
- if (self.setHost) delete self.headers.host
- if (self.req._reusedSocket && error.code === 'ECONNRESET'
- && self.agent.addRequestNoreuse) {
- self.agent = { addRequest: self.agent.addRequestNoreuse.bind(self.agent) }
- self.start()
- self.req.end()
- return
- }
- if (self.timeout && self.timeoutTimer) {
- clearTimeout(self.timeoutTimer)
- self.timeoutTimer = null
- }
- self.emit('error', error)
- }
-
- if (options.form) {
- self.form(options.form)
- }
-
- if (options.oauth) {
- self.oauth(options.oauth)
- }
-
- if (self.uri.auth && !self.headers.authorization) {
- self.headers.authorization = "Basic " + toBase64(self.uri.auth.split(':').map(function(item){ return qs.unescape(item)}).join(':'))
- }
- if (self.proxy && self.proxy.auth && !self.headers['proxy-authorization'] && !self.tunnel) {
- self.headers['proxy-authorization'] = "Basic " + toBase64(self.proxy.auth.split(':').map(function(item){ return qs.unescape(item)}).join(':'))
- }
-
- if (options.qs) self.qs(options.qs)
-
- if (self.uri.path) {
- self.path = self.uri.path
- } else {
- self.path = self.uri.pathname + (self.uri.search || "")
- }
-
- if (self.path.length === 0) self.path = '/'
-
- if (self.proxy && !self.tunnel) self.path = (self.uri.protocol + '//' + self.uri.host + self.path)
-
- if (options.json) {
- self.json(options.json)
- } else if (options.multipart) {
- self.multipart(options.multipart)
- }
-
- if (self.body) {
- var length = 0
- if (!Buffer.isBuffer(self.body)) {
- if (Array.isArray(self.body)) {
- for (var i = 0; i < self.body.length; i++) {
- length += self.body[i].length
- }
- } else {
- self.body = new Buffer(self.body)
- length = self.body.length
- }
- } else {
- length = self.body.length
- }
- if (length) {
- self.headers['content-length'] = length
- } else {
- throw new Error('Argument error, options.body.')
- }
- }
-
- var protocol = self.proxy && !self.tunnel ? self.proxy.protocol : self.uri.protocol
- , defaultModules = {'http:':http, 'https:':https}
- , httpModules = self.httpModules || {}
- ;
- self.httpModule = httpModules[protocol] || defaultModules[protocol]
-
- if (!self.httpModule) throw new Error("Invalid protocol")
-
- if (options.ca) self.ca = options.ca
-
- if (!self.agent) {
- if (options.agentOptions) self.agentOptions = options.agentOptions
-
- if (options.agentClass) {
- self.agentClass = options.agentClass
- } else if (options.forever) {
- self.agentClass = protocol === 'http:' ? ForeverAgent : ForeverAgent.SSL
- } else {
- self.agentClass = self.httpModule.Agent
- }
- }
-
- if (self.pool === false) {
- self.agent = false
- } else {
- self.agent = self.agent || self.getAgent()
- if (self.maxSockets) {
- // Don't use our pooling if node has the refactored client
- self.agent.maxSockets = self.maxSockets
- }
- if (self.pool.maxSockets) {
- // Don't use our pooling if node has the refactored client
- self.agent.maxSockets = self.pool.maxSockets
- }
- }
-
- self.once('pipe', function (src) {
- if (self.ntick) throw new Error("You cannot pipe to this stream after the first nextTick() after creation of the request stream.")
- self.src = src
- if (isReadStream(src)) {
- if (!self.headers['content-type'] && !self.headers['Content-Type'])
- self.headers['content-type'] = mimetypes.lookup(src.path.slice(src.path.lastIndexOf('.')+1))
- } else {
- if (src.headers) {
- for (var i in src.headers) {
- if (!self.headers[i]) {
- self.headers[i] = src.headers[i]
- }
- }
- }
- if (src.method && !self.method) {
- self.method = src.method
- }
- }
-
- self.on('pipe', function () {
- console.error("You have already piped to this stream. Pipeing twice is likely to break the request.")
- })
- })
-
- process.nextTick(function () {
- if (self._aborted) return
-
- if (self.body) {
- if (Array.isArray(self.body)) {
- self.body.forEach(function(part) {
- self.write(part)
- })
- } else {
- self.write(self.body)
- }
- self.end()
- } else if (self.requestBodyStream) {
- console.warn("options.requestBodyStream is deprecated, please pass the request object to stream.pipe.")
- self.requestBodyStream.pipe(self)
- } else if (!self.src) {
- self.headers['content-length'] = 0
- self.end()
- }
- self.ntick = true
- })
-}
-
-Request.prototype.getAgent = function () {
- var Agent = this.agentClass
- var options = {}
- if (this.agentOptions) {
- for (var i in this.agentOptions) {
- options[i] = this.agentOptions[i]
- }
- }
- if (this.ca) options.ca = this.ca
-
- var poolKey = ''
-
- // different types of agents are in different pools
- if (Agent !== this.httpModule.Agent) {
- poolKey += Agent.name
- }
-
- if (!this.httpModule.globalAgent) {
- // node 0.4.x
- options.host = this.host
- options.port = this.port
- if (poolKey) poolKey += ':'
- poolKey += this.host + ':' + this.port
- }
-
- if (options.ca) {
- if (poolKey) poolKey += ':'
- poolKey += options.ca
- }
-
- if (!poolKey && Agent === this.httpModule.Agent && this.httpModule.globalAgent) {
- // not doing anything special. Use the globalAgent
- return this.httpModule.globalAgent
- }
-
- // already generated an agent for this setting
- if (this.pool[poolKey]) return this.pool[poolKey]
-
- return this.pool[poolKey] = new Agent(options)
-}
-
-Request.prototype.start = function () {
- var self = this
-
- if (self._aborted) return
-
- self._started = true
- self.method = self.method || 'GET'
- self.href = self.uri.href
- if (log) log('%method %href', self)
- self.req = self.httpModule.request(self, function (response) {
- if (self._aborted) return
- if (self._paused) response.pause()
-
- self.response = response
- response.request = self
- response.toJSON = toJSON
-
- if (self.httpModule === https &&
- self.strictSSL &&
- !response.client.authorized) {
- var sslErr = response.client.authorizationError
- self.emit('error', new Error('SSL Error: '+ sslErr))
- return
- }
-
- if (self.setHost) delete self.headers.host
- if (self.timeout && self.timeoutTimer) {
- clearTimeout(self.timeoutTimer)
- self.timeoutTimer = null
- }
-
- if (response.headers['set-cookie'] && (!self._disableCookies)) {
- response.headers['set-cookie'].forEach(function(cookie) {
- if (self._jar) self._jar.add(new Cookie(cookie))
- else cookieJar.add(new Cookie(cookie))
- })
- }
-
- if (response.statusCode >= 300 && response.statusCode < 400 &&
- (self.followAllRedirects ||
- (self.followRedirect && (self.method !== 'PUT' && self.method !== 'POST' && self.method !== 'DELETE'))) &&
- response.headers.location) {
- if (self._redirectsFollowed >= self.maxRedirects) {
- self.emit('error', new Error("Exceeded maxRedirects. Probably stuck in a redirect loop."))
- return
- }
- self._redirectsFollowed += 1
-
- if (!isUrl.test(response.headers.location)) {
- response.headers.location = url.resolve(self.uri.href, response.headers.location)
- }
- self.uri = response.headers.location
- self.redirects.push(
- { statusCode : response.statusCode
- , redirectUri: response.headers.location
- }
- )
- if (self.followAllRedirects) self.method = 'GET'
- // self.method = 'GET'; // Force all redirects to use GET || commented out fixes #215
- delete self.req
- delete self.agent
- delete self._started
- delete self.body
- if (self.headers) {
- delete self.headers.host
- }
- if (log) log('Redirect to %uri', self)
- self.init()
- return // Ignore the rest of the response
- } else {
- self._redirectsFollowed = self._redirectsFollowed || 0
- // Be a good stream and emit end when the response is finished.
- // Hack to emit end on close because of a core bug that never fires end
- response.on('close', function () {
- if (!self._ended) self.response.emit('end')
- })
-
- if (self.encoding) {
- if (self.dests.length !== 0) {
- console.error("Ingoring encoding parameter as this stream is being piped to another stream which makes the encoding option invalid.")
- } else {
- response.setEncoding(self.encoding)
- }
- }
-
- self.dests.forEach(function (dest) {
- self.pipeDest(dest)
- })
-
- response.on("data", function (chunk) {
- self._destdata = true
- self.emit("data", chunk)
- })
- response.on("end", function (chunk) {
- self._ended = true
- self.emit("end", chunk)
- })
- response.on("close", function () {self.emit("close")})
-
- self.emit('response', response)
-
- if (self.callback) {
- var buffer = []
- var bodyLen = 0
- self.on("data", function (chunk) {
- buffer.push(chunk)
- bodyLen += chunk.length
- })
- self.on("end", function () {
- if (self._aborted) return
-
- if (buffer.length && Buffer.isBuffer(buffer[0])) {
- var body = new Buffer(bodyLen)
- var i = 0
- buffer.forEach(function (chunk) {
- chunk.copy(body, i, 0, chunk.length)
- i += chunk.length
- })
- if (self.encoding === null) {
- response.body = body
- } else {
- response.body = body.toString()
- }
- } else if (buffer.length) {
- response.body = buffer.join('')
- }
-
- if (self._json) {
- try {
- response.body = JSON.parse(response.body)
- } catch (e) {}
- }
-
- self.emit('complete', response, response.body)
- })
- }
- }
- })
-
- if (self.timeout && !self.timeoutTimer) {
- self.timeoutTimer = setTimeout(function() {
- self.req.abort()
- var e = new Error("ETIMEDOUT")
- e.code = "ETIMEDOUT"
- self.emit("error", e)
- }, self.timeout)
-
- // Set additional timeout on socket - in case if remote
- // server freeze after sending headers
- if (self.req.setTimeout) { // only works on node 0.6+
- self.req.setTimeout(self.timeout, function(){
- if (self.req) {
- self.req.abort()
- var e = new Error("ESOCKETTIMEDOUT")
- e.code = "ESOCKETTIMEDOUT"
- self.emit("error", e)
- }
- })
- }
- }
-
- self.req.on('error', self.clientErrorHandler)
-
- self.emit('request', self.req)
-}
-
-Request.prototype.abort = function() {
- this._aborted = true;
-
- if (this.req) {
- this.req.abort()
- }
- else if (this.response) {
- this.response.abort()
- }
-
- this.emit("abort")
-}
-
-Request.prototype.pipeDest = function (dest) {
- var response = this.response
- // Called after the response is received
- if (dest.headers) {
- dest.headers['content-type'] = response.headers['content-type']
- if (response.headers['content-length']) {
- dest.headers['content-length'] = response.headers['content-length']
- }
- }
- if (dest.setHeader) {
- for (var i in response.headers) {
- dest.setHeader(i, response.headers[i])
- }
- dest.statusCode = response.statusCode
- }
- if (this.pipefilter) this.pipefilter(response, dest)
-}
-
-// Composable API
-Request.prototype.setHeader = function (name, value, clobber) {
- if (clobber === undefined) clobber = true
- if (clobber || !this.headers.hasOwnProperty(name)) this.headers[name] = value
- else this.headers[name] += ',' + value
- return this
-}
-Request.prototype.setHeaders = function (headers) {
- for (i in headers) {this.setHeader(i, headers[i])}
- return this
-}
-Request.prototype.qs = function (q, clobber) {
- var base
- if (!clobber && this.uri.query) base = qs.parse(this.uri.query)
- else base = {}
-
- for (var i in q) {
- base[i] = q[i]
- }
-
- this.uri = url.parse(this.uri.href.split('?')[0] + '?' + qs.stringify(base))
- this.url = this.uri
-
- return this
-}
-Request.prototype.form = function (form) {
- this.headers['content-type'] = 'application/x-www-form-urlencoded; charset=utf-8'
- this.body = qs.stringify(form).toString('utf8')
- return this
-}
-Request.prototype.multipart = function (multipart) {
- var self = this
- self.body = []
-
- if (!self.headers['content-type']) {
- self.headers['content-type'] = 'multipart/related; boundary=frontier';
- } else {
- self.headers['content-type'] = self.headers['content-type'].split(';')[0] + '; boundary=frontier';
- }
-
- if (!multipart.forEach) throw new Error('Argument error, options.multipart.')
-
- multipart.forEach(function (part) {
- var body = part.body
- if(!body) throw Error('Body attribute missing in multipart.')
- delete part.body
- var preamble = '--frontier\r\n'
- Object.keys(part).forEach(function(key){
- preamble += key + ': ' + part[key] + '\r\n'
- })
- preamble += '\r\n'
- self.body.push(new Buffer(preamble))
- self.body.push(new Buffer(body))
- self.body.push(new Buffer('\r\n'))
- })
- self.body.push(new Buffer('--frontier--'))
- return self
-}
-Request.prototype.json = function (val) {
- this.setHeader('content-type', 'application/json')
- this.setHeader('accept', 'application/json')
- this._json = true
- if (typeof val === 'boolean') {
- if (typeof this.body === 'object') this.body = JSON.stringify(this.body)
- } else {
- this.body = JSON.stringify(val)
- }
- return this
-}
-Request.prototype.oauth = function (_oauth) {
- var form
- if (this.headers['content-type'] &&
- this.headers['content-type'].slice(0, 'application/x-www-form-urlencoded'.length) ===
- 'application/x-www-form-urlencoded'
- ) {
- form = qs.parse(this.body)
- }
- if (this.uri.query) {
- form = qs.parse(this.uri.query)
- }
- if (!form) form = {}
- var oa = {}
- for (var i in form) oa[i] = form[i]
- for (var i in _oauth) oa['oauth_'+i] = _oauth[i]
- if (!oa.oauth_version) oa.oauth_version = '1.0'
- if (!oa.oauth_timestamp) oa.oauth_timestamp = Math.floor( (new Date()).getTime() / 1000 ).toString()
- if (!oa.oauth_nonce) oa.oauth_nonce = uuid().replace(/-/g, '')
-
- oa.oauth_signature_method = 'HMAC-SHA1'
-
- var consumer_secret = oa.oauth_consumer_secret
- delete oa.oauth_consumer_secret
- var token_secret = oa.oauth_token_secret
- delete oa.oauth_token_secret
-
- var baseurl = this.uri.protocol + '//' + this.uri.host + this.uri.pathname
- var signature = oauth.hmacsign(this.method, baseurl, oa, consumer_secret, token_secret)
-
- // oa.oauth_signature = signature
- for (var i in form) {
- if ( i.slice(0, 'oauth_') in _oauth) {
- // skip
- } else {
- delete oa['oauth_'+i]
- }
- }
- this.headers.Authorization =
- 'OAuth '+Object.keys(oa).sort().map(function (i) {return i+'="'+oauth.rfc3986(oa[i])+'"'}).join(',')
- this.headers.Authorization += ',oauth_signature="'+oauth.rfc3986(signature)+'"'
- return this
-}
-Request.prototype.jar = function (jar) {
- var cookies
-
- if (this._redirectsFollowed === 0) {
- this.originalCookieHeader = this.headers.cookie
- }
-
- if (jar === false) {
- // disable cookies
- cookies = false;
- this._disableCookies = true;
- } else if (jar) {
- // fetch cookie from the user defined cookie jar
- cookies = jar.get({ url: this.uri.href })
- } else {
- // fetch cookie from the global cookie jar
- cookies = cookieJar.get({ url: this.uri.href })
- }
-
- if (cookies && cookies.length) {
- var cookieString = cookies.map(function (c) {
- return c.name + "=" + c.value
- }).join("; ")
-
- if (this.originalCookieHeader) {
- // Don't overwrite existing Cookie header
- this.headers.cookie = this.originalCookieHeader + '; ' + cookieString
- } else {
- this.headers.cookie = cookieString
- }
- }
- this._jar = jar
- return this
-}
-
-
-// Stream API
-Request.prototype.pipe = function (dest, opts) {
- if (this.response) {
- if (this._destdata) {
- throw new Error("You cannot pipe after data has been emitted from the response.")
- } else if (this._ended) {
- throw new Error("You cannot pipe after the response has been ended.")
- } else {
- stream.Stream.prototype.pipe.call(this, dest, opts)
- this.pipeDest(dest)
- return dest
- }
- } else {
- this.dests.push(dest)
- stream.Stream.prototype.pipe.call(this, dest, opts)
- return dest
- }
-}
-Request.prototype.write = function () {
- if (!this._started) this.start()
- this.req.write.apply(this.req, arguments)
-}
-Request.prototype.end = function (chunk) {
- if (chunk) this.write(chunk)
- if (!this._started) this.start()
- this.req.end()
-}
-Request.prototype.pause = function () {
- if (!this.response) this._paused = true
- else this.response.pause.apply(this.response, arguments)
-}
-Request.prototype.resume = function () {
- if (!this.response) this._paused = false
- else this.response.resume.apply(this.response, arguments)
-}
-Request.prototype.destroy = function () {
- if (!this._ended) this.end()
-}
-
-// organize params for post, put, head, del
-function initParams(uri, options, callback) {
- if ((typeof options === 'function') && !callback) callback = options;
- if (typeof options === 'object') {
- options.uri = uri;
- } else if (typeof uri === 'string') {
- options = {uri:uri};
- } else {
- options = uri;
- uri = options.uri;
- }
- return { uri: uri, options: options, callback: callback };
-}
-
-function request (uri, options, callback) {
- if (typeof uri === 'undefined') throw new Error('undefined is not a valid uri or options object.')
- if ((typeof options === 'function') && !callback) callback = options;
- if (typeof options === 'object') {
- options.uri = uri;
- } else if (typeof uri === 'string') {
- options = {uri:uri};
- } else {
- options = uri;
- }
-
- if (callback) options.callback = callback;
- var r = new Request(options)
- return r
-}
-
-module.exports = request
-
-request.defaults = function (options) {
- var def = function (method) {
- var d = function (uri, opts, callback) {
- var params = initParams(uri, opts, callback);
- for (var i in options) {
- if (params.options[i] === undefined) params.options[i] = options[i]
- }
- return method(params.options, params.callback)
- }
- return d
- }
- var de = def(request)
- de.get = def(request.get)
- de.post = def(request.post)
- de.put = def(request.put)
- de.head = def(request.head)
- de.del = def(request.del)
- de.cookie = def(request.cookie)
- de.jar = def(request.jar)
- return de
-}
-
-request.forever = function (agentOptions, optionsArg) {
- var options = {}
- if (optionsArg) {
- for (option in optionsArg) {
- options[option] = optionsArg[option]
- }
- }
- if (agentOptions) options.agentOptions = agentOptions
- options.forever = true
- return request.defaults(options)
-}
-
-request.get = request
-request.post = function (uri, options, callback) {
- var params = initParams(uri, options, callback);
- params.options.method = 'POST';
- return request(params.uri || null, params.options, params.callback)
-}
-request.put = function (uri, options, callback) {
- var params = initParams(uri, options, callback);
- params.options.method = 'PUT'
- return request(params.uri || null, params.options, params.callback)
-}
-request.head = function (uri, options, callback) {
- var params = initParams(uri, options, callback);
- params.options.method = 'HEAD'
- if (params.options.body ||
- params.options.requestBodyStream ||
- (params.options.json && typeof params.options.json !== 'boolean') ||
- params.options.multipart) {
- throw new Error("HTTP HEAD requests MUST NOT include a request body.")
- }
- return request(params.uri || null, params.options, params.callback)
-}
-request.del = function (uri, options, callback) {
- var params = initParams(uri, options, callback);
- params.options.method = 'DELETE'
- return request(params.uri || null, params.options, params.callback)
-}
-request.jar = function () {
- return new CookieJar
-}
-request.cookie = function (str) {
- if (str && str.uri) str = str.uri
- if (typeof str !== 'string') throw new Error("The cookie function only accepts STRING as param")
- return new Cookie(str)
-}
-
-// Safe toJSON
-
-function getSafe (self, uuid) {
- if (typeof self === 'object' || typeof self === 'function') var safe = {}
- if (Array.isArray(self)) var safe = []
-
- var recurse = []
-
- Object.defineProperty(self, uuid, {})
-
- var attrs = Object.keys(self).filter(function (i) {
- if (i === uuid) return false
- if ( (typeof self[i] !== 'object' && typeof self[i] !== 'function') || self[i] === null) return true
- return !(Object.getOwnPropertyDescriptor(self[i], uuid))
- })
-
-
- for (var i=0;i"
-, "repository" :
- { "type" : "git"
- , "url" : "http://github.com/mikeal/request.git"
+{
+ "name": "request",
+ "description": "Simplified HTTP request client.",
+ "keywords": [
+ "http",
+ "simple",
+ "util",
+ "utility"
+ ],
+ "version": "2.88.1",
+ "author": "Mikeal Rogers ",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/request/request.git"
+ },
+ "bugs": {
+ "url": "http://github.com/request/request/issues"
+ },
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 6"
+ },
+ "main": "index.js",
+ "files": [
+ "lib/",
+ "index.js",
+ "request.js"
+ ],
+ "dependencies": {
+ "aws-sign2": "~0.7.0",
+ "aws4": "^1.8.0",
+ "caseless": "~0.12.0",
+ "combined-stream": "~1.0.6",
+ "extend": "~3.0.2",
+ "forever-agent": "~0.6.1",
+ "form-data": "~2.3.2",
+ "har-validator": "~5.1.3",
+ "http-signature": "~1.2.0",
+ "is-typedarray": "~1.0.0",
+ "isstream": "~0.1.2",
+ "json-stringify-safe": "~5.0.1",
+ "mime-types": "~2.1.19",
+ "oauth-sign": "~0.9.0",
+ "performance-now": "^2.1.0",
+ "qs": "~6.5.2",
+ "safe-buffer": "^5.1.2",
+ "tough-cookie": "~2.5.0",
+ "tunnel-agent": "^0.6.0",
+ "uuid": "^3.3.2"
+ },
+ "scripts": {
+ "test": "npm run lint && npm run test-ci && npm run test-browser",
+ "test-ci": "taper tests/test-*.js",
+ "test-cov": "istanbul cover tape tests/test-*.js",
+ "test-browser": "node tests/browser/start.js",
+ "lint": "standard"
+ },
+ "devDependencies": {
+ "bluebird": "^3.2.1",
+ "browserify": "^13.0.1",
+ "browserify-istanbul": "^2.0.0",
+ "buffer-equal": "^1.0.0",
+ "codecov": "^3.0.4",
+ "coveralls": "^3.0.2",
+ "function-bind": "^1.0.2",
+ "istanbul": "^0.4.0",
+ "karma": "^3.0.0",
+ "karma-browserify": "^5.0.1",
+ "karma-cli": "^1.0.0",
+ "karma-coverage": "^1.0.0",
+ "karma-phantomjs-launcher": "^1.0.0",
+ "karma-tap": "^3.0.1",
+ "phantomjs-prebuilt": "^2.1.3",
+ "rimraf": "^2.2.8",
+ "server-destroy": "^1.0.1",
+ "standard": "^9.0.0",
+ "tape": "^4.6.0",
+ "taper": "^0.5.0"
+ },
+ "greenkeeper": {
+ "ignore": [
+ "hawk",
+ "har-validator"
+ ]
}
-, "bugs" :
- { "url" : "http://github.com/mikeal/request/issues" }
-, "engines" : ["node >= 0.3.6"]
-, "main" : "./main"
-, "scripts": { "test": "node tests/run.js" }
}
diff --git a/release.sh b/release.sh
new file mode 100755
index 000000000..7678bf8d8
--- /dev/null
+++ b/release.sh
@@ -0,0 +1,45 @@
+#!/bin/sh
+
+if [ -z "`which github-changes`" ]; then
+ # specify version because github-changes "is under heavy development. Things
+ # may break between releases" until 0.1.0
+ echo "First, do: [sudo] npm install -g github-changes@0.0.14"
+ exit 1
+fi
+
+if [ -d .git/refs/remotes/upstream ]; then
+ remote=upstream
+else
+ remote=origin
+fi
+
+# Increment v2.x.y -> v2.x+1.0
+npm version minor || exit 1
+
+# Generate changelog from pull requests
+github-changes -o request -r request \
+ --auth --verbose \
+ --file CHANGELOG.md \
+ --only-pulls --use-commit-body \
+ --date-format '(YYYY/MM/DD)' \
+ || exit 1
+
+# Since the tag for the new version hasn't been pushed yet, any changes in it
+# will be marked as "upcoming"
+version="$(grep '"version"' package.json | cut -d'"' -f4)"
+sed -i -e "s/^### upcoming/### v$version/" CHANGELOG.md
+
+# This may fail if no changelog updates
+# TODO: would this ever actually happen? handle it better?
+git add CHANGELOG.md; git commit -m 'Update changelog'
+
+# Publish the new version to npm
+npm publish || exit 1
+
+# Increment v2.x.0 -> v2.x.1
+# For rationale, see:
+# https://github.com/request/oauth-sign/issues/10#issuecomment-58917018
+npm version patch || exit 1
+
+# Push back to the main repo
+git push $remote master --tags || exit 1
diff --git a/request.js b/request.js
new file mode 100644
index 000000000..198b76093
--- /dev/null
+++ b/request.js
@@ -0,0 +1,1553 @@
+'use strict'
+
+var http = require('http')
+var https = require('https')
+var url = require('url')
+var util = require('util')
+var stream = require('stream')
+var zlib = require('zlib')
+var aws2 = require('aws-sign2')
+var aws4 = require('aws4')
+var httpSignature = require('http-signature')
+var mime = require('mime-types')
+var caseless = require('caseless')
+var ForeverAgent = require('forever-agent')
+var FormData = require('form-data')
+var extend = require('extend')
+var isstream = require('isstream')
+var isTypedArray = require('is-typedarray').strict
+var helpers = require('./lib/helpers')
+var cookies = require('./lib/cookies')
+var getProxyFromURI = require('./lib/getProxyFromURI')
+var Querystring = require('./lib/querystring').Querystring
+var Har = require('./lib/har').Har
+var Auth = require('./lib/auth').Auth
+var OAuth = require('./lib/oauth').OAuth
+var hawk = require('./lib/hawk')
+var Multipart = require('./lib/multipart').Multipart
+var Redirect = require('./lib/redirect').Redirect
+var Tunnel = require('./lib/tunnel').Tunnel
+var now = require('performance-now')
+var Buffer = require('safe-buffer').Buffer
+
+var safeStringify = helpers.safeStringify
+var isReadStream = helpers.isReadStream
+var toBase64 = helpers.toBase64
+var defer = helpers.defer
+var copy = helpers.copy
+var version = helpers.version
+var globalCookieJar = cookies.jar()
+
+var globalPool = {}
+
+function filterForNonReserved (reserved, options) {
+ // Filter out properties that are not reserved.
+ // Reserved values are passed in at call site.
+
+ var object = {}
+ for (var i in options) {
+ var notReserved = (reserved.indexOf(i) === -1)
+ if (notReserved) {
+ object[i] = options[i]
+ }
+ }
+ return object
+}
+
+function filterOutReservedFunctions (reserved, options) {
+ // Filter out properties that are functions and are reserved.
+ // Reserved values are passed in at call site.
+
+ var object = {}
+ for (var i in options) {
+ var isReserved = !(reserved.indexOf(i) === -1)
+ var isFunction = (typeof options[i] === 'function')
+ if (!(isReserved && isFunction)) {
+ object[i] = options[i]
+ }
+ }
+ return object
+}
+
+// Return a simpler request object to allow serialization
+function requestToJSON () {
+ var self = this
+ return {
+ uri: self.uri,
+ method: self.method,
+ headers: self.headers
+ }
+}
+
+// Return a simpler response object to allow serialization
+function responseToJSON () {
+ var self = this
+ return {
+ statusCode: self.statusCode,
+ body: self.body,
+ headers: self.headers,
+ request: requestToJSON.call(self.request)
+ }
+}
+
+function Request (options) {
+ // if given the method property in options, set property explicitMethod to true
+
+ // extend the Request instance with any non-reserved properties
+ // remove any reserved functions from the options object
+ // set Request instance to be readable and writable
+ // call init
+
+ var self = this
+
+ // start with HAR, then override with additional options
+ if (options.har) {
+ self._har = new Har(self)
+ options = self._har.options(options)
+ }
+
+ stream.Stream.call(self)
+ var reserved = Object.keys(Request.prototype)
+ var nonReserved = filterForNonReserved(reserved, options)
+
+ extend(self, nonReserved)
+ options = filterOutReservedFunctions(reserved, options)
+
+ self.readable = true
+ self.writable = true
+ if (options.method) {
+ self.explicitMethod = true
+ }
+ self._qs = new Querystring(self)
+ self._auth = new Auth(self)
+ self._oauth = new OAuth(self)
+ self._multipart = new Multipart(self)
+ self._redirect = new Redirect(self)
+ self._tunnel = new Tunnel(self)
+ self.init(options)
+}
+
+util.inherits(Request, stream.Stream)
+
+// Debugging
+Request.debug = process.env.NODE_DEBUG && /\brequest\b/.test(process.env.NODE_DEBUG)
+function debug () {
+ if (Request.debug) {
+ console.error('REQUEST %s', util.format.apply(util, arguments))
+ }
+}
+Request.prototype.debug = debug
+
+Request.prototype.init = function (options) {
+ // init() contains all the code to setup the request object.
+ // the actual outgoing request is not started until start() is called
+ // this function is called from both the constructor and on redirect.
+ var self = this
+ if (!options) {
+ options = {}
+ }
+ self.headers = self.headers ? copy(self.headers) : {}
+
+ // Delete headers with value undefined since they break
+ // ClientRequest.OutgoingMessage.setHeader in node 0.12
+ for (var headerName in self.headers) {
+ if (typeof self.headers[headerName] === 'undefined') {
+ delete self.headers[headerName]
+ }
+ }
+
+ caseless.httpify(self, self.headers)
+
+ if (!self.method) {
+ self.method = options.method || 'GET'
+ }
+ if (!self.localAddress) {
+ self.localAddress = options.localAddress
+ }
+
+ self._qs.init(options)
+
+ debug(options)
+ if (!self.pool && self.pool !== false) {
+ self.pool = globalPool
+ }
+ self.dests = self.dests || []
+ self.__isRequestRequest = true
+
+ // Protect against double callback
+ if (!self._callback && self.callback) {
+ self._callback = self.callback
+ self.callback = function () {
+ if (self._callbackCalled) {
+ return // Print a warning maybe?
+ }
+ self._callbackCalled = true
+ self._callback.apply(self, arguments)
+ }
+ self.on('error', self.callback.bind())
+ self.on('complete', self.callback.bind(self, null))
+ }
+
+ // People use this property instead all the time, so support it
+ if (!self.uri && self.url) {
+ self.uri = self.url
+ delete self.url
+ }
+
+ // If there's a baseUrl, then use it as the base URL (i.e. uri must be
+ // specified as a relative path and is appended to baseUrl).
+ if (self.baseUrl) {
+ if (typeof self.baseUrl !== 'string') {
+ return self.emit('error', new Error('options.baseUrl must be a string'))
+ }
+
+ if (typeof self.uri !== 'string') {
+ return self.emit('error', new Error('options.uri must be a string when using options.baseUrl'))
+ }
+
+ if (self.uri.indexOf('//') === 0 || self.uri.indexOf('://') !== -1) {
+ return self.emit('error', new Error('options.uri must be a path when using options.baseUrl'))
+ }
+
+ // Handle all cases to make sure that there's only one slash between
+ // baseUrl and uri.
+ var baseUrlEndsWithSlash = self.baseUrl.lastIndexOf('/') === self.baseUrl.length - 1
+ var uriStartsWithSlash = self.uri.indexOf('/') === 0
+
+ if (baseUrlEndsWithSlash && uriStartsWithSlash) {
+ self.uri = self.baseUrl + self.uri.slice(1)
+ } else if (baseUrlEndsWithSlash || uriStartsWithSlash) {
+ self.uri = self.baseUrl + self.uri
+ } else if (self.uri === '') {
+ self.uri = self.baseUrl
+ } else {
+ self.uri = self.baseUrl + '/' + self.uri
+ }
+ delete self.baseUrl
+ }
+
+ // A URI is needed by this point, emit error if we haven't been able to get one
+ if (!self.uri) {
+ return self.emit('error', new Error('options.uri is a required argument'))
+ }
+
+ // If a string URI/URL was given, parse it into a URL object
+ if (typeof self.uri === 'string') {
+ self.uri = url.parse(self.uri)
+ }
+
+ // Some URL objects are not from a URL parsed string and need href added
+ if (!self.uri.href) {
+ self.uri.href = url.format(self.uri)
+ }
+
+ // DEPRECATED: Warning for users of the old Unix Sockets URL Scheme
+ if (self.uri.protocol === 'unix:') {
+ return self.emit('error', new Error('`unix://` URL scheme is no longer supported. Please use the format `http://unix:SOCKET:PATH`'))
+ }
+
+ // Support Unix Sockets
+ if (self.uri.host === 'unix') {
+ self.enableUnixSocket()
+ }
+
+ if (self.strictSSL === false) {
+ self.rejectUnauthorized = false
+ }
+
+ if (!self.uri.pathname) { self.uri.pathname = '/' }
+
+ if (!(self.uri.host || (self.uri.hostname && self.uri.port)) && !self.uri.isUnix) {
+ // Invalid URI: it may generate lot of bad errors, like 'TypeError: Cannot call method `indexOf` of undefined' in CookieJar
+ // Detect and reject it as soon as possible
+ var faultyUri = url.format(self.uri)
+ var message = 'Invalid URI "' + faultyUri + '"'
+ if (Object.keys(options).length === 0) {
+ // No option ? This can be the sign of a redirect
+ // As this is a case where the user cannot do anything (they didn't call request directly with this URL)
+ // they should be warned that it can be caused by a redirection (can save some hair)
+ message += '. This can be caused by a crappy redirection.'
+ }
+ // This error was fatal
+ self.abort()
+ return self.emit('error', new Error(message))
+ }
+
+ if (!self.hasOwnProperty('proxy')) {
+ self.proxy = getProxyFromURI(self.uri)
+ }
+
+ self.tunnel = self._tunnel.isEnabled()
+ if (self.proxy) {
+ self._tunnel.setup(options)
+ }
+
+ self._redirect.onRequest(options)
+
+ self.setHost = false
+ if (!self.hasHeader('host')) {
+ var hostHeaderName = self.originalHostHeaderName || 'host'
+ self.setHeader(hostHeaderName, self.uri.host)
+ // Drop :port suffix from Host header if known protocol.
+ if (self.uri.port) {
+ if ((self.uri.port === '80' && self.uri.protocol === 'http:') ||
+ (self.uri.port === '443' && self.uri.protocol === 'https:')) {
+ self.setHeader(hostHeaderName, self.uri.hostname)
+ }
+ }
+ self.setHost = true
+ }
+
+ self.jar(self._jar || options.jar)
+
+ if (!self.uri.port) {
+ if (self.uri.protocol === 'http:') { self.uri.port = 80 } else if (self.uri.protocol === 'https:') { self.uri.port = 443 }
+ }
+
+ if (self.proxy && !self.tunnel) {
+ self.port = self.proxy.port
+ self.host = self.proxy.hostname
+ } else {
+ self.port = self.uri.port
+ self.host = self.uri.hostname
+ }
+
+ if (options.form) {
+ self.form(options.form)
+ }
+
+ if (options.formData) {
+ var formData = options.formData
+ var requestForm = self.form()
+ var appendFormValue = function (key, value) {
+ if (value && value.hasOwnProperty('value') && value.hasOwnProperty('options')) {
+ requestForm.append(key, value.value, value.options)
+ } else {
+ requestForm.append(key, value)
+ }
+ }
+ for (var formKey in formData) {
+ if (formData.hasOwnProperty(formKey)) {
+ var formValue = formData[formKey]
+ if (formValue instanceof Array) {
+ for (var j = 0; j < formValue.length; j++) {
+ appendFormValue(formKey, formValue[j])
+ }
+ } else {
+ appendFormValue(formKey, formValue)
+ }
+ }
+ }
+ }
+
+ if (options.qs) {
+ self.qs(options.qs)
+ }
+
+ if (self.uri.path) {
+ self.path = self.uri.path
+ } else {
+ self.path = self.uri.pathname + (self.uri.search || '')
+ }
+
+ if (self.path.length === 0) {
+ self.path = '/'
+ }
+
+ // Auth must happen last in case signing is dependent on other headers
+ if (options.aws) {
+ self.aws(options.aws)
+ }
+
+ if (options.hawk) {
+ self.hawk(options.hawk)
+ }
+
+ if (options.httpSignature) {
+ self.httpSignature(options.httpSignature)
+ }
+
+ if (options.auth) {
+ if (Object.prototype.hasOwnProperty.call(options.auth, 'username')) {
+ options.auth.user = options.auth.username
+ }
+ if (Object.prototype.hasOwnProperty.call(options.auth, 'password')) {
+ options.auth.pass = options.auth.password
+ }
+
+ self.auth(
+ options.auth.user,
+ options.auth.pass,
+ options.auth.sendImmediately,
+ options.auth.bearer
+ )
+ }
+
+ if (self.gzip && !self.hasHeader('accept-encoding')) {
+ self.setHeader('accept-encoding', 'gzip, deflate')
+ }
+
+ if (self.uri.auth && !self.hasHeader('authorization')) {
+ var uriAuthPieces = self.uri.auth.split(':').map(function (item) { return self._qs.unescape(item) })
+ self.auth(uriAuthPieces[0], uriAuthPieces.slice(1).join(':'), true)
+ }
+
+ if (!self.tunnel && self.proxy && self.proxy.auth && !self.hasHeader('proxy-authorization')) {
+ var proxyAuthPieces = self.proxy.auth.split(':').map(function (item) { return self._qs.unescape(item) })
+ var authHeader = 'Basic ' + toBase64(proxyAuthPieces.join(':'))
+ self.setHeader('proxy-authorization', authHeader)
+ }
+
+ if (self.proxy && !self.tunnel) {
+ self.path = (self.uri.protocol + '//' + self.uri.host + self.path)
+ }
+
+ if (options.json) {
+ self.json(options.json)
+ }
+ if (options.multipart) {
+ self.multipart(options.multipart)
+ }
+
+ if (options.time) {
+ self.timing = true
+
+ // NOTE: elapsedTime is deprecated in favor of .timings
+ self.elapsedTime = self.elapsedTime || 0
+ }
+
+ function setContentLength () {
+ if (isTypedArray(self.body)) {
+ self.body = Buffer.from(self.body)
+ }
+
+ if (!self.hasHeader('content-length')) {
+ var length
+ if (typeof self.body === 'string') {
+ length = Buffer.byteLength(self.body)
+ } else if (Array.isArray(self.body)) {
+ length = self.body.reduce(function (a, b) { return a + b.length }, 0)
+ } else {
+ length = self.body.length
+ }
+
+ if (length) {
+ self.setHeader('content-length', length)
+ } else {
+ self.emit('error', new Error('Argument error, options.body.'))
+ }
+ }
+ }
+ if (self.body && !isstream(self.body)) {
+ setContentLength()
+ }
+
+ if (options.oauth) {
+ self.oauth(options.oauth)
+ } else if (self._oauth.params && self.hasHeader('authorization')) {
+ self.oauth(self._oauth.params)
+ }
+
+ var protocol = self.proxy && !self.tunnel ? self.proxy.protocol : self.uri.protocol
+ var defaultModules = {'http:': http, 'https:': https}
+ var httpModules = self.httpModules || {}
+
+ self.httpModule = httpModules[protocol] || defaultModules[protocol]
+
+ if (!self.httpModule) {
+ return self.emit('error', new Error('Invalid protocol: ' + protocol))
+ }
+
+ if (options.ca) {
+ self.ca = options.ca
+ }
+
+ if (!self.agent) {
+ if (options.agentOptions) {
+ self.agentOptions = options.agentOptions
+ }
+
+ if (options.agentClass) {
+ self.agentClass = options.agentClass
+ } else if (options.forever) {
+ var v = version()
+ // use ForeverAgent in node 0.10- only
+ if (v.major === 0 && v.minor <= 10) {
+ self.agentClass = protocol === 'http:' ? ForeverAgent : ForeverAgent.SSL
+ } else {
+ self.agentClass = self.httpModule.Agent
+ self.agentOptions = self.agentOptions || {}
+ self.agentOptions.keepAlive = true
+ }
+ } else {
+ self.agentClass = self.httpModule.Agent
+ }
+ }
+
+ if (self.pool === false) {
+ self.agent = false
+ } else {
+ self.agent = self.agent || self.getNewAgent()
+ }
+
+ self.on('pipe', function (src) {
+ if (self.ntick && self._started) {
+ self.emit('error', new Error('You cannot pipe to this stream after the outbound request has started.'))
+ }
+ self.src = src
+ if (isReadStream(src)) {
+ if (!self.hasHeader('content-type')) {
+ self.setHeader('content-type', mime.lookup(src.path))
+ }
+ } else {
+ if (src.headers) {
+ for (var i in src.headers) {
+ if (!self.hasHeader(i)) {
+ self.setHeader(i, src.headers[i])
+ }
+ }
+ }
+ if (self._json && !self.hasHeader('content-type')) {
+ self.setHeader('content-type', 'application/json')
+ }
+ if (src.method && !self.explicitMethod) {
+ self.method = src.method
+ }
+ }
+
+ // self.on('pipe', function () {
+ // console.error('You have already piped to this stream. Pipeing twice is likely to break the request.')
+ // })
+ })
+
+ defer(function () {
+ if (self._aborted) {
+ return
+ }
+
+ var end = function () {
+ if (self._form) {
+ if (!self._auth.hasAuth) {
+ self._form.pipe(self)
+ } else if (self._auth.hasAuth && self._auth.sentAuth) {
+ self._form.pipe(self)
+ }
+ }
+ if (self._multipart && self._multipart.chunked) {
+ self._multipart.body.pipe(self)
+ }
+ if (self.body) {
+ if (isstream(self.body)) {
+ self.body.pipe(self)
+ } else {
+ setContentLength()
+ if (Array.isArray(self.body)) {
+ self.body.forEach(function (part) {
+ self.write(part)
+ })
+ } else {
+ self.write(self.body)
+ }
+ self.end()
+ }
+ } else if (self.requestBodyStream) {
+ console.warn('options.requestBodyStream is deprecated, please pass the request object to stream.pipe.')
+ self.requestBodyStream.pipe(self)
+ } else if (!self.src) {
+ if (self._auth.hasAuth && !self._auth.sentAuth) {
+ self.end()
+ return
+ }
+ if (self.method !== 'GET' && typeof self.method !== 'undefined') {
+ self.setHeader('content-length', 0)
+ }
+ self.end()
+ }
+ }
+
+ if (self._form && !self.hasHeader('content-length')) {
+ // Before ending the request, we had to compute the length of the whole form, asyncly
+ self.setHeader(self._form.getHeaders(), true)
+ self._form.getLength(function (err, length) {
+ if (!err && !isNaN(length)) {
+ self.setHeader('content-length', length)
+ }
+ end()
+ })
+ } else {
+ end()
+ }
+
+ self.ntick = true
+ })
+}
+
+Request.prototype.getNewAgent = function () {
+ var self = this
+ var Agent = self.agentClass
+ var options = {}
+ if (self.agentOptions) {
+ for (var i in self.agentOptions) {
+ options[i] = self.agentOptions[i]
+ }
+ }
+ if (self.ca) {
+ options.ca = self.ca
+ }
+ if (self.ciphers) {
+ options.ciphers = self.ciphers
+ }
+ if (self.secureProtocol) {
+ options.secureProtocol = self.secureProtocol
+ }
+ if (self.secureOptions) {
+ options.secureOptions = self.secureOptions
+ }
+ if (typeof self.rejectUnauthorized !== 'undefined') {
+ options.rejectUnauthorized = self.rejectUnauthorized
+ }
+
+ if (self.cert && self.key) {
+ options.key = self.key
+ options.cert = self.cert
+ }
+
+ if (self.pfx) {
+ options.pfx = self.pfx
+ }
+
+ if (self.passphrase) {
+ options.passphrase = self.passphrase
+ }
+
+ var poolKey = ''
+
+ // different types of agents are in different pools
+ if (Agent !== self.httpModule.Agent) {
+ poolKey += Agent.name
+ }
+
+ // ca option is only relevant if proxy or destination are https
+ var proxy = self.proxy
+ if (typeof proxy === 'string') {
+ proxy = url.parse(proxy)
+ }
+ var isHttps = (proxy && proxy.protocol === 'https:') || this.uri.protocol === 'https:'
+
+ if (isHttps) {
+ if (options.ca) {
+ if (poolKey) {
+ poolKey += ':'
+ }
+ poolKey += options.ca
+ }
+
+ if (typeof options.rejectUnauthorized !== 'undefined') {
+ if (poolKey) {
+ poolKey += ':'
+ }
+ poolKey += options.rejectUnauthorized
+ }
+
+ if (options.cert) {
+ if (poolKey) {
+ poolKey += ':'
+ }
+ poolKey += options.cert.toString('ascii') + options.key.toString('ascii')
+ }
+
+ if (options.pfx) {
+ if (poolKey) {
+ poolKey += ':'
+ }
+ poolKey += options.pfx.toString('ascii')
+ }
+
+ if (options.ciphers) {
+ if (poolKey) {
+ poolKey += ':'
+ }
+ poolKey += options.ciphers
+ }
+
+ if (options.secureProtocol) {
+ if (poolKey) {
+ poolKey += ':'
+ }
+ poolKey += options.secureProtocol
+ }
+
+ if (options.secureOptions) {
+ if (poolKey) {
+ poolKey += ':'
+ }
+ poolKey += options.secureOptions
+ }
+ }
+
+ if (self.pool === globalPool && !poolKey && Object.keys(options).length === 0 && self.httpModule.globalAgent) {
+ // not doing anything special. Use the globalAgent
+ return self.httpModule.globalAgent
+ }
+
+ // we're using a stored agent. Make sure it's protocol-specific
+ poolKey = self.uri.protocol + poolKey
+
+ // generate a new agent for this setting if none yet exists
+ if (!self.pool[poolKey]) {
+ self.pool[poolKey] = new Agent(options)
+ // properly set maxSockets on new agents
+ if (self.pool.maxSockets) {
+ self.pool[poolKey].maxSockets = self.pool.maxSockets
+ }
+ }
+
+ return self.pool[poolKey]
+}
+
+Request.prototype.start = function () {
+ // start() is called once we are ready to send the outgoing HTTP request.
+ // this is usually called on the first write(), end() or on nextTick()
+ var self = this
+
+ if (self.timing) {
+ // All timings will be relative to this request's startTime. In order to do this,
+ // we need to capture the wall-clock start time (via Date), immediately followed
+ // by the high-resolution timer (via now()). While these two won't be set
+ // at the _exact_ same time, they should be close enough to be able to calculate
+ // high-resolution, monotonically non-decreasing timestamps relative to startTime.
+ var startTime = new Date().getTime()
+ var startTimeNow = now()
+ }
+
+ if (self._aborted) {
+ return
+ }
+
+ self._started = true
+ self.method = self.method || 'GET'
+ self.href = self.uri.href
+
+ if (self.src && self.src.stat && self.src.stat.size && !self.hasHeader('content-length')) {
+ self.setHeader('content-length', self.src.stat.size)
+ }
+ if (self._aws) {
+ self.aws(self._aws, true)
+ }
+
+ // We have a method named auth, which is completely different from the http.request
+ // auth option. If we don't remove it, we're gonna have a bad time.
+ var reqOptions = copy(self)
+ delete reqOptions.auth
+
+ debug('make request', self.uri.href)
+
+ // node v6.8.0 now supports a `timeout` value in `http.request()`, but we
+ // should delete it for now since we handle timeouts manually for better
+ // consistency with node versions before v6.8.0
+ delete reqOptions.timeout
+
+ try {
+ self.req = self.httpModule.request(reqOptions)
+ } catch (err) {
+ self.emit('error', err)
+ return
+ }
+
+ if (self.timing) {
+ self.startTime = startTime
+ self.startTimeNow = startTimeNow
+
+ // Timing values will all be relative to startTime (by comparing to startTimeNow
+ // so we have an accurate clock)
+ self.timings = {}
+ }
+
+ var timeout
+ if (self.timeout && !self.timeoutTimer) {
+ if (self.timeout < 0) {
+ timeout = 0
+ } else if (typeof self.timeout === 'number' && isFinite(self.timeout)) {
+ timeout = self.timeout
+ }
+ }
+
+ self.req.on('response', self.onRequestResponse.bind(self))
+ self.req.on('error', self.onRequestError.bind(self))
+ self.req.on('drain', function () {
+ self.emit('drain')
+ })
+
+ self.req.on('socket', function (socket) {
+ // `._connecting` was the old property which was made public in node v6.1.0
+ var isConnecting = socket._connecting || socket.connecting
+ if (self.timing) {
+ self.timings.socket = now() - self.startTimeNow
+
+ if (isConnecting) {
+ var onLookupTiming = function () {
+ self.timings.lookup = now() - self.startTimeNow
+ }
+
+ var onConnectTiming = function () {
+ self.timings.connect = now() - self.startTimeNow
+ }
+
+ socket.once('lookup', onLookupTiming)
+ socket.once('connect', onConnectTiming)
+
+ // clean up timing event listeners if needed on error
+ self.req.once('error', function () {
+ socket.removeListener('lookup', onLookupTiming)
+ socket.removeListener('connect', onConnectTiming)
+ })
+ }
+ }
+
+ var setReqTimeout = function () {
+ // This timeout sets the amount of time to wait *between* bytes sent
+ // from the server once connected.
+ //
+ // In particular, it's useful for erroring if the server fails to send
+ // data halfway through streaming a response.
+ self.req.setTimeout(timeout, function () {
+ if (self.req) {
+ self.abort()
+ var e = new Error('ESOCKETTIMEDOUT')
+ e.code = 'ESOCKETTIMEDOUT'
+ e.connect = false
+ self.emit('error', e)
+ }
+ })
+ }
+ if (timeout !== undefined) {
+ // Only start the connection timer if we're actually connecting a new
+ // socket, otherwise if we're already connected (because this is a
+ // keep-alive connection) do not bother. This is important since we won't
+ // get a 'connect' event for an already connected socket.
+ if (isConnecting) {
+ var onReqSockConnect = function () {
+ socket.removeListener('connect', onReqSockConnect)
+ self.clearTimeout()
+ setReqTimeout()
+ }
+
+ socket.on('connect', onReqSockConnect)
+
+ self.req.on('error', function (err) { // eslint-disable-line handle-callback-err
+ socket.removeListener('connect', onReqSockConnect)
+ })
+
+ // Set a timeout in memory - this block will throw if the server takes more
+ // than `timeout` to write the HTTP status and headers (corresponding to
+ // the on('response') event on the client). NB: this measures wall-clock
+ // time, not the time between bytes sent by the server.
+ self.timeoutTimer = setTimeout(function () {
+ socket.removeListener('connect', onReqSockConnect)
+ self.abort()
+ var e = new Error('ETIMEDOUT')
+ e.code = 'ETIMEDOUT'
+ e.connect = true
+ self.emit('error', e)
+ }, timeout)
+ } else {
+ // We're already connected
+ setReqTimeout()
+ }
+ }
+ self.emit('socket', socket)
+ })
+
+ self.emit('request', self.req)
+}
+
+Request.prototype.onRequestError = function (error) {
+ var self = this
+ if (self._aborted) {
+ return
+ }
+ if (self.req && self.req._reusedSocket && error.code === 'ECONNRESET' &&
+ self.agent.addRequestNoreuse) {
+ self.agent = { addRequest: self.agent.addRequestNoreuse.bind(self.agent) }
+ self.start()
+ self.req.end()
+ return
+ }
+ self.clearTimeout()
+ self.emit('error', error)
+}
+
+Request.prototype.onRequestResponse = function (response) {
+ var self = this
+
+ if (self.timing) {
+ self.timings.response = now() - self.startTimeNow
+ }
+
+ debug('onRequestResponse', self.uri.href, response.statusCode, response.headers)
+ response.on('end', function () {
+ if (self.timing) {
+ self.timings.end = now() - self.startTimeNow
+ response.timingStart = self.startTime
+
+ // fill in the blanks for any periods that didn't trigger, such as
+ // no lookup or connect due to keep alive
+ if (!self.timings.socket) {
+ self.timings.socket = 0
+ }
+ if (!self.timings.lookup) {
+ self.timings.lookup = self.timings.socket
+ }
+ if (!self.timings.connect) {
+ self.timings.connect = self.timings.lookup
+ }
+ if (!self.timings.response) {
+ self.timings.response = self.timings.connect
+ }
+
+ debug('elapsed time', self.timings.end)
+
+ // elapsedTime includes all redirects
+ self.elapsedTime += Math.round(self.timings.end)
+
+ // NOTE: elapsedTime is deprecated in favor of .timings
+ response.elapsedTime = self.elapsedTime
+
+ // timings is just for the final fetch
+ response.timings = self.timings
+
+ // pre-calculate phase timings as well
+ response.timingPhases = {
+ wait: self.timings.socket,
+ dns: self.timings.lookup - self.timings.socket,
+ tcp: self.timings.connect - self.timings.lookup,
+ firstByte: self.timings.response - self.timings.connect,
+ download: self.timings.end - self.timings.response,
+ total: self.timings.end
+ }
+ }
+ debug('response end', self.uri.href, response.statusCode, response.headers)
+ })
+
+ if (self._aborted) {
+ debug('aborted', self.uri.href)
+ response.resume()
+ return
+ }
+
+ self.response = response
+ response.request = self
+ response.toJSON = responseToJSON
+
+ // XXX This is different on 0.10, because SSL is strict by default
+ if (self.httpModule === https &&
+ self.strictSSL && (!response.hasOwnProperty('socket') ||
+ !response.socket.authorized)) {
+ debug('strict ssl error', self.uri.href)
+ var sslErr = response.hasOwnProperty('socket') ? response.socket.authorizationError : self.uri.href + ' does not support SSL'
+ self.emit('error', new Error('SSL Error: ' + sslErr))
+ return
+ }
+
+ // Save the original host before any redirect (if it changes, we need to
+ // remove any authorization headers). Also remember the case of the header
+ // name because lots of broken servers expect Host instead of host and we
+ // want the caller to be able to specify this.
+ self.originalHost = self.getHeader('host')
+ if (!self.originalHostHeaderName) {
+ self.originalHostHeaderName = self.hasHeader('host')
+ }
+ if (self.setHost) {
+ self.removeHeader('host')
+ }
+ self.clearTimeout()
+
+ var targetCookieJar = (self._jar && self._jar.setCookie) ? self._jar : globalCookieJar
+ var addCookie = function (cookie) {
+ // set the cookie if it's domain in the href's domain.
+ try {
+ targetCookieJar.setCookie(cookie, self.uri.href, {ignoreError: true})
+ } catch (e) {
+ self.emit('error', e)
+ }
+ }
+
+ response.caseless = caseless(response.headers)
+
+ if (response.caseless.has('set-cookie') && (!self._disableCookies)) {
+ var headerName = response.caseless.has('set-cookie')
+ if (Array.isArray(response.headers[headerName])) {
+ response.headers[headerName].forEach(addCookie)
+ } else {
+ addCookie(response.headers[headerName])
+ }
+ }
+
+ if (self._redirect.onResponse(response)) {
+ return // Ignore the rest of the response
+ } else {
+ // Be a good stream and emit end when the response is finished.
+ // Hack to emit end on close because of a core bug that never fires end
+ response.on('close', function () {
+ if (!self._ended) {
+ self.response.emit('end')
+ }
+ })
+
+ response.once('end', function () {
+ self._ended = true
+ })
+
+ var noBody = function (code) {
+ return (
+ self.method === 'HEAD' ||
+ // Informational
+ (code >= 100 && code < 200) ||
+ // No Content
+ code === 204 ||
+ // Not Modified
+ code === 304
+ )
+ }
+
+ var responseContent
+ if (self.gzip && !noBody(response.statusCode)) {
+ var contentEncoding = response.headers['content-encoding'] || 'identity'
+ contentEncoding = contentEncoding.trim().toLowerCase()
+
+ // Be more lenient with decoding compressed responses, since (very rarely)
+ // servers send slightly invalid gzip responses that are still accepted
+ // by common browsers.
+ // Always using Z_SYNC_FLUSH is what cURL does.
+ var zlibOptions = {
+ flush: zlib.Z_SYNC_FLUSH,
+ finishFlush: zlib.Z_SYNC_FLUSH
+ }
+
+ if (contentEncoding === 'gzip') {
+ responseContent = zlib.createGunzip(zlibOptions)
+ response.pipe(responseContent)
+ } else if (contentEncoding === 'deflate') {
+ responseContent = zlib.createInflate(zlibOptions)
+ response.pipe(responseContent)
+ } else {
+ // Since previous versions didn't check for Content-Encoding header,
+ // ignore any invalid values to preserve backwards-compatibility
+ if (contentEncoding !== 'identity') {
+ debug('ignoring unrecognized Content-Encoding ' + contentEncoding)
+ }
+ responseContent = response
+ }
+ } else {
+ responseContent = response
+ }
+
+ if (self.encoding) {
+ if (self.dests.length !== 0) {
+ console.error('Ignoring encoding parameter as this stream is being piped to another stream which makes the encoding option invalid.')
+ } else {
+ responseContent.setEncoding(self.encoding)
+ }
+ }
+
+ if (self._paused) {
+ responseContent.pause()
+ }
+
+ self.responseContent = responseContent
+
+ self.emit('response', response)
+
+ self.dests.forEach(function (dest) {
+ self.pipeDest(dest)
+ })
+
+ responseContent.on('data', function (chunk) {
+ if (self.timing && !self.responseStarted) {
+ self.responseStartTime = (new Date()).getTime()
+
+ // NOTE: responseStartTime is deprecated in favor of .timings
+ response.responseStartTime = self.responseStartTime
+ }
+ self._destdata = true
+ self.emit('data', chunk)
+ })
+ responseContent.once('end', function (chunk) {
+ self.emit('end', chunk)
+ })
+ responseContent.on('error', function (error) {
+ self.emit('error', error)
+ })
+ responseContent.on('close', function () { self.emit('close') })
+
+ if (self.callback) {
+ self.readResponseBody(response)
+ } else { // if no callback
+ self.on('end', function () {
+ if (self._aborted) {
+ debug('aborted', self.uri.href)
+ return
+ }
+ self.emit('complete', response)
+ })
+ }
+ }
+ debug('finish init function', self.uri.href)
+}
+
+Request.prototype.readResponseBody = function (response) {
+ var self = this
+ debug("reading response's body")
+ var buffers = []
+ var bufferLength = 0
+ var strings = []
+
+ self.on('data', function (chunk) {
+ if (!Buffer.isBuffer(chunk)) {
+ strings.push(chunk)
+ } else if (chunk.length) {
+ bufferLength += chunk.length
+ buffers.push(chunk)
+ }
+ })
+ self.on('end', function () {
+ debug('end event', self.uri.href)
+ if (self._aborted) {
+ debug('aborted', self.uri.href)
+ // `buffer` is defined in the parent scope and used in a closure it exists for the life of the request.
+ // This can lead to leaky behavior if the user retains a reference to the request object.
+ buffers = []
+ bufferLength = 0
+ return
+ }
+
+ if (bufferLength) {
+ debug('has body', self.uri.href, bufferLength)
+ response.body = Buffer.concat(buffers, bufferLength)
+ if (self.encoding !== null) {
+ response.body = response.body.toString(self.encoding)
+ }
+ // `buffer` is defined in the parent scope and used in a closure it exists for the life of the Request.
+ // This can lead to leaky behavior if the user retains a reference to the request object.
+ buffers = []
+ bufferLength = 0
+ } else if (strings.length) {
+ // The UTF8 BOM [0xEF,0xBB,0xBF] is converted to [0xFE,0xFF] in the JS UTC16/UCS2 representation.
+ // Strip this value out when the encoding is set to 'utf8', as upstream consumers won't expect it and it breaks JSON.parse().
+ if (self.encoding === 'utf8' && strings[0].length > 0 && strings[0][0] === '\uFEFF') {
+ strings[0] = strings[0].substring(1)
+ }
+ response.body = strings.join('')
+ }
+
+ if (self._json) {
+ try {
+ response.body = JSON.parse(response.body, self._jsonReviver)
+ } catch (e) {
+ debug('invalid JSON received', self.uri.href)
+ }
+ }
+ debug('emitting complete', self.uri.href)
+ if (typeof response.body === 'undefined' && !self._json) {
+ response.body = self.encoding === null ? Buffer.alloc(0) : ''
+ }
+ self.emit('complete', response, response.body)
+ })
+}
+
+Request.prototype.abort = function () {
+ var self = this
+ self._aborted = true
+
+ if (self.req) {
+ self.req.abort()
+ } else if (self.response) {
+ self.response.destroy()
+ }
+
+ self.clearTimeout()
+ self.emit('abort')
+}
+
+Request.prototype.pipeDest = function (dest) {
+ var self = this
+ var response = self.response
+ // Called after the response is received
+ if (dest.headers && !dest.headersSent) {
+ if (response.caseless.has('content-type')) {
+ var ctname = response.caseless.has('content-type')
+ if (dest.setHeader) {
+ dest.setHeader(ctname, response.headers[ctname])
+ } else {
+ dest.headers[ctname] = response.headers[ctname]
+ }
+ }
+
+ if (response.caseless.has('content-length')) {
+ var clname = response.caseless.has('content-length')
+ if (dest.setHeader) {
+ dest.setHeader(clname, response.headers[clname])
+ } else {
+ dest.headers[clname] = response.headers[clname]
+ }
+ }
+ }
+ if (dest.setHeader && !dest.headersSent) {
+ for (var i in response.headers) {
+ // If the response content is being decoded, the Content-Encoding header
+ // of the response doesn't represent the piped content, so don't pass it.
+ if (!self.gzip || i !== 'content-encoding') {
+ dest.setHeader(i, response.headers[i])
+ }
+ }
+ dest.statusCode = response.statusCode
+ }
+ if (self.pipefilter) {
+ self.pipefilter(response, dest)
+ }
+}
+
+Request.prototype.qs = function (q, clobber) {
+ var self = this
+ var base
+ if (!clobber && self.uri.query) {
+ base = self._qs.parse(self.uri.query)
+ } else {
+ base = {}
+ }
+
+ for (var i in q) {
+ base[i] = q[i]
+ }
+
+ var qs = self._qs.stringify(base)
+
+ if (qs === '') {
+ return self
+ }
+
+ self.uri = url.parse(self.uri.href.split('?')[0] + '?' + qs)
+ self.url = self.uri
+ self.path = self.uri.path
+
+ if (self.uri.host === 'unix') {
+ self.enableUnixSocket()
+ }
+
+ return self
+}
+Request.prototype.form = function (form) {
+ var self = this
+ if (form) {
+ if (!/^application\/x-www-form-urlencoded\b/.test(self.getHeader('content-type'))) {
+ self.setHeader('content-type', 'application/x-www-form-urlencoded')
+ }
+ self.body = (typeof form === 'string')
+ ? self._qs.rfc3986(form.toString('utf8'))
+ : self._qs.stringify(form).toString('utf8')
+ return self
+ }
+ // create form-data object
+ self._form = new FormData()
+ self._form.on('error', function (err) {
+ err.message = 'form-data: ' + err.message
+ self.emit('error', err)
+ self.abort()
+ })
+ return self._form
+}
+Request.prototype.multipart = function (multipart) {
+ var self = this
+
+ self._multipart.onRequest(multipart)
+
+ if (!self._multipart.chunked) {
+ self.body = self._multipart.body
+ }
+
+ return self
+}
+Request.prototype.json = function (val) {
+ var self = this
+
+ if (!self.hasHeader('accept')) {
+ self.setHeader('accept', 'application/json')
+ }
+
+ if (typeof self.jsonReplacer === 'function') {
+ self._jsonReplacer = self.jsonReplacer
+ }
+
+ self._json = true
+ if (typeof val === 'boolean') {
+ if (self.body !== undefined) {
+ if (!/^application\/x-www-form-urlencoded\b/.test(self.getHeader('content-type'))) {
+ self.body = safeStringify(self.body, self._jsonReplacer)
+ } else {
+ self.body = self._qs.rfc3986(self.body)
+ }
+ if (!self.hasHeader('content-type')) {
+ self.setHeader('content-type', 'application/json')
+ }
+ }
+ } else {
+ self.body = safeStringify(val, self._jsonReplacer)
+ if (!self.hasHeader('content-type')) {
+ self.setHeader('content-type', 'application/json')
+ }
+ }
+
+ if (typeof self.jsonReviver === 'function') {
+ self._jsonReviver = self.jsonReviver
+ }
+
+ return self
+}
+Request.prototype.getHeader = function (name, headers) {
+ var self = this
+ var result, re, match
+ if (!headers) {
+ headers = self.headers
+ }
+ Object.keys(headers).forEach(function (key) {
+ if (key.length !== name.length) {
+ return
+ }
+ re = new RegExp(name, 'i')
+ match = key.match(re)
+ if (match) {
+ result = headers[key]
+ }
+ })
+ return result
+}
+Request.prototype.enableUnixSocket = function () {
+ // Get the socket & request paths from the URL
+ var unixParts = this.uri.path.split(':')
+ var host = unixParts[0]
+ var path = unixParts[1]
+ // Apply unix properties to request
+ this.socketPath = host
+ this.uri.pathname = path
+ this.uri.path = path
+ this.uri.host = host
+ this.uri.hostname = host
+ this.uri.isUnix = true
+}
+
+Request.prototype.auth = function (user, pass, sendImmediately, bearer) {
+ var self = this
+
+ self._auth.onRequest(user, pass, sendImmediately, bearer)
+
+ return self
+}
+Request.prototype.aws = function (opts, now) {
+ var self = this
+
+ if (!now) {
+ self._aws = opts
+ return self
+ }
+
+ if (opts.sign_version === 4 || opts.sign_version === '4') {
+ // use aws4
+ var options = {
+ host: self.uri.host,
+ path: self.uri.path,
+ method: self.method,
+ headers: self.headers,
+ body: self.body
+ }
+ if (opts.service) {
+ options.service = opts.service
+ }
+ var signRes = aws4.sign(options, {
+ accessKeyId: opts.key,
+ secretAccessKey: opts.secret,
+ sessionToken: opts.session
+ })
+ self.setHeader('authorization', signRes.headers.Authorization)
+ self.setHeader('x-amz-date', signRes.headers['X-Amz-Date'])
+ if (signRes.headers['X-Amz-Security-Token']) {
+ self.setHeader('x-amz-security-token', signRes.headers['X-Amz-Security-Token'])
+ }
+ } else {
+ // default: use aws-sign2
+ var date = new Date()
+ self.setHeader('date', date.toUTCString())
+ var auth = {
+ key: opts.key,
+ secret: opts.secret,
+ verb: self.method.toUpperCase(),
+ date: date,
+ contentType: self.getHeader('content-type') || '',
+ md5: self.getHeader('content-md5') || '',
+ amazonHeaders: aws2.canonicalizeHeaders(self.headers)
+ }
+ var path = self.uri.path
+ if (opts.bucket && path) {
+ auth.resource = '/' + opts.bucket + path
+ } else if (opts.bucket && !path) {
+ auth.resource = '/' + opts.bucket
+ } else if (!opts.bucket && path) {
+ auth.resource = path
+ } else if (!opts.bucket && !path) {
+ auth.resource = '/'
+ }
+ auth.resource = aws2.canonicalizeResource(auth.resource)
+ self.setHeader('authorization', aws2.authorization(auth))
+ }
+
+ return self
+}
+Request.prototype.httpSignature = function (opts) {
+ var self = this
+ httpSignature.signRequest({
+ getHeader: function (header) {
+ return self.getHeader(header, self.headers)
+ },
+ setHeader: function (header, value) {
+ self.setHeader(header, value)
+ },
+ method: self.method,
+ path: self.path
+ }, opts)
+ debug('httpSignature authorization', self.getHeader('authorization'))
+
+ return self
+}
+Request.prototype.hawk = function (opts) {
+ var self = this
+ self.setHeader('Authorization', hawk.header(self.uri, self.method, opts))
+}
+Request.prototype.oauth = function (_oauth) {
+ var self = this
+
+ self._oauth.onRequest(_oauth)
+
+ return self
+}
+
+Request.prototype.jar = function (jar) {
+ var self = this
+ var cookies
+
+ if (self._redirect.redirectsFollowed === 0) {
+ self.originalCookieHeader = self.getHeader('cookie')
+ }
+
+ if (!jar) {
+ // disable cookies
+ cookies = false
+ self._disableCookies = true
+ } else {
+ var targetCookieJar = jar.getCookieString ? jar : globalCookieJar
+ var urihref = self.uri.href
+ // fetch cookie in the Specified host
+ if (targetCookieJar) {
+ cookies = targetCookieJar.getCookieString(urihref)
+ }
+ }
+
+ // if need cookie and cookie is not empty
+ if (cookies && cookies.length) {
+ if (self.originalCookieHeader) {
+ // Don't overwrite existing Cookie header
+ self.setHeader('cookie', self.originalCookieHeader + '; ' + cookies)
+ } else {
+ self.setHeader('cookie', cookies)
+ }
+ }
+ self._jar = jar
+ return self
+}
+
+// Stream API
+Request.prototype.pipe = function (dest, opts) {
+ var self = this
+
+ if (self.response) {
+ if (self._destdata) {
+ self.emit('error', new Error('You cannot pipe after data has been emitted from the response.'))
+ } else if (self._ended) {
+ self.emit('error', new Error('You cannot pipe after the response has been ended.'))
+ } else {
+ stream.Stream.prototype.pipe.call(self, dest, opts)
+ self.pipeDest(dest)
+ return dest
+ }
+ } else {
+ self.dests.push(dest)
+ stream.Stream.prototype.pipe.call(self, dest, opts)
+ return dest
+ }
+}
+Request.prototype.write = function () {
+ var self = this
+ if (self._aborted) { return }
+
+ if (!self._started) {
+ self.start()
+ }
+ if (self.req) {
+ return self.req.write.apply(self.req, arguments)
+ }
+}
+Request.prototype.end = function (chunk) {
+ var self = this
+ if (self._aborted) { return }
+
+ if (chunk) {
+ self.write(chunk)
+ }
+ if (!self._started) {
+ self.start()
+ }
+ if (self.req) {
+ self.req.end()
+ }
+}
+Request.prototype.pause = function () {
+ var self = this
+ if (!self.responseContent) {
+ self._paused = true
+ } else {
+ self.responseContent.pause.apply(self.responseContent, arguments)
+ }
+}
+Request.prototype.resume = function () {
+ var self = this
+ if (!self.responseContent) {
+ self._paused = false
+ } else {
+ self.responseContent.resume.apply(self.responseContent, arguments)
+ }
+}
+Request.prototype.destroy = function () {
+ var self = this
+ this.clearTimeout()
+ if (!self._ended) {
+ self.end()
+ } else if (self.response) {
+ self.response.destroy()
+ }
+}
+
+Request.prototype.clearTimeout = function () {
+ if (this.timeoutTimer) {
+ clearTimeout(this.timeoutTimer)
+ this.timeoutTimer = null
+ }
+}
+
+Request.defaultProxyHeaderWhiteList =
+ Tunnel.defaultProxyHeaderWhiteList.slice()
+
+Request.defaultProxyHeaderExclusiveList =
+ Tunnel.defaultProxyHeaderExclusiveList.slice()
+
+// Exports
+
+Request.prototype.toJSON = requestToJSON
+module.exports = Request
diff --git a/tests/browser/karma.conf.js b/tests/browser/karma.conf.js
new file mode 100644
index 000000000..5fbf31a45
--- /dev/null
+++ b/tests/browser/karma.conf.js
@@ -0,0 +1,57 @@
+'use strict'
+var istanbul = require('browserify-istanbul')
+
+module.exports = function (config) {
+ config.set({
+ client: { requestTestUrl: process.argv[4] },
+ basePath: '../..',
+ frameworks: ['tap', 'browserify'],
+ preprocessors: {
+ 'tests/browser/test.js': ['browserify'],
+ '*.js,!(tests)/**/*.js': ['coverage']
+ },
+ files: [
+ 'tests/browser/test.js'
+ ],
+ port: 9876,
+
+ reporters: ['dots', 'coverage'],
+
+ colors: true,
+
+ logLevel: config.LOG_ERROR,
+
+ autoWatch: false,
+
+ browsers: ['PhantomJS_without_security'],
+
+ singleRun: true,
+
+ plugins: [
+ 'karma-phantomjs-launcher',
+ 'karma-coverage',
+ 'karma-browserify',
+ 'karma-tap'
+ ],
+ browserify: {
+ debug: true,
+ transform: [istanbul({
+ ignore: ['**/node_modules/**', '**/tests/**']
+ })]
+ },
+ coverageReporter: {
+ type: 'lcov',
+ dir: 'coverage/'
+ },
+
+ // Custom launcher to allowe self signed certs.
+ customLaunchers: {
+ PhantomJS_without_security: {
+ base: 'PhantomJS',
+ flags: [
+ '--ignore-ssl-errors=true'
+ ]
+ }
+ }
+ })
+}
diff --git a/tests/browser/ssl/ca.crt b/tests/browser/ssl/ca.crt
new file mode 100644
index 000000000..6e9e3ceb3
--- /dev/null
+++ b/tests/browser/ssl/ca.crt
@@ -0,0 +1,14 @@
+-----BEGIN CERTIFICATE-----
+MIICKTCCAZICCQDB/6lRlsirjzANBgkqhkiG9w0BAQUFADBZMQswCQYDVQQGEwJB
+VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0
+cyBQdHkgTHRkMRIwEAYDVQQDEwlsb2NhbGhvc3QwHhcNMTUwMTA3MTcwODM2WhcN
+MjUwMTA0MTcwODM2WjBZMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0
+ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDEwls
+b2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMba6FQ1VkgW8vWa
+FBxV1VdLhQ5HP0eKZ/CyEGG4r89CzfzC0+V3bnFWGBGF2kSJlVjc7eVSSVio383A
+inq3i+86Mavfy18BwcP4I0NqUSvvcV9yduBLUySklJhOlhhHeFUlycQyxuODbrG9
+QOd411c4eccsbDHq5vSnS7AJh6tVAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAI0H3
+bJIUQgnSGyrSHmjoKm66QLBPvQ1Zd7Zxjlg1uFv6glPOhdkeTQx9XQPT/WDG3qmJ
+BdHvQLDtPS9P8vRaiQW1OCP7dQJkVYCxyFbSQiieuzwBAEGtZcLdZbvcp3PKRGbx
+sIrkzyYdAXE1EZ5z7yLVcpWwTKnBnuRz2D0XOk4=
+-----END CERTIFICATE-----
diff --git a/tests/browser/ssl/server.crt b/tests/browser/ssl/server.crt
new file mode 100644
index 000000000..f97713b67
--- /dev/null
+++ b/tests/browser/ssl/server.crt
@@ -0,0 +1,14 @@
+-----BEGIN CERTIFICATE-----
+MIICKTCCAZICCQDl9xx8ZXLMPTANBgkqhkiG9w0BAQUFADBZMQswCQYDVQQGEwJB
+VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0
+cyBQdHkgTHRkMRIwEAYDVQQDEwlsb2NhbGhvc3QwHhcNMTUwMTA3MTcwOTQ4WhcN
+MjUwMTA0MTcwOTQ4WjBZMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0
+ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDEwls
+b2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKWxvvLNi8AcT0wI
+sf+LoWAvtoIV29ypI6j1JRqmsPO433JP/ijLhJLFc6RKXpKs6pd4am82vzk8Bxs3
+VtUXJ0yKh3KMevT7L4X1hw+QxvYAZD6Kl/kaNvKFTuAgcaeSnmnWGjQYLF/i20w7
+7dpeXDmnNMCKwdg+kLeJdPtW0d+vAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEADL6l
+Z2mDKj4kPyGD58uBGeBHjmcDA2oJcnsHhOiC1u1NuCwQW4RDWs6Ili0GhuHYHP0B
+JDcPw6ZSa1Gx3ZaUJ5yM/+YHpbLev34CjmiwQeG36DF2rAxfoIQk/wI4iWmu1+ed
+5Wwc0cZAb10uy0ihmMK98yhVQPmkBOEyw2O1xJw=
+-----END CERTIFICATE-----
diff --git a/tests/browser/ssl/server.key b/tests/browser/ssl/server.key
new file mode 100644
index 000000000..dcb89027b
--- /dev/null
+++ b/tests/browser/ssl/server.key
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQClsb7yzYvAHE9MCLH/i6FgL7aCFdvcqSOo9SUaprDzuN9yT/4o
+y4SSxXOkSl6SrOqXeGpvNr85PAcbN1bVFydMiodyjHr0+y+F9YcPkMb2AGQ+ipf5
+GjbyhU7gIHGnkp5p1ho0GCxf4ttMO+3aXlw5pzTAisHYPpC3iXT7VtHfrwIDAQAB
+AoGBAJa5edmk4NuA5SFlR4YOnl3BCWSMPdQciDPJzFbSC2WpZpm16p1xhMd+lhN9
+E0qZwUzIXQmN46VM1aoMTRDKXxPqujUIvhn7kxMLmD8lajHzFUIhgnp1XQCfxIIV
+sCcnIoP+cbnzP+AegAEPjds/0ngI3YM28UeooqZAmZCHQ0cBAkEAz0go7tCxXqED
++cY+P2axAKuGR+6srx08g5kONTpUx8jXr4su02F376dxhPB9UXWOJkYiGEBwKEds
+OnUSNTF/RQJBAMyjUzjb/u6sZqTcHd3owes3UsCC+kfSb814qdG3Z9qYX9p55ESu
+hA7Sbjq0WdTHGZdgEexwpfLtTRS8x5ldiGMCQFC3GLlmKqtep92rhLHLm0FXiYKZ
+PkUybU4RW6b+f+UMIHELEcDeQ4Xe/iV2QFZoIGJnDP/El+gXZ92bmOt9ysECQD/i
+zVx28gO5NuJJBdn9jGzOfLs1KMW7YMQY44thYr7Pyzz9yNHYWcn20Arruw++iLLF
+f1L9aBGLHAFZXkb2+FkCQA5/3Z3SgiZrRYX/bWcNe6N65+nGfJv8ZBVsX13pKOxR
+8JzJLyEmx67IOGZvVgfVABrCHJvTrKlUO3x3mDoEPzI=
+-----END RSA PRIVATE KEY-----
diff --git a/tests/browser/start.js b/tests/browser/start.js
new file mode 100644
index 000000000..3ce0c6a81
--- /dev/null
+++ b/tests/browser/start.js
@@ -0,0 +1,37 @@
+'use strict'
+var spawn = require('child_process').spawn
+var https = require('https')
+var fs = require('fs')
+var path = require('path')
+
+var server = https.createServer({
+ key: fs.readFileSync(path.join(__dirname, '/ssl/server.key')),
+ cert: fs.readFileSync(path.join(__dirname, '/ssl/server.crt')),
+ ca: fs.readFileSync(path.join(__dirname, '/ssl/ca.crt')),
+ requestCert: true,
+ rejectUnauthorized: false
+}, function (req, res) {
+ // Set CORS header, since that is something we are testing.
+ res.setHeader('Access-Control-Allow-Origin', '*')
+ res.writeHead(200)
+ res.end('Can you hear the sound of an enormous door slamming in the depths of hell?\n')
+})
+server.listen(0, function () {
+ var port = this.address().port
+ console.log('Started https server for karma tests on port ' + port)
+ // Spawn process for karma.
+ var c = spawn('karma', [
+ 'start',
+ path.join(__dirname, '/karma.conf.js'),
+ 'https://localhost:' + port
+ ])
+ c.stdout.pipe(process.stdout)
+ c.stderr.pipe(process.stderr)
+ c.on('exit', function (c) {
+ // Exit process with karma exit code.
+ if (c !== 0) {
+ throw new Error('Karma exited with status code ' + c)
+ }
+ server.close()
+ })
+})
diff --git a/tests/browser/test.js b/tests/browser/test.js
new file mode 100644
index 000000000..34135a398
--- /dev/null
+++ b/tests/browser/test.js
@@ -0,0 +1,34 @@
+'use strict'
+
+if (!Function.prototype.bind) {
+ // This is because of the fact that phantom.js does not have Function.bind.
+ // This is a bug in phantom.js.
+ // More info: https://github.com/ariya/phantomjs/issues/10522
+ /* eslint no-extend-native:0 */
+ Function.prototype.bind = require('function-bind')
+}
+
+var tape = require('tape')
+var request = require('../../index')
+
+tape('returns on error', function (t) {
+ t.plan(1)
+ request({
+ uri: 'https://stupid.nonexistent.path:port123/\\<-great-idea',
+ withCredentials: false
+ }, function (error, response) {
+ t.equal(typeof error, 'object')
+ t.end()
+ })
+})
+
+tape('succeeds on valid URLs (with https and CORS)', function (t) {
+ t.plan(1)
+ request({
+ uri: __karma__.config.requestTestUrl, // eslint-disable-line no-undef
+ withCredentials: false
+ }, function (_, response) {
+ t.equal(response.statusCode, 200)
+ t.end()
+ })
+})
diff --git a/tests/fixtures/har.json b/tests/fixtures/har.json
new file mode 100644
index 000000000..4864a1b28
--- /dev/null
+++ b/tests/fixtures/har.json
@@ -0,0 +1,158 @@
+{
+ "application-form-encoded": {
+ "method": "POST",
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "application/x-www-form-urlencoded"
+ }
+ ],
+ "postData": {
+ "mimeType": "application/x-www-form-urlencoded; charset=UTF-8",
+ "params": [
+ {
+ "name": "foo",
+ "value": "bar"
+ },
+ {
+ "name": "hello",
+ "value": "world"
+ }
+ ]
+ }
+ },
+
+ "application-json": {
+ "method": "POST",
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "application/json"
+ }
+ ],
+ "postData": {
+ "mimeType": "application/json",
+ "text": "{\"number\":1,\"string\":\"f\\\"oo\",\"arr\":[1,2,3],\"nested\":{\"a\":\"b\"},\"arr_mix\":[1,\"a\",{\"arr_mix_nested\":{}}]}"
+ }
+ },
+
+ "cookies": {
+ "method": "POST",
+ "cookies": [
+ {
+ "name": "foo",
+ "value": "bar"
+ },
+ {
+ "name": "bar",
+ "value": "baz"
+ }
+ ]
+ },
+
+ "custom-method": {
+ "method": "PROPFIND"
+ },
+
+ "headers": {
+ "method": "GET",
+ "headers": [
+ {
+ "name": "x-foo",
+ "value": "Bar"
+ }
+ ]
+ },
+
+ "multipart-data": {
+ "method": "POST",
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "multipart/form-data"
+ }
+ ],
+ "postData": {
+ "mimeType": "multipart/form-data",
+ "params": [
+ {
+ "name": "foo",
+ "value": "Hello World",
+ "fileName": "hello.txt",
+ "contentType": "text/plain"
+ }
+ ]
+ }
+ },
+
+ "multipart-file": {
+ "method": "POST",
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "multipart/form-data"
+ }
+ ],
+ "postData": {
+ "mimeType": "multipart/form-data",
+ "params": [
+ {
+ "name": "foo",
+ "fileName": "../tests/unicycle.jpg",
+ "contentType": "image/jpeg"
+ }
+ ]
+ }
+ },
+
+ "multipart-form-data": {
+ "method": "POST",
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "multipart/form-data"
+ }
+ ],
+ "postData": {
+ "mimeType": "multipart/form-data",
+ "params": [
+ {
+ "name": "foo",
+ "value": "bar"
+ }
+ ]
+ }
+ },
+
+ "query": {
+ "method": "GET",
+ "queryString": [
+ {
+ "name": "foo",
+ "value": "bar"
+ },
+ {
+ "name": "foo",
+ "value": "baz"
+ },
+ {
+ "name": "baz",
+ "value": "abc"
+ }
+ ]
+ },
+
+ "text-plain": {
+ "method": "POST",
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "text/plain"
+ }
+ ],
+ "postData": {
+ "mimeType": "text/plain",
+ "text": "Hello World"
+ }
+ }
+}
diff --git a/tests/googledoodle.jpg b/tests/googledoodle.jpg
new file mode 100644
index 000000000..377ff1a18
Binary files /dev/null and b/tests/googledoodle.jpg differ
diff --git a/tests/googledoodle.png b/tests/googledoodle.png
deleted file mode 100644
index f80c9c52d..000000000
Binary files a/tests/googledoodle.png and /dev/null differ
diff --git a/tests/run.js b/tests/run.js
deleted file mode 100644
index 205fc3ae5..000000000
--- a/tests/run.js
+++ /dev/null
@@ -1,38 +0,0 @@
-var spawn = require('child_process').spawn
- , exitCode = 0
- ;
-
-var tests = [
- 'test-body.js'
- , 'test-cookie.js'
- , 'test-cookiejar.js'
- , 'test-defaults.js'
- , 'test-errors.js'
- , 'test-headers.js'
- , 'test-httpModule.js'
- , 'test-https.js'
- , 'test-https-strict.js'
- , 'test-oauth.js'
- , 'test-pipes.js'
- , 'test-proxy.js'
- , 'test-qs.js'
- , 'test-redirect.js'
- , 'test-timeout.js'
- , 'test-toJSON.js'
- , 'test-tunnel.js'
-]
-
-var next = function () {
- if (tests.length === 0) process.exit(exitCode);
-
- var file = tests.shift()
- console.log(file)
- var proc = spawn('node', [ 'tests/' + file ])
- proc.stdout.pipe(process.stdout)
- proc.stderr.pipe(process.stderr)
- proc.on('exit', function (code) {
- exitCode += code || 0
- next()
- })
-}
-next()
diff --git a/tests/server.js b/tests/server.js
index 921f51204..93a68913d 100644
--- a/tests/server.js
+++ b/tests/server.js
@@ -1,82 +1,142 @@
+'use strict'
+
var fs = require('fs')
- , http = require('http')
- , path = require('path')
- , https = require('https')
- , events = require('events')
- , stream = require('stream')
- , assert = require('assert')
- ;
+var http = require('http')
+var path = require('path')
+var https = require('https')
+var stream = require('stream')
+var assert = require('assert')
-exports.createServer = function (port) {
- port = port || 6767
+exports.createServer = function () {
var s = http.createServer(function (req, resp) {
- s.emit(req.url, req, resp);
+ s.emit(req.url.replace(/(\?.*)/, ''), req, resp)
+ })
+ s.on('listening', function () {
+ s.port = this.address().port
+ s.url = 'http://localhost:' + s.port
})
- s.port = port
- s.url = 'http://localhost:'+port
- return s;
+ s.port = 0
+ s.protocol = 'http'
+ return s
}
-exports.createSSLServer = function(port, opts) {
- port = port || 16767
+exports.createEchoServer = function () {
+ var s = http.createServer(function (req, resp) {
+ var b = ''
+ req.on('data', function (chunk) { b += chunk })
+ req.on('end', function () {
+ resp.writeHead(200, {'content-type': 'application/json'})
+ resp.write(JSON.stringify({
+ url: req.url,
+ method: req.method,
+ headers: req.headers,
+ body: b
+ }))
+ resp.end()
+ })
+ })
+ s.on('listening', function () {
+ s.port = this.address().port
+ s.url = 'http://localhost:' + s.port
+ })
+ s.port = 0
+ s.protocol = 'http'
+ return s
+}
- var options = { 'key' : path.join(__dirname, 'ssl', 'test.key')
- , 'cert': path.join(__dirname, 'ssl', 'test.crt')
- }
+exports.createSSLServer = function (opts) {
+ var i
+ var options = { 'key': path.join(__dirname, 'ssl', 'test.key'), 'cert': path.join(__dirname, 'ssl', 'test.crt') }
if (opts) {
- for (var i in opts) options[i] = opts[i]
+ for (i in opts) {
+ options[i] = opts[i]
+ }
}
- for (var i in options) {
- options[i] = fs.readFileSync(options[i])
+ for (i in options) {
+ if (i !== 'requestCert' && i !== 'rejectUnauthorized' && i !== 'ciphers') {
+ options[i] = fs.readFileSync(options[i])
+ }
}
var s = https.createServer(options, function (req, resp) {
- s.emit(req.url, req, resp);
+ s.emit(req.url, req, resp)
})
- s.port = port
- s.url = 'https://localhost:'+port
- return s;
+ s.on('listening', function () {
+ s.port = this.address().port
+ s.url = 'https://localhost:' + s.port
+ })
+ s.port = 0
+ s.protocol = 'https'
+ return s
}
exports.createPostStream = function (text) {
- var postStream = new stream.Stream();
- postStream.writeable = true;
- postStream.readable = true;
- setTimeout(function () {postStream.emit('data', new Buffer(text)); postStream.emit('end')}, 0);
- return postStream;
+ var postStream = new stream.Stream()
+ postStream.writeable = true
+ postStream.readable = true
+ setTimeout(function () {
+ postStream.emit('data', Buffer.from(text))
+ postStream.emit('end')
+ }, 0)
+ return postStream
}
-exports.createPostValidator = function (text) {
+exports.createPostValidator = function (text, reqContentType) {
var l = function (req, resp) {
- var r = '';
- req.on('data', function (chunk) {r += chunk})
+ var r = ''
+ req.on('data', function (chunk) { r += chunk })
req.on('end', function () {
- if (r !== text) console.log(r, text);
- assert.equal(r, text)
- resp.writeHead(200, {'content-type':'text/plain'})
- resp.write('OK')
- resp.end()
+ if (req.headers['content-type'] && req.headers['content-type'].indexOf('boundary=') >= 0) {
+ var boundary = req.headers['content-type'].split('boundary=')[1]
+ text = text.replace(/__BOUNDARY__/g, boundary)
+ }
+ assert.equal(r, text)
+ if (reqContentType) {
+ assert.ok(req.headers['content-type'])
+ assert.ok(~req.headers['content-type'].indexOf(reqContentType))
+ }
+ resp.writeHead(200, {'content-type': 'text/plain'})
+ resp.write(r)
+ resp.end()
+ })
+ }
+ return l
+}
+exports.createPostJSONValidator = function (value, reqContentType) {
+ var l = function (req, resp) {
+ var r = ''
+ req.on('data', function (chunk) { r += chunk })
+ req.on('end', function () {
+ var parsedValue = JSON.parse(r)
+ assert.deepEqual(parsedValue, value)
+ if (reqContentType) {
+ assert.ok(req.headers['content-type'])
+ assert.ok(~req.headers['content-type'].indexOf(reqContentType))
+ }
+ resp.writeHead(200, {'content-type': 'application/json'})
+ resp.write(r)
+ resp.end()
})
}
- return l;
+ return l
}
exports.createGetResponse = function (text, contentType) {
var l = function (req, resp) {
contentType = contentType || 'text/plain'
- resp.writeHead(200, {'content-type':contentType})
+ resp.writeHead(200, {'content-type': contentType})
resp.write(text)
resp.end()
}
- return l;
+ return l
}
exports.createChunkResponse = function (chunks, contentType) {
var l = function (req, resp) {
contentType = contentType || 'text/plain'
- resp.writeHead(200, {'content-type':contentType})
+ resp.writeHead(200, {'content-type': contentType})
chunks.forEach(function (chunk) {
resp.write(chunk)
})
resp.end()
}
- return l;
+ return l
}
diff --git a/tests/squid.conf b/tests/squid.conf
index 0d4a3b6fe..ba3a963a9 100644
--- a/tests/squid.conf
+++ b/tests/squid.conf
@@ -1,7 +1,6 @@
#
# Recommended minimum configuration:
#
-acl manager proto cache_object
acl localhost src 127.0.0.1/32 ::1
acl to_localhost dst 127.0.0.0/8 0.0.0.0/32 ::1
diff --git a/tests/ssl/ca/README.md b/tests/ssl/ca/README.md
new file mode 100644
index 000000000..f92eb0708
--- /dev/null
+++ b/tests/ssl/ca/README.md
@@ -0,0 +1,8 @@
+# Generating SSL Certs for tests
+
+Certs are generated for TLS tests. The localhost, client and server certs for use in test are generated by running;
+```
+./gen-all-certs.sh
+```
+
+They should last 10 years, but can be updated at any time.
diff --git a/tests/ssl/ca/ca.srl b/tests/ssl/ca/ca.srl
index 17128db3a..28a2de0e1 100644
--- a/tests/ssl/ca/ca.srl
+++ b/tests/ssl/ca/ca.srl
@@ -1 +1 @@
-ADF62016AA40C9C3
+ADF62016AA40C9C7
diff --git a/tests/ssl/ca/client-enc.key b/tests/ssl/ca/client-enc.key
new file mode 100644
index 000000000..19fb73b2f
--- /dev/null
+++ b/tests/ssl/ca/client-enc.key
@@ -0,0 +1,30 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,F383CD86ECED9F72E8A8A5671B9B478F
+
+0UwxIzEIrq2gNvP2M4S7ZlHKdZ9W69YyprT4tEVmauBYwZwwqLTdtsxZ7uWvPIiW
+oJHUNCUlgZ0qiUJBEAl23YArUS8PJmPCA57t8MK/8mnuOi2ke/w9FAERcRNZu5U2
+Ksxh8lUwyvwoFbsFuUkIJEAL7uRn6nhPZ2ftIp39k6oaP7TSuKHZCNUizQT4vHsl
+l9zg0BWsK+ORtQcckaCu3o/AnX5r1kTRR0DXaaagN6fCgxcGDljPv1ByHqvH+Kkp
+TjDbM7io+TOrjjfeoGV9EeeS4TJ0PRy9zouJRbeqnMqu1RUqhMGE2ey5MjXmU0EV
+gHs30563fySFFfNS2ziV5v4ml+ar06wqFaYKEQmxYdjUpLuMDFP186bak+WZ7OAe
+NQpdgdfLCB9OmAOMFlnNKEsMo145ChlpEEhcwtX9nYJlrDU9wvHm0AvFl1hMW11B
+7I9G8B/XyPM/i92hmcwAtgd1A+90G1mWg0HjkQmzaOkGcYFVK+nlEyNleZI5I9CM
+Eelu0kBuibCSKztMoWmAKJEXUHtCvGCpCauvr+GkwPV/7lT8D68HGtpynOEqHl5g
+guTLzg0FxCOkpJgZapPiI77TxwbqJGnI8C5b1He1YyPkTHJNeP7lu4HJDbYEny5/
+C+zSagaP+i4ffa00zeiVHRKlEhygsz6gdGY0phnB9sCQvK3VIsrBEqk4U4FP5w+K
+L8JZJGDWmMwDl4mgVvSiRsDFTPCSfCd0FRmpZnlrVr4306qvkz/1aBE86y0bjZfk
+EcJTWnXVf09J0YbnCHGblNi8MwRi7MCoqmGXHGTxwmpB0gdXuUJCigmm8xSMRqSr
+To8La6apf/QujfSHcxBXnf8JCFO6v59SUMDhx2oce/gYRTejx7L+f/UhVo4EvRF6
+W92pNDCkjeJKU2nNwsV/HeAJml81xxCYQA80PsVMu/27inkDSSP4egapcXr0T1OV
+ic+VTR4Tl3g9hI4YBL43+hsJWaxVMpT022ZB6bcTgYFTDQcoAwilOoadF+rVxer/
+ry8ORowY5GRH+zlLE3zetn87EAijwux2/QhnGE3eDFc0FbI008wzRQwhqV/S22yF
+XYKh5ni5Xg2tguUMuBC8hr9xjpUe3nP7u/W45f/0BCE6eeJzXTunMWTrBCigMzEq
+7OvmIhz7sqb18NC68z4Fj5vmpSqf8//RCyE7Sk0pX7VgSIKX92qopqi/5yroQwFf
+jLanlxi4QRBLTBJoXnm+I5kh6dEbV0OaUskKwXqW5T2wAgOx5O6TTK+ZeFGyLSMW
+eJY6UnT/6vWAY10O5JiieRbzBaGGJgAQ1fTZYs16F3kbKYR9YvrQKi0ACo2pkYs8
+1FvblxhmpMNRjpvOSpb+71vVgk8hD/v5XaVvVC4Wrm6gnsjlNZD0vQjcOWs1Q/a/
+jWKeB/gTUMIa5CWdzNRvSq2Cvgu5FJYf5qIdii7+FOgDv5VKgUugP5ZHHDQ1iwMZ
+o8NfMheG2x5ottq1m7VFS8IB+pWjJfsxSFtybJ8rdHzlbmEQyPzWYCWIQXPVvpNs
+oBm36btgWMrzQTjDrt+WKhkuau0NacTZqjugpCJKimBQ/lLYiuxjItUoWKn0mQO4
+-----END RSA PRIVATE KEY-----
diff --git a/tests/ssl/ca/client.cnf b/tests/ssl/ca/client.cnf
new file mode 100644
index 000000000..ab997ebf8
--- /dev/null
+++ b/tests/ssl/ca/client.cnf
@@ -0,0 +1,20 @@
+[ req ]
+default_bits = 1024
+days = 3650
+distinguished_name = req_distinguished_name
+attributes = req_attributes
+prompt = no
+output_password = password
+
+[ req_distinguished_name ]
+C = US
+ST = CA
+L = Oakland
+O = request
+OU = request@localhost
+CN = TestClient
+emailAddress = do.not@email.me
+
+[ req_attributes ]
+challengePassword = password challenge
+
diff --git a/tests/ssl/ca/client.crt b/tests/ssl/ca/client.crt
new file mode 100644
index 000000000..ab641d916
--- /dev/null
+++ b/tests/ssl/ca/client.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDLjCCApcCCQCt9iAWqkDJxzANBgkqhkiG9w0BAQUFADCBojELMAkGA1UEBhMC
+VVMxCzAJBgNVBAgTAkNBMRAwDgYDVQQHEwdPYWtsYW5kMRAwDgYDVQQKEwdyZXF1
+ZXN0MSYwJAYDVQQLEx1yZXF1ZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTESMBAG
+A1UEAxMJcmVxdWVzdENBMSYwJAYJKoZIhvcNAQkBFhdtaWtlYWxAbWlrZWFscm9n
+ZXJzLmNvbTAeFw0xODExMjIxNTIzMjNaFw0yMTExMjExNTIzMjNaMIGPMQswCQYD
+VQQGEwJVUzELMAkGA1UECAwCQ0ExEDAOBgNVBAcMB09ha2xhbmQxEDAOBgNVBAoM
+B3JlcXVlc3QxGjAYBgNVBAsMEXJlcXVlc3RAbG9jYWxob3N0MRMwEQYDVQQDDApU
+ZXN0Q2xpZW50MR4wHAYJKoZIhvcNAQkBFg9kby5ub3RAZW1haWwubWUwggEiMA0G
+CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFNIUKI5I/cOYMv7dYJzPMNEpd9d80
+lXueDWOtFflpKpnt3ssCk+pFIxlbnlBnFl1CIkpjAY9pqB4WoGpDARkkW0RqVleZ
+mlxBRMwfkzzTriURb0PtB7P7iRp+lcr+uraJg4sP9xEpZbq/CO1q584EQmiANVL3
+NAPdXf2kP91lUy+F5gZgZqkuPtDRXk1jqxTswRLqBmP5ublM/b9ZQS2Jmih7rVL4
+FiTo6E2DdjSYiZ1cQKSBw3rFWhiFLe3R2BjqK+/uD/hDdcT9sEtdqxLyLqFFBKLa
+cYcHcnZOMaohmN8vup26D199r5VP6cJvAh93XfxpCiNDl/S4KSCDq5G5AgMBAAEw
+DQYJKoZIhvcNAQEFBQADgYEAjINjRQxACmbp77ymVwaiUiy9YXbXLSsu8auZYDlu
+LqiZMIjm6hOOHZPlCC7QXBBRyUKXQnRC4+mOxWfcMwEztYqrX51fP/hVB0aF7hXg
+hcn0Ge65N9P8Kby6k/2ha/NQW7I17Sgg1PrvN5BkDFnZBrhNwZbzWwxwezifUwob
+ITQ=
+-----END CERTIFICATE-----
diff --git a/tests/ssl/ca/client.csr b/tests/ssl/ca/client.csr
new file mode 100644
index 000000000..6bb0449be
--- /dev/null
+++ b/tests/ssl/ca/client.csr
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIC+DCCAeACAQAwgY8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEQMA4GA1UE
+BwwHT2FrbGFuZDEQMA4GA1UECgwHcmVxdWVzdDEaMBgGA1UECwwRcmVxdWVzdEBs
+b2NhbGhvc3QxEzARBgNVBAMMClRlc3RDbGllbnQxHjAcBgkqhkiG9w0BCQEWD2Rv
+Lm5vdEBlbWFpbC5tZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMU0
+hQojkj9w5gy/t1gnM8w0Sl313zSVe54NY60V+Wkqme3eywKT6kUjGVueUGcWXUIi
+SmMBj2moHhagakMBGSRbRGpWV5maXEFEzB+TPNOuJRFvQ+0Hs/uJGn6Vyv66tomD
+iw/3ESllur8I7WrnzgRCaIA1Uvc0A91d/aQ/3WVTL4XmBmBmqS4+0NFeTWOrFOzB
+EuoGY/m5uUz9v1lBLYmaKHutUvgWJOjoTYN2NJiJnVxApIHDesVaGIUt7dHYGOor
+7+4P+EN1xP2wS12rEvIuoUUEotpxhwdydk4xqiGY3y+6nboPX32vlU/pwm8CH3dd
+/GkKI0OX9LgpIIOrkbkCAwEAAaAjMCEGCSqGSIb3DQEJBzEUDBJwYXNzd29yZCBj
+aGFsbGVuZ2UwDQYJKoZIhvcNAQELBQADggEBALppUGqe3AVfnD28k8SPXI8LMl16
+0VJWabujQVe1ycDZb/T+9Lcy5Xc6PKhn2yb4da/f528j3M1DsOafTUk+aqOaKHce
+o83TCZEdysKjntKQHTBWFT1lf1iOSsD7mT1GOaFmmc81Z6pUUjN8WNk7ybfPoLfb
+b+MNKkEqVVw1Ta/TeKGbc+K4Xi/T7VAAP2pJRY9ftrjDm1nciGJHu9NGyjAc8TQB
+YabIRAD7sdZkOWR0RSk06gJDqQGNqhn0tjzruDRdrtv5l1UiSPrrnyTOd75mZwXj
+LFs5qIQT5W/LRLJ4BkW3YNSiwFJxQ8a4DddYhkd+CadefQtRdgEI3+GhNcQ=
+-----END CERTIFICATE REQUEST-----
diff --git a/tests/ssl/ca/client.key b/tests/ssl/ca/client.key
new file mode 100644
index 000000000..c21878c96
--- /dev/null
+++ b/tests/ssl/ca/client.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAxTSFCiOSP3DmDL+3WCczzDRKXfXfNJV7ng1jrRX5aSqZ7d7L
+ApPqRSMZW55QZxZdQiJKYwGPaageFqBqQwEZJFtEalZXmZpcQUTMH5M8064lEW9D
+7Qez+4kafpXK/rq2iYOLD/cRKWW6vwjtaufOBEJogDVS9zQD3V39pD/dZVMvheYG
+YGapLj7Q0V5NY6sU7MES6gZj+bm5TP2/WUEtiZooe61S+BYk6OhNg3Y0mImdXECk
+gcN6xVoYhS3t0dgY6ivv7g/4Q3XE/bBLXasS8i6hRQSi2nGHB3J2TjGqIZjfL7qd
+ug9ffa+VT+nCbwIfd138aQojQ5f0uCkgg6uRuQIDAQABAoIBAF062haUAIT7k9a9
+ICmNxwAoTGwlXBOZA+sRu2jNta7RVBpPtLwQP7XVxRw6ORqzSP2GBpLN3wX9U9Qw
+nGv27fLxLuPy09ErV6gHpVTcH+qXLrESYBOEC8PD6oGjwWcx0DAsvyaaEEP48xNz
+XgKneg8rcgoCq6lwrs8Nq2bmRn2qw6pnecQRt/xuJMMn83UforHyiH5Xy+WFart9
+5Oz4VJmngOzd/dRXuziCmfDpJnCYP7YPbG+ATbsWR9BhGoO4x0cxZP73lQKMc9l/
+tPo+42rtJCjhHoqZaBVzQmY9kWrb5ItF6Nma11M5Uf0YsEM7XbsWw1gfOeJvVIPw
+Q3w3NQECgYEA9wm4QQjtrCvBjkMwY4tkegD3F3SgVDwxqGco3ZJlLK4JX4oHH8oC
+P5vMXjy+3SigFNeTVo3MKNkADimkQ1E3R2ar07S31fBHAW7ymeZbK3ixJgB55JEk
+pBWT6vgBtZQfW0DfdVIAQz8rZlcqNrGBp2ZlgKKswy2HIVTwXU9UXiECgYEAzFv+
+rDPOp4pK0LPeDwSDUflasq7Dc52xcWfYupajAvK/4pzeDr07Frv8gh+EhhCp4kwN
+YtmHJ0KfE0R4Ijh8LH5MNhl2YBhTCqp3NZf3jhdiPTDRHMNfUq5aUbercU5Yi1W/
+FrqBeTbid1k7tHV/EqwWqcYQBVdFuSjuUkA1UJkCgYEA4UT5wkRUBzZ3cDUQwRVx
+cFfE+pydP3MMjVZUy4gdvpqNbZO+X1ykpEB8IkseeSn8oETc1IbFb1JCXKfYZJKA
+6BlWAt2+7dYHyeTUUUbgSEnssIyqmqVIVmBe3Ft/o4cI+Pu1SZSXLLtD5jUCB5Hi
+ezZCxQSSqgCwQtLjxRL8CkECgYEAwI6OUUQfnM454J0ax5vBASSryWHS2MXlxK3N
+EUOPJeAF3klhExJK8wj+zL1V6d0ZthljI5lEOEIWEdmaOORwXJxEw1UKrVE+Lfah
+jOY8ZK6z6mRtJWUSFJ4kjIs8B++Cjwekno3uIYENstdp4ogzzCxKzn3J6r5o/CcN
+KINHuUECgYB82O06BuRuSWYYM3qHxgo4bKqIQYHZKI528p90bMSyTH7M2sXcGR+z
+ADcs1Ald0acyJkI4IpzBs+YK+WVihQKuSh69YGKKru0xq0hONN8j2pgphVDS0hbk
+bMzyxx1QHK9cRVAXFdiqeQ36U3f70enItxXvOYGwWGvD5v7bCpYuCA==
+-----END RSA PRIVATE KEY-----
diff --git a/tests/ssl/ca/gen-all-certs.sh b/tests/ssl/ca/gen-all-certs.sh
new file mode 100755
index 000000000..b3a572582
--- /dev/null
+++ b/tests/ssl/ca/gen-all-certs.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -ex
+
+./gen-server.sh
+./gen-client.sh
+./gen-localhost.sh
diff --git a/tests/ssl/ca/gen-client.sh b/tests/ssl/ca/gen-client.sh
new file mode 100755
index 000000000..1d5cfd203
--- /dev/null
+++ b/tests/ssl/ca/gen-client.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+set -ex
+
+# Adapted from:
+# http://nodejs.org/api/tls.html
+# https://github.com/joyent/node/blob/master/test/fixtures/keys/Makefile
+
+# Create a private key
+openssl genrsa -out client.key 2048
+
+# Create a certificate signing request
+openssl req -new -sha256 -key client.key -out client.csr -config client.cnf -days 1095
+
+# Use the CSR and the CA key (previously generated) to create a certificate
+openssl x509 -req \
+ -in client.csr \
+ -CA ca.crt \
+ -CAkey ca.key \
+ -set_serial 0x`cat ca.srl` \
+ -passin 'pass:password' \
+ -out client.crt \
+ -days 1095
+
+# Encrypt with password
+openssl rsa -aes128 -in client.key -out client-enc.key -passout 'pass:password'
diff --git a/tests/ssl/ca/gen-localhost.sh b/tests/ssl/ca/gen-localhost.sh
new file mode 100755
index 000000000..5dbd04b53
--- /dev/null
+++ b/tests/ssl/ca/gen-localhost.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+set -ex
+
+# Adapted from:
+# http://nodejs.org/api/tls.html
+# https://github.com/joyent/node/blob/master/test/fixtures/keys/Makefile
+
+# Create a private key
+openssl genrsa -out localhost.key 2048
+
+# Create a certificate signing request
+openssl req -new -sha256 -key localhost.key -out localhost.csr -config localhost.cnf -days 1095
+
+# Use the CSR and the CA key (previously generated) to create a certificate
+openssl x509 -req \
+ -in localhost.csr \
+ -CA ca.crt \
+ -CAkey ca.key \
+ -set_serial 0x`cat ca.srl` \
+ -passin 'pass:password' \
+ -out localhost.crt \
+ -days 3650
diff --git a/tests/ssl/ca/gen-server.sh b/tests/ssl/ca/gen-server.sh
new file mode 100755
index 000000000..4223ddc7c
--- /dev/null
+++ b/tests/ssl/ca/gen-server.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+set -ex
+# fixes:
+# Error: error:140AB18F:SSL routines:SSL_CTX_use_certificate:ee key too small
+# on Node > v10
+
+openssl genrsa 4096 > server.key
+
+openssl req -new -nodes -sha256 -key server.key -config server.cnf -out server.csr
+
+openssl x509 -req \
+ -sha256 \
+ -in server.csr \
+ -CA ca.crt \
+ -CAkey ca.key \
+ -out server.crt \
+ -passin 'pass:password' \
+ -days 3650
diff --git a/tests/ssl/ca/localhost.cnf b/tests/ssl/ca/localhost.cnf
new file mode 100644
index 000000000..d8465085c
--- /dev/null
+++ b/tests/ssl/ca/localhost.cnf
@@ -0,0 +1,20 @@
+[ req ]
+default_bits = 1024
+days = 3650
+distinguished_name = req_distinguished_name
+attributes = req_attributes
+prompt = no
+output_password = password
+
+[ req_distinguished_name ]
+C = US
+ST = CA
+L = Oakland
+O = request
+OU = request@localhost
+CN = localhost
+emailAddress = do.not@email.me
+
+[ req_attributes ]
+challengePassword = password challenge
+
diff --git a/tests/ssl/ca/localhost.crt b/tests/ssl/ca/localhost.crt
new file mode 100644
index 000000000..4a0fe1003
--- /dev/null
+++ b/tests/ssl/ca/localhost.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDLTCCApYCCQCt9iAWqkDJxzANBgkqhkiG9w0BAQUFADCBojELMAkGA1UEBhMC
+VVMxCzAJBgNVBAgTAkNBMRAwDgYDVQQHEwdPYWtsYW5kMRAwDgYDVQQKEwdyZXF1
+ZXN0MSYwJAYDVQQLEx1yZXF1ZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTESMBAG
+A1UEAxMJcmVxdWVzdENBMSYwJAYJKoZIhvcNAQkBFhdtaWtlYWxAbWlrZWFscm9n
+ZXJzLmNvbTAeFw0xODExMjIxNTIzMjVaFw0yODExMTkxNTIzMjVaMIGOMQswCQYD
+VQQGEwJVUzELMAkGA1UECAwCQ0ExEDAOBgNVBAcMB09ha2xhbmQxEDAOBgNVBAoM
+B3JlcXVlc3QxGjAYBgNVBAsMEXJlcXVlc3RAbG9jYWxob3N0MRIwEAYDVQQDDAls
+b2NhbGhvc3QxHjAcBgkqhkiG9w0BCQEWD2RvLm5vdEBlbWFpbC5tZTCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBAPZ8A1Kgccmg3o/SNiGSNyIv7jyEZCYE
+oJgiu6kkENIykHIN9iL+cjaCjDIOg7BGM9/IeERyM/EIy4hXIFecjkt0Zc12p8OS
+UN94MBzyCyaFlDWxoneP17FFBgMD0/qDbYAYgwRE308TFlnA4rbI0g3f5/Ft7bhj
+dd18Sw0/p0RoIdr7FezM5chSW62AwJ/QHEmjZt/VXzs1ITMG1549r5T1fngw5x+G
+eTG1HagM6/CMrtLk0nXTDRR469A0n0ZgdXdSBnN3igSyVIy9gaUGjrXhs2GImnvL
+LqzgYUSxVIopI9T0umbKGtoVp79RIU+P59di0ybtiAI8P1CElj0bpT8CAwEAATAN
+BgkqhkiG9w0BAQUFAAOBgQAeTesuuLfnlqqVE0sq6kl+Va2MnJJSvgHuadCaSnr3
+EvieJYiE5uydesI7nSBTs9z873RBFlLdAXx/FyShRSnVB/WaNZP9lb97oyWeTj21
+q0nTXHSZFOb9nRdtwyLmX9L6EO2KAbNSxmAdt8ZJd2FZNahHXDEiNtwemzNVlkw8
+bQ==
+-----END CERTIFICATE-----
diff --git a/tests/ssl/ca/localhost.csr b/tests/ssl/ca/localhost.csr
new file mode 100644
index 000000000..44aed654b
--- /dev/null
+++ b/tests/ssl/ca/localhost.csr
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIC9zCCAd8CAQAwgY4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEQMA4GA1UE
+BwwHT2FrbGFuZDEQMA4GA1UECgwHcmVxdWVzdDEaMBgGA1UECwwRcmVxdWVzdEBs
+b2NhbGhvc3QxEjAQBgNVBAMMCWxvY2FsaG9zdDEeMBwGCSqGSIb3DQEJARYPZG8u
+bm90QGVtYWlsLm1lMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9nwD
+UqBxyaDej9I2IZI3Ii/uPIRkJgSgmCK7qSQQ0jKQcg32Iv5yNoKMMg6DsEYz38h4
+RHIz8QjLiFcgV5yOS3RlzXanw5JQ33gwHPILJoWUNbGid4/XsUUGAwPT+oNtgBiD
+BETfTxMWWcDitsjSDd/n8W3tuGN13XxLDT+nRGgh2vsV7MzlyFJbrYDAn9AcSaNm
+39VfOzUhMwbXnj2vlPV+eDDnH4Z5MbUdqAzr8Iyu0uTSddMNFHjr0DSfRmB1d1IG
+c3eKBLJUjL2BpQaOteGzYYiae8surOBhRLFUiikj1PS6Zsoa2hWnv1EhT4/n12LT
+Ju2IAjw/UISWPRulPwIDAQABoCMwIQYJKoZIhvcNAQkHMRQMEnBhc3N3b3JkIGNo
+YWxsZW5nZTANBgkqhkiG9w0BAQsFAAOCAQEArDxFTCfg/ysXMYA9BOffqO4VCsw3
+7/4DEZtqvNIbRB2zLkzcAOUq/kwPr0pQ8AX1YjotAMIONI1R1Gr4ttlbUfbtqfOH
+zk7d+wfYUKrUlqGCD0E0EKNRtn76lJD3r5CQtLbeAd3d+b5bpsHVYErsAyrWqkOx
+gRnYmAX3vLDoXFZwp0L3577MJLEzjnV+uPrJVtF4I4wDxU7qoaC5wYE8oExE+2MA
+POYO+6GYWOPnIViVGnkbZXlRkBufD9cLcMhKVSo2nfNiFqZm1+nTcf9EC8ILdqtb
+JkMcBHNBje6KTC3Ue2vJkKg61hbVoj/MoYo63UeXA1ACOjvfnE8cMP4pjw==
+-----END CERTIFICATE REQUEST-----
diff --git a/tests/ssl/ca/localhost.js b/tests/ssl/ca/localhost.js
new file mode 100644
index 000000000..567bdb8e6
--- /dev/null
+++ b/tests/ssl/ca/localhost.js
@@ -0,0 +1,33 @@
+'use strict'
+
+var fs = require('fs')
+var https = require('https')
+var options = { key: fs.readFileSync('./localhost.key'),
+ cert: fs.readFileSync('./localhost.crt') }
+
+var server = https.createServer(options, function (req, res) {
+ res.writeHead(200)
+ res.end()
+ server.close()
+})
+server.listen(0, function () {
+ var ca = fs.readFileSync('./ca.crt')
+ var agent = new https.Agent({
+ host: 'localhost',
+ port: this.address().port,
+ ca: ca
+ })
+
+ https.request({ host: 'localhost',
+ method: 'HEAD',
+ port: this.address().port,
+ agent: agent,
+ ca: [ ca ],
+ path: '/' }, function (res) {
+ if (res.socket.authorized) {
+ console.log('node test: OK')
+ } else {
+ throw new Error(res.socket.authorizationError)
+ }
+ }).end()
+})
diff --git a/tests/ssl/ca/localhost.key b/tests/ssl/ca/localhost.key
new file mode 100644
index 000000000..211578ddb
--- /dev/null
+++ b/tests/ssl/ca/localhost.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpgIBAAKCAQEA9nwDUqBxyaDej9I2IZI3Ii/uPIRkJgSgmCK7qSQQ0jKQcg32
+Iv5yNoKMMg6DsEYz38h4RHIz8QjLiFcgV5yOS3RlzXanw5JQ33gwHPILJoWUNbGi
+d4/XsUUGAwPT+oNtgBiDBETfTxMWWcDitsjSDd/n8W3tuGN13XxLDT+nRGgh2vsV
+7MzlyFJbrYDAn9AcSaNm39VfOzUhMwbXnj2vlPV+eDDnH4Z5MbUdqAzr8Iyu0uTS
+ddMNFHjr0DSfRmB1d1IGc3eKBLJUjL2BpQaOteGzYYiae8surOBhRLFUiikj1PS6
+Zsoa2hWnv1EhT4/n12LTJu2IAjw/UISWPRulPwIDAQABAoIBAQDe5TyX/tGHdTtu
+sbkT2MaU2uVEwrBSFQMpMNelWCECBInNKkT4VkLwelPPfIKn6IRGjWH8+41vHfX4
+oFl2APRI1cSt7ew+FlWeEHDp7BQbTNa/S5jRKDn0a6fJGDAcrbdbDE+Gj8WlG2yt
+05jxlF8n/uAf2roLcZ4Hobu5CmP3nbEU7W0A2QOk9k4ClUz4nVICUqkC+1mkN5ID
+ebNLaUkWWntViCqPo13j0pgCqRApdWHQ17cOCL7ghirQirM+eakexdS5Nf1uiQr0
++IiEy+f6db9VWwjUB/faaZ+1r2BeLUI980r1ZRJMlb3Z6BH0XCds2Uu3C3e73ncT
+AZkc5b2ZAoGBAPwf1WmLSZYZaegVICco+17QIuvrD2jcy9w1Hk+B9F2zkyX2CV4g
+jCQXuSXEnhEDX2wt6Rxti+F0JpC2WBrVBxuyE1kU+mOUaGDSvauyBjY4S8iOWnl7
+IYR0jc1OlG0XBvEbAaHVWrET4aBcXE8eDF+6OKLLVDYUtzamcdc5ya7dAoGBAPpF
+/CZHP/MX1c7wBTL04SSt+kLEwVir35PYeMSQA53uuZKIhg5KXioQj6fQst0KcsCo
+nRzYAe6mXnHljsQMP+ffzCm4lWf5GdajcL0lDyQmC9EcKYlR+JsUBwK8V6remiEo
+YJxXtUv0DRTlOOgragD/VcD2hRjBtsPzYvbuoCzLAoGBANoLFeAPa/Z5yBPEoWf8
+k1huHKV3Rn5j5ZJuBeaw9wtKUEoWPAfBkjFsqty07Ba+mfnOwrmpK74xW2DvscaS
+0XDsUrtJ3znbkWGbIBmq/qBJk5DBPBGvoU8SFcim2sp1jbVaq9Cv2Z0nGow7FEIA
+NKddP7naqtuSktianf2KppepAoGBAPE6/dTjfkdQ5Rwmi8xW7qANNZifz4EpgUIf
+OCC2c1YKIUKVZyllEyhWeDEX3x9hj8QVggKoTgx6vbPowVhEOmDEfSSFrzTdjMMv
+HF6j1tlP9rni/EJJCWhowG0pnxKqp0NoiN6JR81i+iz22IgoOG+nrT9mHloDdaef
+8/bxgOBLAoGBALMQ9GMCdm7wVOkPktz8enAkd2mt6+BHXtNgKaBvfWED7T+YEVtp
+aw/0eSSnRh2RgnqVAw9HJOCGATecK/p/JrcTzbTYr1n7FzuXp7nX1eoi95sT5XLm
+7b0/4EdL9dXGQP5PXxgMZY/Vbk3TD5fdUxam4QpA1opl+rEOYO+GhMFo
+-----END RSA PRIVATE KEY-----
diff --git a/tests/ssl/ca/server.crt b/tests/ssl/ca/server.crt
index efe96cefc..72a408fb7 100644
--- a/tests/ssl/ca/server.crt
+++ b/tests/ssl/ca/server.crt
@@ -1,16 +1,25 @@
-----BEGIN CERTIFICATE-----
-MIICejCCAeMCCQCt9iAWqkDJwzANBgkqhkiG9w0BAQUFADCBojELMAkGA1UEBhMC
+MIIEQjCCA6sCCQCt9iAWqkDJxzANBgkqhkiG9w0BAQsFADCBojELMAkGA1UEBhMC
VVMxCzAJBgNVBAgTAkNBMRAwDgYDVQQHEwdPYWtsYW5kMRAwDgYDVQQKEwdyZXF1
ZXN0MSYwJAYDVQQLEx1yZXF1ZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTESMBAG
A1UEAxMJcmVxdWVzdENBMSYwJAYJKoZIhvcNAQkBFhdtaWtlYWxAbWlrZWFscm9n
-ZXJzLmNvbTAeFw0xMjAzMDEyMjUwNTZaFw0yMjAyMjcyMjUwNTZaMIGjMQswCQYD
-VQQGEwJVUzELMAkGA1UECBMCQ0ExEDAOBgNVBAcTB09ha2xhbmQxEDAOBgNVBAoT
-B3JlcXVlc3QxEDAOBgNVBAsTB3Rlc3RpbmcxKTAnBgNVBAMTIHRlc3RpbmcucmVx
+ZXJzLmNvbTAeFw0xODExMjIxNTIzMjBaFw0yODExMTkxNTIzMjBaMIGjMQswCQYD
+VQQGEwJVUzELMAkGA1UECAwCQ0ExEDAOBgNVBAcMB09ha2xhbmQxEDAOBgNVBAoM
+B3JlcXVlc3QxEDAOBgNVBAsMB3Rlc3RpbmcxKTAnBgNVBAMMIHRlc3RpbmcucmVx
dWVzdC5taWtlYWxyb2dlcnMuY29tMSYwJAYJKoZIhvcNAQkBFhdtaWtlYWxAbWlr
-ZWFscm9nZXJzLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQDgVl0jMumvOpmM
-20W5v9yhGgZj8hPhEQF/N7yCBVBn/rWGYm70IHC8T/pR5c0LkWc5gdnCJEvKWQjh
-DBKxZD8FAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEABShRkNgFbgs4vUWW9R9deNJj
-7HJoiTmvkmoOC7QzcYkjdgHbOxsSq3rBnwxsVjY9PAtPwBn0GRspOeG7KzKRgySB
-kb22LyrCFKbEOfKO/+CJc80ioK9zEPVjGsFMyAB+ftYRqM+s/4cQlTg/m89l01wC
-yapjN3RxZbInGhWR+jA=
+ZWFscm9nZXJzLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANQc
+6Vm7UwK1JQc8gipa++0qsAUq2iv9PGeo8cE+ewWzjKCoh4hOYoDS95oq303Qq+0v
+vzeY9sfe1eZJOdwqCWPPClf2Dkd9rEBtQw6Zm5dodTsqUQtILLiooDUi83OvPcCn
+bc25y5qPC+EbRNYPF9penpry/MhWuqOPO6NzTbsIjW1KRvOsivNIetUr/48S1Cmm
+SILvVQAqbzKGda4ycMkF8XZqIvDnUOBPDAo5ioEY92eNdfcKeJVu9Gv7PFybEWD5
++jZw/nw9e01q55t+BzF0Kq9yyldeAuldu25nhzZTyZi+umJsI2mpv8R50rvCtYbX
+4ksQy17UlxvEt9ClAYF1cs04f6eAivzKNA4veVSB3ePRKwGCwCIwPA33CzZFO3pw
+1iMZ936nVeb9oNFK4YC7tYid/j6PI2+032tGxS18MGB8FSSGyTCjsMqHCJcOi9fL
+wn1yiLcXt4BKqVfWyi+vsXM3Xh2cdSKQVgIMoRHnr478lK9gT8QwtxNIbF3F9OR6
+qyrZ1VHlTDp1rSEEj6uV/gyx4nh+V9/qPCVYVPKSRGKXP8BI6ujvarOiKx96Pjly
+A7BBDGblF2FJEnKGNGV2XCUJnjV2fNuFRrV3UYkMhbq0SXpSA8FcK/0YhKxKxIsV
+/pUrR//nTlsoYHwQR4AFp0Rhpy6XntO9vsrDetE3AgMBAAEwDQYJKoZIhvcNAQEL
+BQADgYEAnjXSTIfGpBx9/0ZkOQRSGdTjtpy5TQ/VDHtEhRKZYY6dpe6lVpT0hSoT
+SzA8YF+bpFIF+1ZpAgQldBFCmPpVDBCy/ymf8t/V2zSd2c80w6pmxXWQEFq25pib
+OLCcTex2nVGmiUXwIbwnEhWPJvB8T8L8a75x0fPZDHHHoi+K/wQ=
-----END CERTIFICATE-----
diff --git a/tests/ssl/ca/server.csr b/tests/ssl/ca/server.csr
index a8e7595a5..55395d765 100644
--- a/tests/ssl/ca/server.csr
+++ b/tests/ssl/ca/server.csr
@@ -1,11 +1,29 @@
-----BEGIN CERTIFICATE REQUEST-----
-MIIBgjCCASwCAQAwgaMxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEQMA4GA1UE
-BxMHT2FrbGFuZDEQMA4GA1UEChMHcmVxdWVzdDEQMA4GA1UECxMHdGVzdGluZzEp
-MCcGA1UEAxMgdGVzdGluZy5yZXF1ZXN0Lm1pa2VhbHJvZ2Vycy5jb20xJjAkBgkq
-hkiG9w0BCQEWF21pa2VhbEBtaWtlYWxyb2dlcnMuY29tMFwwDQYJKoZIhvcNAQEB
-BQADSwAwSAJBAOBWXSMy6a86mYzbRbm/3KEaBmPyE+ERAX83vIIFUGf+tYZibvQg
-cLxP+lHlzQuRZzmB2cIkS8pZCOEMErFkPwUCAwEAAaAjMCEGCSqGSIb3DQEJBzEU
-ExJwYXNzd29yZCBjaGFsbGVuZ2UwDQYJKoZIhvcNAQEFBQADQQBD3E5WekQzCEJw
-7yOcqvtPYIxGaX8gRKkYfLPoj3pm3GF5SGqtJKhylKfi89szHXgktnQgzff9FN+A
-HidVJ/3u
+MIIFDDCCAvQCAQAwgaMxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEQMA4GA1UE
+BwwHT2FrbGFuZDEQMA4GA1UECgwHcmVxdWVzdDEQMA4GA1UECwwHdGVzdGluZzEp
+MCcGA1UEAwwgdGVzdGluZy5yZXF1ZXN0Lm1pa2VhbHJvZ2Vycy5jb20xJjAkBgkq
+hkiG9w0BCQEWF21pa2VhbEBtaWtlYWxyb2dlcnMuY29tMIICIjANBgkqhkiG9w0B
+AQEFAAOCAg8AMIICCgKCAgEA1BzpWbtTArUlBzyCKlr77SqwBSraK/08Z6jxwT57
+BbOMoKiHiE5igNL3mirfTdCr7S+/N5j2x97V5kk53CoJY88KV/YOR32sQG1DDpmb
+l2h1OypRC0gsuKigNSLzc689wKdtzbnLmo8L4RtE1g8X2l6emvL8yFa6o487o3NN
+uwiNbUpG86yK80h61Sv/jxLUKaZIgu9VACpvMoZ1rjJwyQXxdmoi8OdQ4E8MCjmK
+gRj3Z4119wp4lW70a/s8XJsRYPn6NnD+fD17TWrnm34HMXQqr3LKV14C6V27bmeH
+NlPJmL66Ymwjaam/xHnSu8K1htfiSxDLXtSXG8S30KUBgXVyzTh/p4CK/Mo0Di95
+VIHd49ErAYLAIjA8DfcLNkU7enDWIxn3fqdV5v2g0UrhgLu1iJ3+Po8jb7Tfa0bF
+LXwwYHwVJIbJMKOwyocIlw6L18vCfXKItxe3gEqpV9bKL6+xczdeHZx1IpBWAgyh
+EeevjvyUr2BPxDC3E0hsXcX05HqrKtnVUeVMOnWtIQSPq5X+DLHieH5X3+o8JVhU
+8pJEYpc/wEjq6O9qs6IrH3o+OXIDsEEMZuUXYUkScoY0ZXZcJQmeNXZ824VGtXdR
+iQyFurRJelIDwVwr/RiErErEixX+lStH/+dOWyhgfBBHgAWnRGGnLpee072+ysN6
+0TcCAwEAAaAjMCEGCSqGSIb3DQEJBzEUDBJwYXNzd29yZCBjaGFsbGVuZ2UwDQYJ
+KoZIhvcNAQELBQADggIBAI7XFsvAGB92isJj5vGtrVh3ZRLwpnz8Mv3UcG1Z7aHx
+oRFcoLHoSdzCRMKmrc2BU9WKtV6xnmVst5wIaxvk+1HpbuLJqrbWyjcLnOMqDuYR
+1WJuJLUd1n7vjoofkbEPeCP1+E8s2wOEhn2cknlIa5Yh4wtQ8ufrT9M0RFnzVb9+
+KCwm2kfZA5guFz0XllylJzaNly3jIcYp6EBfUZLTGvboio9NSBDtU04u4qhfTHEy
+gKERDU9BIdY8ZL9RExlZokMS9VgC7xG6qXt6QEctRHpRcJ0GEeZksVPeVqgv9gqk
+aekh6WaAGIdGJJrnM19KuAwlrYwjl8WSeFNRxTOfvwkvlCmsEVoXANCBOhmNWO+3
+0HSy4S2ZfPtjlBxZOT0EFMaOM9LEuZqF9Mc3DU8xgC+/ZMFMJiWhzyo7/JVrr623
+/kLtc/RirJVHdEF5iZTxiz3mkVWqKYzdAlb+iSfn3YdwCWh/du3lXWW8Ctg8HufM
+o/6xOYnzJubCKWwHBtSfo7hjaGMDOGSzXTyNxqlzRW50zXpgAxIcf9XJ+Gq36++Q
+QoyMKX6O2r6oHXSnF5ojDW6QOAfOSdrX5fc9uXsbVAGh5vYeLDcekZwGSZbZ608a
+2P4ARIWNNOYBaGQsoElfPXRFqcU9SLB+qXEMMDde/y0FNWEOe+b+vlH1g14aiCSE
-----END CERTIFICATE REQUEST-----
diff --git a/tests/ssl/ca/server.js b/tests/ssl/ca/server.js
index 05e21c116..ca2a00e77 100644
--- a/tests/ssl/ca/server.js
+++ b/tests/ssl/ca/server.js
@@ -1,28 +1,34 @@
-var fs = require("fs")
-var https = require("https")
-var options = { key: fs.readFileSync("./server.key")
- , cert: fs.readFileSync("./server.crt") }
+'use strict'
+
+var fs = require('fs')
+var https = require('https')
+var options = { key: fs.readFileSync('./server.key'),
+ cert: fs.readFileSync('./server.crt') }
var server = https.createServer(options, function (req, res) {
res.writeHead(200)
res.end()
server.close()
})
-server.listen(1337)
-
-var ca = fs.readFileSync("./ca.crt")
-var agent = new https.Agent({ host: "localhost", port: 1337, ca: ca })
+server.listen(0, function () {
+ var ca = fs.readFileSync('./ca.crt')
+ var agent = new https.Agent({
+ host: 'localhost',
+ port: this.address().port,
+ ca: ca
+ })
-https.request({ host: "localhost"
- , method: "HEAD"
- , port: 1337
- , headers: { host: "testing.request.mikealrogers.com" }
- , agent: agent
- , ca: [ ca ]
- , path: "/" }, function (res) {
- if (res.client.authorized) {
- console.log("node test: OK")
- } else {
- throw new Error(res.client.authorizationError)
- }
-}).end()
+ https.request({ host: 'localhost',
+ method: 'HEAD',
+ port: this.address().port,
+ headers: { host: 'testing.request.mikealrogers.com' },
+ agent: agent,
+ ca: [ ca ],
+ path: '/' }, function (res) {
+ if (res.socket.authorized) {
+ console.log('node test: OK')
+ } else {
+ throw new Error(res.socket.authorizationError)
+ }
+ }).end()
+})
diff --git a/tests/ssl/ca/server.key b/tests/ssl/ca/server.key
index 72d86984f..9e9975700 100644
--- a/tests/ssl/ca/server.key
+++ b/tests/ssl/ca/server.key
@@ -1,9 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
-MIIBOwIBAAJBAOBWXSMy6a86mYzbRbm/3KEaBmPyE+ERAX83vIIFUGf+tYZibvQg
-cLxP+lHlzQuRZzmB2cIkS8pZCOEMErFkPwUCAwEAAQJAK+r8ZM2sze8s7FRo/ApB
-iRBtO9fCaIdJwbwJnXKo4RKwZDt1l2mm+fzZ+/QaQNjY1oTROkIIXmnwRvZWfYlW
-gQIhAPKYsG+YSBN9o8Sdp1DMyZ/rUifKX3OE6q9tINkgajDVAiEA7Ltqh01+cnt0
-JEnud/8HHcuehUBLMofeg0G+gCnSbXECIQCqDvkXsWNNLnS/3lgsnvH0Baz4sbeJ
-rjIpuVEeg8eM5QIgbu0+9JmOV6ybdmmiMV4yAncoF35R/iKGVHDZCAsQzDECIQDZ
-0jGz22tlo5YMcYSqrdD3U4sds1pwiAaWFRbCunoUJw==
+MIIJKAIBAAKCAgEA1BzpWbtTArUlBzyCKlr77SqwBSraK/08Z6jxwT57BbOMoKiH
+iE5igNL3mirfTdCr7S+/N5j2x97V5kk53CoJY88KV/YOR32sQG1DDpmbl2h1OypR
+C0gsuKigNSLzc689wKdtzbnLmo8L4RtE1g8X2l6emvL8yFa6o487o3NNuwiNbUpG
+86yK80h61Sv/jxLUKaZIgu9VACpvMoZ1rjJwyQXxdmoi8OdQ4E8MCjmKgRj3Z411
+9wp4lW70a/s8XJsRYPn6NnD+fD17TWrnm34HMXQqr3LKV14C6V27bmeHNlPJmL66
+Ymwjaam/xHnSu8K1htfiSxDLXtSXG8S30KUBgXVyzTh/p4CK/Mo0Di95VIHd49Er
+AYLAIjA8DfcLNkU7enDWIxn3fqdV5v2g0UrhgLu1iJ3+Po8jb7Tfa0bFLXwwYHwV
+JIbJMKOwyocIlw6L18vCfXKItxe3gEqpV9bKL6+xczdeHZx1IpBWAgyhEeevjvyU
+r2BPxDC3E0hsXcX05HqrKtnVUeVMOnWtIQSPq5X+DLHieH5X3+o8JVhU8pJEYpc/
+wEjq6O9qs6IrH3o+OXIDsEEMZuUXYUkScoY0ZXZcJQmeNXZ824VGtXdRiQyFurRJ
+elIDwVwr/RiErErEixX+lStH/+dOWyhgfBBHgAWnRGGnLpee072+ysN60TcCAwEA
+AQKCAgBevj841mRArFrKvatCcftfNxcCZ96lkWpevualM1xN8qIYzM4lAyYadqEk
+Gow9vLxeqFoX4lowcodGYmTWw2wISd1L5tr/8dFzwZoXNmN6IK1kbQVgLa/UF3Xf
+5imp/ZduqxpvrtKTyds7hCueFYXJA0SC35AriBm7num7m3AX370UGP5SLzqtai17
+dDilVnqv09dFrNNhzJJ4lfiQg3U/RUlSZBwRULEeUBCHrKYB/f3cIiKT4vhzfujs
+Jn8SuizsDRxHHvd81RVzQhILsSJTY5kBXxukJJjWVgi3SsTpbkl40ZB9D+JNewXu
+I6AOP+1HOryYXPsJ85k/TQHxzxI5SSo6iJ5+p8NQAKndcCqGU1nKwGD3aq5P758F
+z+W84YWKbACPuurwJOfbflXCHkTc544CPSgWI57hMrgihXfqWDsQNhxFL/guIr7c
+/+Iytnx9Hh8ZIEDm1XLtTr0Ru3/x3cXzCWtU2CU5sYNh0lDBi4orr8oayKnToHjs
+RkWjNG1+SbI10OTRq3HAyrhU5y6IOIVBSlUmtfG6s5jN60tShCfWPiOA/W1KQ2sB
+5j5/Cj1HomaGdQbd3xDReIo3nNA7tk4sfHwJfmHB1O6E6dqTlFibgYMheZUJ+bRJ
+e2PgWPVA0e+2RKJK8ybsxs7D+JmjgDtnWWQlJY7kas/qip9s6QKCAQEA/aPratb1
+S+AMpxNMP0R6SKLDXrlZK2BigXjdzJNHaG2UzSVRADFOShJ7q5zfdublaQcQXJgm
+CGnnE3vyNkXwxk1Z9Mx7+bX2QeTa+EMjj/QhuyW4XGI+1YAiCX4fnF30LgSamVRX
+fkPrOLQ9CoIoA0hRtixzj+vjtbVeiAmHTS9rqaBaY3LGBF0rOW2Cu3zHacKUFt+6
+e17NTjac7Z//PtS4dzZUpcmOp6/ENU4VWKxGA3CkmhRiL9M2KFeX2ri0HpXX0ASD
+U7SPndz1X9MZ/a3Zn/qAqxSaGlrUOfzVQAH8DSJje38UpajoeUo5SYHbarN3on09
+wPRkP3oY29NfiwKCAQEA1hYWrbQbNTOTdsKR+qGRt7rpXi8FPssgXLKB1G/CF9/0
+3DPloiaR5I+u4nMuLci/nLX+EvDu1xWzm68J4XPTgzIa4so+OV76hBqyo/NZjNHE
+BFmCBljrn4EKVoV+KvbHyHGFHUdLZDuAhCUGNPOv4d6grsieb7S5aa1wXuCQcGwb
+SwjFrbpntLkL9eIQlxqcHsBvik/o963QZ61DMEBcP1PnUx69gs4rorIv7ZcXrgrd
+LZQGtw6pJ4+QvqDYLVxB958ZNhAN7CYI+q0C8i6sWqv6s69vfznpZTcuIwC8nYSH
+0W/P8lTUS9XqMvF4sk/BiSXYBWs+5IAb0jhMwKRKhQKCAQAQdbvIUizXALIxgXoY
+PPxmjFF7azHTM80Qs+RI62Hd8AaRDZPlHE4FVo+6AlMqJy/KEhBIwgLt1tmNFSUR
+ypYmeEyXK1H8UYeqnQxswgajx+cMexUswZ9sQYVz8kBg6GP5PIk/3A5VfljcdC3l
+6a5pEB9lYBsbwuYjG6MH1v51ztcAygwzmfYpwFYWwvmR6zYRsfPkTB6Q9QUDx12F
+ujVZQXq7GcaCf8MHNMvZ3bha6csdXAkCisIYcm94TL7pDcV6mqTHthNDslsDlpxB
+3LQ6FzchP6Nr9slNXomZPcQlBDv0KkAkeom/emejv2JaV9gCY6Um4VPJmtKKoATO
+9zejAoIBAQCcx4xQJQePzHd/jznMa6oE/RKN8K1MsQDAIdHGOxnO1inBYRgXyVsq
+ILcYCvWUfeEk6HpqcJrYVII1ztfTjTkmaPkbgLRU22Nmfw631iyMXcnIzavU7iWP
+p7ZkalpdKGBiQBAVwvJJMvII0/xZpuP0606M8Uplz9nAtE0Ijjf4vJK4PnJVqZ7s
+0F8b8DPqFIikVJTam26mg1mNs2ry2Q81KULMskRimI2IFinXOsESqc4T5MWOJWRn
+HlIH6E6n2VpN9utFljg76hbFTRJNPTTnKe7sy9tBNq3fe6uD4rQ+PqIgFFwawVi/
+OKbMK94R5yp6P4aVYVari83UA3rh0O7pAoIBAAUJ+l+Z7ZV/mG0AuQ8CxDyapHjE
+LCFLUcZuelgpzYBLabejwVKWa49e87mE+OLVJxpb23az16ILAz/717BPSeBssBSN
+o33M2oEP79INlUGpc2rBxQi6uQA9DYASoLn1T8Fs/dhvIN/qxL3+sK3gCA9AKIyF
+IAgYpcQrlMAl07jjzSl47R/0BDOe/jzmH7JqpFQOfw9e7U0XThgaVEVHSF9qJVRS
+LlFUhijpG14Qyr8gwfR3RrnO7TKfdXW3GX/5ts0Oac9B+gOMkrksNalgLHnOZSzO
+JuiTAH7CdUt1OC0NaaCBZiI3A5C1Gn1J9vskW4yCwhW0UNnW4h7m+eru0ok=
-----END RSA PRIVATE KEY-----
diff --git a/tests/test-agent.js b/tests/test-agent.js
new file mode 100644
index 000000000..40cdac05f
--- /dev/null
+++ b/tests/test-agent.js
@@ -0,0 +1,102 @@
+'use strict'
+
+var request = require('../index')
+var version = require('../lib/helpers').version
+var http = require('http')
+var ForeverAgent = require('forever-agent')
+var tape = require('tape')
+
+var s = http.createServer(function (req, res) {
+ res.statusCode = 200
+ res.end()
+})
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ s.port = this.address().port
+ s.url = 'http://localhost:' + s.port
+ t.end()
+ })
+})
+
+function httpAgent (t, options, req) {
+ var r = (req || request)(options, function (_err, res, body) {
+ t.ok(r.agent instanceof http.Agent, 'is http.Agent')
+ t.equal(r.agent.options.keepAlive, true, 'is keepAlive')
+ t.equal(Object.keys(r.agent.sockets).length, 1, '1 socket name')
+
+ var name = (typeof r.agent.getName === 'function')
+ ? r.agent.getName({port: s.port})
+ : 'localhost:' + s.port // node 0.10-
+ t.equal(r.agent.sockets[name].length, 1, '1 open socket')
+
+ var socket = r.agent.sockets[name][0]
+ socket.on('close', function () {
+ t.equal(Object.keys(r.agent.sockets).length, 0, '0 open sockets')
+ t.end()
+ })
+ socket.end()
+ })
+}
+
+function foreverAgent (t, options, req) {
+ var r = (req || request)(options, function (_err, res, body) {
+ t.ok(r.agent instanceof ForeverAgent, 'is ForeverAgent')
+ t.equal(Object.keys(r.agent.sockets).length, 1, '1 socket name')
+
+ var name = 'localhost:' + s.port // node 0.10-
+ t.equal(r.agent.sockets[name].length, 1, '1 open socket')
+
+ var socket = r.agent.sockets[name][0]
+ socket.on('close', function () {
+ t.equal(Object.keys(r.agent.sockets[name]).length, 0, '0 open sockets')
+ t.end()
+ })
+ socket.end()
+ })
+}
+
+// http.Agent
+
+tape('options.agent', function (t) {
+ httpAgent(t, {
+ uri: s.url,
+ agent: new http.Agent({keepAlive: true})
+ })
+})
+
+tape('options.agentClass + options.agentOptions', function (t) {
+ httpAgent(t, {
+ uri: s.url,
+ agentClass: http.Agent,
+ agentOptions: {keepAlive: true}
+ })
+})
+
+// forever-agent
+
+tape('options.forever = true', function (t) {
+ var v = version()
+ var options = {
+ uri: s.url,
+ forever: true
+ }
+
+ if (v.major === 0 && v.minor <= 10) { foreverAgent(t, options) } else { httpAgent(t, options) }
+})
+
+tape('forever() method', function (t) {
+ var v = version()
+ var options = {
+ uri: s.url
+ }
+ var r = request.forever({maxSockets: 1})
+
+ if (v.major === 0 && v.minor <= 10) { foreverAgent(t, options, r) } else { httpAgent(t, options, r) }
+})
+
+tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-agentOptions.js b/tests/test-agentOptions.js
new file mode 100644
index 000000000..4682cbbba
--- /dev/null
+++ b/tests/test-agentOptions.js
@@ -0,0 +1,51 @@
+'use strict'
+
+// test-agent.js modifies the process state
+// causing these tests to fail when running under single process via tape
+if (!process.env.running_under_istanbul) {
+ var request = require('../index')
+ var http = require('http')
+ var server = require('./server')
+ var tape = require('tape')
+
+ var s = server.createServer()
+
+ s.on('/', function (req, resp) {
+ resp.statusCode = 200
+ resp.end('')
+ })
+
+ tape('setup', function (t) {
+ s.listen(0, function () {
+ t.end()
+ })
+ })
+
+ tape('without agentOptions should use global agent', function (t) {
+ var r = request(s.url, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.deepEqual(r.agent, http.globalAgent)
+ t.equal(Object.keys(r.pool).length, 0)
+ t.end()
+ })
+ })
+
+ tape('with agentOptions should apply to new agent in pool', function (t) {
+ var r = request(s.url, {
+ agentOptions: { foo: 'bar' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(r.agent.options.foo, 'bar')
+ t.equal(Object.keys(r.pool).length, 1)
+ t.end()
+ })
+ })
+
+ tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
+ })
+}
diff --git a/tests/test-api.js b/tests/test-api.js
new file mode 100644
index 000000000..3aa12fdc3
--- /dev/null
+++ b/tests/test-api.js
@@ -0,0 +1,33 @@
+'use strict'
+
+var http = require('http')
+var request = require('../index')
+var tape = require('tape')
+var server
+
+tape('setup', function (t) {
+ server = http.createServer()
+ server.on('request', function (req, res) {
+ res.writeHead(202)
+ req.pipe(res)
+ })
+ server.listen(0, function () {
+ server.url = 'http://localhost:' + this.address().port
+ t.end()
+ })
+})
+
+tape('callback option', function (t) {
+ request({
+ url: server.url,
+ callback: function (err, res, body) {
+ t.error(err)
+ t.equal(res.statusCode, 202)
+ t.end()
+ }
+ })
+})
+
+tape('cleanup', function (t) {
+ server.close(t.end)
+})
diff --git a/tests/test-aws.js b/tests/test-aws.js
new file mode 100644
index 000000000..44f4f0b04
--- /dev/null
+++ b/tests/test-aws.js
@@ -0,0 +1,123 @@
+'use strict'
+
+var request = require('../index')
+var server = require('./server')
+var tape = require('tape')
+
+var s = server.createServer()
+
+var path = '/aws.json'
+
+s.on(path, function (req, res) {
+ res.writeHead(200, {
+ 'Content-Type': 'application/json'
+ })
+ res.end(JSON.stringify(req.headers))
+})
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ t.end()
+ })
+})
+
+tape('default behaviour: aws-sign2 without sign_version key', function (t) {
+ var options = {
+ url: s.url + path,
+ aws: {
+ key: 'my_key',
+ secret: 'my_secret'
+ },
+ json: true
+ }
+ request(options, function (err, res, body) {
+ t.error(err)
+ t.ok(body.authorization)
+ t.notOk(body['x-amz-date'])
+ t.end()
+ })
+})
+
+tape('aws-sign4 options', function (t) {
+ var options = {
+ url: s.url + path,
+ aws: {
+ key: 'my_key',
+ secret: 'my_secret',
+ sign_version: 4
+ },
+ json: true
+ }
+ request(options, function (err, res, body) {
+ t.error(err)
+ t.ok(body.authorization)
+ t.ok(body['x-amz-date'])
+ t.notok(body['x-amz-security-token'])
+ t.end()
+ })
+})
+
+tape('aws-sign4 options with session token', function (t) {
+ var options = {
+ url: s.url + path,
+ aws: {
+ key: 'my_key',
+ secret: 'my_secret',
+ session: 'session',
+ sign_version: 4
+ },
+ json: true
+ }
+ request(options, function (err, res, body) {
+ t.error(err)
+ t.ok(body.authorization)
+ t.ok(body['x-amz-date'])
+ t.ok(body['x-amz-security-token'])
+ t.end()
+ })
+})
+
+tape('aws-sign4 options with service', function (t) {
+ var serviceName = 'UNIQUE_SERVICE_NAME'
+ var options = {
+ url: s.url + path,
+ aws: {
+ key: 'my_key',
+ secret: 'my_secret',
+ sign_version: 4,
+ service: serviceName
+ },
+ json: true
+ }
+ request(options, function (err, res, body) {
+ t.error(err)
+ t.ok(body.authorization.includes(serviceName))
+ t.end()
+ })
+})
+
+tape('aws-sign4 with additional headers', function (t) {
+ var options = {
+ url: s.url + path,
+ headers: {
+ 'X-Custom-Header': 'custom'
+ },
+ aws: {
+ key: 'my_key',
+ secret: 'my_secret',
+ sign_version: 4
+ },
+ json: true
+ }
+ request(options, function (err, res, body) {
+ t.error(err)
+ t.ok(body.authorization.includes('x-custom-header'))
+ t.end()
+ })
+})
+
+tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-baseUrl.js b/tests/test-baseUrl.js
new file mode 100644
index 000000000..a9a5e1378
--- /dev/null
+++ b/tests/test-baseUrl.js
@@ -0,0 +1,133 @@
+'use strict'
+
+var http = require('http')
+var request = require('../index')
+var tape = require('tape')
+var url = require('url')
+
+var s = http.createServer(function (req, res) {
+ if (req.url === '/redirect/') {
+ res.writeHead(302, {
+ location: '/'
+ })
+ } else {
+ res.statusCode = 200
+ res.setHeader('X-PATH', req.url)
+ }
+ res.end('ok')
+})
+
+function addTest (baseUrl, uri, expected) {
+ tape('test baseurl="' + baseUrl + '" uri="' + uri + '"', function (t) {
+ request(uri, { baseUrl: baseUrl }, function (err, resp, body) {
+ t.equal(err, null)
+ t.equal(body, 'ok')
+ t.equal(resp.headers['x-path'], expected)
+ t.end()
+ })
+ })
+}
+
+function addTests () {
+ addTest(s.url, '', '/')
+ addTest(s.url + '/', '', '/')
+ addTest(s.url, '/', '/')
+ addTest(s.url + '/', '/', '/')
+ addTest(s.url + '/api', '', '/api')
+ addTest(s.url + '/api/', '', '/api/')
+ addTest(s.url + '/api', '/', '/api/')
+ addTest(s.url + '/api/', '/', '/api/')
+ addTest(s.url + '/api', 'resource', '/api/resource')
+ addTest(s.url + '/api/', 'resource', '/api/resource')
+ addTest(s.url + '/api', '/resource', '/api/resource')
+ addTest(s.url + '/api/', '/resource', '/api/resource')
+ addTest(s.url + '/api', 'resource/', '/api/resource/')
+ addTest(s.url + '/api/', 'resource/', '/api/resource/')
+ addTest(s.url + '/api', '/resource/', '/api/resource/')
+ addTest(s.url + '/api/', '/resource/', '/api/resource/')
+}
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ s.url = 'http://localhost:' + this.address().port
+ addTests()
+ tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
+ })
+ t.end()
+ })
+})
+
+tape('baseUrl', function (t) {
+ request('resource', {
+ baseUrl: s.url
+ }, function (err, resp, body) {
+ t.equal(err, null)
+ t.equal(body, 'ok')
+ t.end()
+ })
+})
+
+tape('baseUrl defaults', function (t) {
+ var withDefaults = request.defaults({
+ baseUrl: s.url
+ })
+ withDefaults('resource', function (err, resp, body) {
+ t.equal(err, null)
+ t.equal(body, 'ok')
+ t.end()
+ })
+})
+
+tape('baseUrl and redirects', function (t) {
+ request('/', {
+ baseUrl: s.url + '/redirect'
+ }, function (err, resp, body) {
+ t.equal(err, null)
+ t.equal(body, 'ok')
+ t.equal(resp.headers['x-path'], '/')
+ t.end()
+ })
+})
+
+tape('error when baseUrl is not a String', function (t) {
+ request('resource', {
+ baseUrl: url.parse(s.url + '/path')
+ }, function (err, resp, body) {
+ t.notEqual(err, null)
+ t.equal(err.message, 'options.baseUrl must be a string')
+ t.end()
+ })
+})
+
+tape('error when uri is not a String', function (t) {
+ request(url.parse('resource'), {
+ baseUrl: s.url + '/path'
+ }, function (err, resp, body) {
+ t.notEqual(err, null)
+ t.equal(err.message, 'options.uri must be a string when using options.baseUrl')
+ t.end()
+ })
+})
+
+tape('error on baseUrl and uri with scheme', function (t) {
+ request(s.url + '/path/ignoring/baseUrl', {
+ baseUrl: s.url + '/path/'
+ }, function (err, resp, body) {
+ t.notEqual(err, null)
+ t.equal(err.message, 'options.uri must be a path when using options.baseUrl')
+ t.end()
+ })
+})
+
+tape('error on baseUrl and uri with scheme-relative url', function (t) {
+ request(s.url.slice('http:'.length) + '/path/ignoring/baseUrl', {
+ baseUrl: s.url + '/path/'
+ }, function (err, resp, body) {
+ t.notEqual(err, null)
+ t.equal(err.message, 'options.uri must be a path when using options.baseUrl')
+ t.end()
+ })
+})
diff --git a/tests/test-basic-auth.js b/tests/test-basic-auth.js
new file mode 100644
index 000000000..5368b0584
--- /dev/null
+++ b/tests/test-basic-auth.js
@@ -0,0 +1,221 @@
+'use strict'
+
+var assert = require('assert')
+var http = require('http')
+var request = require('../index')
+var tape = require('tape')
+
+var numBasicRequests = 0
+var basicServer
+
+tape('setup', function (t) {
+ basicServer = http.createServer(function (req, res) {
+ numBasicRequests++
+
+ var ok
+
+ if (req.headers.authorization) {
+ if (req.headers.authorization === 'Basic ' + Buffer.from('user:pass').toString('base64')) {
+ ok = true
+ } else if (req.headers.authorization === 'Basic ' + Buffer.from('user:').toString('base64')) {
+ ok = true
+ } else if (req.headers.authorization === 'Basic ' + Buffer.from(':pass').toString('base64')) {
+ ok = true
+ } else if (req.headers.authorization === 'Basic ' + Buffer.from('user:pâss').toString('base64')) {
+ ok = true
+ } else {
+ // Bad auth header, don't send back WWW-Authenticate header
+ ok = false
+ }
+ } else {
+ // No auth header, send back WWW-Authenticate header
+ ok = false
+ res.setHeader('www-authenticate', 'Basic realm="Private"')
+ }
+
+ if (req.url === '/post/') {
+ var expectedContent = 'key=value'
+ req.on('data', function (data) {
+ assert.equal(data, expectedContent)
+ })
+ assert.equal(req.method, 'POST')
+ assert.equal(req.headers['content-length'], '' + expectedContent.length)
+ assert.equal(req.headers['content-type'], 'application/x-www-form-urlencoded')
+ }
+
+ if (ok) {
+ res.end('ok')
+ } else {
+ res.statusCode = 401
+ res.end('401')
+ }
+ }).listen(0, function () {
+ basicServer.port = this.address().port
+ basicServer.url = 'http://localhost:' + basicServer.port
+ t.end()
+ })
+})
+
+tape('sendImmediately - false', function (t) {
+ var r = request({
+ 'method': 'GET',
+ 'uri': basicServer.url + '/test/',
+ 'auth': {
+ 'user': 'user',
+ 'pass': 'pass',
+ 'sendImmediately': false
+ }
+ }, function (error, res, body) {
+ t.error(error)
+ t.equal(r._auth.user, 'user')
+ t.equal(res.statusCode, 200)
+ t.equal(numBasicRequests, 2)
+ t.end()
+ })
+})
+
+tape('sendImmediately - true', function (t) {
+ // If we don't set sendImmediately = false, request will send basic auth
+ var r = request({
+ 'method': 'GET',
+ 'uri': basicServer.url + '/test2/',
+ 'auth': {
+ 'user': 'user',
+ 'pass': 'pass'
+ }
+ }, function (error, res, body) {
+ t.error(error)
+ t.equal(r._auth.user, 'user')
+ t.equal(res.statusCode, 200)
+ t.equal(numBasicRequests, 3)
+ t.end()
+ })
+})
+
+tape('credentials in url', function (t) {
+ var r = request({
+ 'method': 'GET',
+ 'uri': basicServer.url.replace(/:\/\//, '$&user:pass@') + '/test2/'
+ }, function (error, res, body) {
+ t.error(error)
+ t.equal(r._auth.user, 'user')
+ t.equal(res.statusCode, 200)
+ t.equal(numBasicRequests, 4)
+ t.end()
+ })
+})
+
+tape('POST request', function (t) {
+ var r = request({
+ 'method': 'POST',
+ 'form': { 'key': 'value' },
+ 'uri': basicServer.url + '/post/',
+ 'auth': {
+ 'user': 'user',
+ 'pass': 'pass',
+ 'sendImmediately': false
+ }
+ }, function (error, res, body) {
+ t.error(error)
+ t.equal(r._auth.user, 'user')
+ t.equal(res.statusCode, 200)
+ t.equal(numBasicRequests, 6)
+ t.end()
+ })
+})
+
+tape('user - empty string', function (t) {
+ t.doesNotThrow(function () {
+ var r = request({
+ 'method': 'GET',
+ 'uri': basicServer.url + '/allow_empty_user/',
+ 'auth': {
+ 'user': '',
+ 'pass': 'pass',
+ 'sendImmediately': false
+ }
+ }, function (error, res, body) {
+ t.error(error)
+ t.equal(r._auth.user, '')
+ t.equal(res.statusCode, 200)
+ t.equal(numBasicRequests, 8)
+ t.end()
+ })
+ })
+})
+
+tape('pass - undefined', function (t) {
+ t.doesNotThrow(function () {
+ var r = request({
+ 'method': 'GET',
+ 'uri': basicServer.url + '/allow_undefined_password/',
+ 'auth': {
+ 'user': 'user',
+ 'pass': undefined,
+ 'sendImmediately': false
+ }
+ }, function (error, res, body) {
+ t.error(error)
+ t.equal(r._auth.user, 'user')
+ t.equal(res.statusCode, 200)
+ t.equal(numBasicRequests, 10)
+ t.end()
+ })
+ })
+})
+
+tape('pass - utf8', function (t) {
+ t.doesNotThrow(function () {
+ var r = request({
+ 'method': 'GET',
+ 'uri': basicServer.url + '/allow_undefined_password/',
+ 'auth': {
+ 'user': 'user',
+ 'pass': 'pâss',
+ 'sendImmediately': false
+ }
+ }, function (error, res, body) {
+ t.error(error)
+ t.equal(r._auth.user, 'user')
+ t.equal(r._auth.pass, 'pâss')
+ t.equal(res.statusCode, 200)
+ t.equal(numBasicRequests, 12)
+ t.end()
+ })
+ })
+})
+
+tape('auth method', function (t) {
+ var r = request
+ .get(basicServer.url + '/test/')
+ .auth('user', '', false)
+ .on('response', function (res) {
+ t.equal(r._auth.user, 'user')
+ t.equal(res.statusCode, 200)
+ t.equal(numBasicRequests, 14)
+ t.end()
+ })
+})
+
+tape('get method', function (t) {
+ var r = request.get(basicServer.url + '/test/',
+ {
+ auth: {
+ user: 'user',
+ pass: '',
+ sendImmediately: false
+ }
+ }, function (err, res) {
+ t.equal(r._auth.user, 'user')
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(numBasicRequests, 16)
+ t.end()
+ })
+})
+
+tape('cleanup', function (t) {
+ basicServer.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-bearer-auth.js b/tests/test-bearer-auth.js
new file mode 100644
index 000000000..032ccc7ee
--- /dev/null
+++ b/tests/test-bearer-auth.js
@@ -0,0 +1,187 @@
+'use strict'
+
+var assert = require('assert')
+var http = require('http')
+var request = require('../index')
+var tape = require('tape')
+
+var numBearerRequests = 0
+var bearerServer
+
+tape('setup', function (t) {
+ bearerServer = http.createServer(function (req, res) {
+ numBearerRequests++
+
+ var ok
+
+ if (req.headers.authorization) {
+ if (req.headers.authorization === 'Bearer theToken') {
+ ok = true
+ } else {
+ // Bad auth header, don't send back WWW-Authenticate header
+ ok = false
+ }
+ } else {
+ // No auth header, send back WWW-Authenticate header
+ ok = false
+ res.setHeader('www-authenticate', 'Bearer realm="Private"')
+ }
+
+ if (req.url === '/post/') {
+ var expectedContent = 'data_key=data_value'
+ req.on('data', function (data) {
+ assert.equal(data, expectedContent)
+ })
+ assert.equal(req.method, 'POST')
+ assert.equal(req.headers['content-length'], '' + expectedContent.length)
+ assert.equal(req.headers['content-type'], 'application/x-www-form-urlencoded')
+ }
+
+ if (ok) {
+ res.end('ok')
+ } else {
+ res.statusCode = 401
+ res.end('401')
+ }
+ }).listen(0, function () {
+ bearerServer.url = 'http://localhost:' + this.address().port
+ t.end()
+ })
+})
+
+tape('bearer auth', function (t) {
+ request({
+ 'method': 'GET',
+ 'uri': bearerServer.url + '/test/',
+ 'auth': {
+ 'bearer': 'theToken',
+ 'sendImmediately': false
+ }
+ }, function (error, res, body) {
+ t.error(error)
+ t.equal(res.statusCode, 200)
+ t.equal(numBearerRequests, 2)
+ t.end()
+ })
+})
+
+tape('bearer auth with default sendImmediately', function (t) {
+ // If we don't set sendImmediately = false, request will send bearer auth
+ request({
+ 'method': 'GET',
+ 'uri': bearerServer.url + '/test2/',
+ 'auth': {
+ 'bearer': 'theToken'
+ }
+ }, function (error, res, body) {
+ t.error(error)
+ t.equal(res.statusCode, 200)
+ t.equal(numBearerRequests, 3)
+ t.end()
+ })
+})
+
+tape('', function (t) {
+ request({
+ 'method': 'POST',
+ 'form': { 'data_key': 'data_value' },
+ 'uri': bearerServer.url + '/post/',
+ 'auth': {
+ 'bearer': 'theToken',
+ 'sendImmediately': false
+ }
+ }, function (error, res, body) {
+ t.error(error)
+ t.equal(res.statusCode, 200)
+ t.equal(numBearerRequests, 5)
+ t.end()
+ })
+})
+
+tape('using .auth, sendImmediately = false', function (t) {
+ request
+ .get(bearerServer.url + '/test/')
+ .auth(null, null, false, 'theToken')
+ .on('response', function (res) {
+ t.equal(res.statusCode, 200)
+ t.equal(numBearerRequests, 7)
+ t.end()
+ })
+})
+
+tape('using .auth, sendImmediately = true', function (t) {
+ request
+ .get(bearerServer.url + '/test/')
+ .auth(null, null, true, 'theToken')
+ .on('response', function (res) {
+ t.equal(res.statusCode, 200)
+ t.equal(numBearerRequests, 8)
+ t.end()
+ })
+})
+
+tape('bearer is a function', function (t) {
+ request({
+ 'method': 'GET',
+ 'uri': bearerServer.url + '/test/',
+ 'auth': {
+ 'bearer': function () { return 'theToken' },
+ 'sendImmediately': false
+ }
+ }, function (error, res, body) {
+ t.error(error)
+ t.equal(res.statusCode, 200)
+ t.equal(numBearerRequests, 10)
+ t.end()
+ })
+})
+
+tape('bearer is a function, path = test2', function (t) {
+ // If we don't set sendImmediately = false, request will send bearer auth
+ request({
+ 'method': 'GET',
+ 'uri': bearerServer.url + '/test2/',
+ 'auth': {
+ 'bearer': function () { return 'theToken' }
+ }
+ }, function (error, res, body) {
+ t.error(error)
+ t.equal(res.statusCode, 200)
+ t.equal(numBearerRequests, 11)
+ t.end()
+ })
+})
+
+tape('no auth method', function (t) {
+ request({
+ 'method': 'GET',
+ 'uri': bearerServer.url + '/test2/',
+ 'auth': {
+ 'bearer': undefined
+ }
+ }, function (error, res, body) {
+ t.equal(error.message, 'no auth mechanism defined')
+ t.end()
+ })
+})
+
+tape('null bearer', function (t) {
+ request({
+ 'method': 'GET',
+ 'uri': bearerServer.url + '/test2/',
+ 'auth': {
+ 'bearer': null
+ }
+ }, function (error, res, body) {
+ t.error(error)
+ t.equal(res.statusCode, 401)
+ t.equal(numBearerRequests, 13)
+ t.end()
+ })
+})
+
+tape('cleanup', function (t) {
+ bearerServer.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-body.js b/tests/test-body.js
index 9d2e18851..dc482125a 100644
--- a/tests/test-body.js
+++ b/tests/test-body.js
@@ -1,95 +1,154 @@
+'use strict'
+
var server = require('./server')
- , events = require('events')
- , stream = require('stream')
- , assert = require('assert')
- , request = require('../main.js')
- ;
-
-var s = server.createServer();
-
-var tests =
- { testGet :
- { resp : server.createGetResponse("TESTING!")
- , expectBody: "TESTING!"
- }
- , testGetChunkBreak :
- { resp : server.createChunkResponse(
- [ new Buffer([239])
- , new Buffer([163])
- , new Buffer([191])
- , new Buffer([206])
- , new Buffer([169])
- , new Buffer([226])
- , new Buffer([152])
- , new Buffer([131])
- ])
- , expectBody: "Ω☃"
- }
- , testGetBuffer :
- { resp : server.createGetResponse(new Buffer("TESTING!"))
- , encoding: null
- , expectBody: new Buffer("TESTING!")
- }
- , testGetJSON :
- { resp : server.createGetResponse('{"test":true}', 'application/json')
- , json : true
- , expectBody: {"test":true}
- }
- , testPutString :
- { resp : server.createPostValidator("PUTTINGDATA")
- , method : "PUT"
- , body : "PUTTINGDATA"
- }
- , testPutBuffer :
- { resp : server.createPostValidator("PUTTINGDATA")
- , method : "PUT"
- , body : new Buffer("PUTTINGDATA")
- }
- , testPutJSON :
- { resp : server.createPostValidator(JSON.stringify({foo: 'bar'}))
- , method: "PUT"
- , json: {foo: 'bar'}
- }
- , testPutMultipart :
- { resp: server.createPostValidator(
- '--frontier\r\n' +
- 'content-type: text/html\r\n' +
- '\r\n' +
- 'Oh hi.' +
- '\r\n--frontier\r\n\r\n' +
- 'Oh hi.' +
- '\r\n--frontier--'
- )
- , method: "PUT"
- , multipart:
- [ {'content-type': 'text/html', 'body': 'Oh hi.'}
- , {'body': 'Oh hi.'}
- ]
- }
- }
-
-s.listen(s.port, function () {
-
- var counter = 0
-
- for (i in tests) {
- (function () {
- var test = tests[i]
- s.on('/'+i, test.resp)
- test.uri = s.url + '/' + i
- request(test, function (err, resp, body) {
- if (err) throw err
- if (test.expectBody) {
- assert.deepEqual(test.expectBody, body)
- }
- counter = counter - 1;
- if (counter === 0) {
- console.log(Object.keys(tests).length+" tests passed.")
- s.close()
- }
- })
- counter++
- })()
- }
+var request = require('../index')
+var tape = require('tape')
+var http = require('http')
+
+var s = server.createServer()
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ t.end()
+ })
+})
+
+function addTest (name, data) {
+ tape('test ' + name, function (t) {
+ s.on('/' + name, data.resp)
+ data.uri = s.url + '/' + name
+ request(data, function (err, resp, body) {
+ t.equal(err, null)
+ if (data.expectBody && Buffer.isBuffer(data.expectBody)) {
+ t.deepEqual(data.expectBody.toString(), body.toString())
+ } else if (data.expectBody) {
+ t.deepEqual(data.expectBody, body)
+ }
+ t.end()
+ })
+ })
+}
+
+addTest('testGet', {
+ resp: server.createGetResponse('TESTING!'), expectBody: 'TESTING!'
+})
+
+addTest('testGetChunkBreak', {
+ resp: server.createChunkResponse(
+ [ Buffer.from([239]),
+ Buffer.from([163]),
+ Buffer.from([191]),
+ Buffer.from([206]),
+ Buffer.from([169]),
+ Buffer.from([226]),
+ Buffer.from([152]),
+ Buffer.from([131])
+ ]),
+ expectBody: '\uF8FF\u03A9\u2603'
+})
+
+addTest('testGetBuffer', {
+ resp: server.createGetResponse(Buffer.from('TESTING!')), encoding: null, expectBody: Buffer.from('TESTING!')
+})
+
+addTest('testGetEncoding', {
+ resp: server.createGetResponse(Buffer.from('efa3bfcea9e29883', 'hex')), encoding: 'hex', expectBody: 'efa3bfcea9e29883'
+})
+
+addTest('testGetUTF', {
+ resp: server.createGetResponse(Buffer.from([0xEF, 0xBB, 0xBF, 226, 152, 131])), encoding: 'utf8', expectBody: '\u2603'
+})
+
+addTest('testGetJSON', {
+ resp: server.createGetResponse('{"test":true}', 'application/json'), json: true, expectBody: {'test': true}
})
+addTest('testPutString', {
+ resp: server.createPostValidator('PUTTINGDATA'), method: 'PUT', body: 'PUTTINGDATA'
+})
+
+addTest('testPutBuffer', {
+ resp: server.createPostValidator('PUTTINGDATA'), method: 'PUT', body: Buffer.from('PUTTINGDATA')
+})
+
+addTest('testPutJSON', {
+ resp: server.createPostValidator(JSON.stringify({foo: 'bar'})), method: 'PUT', json: {foo: 'bar'}
+})
+
+addTest('testPutMultipart', {
+ resp: server.createPostValidator(
+ '--__BOUNDARY__\r\n' +
+ 'content-type: text/html\r\n' +
+ '\r\n' +
+ 'Oh hi.' +
+ '\r\n--__BOUNDARY__\r\n\r\n' +
+ 'Oh hi.' +
+ '\r\n--__BOUNDARY__--'
+ ),
+ method: 'PUT',
+ multipart: [ {'content-type': 'text/html', 'body': 'Oh hi.'},
+ {'body': 'Oh hi.'}
+ ]
+})
+
+addTest('testPutMultipartPreambleCRLF', {
+ resp: server.createPostValidator(
+ '\r\n--__BOUNDARY__\r\n' +
+ 'content-type: text/html\r\n' +
+ '\r\n' +
+ 'Oh hi.' +
+ '\r\n--__BOUNDARY__\r\n\r\n' +
+ 'Oh hi.' +
+ '\r\n--__BOUNDARY__--'
+ ),
+ method: 'PUT',
+ preambleCRLF: true,
+ multipart: [ {'content-type': 'text/html', 'body': 'Oh hi.'},
+ {'body': 'Oh hi.'}
+ ]
+})
+
+addTest('testPutMultipartPostambleCRLF', {
+ resp: server.createPostValidator(
+ '\r\n--__BOUNDARY__\r\n' +
+ 'content-type: text/html\r\n' +
+ '\r\n' +
+ 'Oh hi.' +
+ '\r\n--__BOUNDARY__\r\n\r\n' +
+ 'Oh hi.' +
+ '\r\n--__BOUNDARY__--' +
+ '\r\n'
+ ),
+ method: 'PUT',
+ preambleCRLF: true,
+ postambleCRLF: true,
+ multipart: [ {'content-type': 'text/html', 'body': 'Oh hi.'},
+ {'body': 'Oh hi.'}
+ ]
+})
+
+tape('typed array', function (t) {
+ var server = http.createServer()
+ server.on('request', function (req, res) {
+ req.pipe(res)
+ })
+ server.listen(0, function () {
+ var data = new Uint8Array([1, 2, 3])
+ request({
+ uri: 'http://localhost:' + this.address().port,
+ method: 'POST',
+ body: data,
+ encoding: null
+ }, function (err, res, body) {
+ t.error(err)
+ t.deepEqual(Buffer.from(data), body)
+ server.close(t.end)
+ })
+ })
+})
+
+tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-cookie.js b/tests/test-cookie.js
deleted file mode 100644
index 6c6a7a779..000000000
--- a/tests/test-cookie.js
+++ /dev/null
@@ -1,29 +0,0 @@
-var Cookie = require('../vendor/cookie')
- , assert = require('assert');
-
-var str = 'Sid="s543qactge.wKE61E01Bs%2BKhzmxrwrnug="; Path=/; httpOnly; Expires=Sat, 04 Dec 2010 23:27:28 GMT';
-var cookie = new Cookie(str);
-
-// test .toString()
-assert.equal(cookie.toString(), str);
-
-// test .path
-assert.equal(cookie.path, '/');
-
-// test .httpOnly
-assert.equal(cookie.httpOnly, true);
-
-// test .name
-assert.equal(cookie.name, 'Sid');
-
-// test .value
-assert.equal(cookie.value, '"s543qactge.wKE61E01Bs%2BKhzmxrwrnug="');
-
-// test .expires
-assert.equal(cookie.expires instanceof Date, true);
-
-// test .path default
-var cookie = new Cookie('foo=bar', { url: 'http://foo.com/bar' });
-assert.equal(cookie.path, '/bar');
-
-console.log('All tests passed');
diff --git a/tests/test-cookiejar.js b/tests/test-cookiejar.js
deleted file mode 100644
index 76fcd7161..000000000
--- a/tests/test-cookiejar.js
+++ /dev/null
@@ -1,90 +0,0 @@
-var Cookie = require('../vendor/cookie')
- , Jar = require('../vendor/cookie/jar')
- , assert = require('assert');
-
-function expires(ms) {
- return new Date(Date.now() + ms).toUTCString();
-}
-
-// test .get() expiration
-(function() {
- var jar = new Jar;
- var cookie = new Cookie('sid=1234; path=/; expires=' + expires(1000));
- jar.add(cookie);
- setTimeout(function(){
- var cookies = jar.get({ url: 'http://foo.com/foo' });
- assert.equal(cookies.length, 1);
- assert.equal(cookies[0], cookie);
- setTimeout(function(){
- var cookies = jar.get({ url: 'http://foo.com/foo' });
- assert.equal(cookies.length, 0);
- }, 1000);
- }, 5);
-})();
-
-// test .get() path support
-(function() {
- var jar = new Jar;
- var a = new Cookie('sid=1234; path=/');
- var b = new Cookie('sid=1111; path=/foo/bar');
- var c = new Cookie('sid=2222; path=/');
- jar.add(a);
- jar.add(b);
- jar.add(c);
-
- // should remove the duplicates
- assert.equal(jar.cookies.length, 2);
-
- // same name, same path, latter prevails
- var cookies = jar.get({ url: 'http://foo.com/' });
- assert.equal(cookies.length, 1);
- assert.equal(cookies[0], c);
-
- // same name, diff path, path specifity prevails, latter prevails
- var cookies = jar.get({ url: 'http://foo.com/foo/bar' });
- assert.equal(cookies.length, 1);
- assert.equal(cookies[0], b);
-
- var jar = new Jar;
- var a = new Cookie('sid=1111; path=/foo/bar');
- var b = new Cookie('sid=1234; path=/');
- jar.add(a);
- jar.add(b);
-
- var cookies = jar.get({ url: 'http://foo.com/foo/bar' });
- assert.equal(cookies.length, 1);
- assert.equal(cookies[0], a);
-
- var cookies = jar.get({ url: 'http://foo.com/' });
- assert.equal(cookies.length, 1);
- assert.equal(cookies[0], b);
-
- var jar = new Jar;
- var a = new Cookie('sid=1111; path=/foo/bar');
- var b = new Cookie('sid=3333; path=/foo/bar');
- var c = new Cookie('pid=3333; path=/foo/bar');
- var d = new Cookie('sid=2222; path=/foo/');
- var e = new Cookie('sid=1234; path=/');
- jar.add(a);
- jar.add(b);
- jar.add(c);
- jar.add(d);
- jar.add(e);
-
- var cookies = jar.get({ url: 'http://foo.com/foo/bar' });
- assert.equal(cookies.length, 2);
- assert.equal(cookies[0], b);
- assert.equal(cookies[1], c);
-
- var cookies = jar.get({ url: 'http://foo.com/foo/' });
- assert.equal(cookies.length, 1);
- assert.equal(cookies[0], d);
-
- var cookies = jar.get({ url: 'http://foo.com/' });
- assert.equal(cookies.length, 1);
- assert.equal(cookies[0], e);
-})();
-
-setTimeout(function() {
- console.log('All tests passed');
-}, 1200);
diff --git a/tests/test-cookies.js b/tests/test-cookies.js
new file mode 100644
index 000000000..6bebcaf12
--- /dev/null
+++ b/tests/test-cookies.js
@@ -0,0 +1,130 @@
+'use strict'
+
+var http = require('http')
+var request = require('../index')
+var tape = require('tape')
+
+var validUrl
+var malformedUrl
+var invalidUrl
+
+var server = http.createServer(function (req, res) {
+ if (req.url === '/valid') {
+ res.setHeader('set-cookie', 'foo=bar')
+ } else if (req.url === '/malformed') {
+ res.setHeader('set-cookie', 'foo')
+ } else if (req.url === '/invalid') {
+ res.setHeader('set-cookie', 'foo=bar; Domain=foo.com')
+ }
+ res.end('okay')
+})
+
+tape('setup', function (t) {
+ server.listen(0, function () {
+ server.url = 'http://localhost:' + this.address().port
+ validUrl = server.url + '/valid'
+ malformedUrl = server.url + '/malformed'
+ invalidUrl = server.url + '/invalid'
+ t.end()
+ })
+})
+
+tape('simple cookie creation', function (t) {
+ var cookie = request.cookie('foo=bar')
+ t.equals(cookie.key, 'foo')
+ t.equals(cookie.value, 'bar')
+ t.end()
+})
+
+tape('simple malformed cookie creation', function (t) {
+ var cookie = request.cookie('foo')
+ t.equals(cookie.key, '')
+ t.equals(cookie.value, 'foo')
+ t.end()
+})
+
+tape('after server sends a cookie', function (t) {
+ var jar1 = request.jar()
+ request({
+ method: 'GET',
+ url: validUrl,
+ jar: jar1
+ },
+ function (error, response, body) {
+ t.equal(error, null)
+ t.equal(jar1.getCookieString(validUrl), 'foo=bar')
+ t.equal(body, 'okay')
+
+ var cookies = jar1.getCookies(validUrl)
+ t.equal(cookies.length, 1)
+ t.equal(cookies[0].key, 'foo')
+ t.equal(cookies[0].value, 'bar')
+ t.end()
+ })
+})
+
+tape('after server sends a malformed cookie', function (t) {
+ var jar = request.jar()
+ request({
+ method: 'GET',
+ url: malformedUrl,
+ jar: jar
+ },
+ function (error, response, body) {
+ t.equal(error, null)
+ t.equal(jar.getCookieString(malformedUrl), 'foo')
+ t.equal(body, 'okay')
+
+ var cookies = jar.getCookies(malformedUrl)
+ t.equal(cookies.length, 1)
+ t.equal(cookies[0].key, '')
+ t.equal(cookies[0].value, 'foo')
+ t.end()
+ })
+})
+
+tape('after server sends a cookie for a different domain', function (t) {
+ var jar2 = request.jar()
+ request({
+ method: 'GET',
+ url: invalidUrl,
+ jar: jar2
+ },
+ function (error, response, body) {
+ t.equal(error, null)
+ t.equal(jar2.getCookieString(validUrl), '')
+ t.deepEqual(jar2.getCookies(validUrl), [])
+ t.equal(body, 'okay')
+ t.end()
+ })
+})
+
+tape('make sure setCookie works', function (t) {
+ var jar3 = request.jar()
+ var err = null
+ try {
+ jar3.setCookie(request.cookie('foo=bar'), validUrl)
+ } catch (e) {
+ err = e
+ }
+ t.equal(err, null)
+ var cookies = jar3.getCookies(validUrl)
+ t.equal(cookies.length, 1)
+ t.equal(cookies[0].key, 'foo')
+ t.equal(cookies[0].value, 'bar')
+ t.end()
+})
+
+tape('custom store', function (t) {
+ var Store = function () {}
+ var store = new Store()
+ var jar = request.jar(store)
+ t.equals(store, jar._jar.store)
+ t.end()
+})
+
+tape('cleanup', function (t) {
+ server.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-defaults.js b/tests/test-defaults.js
index 6c8b58fa6..f75f5d7bc 100644
--- a/tests/test-defaults.js
+++ b/tests/test-defaults.js
@@ -1,68 +1,340 @@
+'use strict'
+
var server = require('./server')
- , assert = require('assert')
- , request = require('../main.js')
- ;
-
-var s = server.createServer();
-
-s.listen(s.port, function () {
- var counter = 0;
- s.on('/get', function (req, resp) {
- assert.equal(req.headers.foo, 'bar');
- assert.equal(req.method, 'GET')
- resp.writeHead(200, {'Content-Type': 'text/plain'});
- resp.end('TESTING!');
- });
-
- // test get(string, function)
- request.defaults({headers:{foo:"bar"}})(s.url + '/get', function (e, r, b){
- if (e) throw e;
- assert.deepEqual("TESTING!", b);
- counter += 1;
- });
-
- s.on('/post', function (req, resp) {
- assert.equal(req.headers.foo, 'bar');
- assert.equal(req.headers['content-type'], 'application/json');
- assert.equal(req.method, 'POST')
- resp.writeHead(200, {'Content-Type': 'application/json'});
- resp.end(JSON.stringify({foo:'bar'}));
- });
-
- // test post(string, object, function)
- request.defaults({headers:{foo:"bar"}}).post(s.url + '/post', {json: true}, function (e, r, b){
- if (e) throw e;
- assert.deepEqual('bar', b.foo);
- counter += 1;
- });
-
- s.on('/del', function (req, resp) {
- assert.equal(req.headers.foo, 'bar');
- assert.equal(req.method, 'DELETE')
- resp.writeHead(200, {'Content-Type': 'application/json'});
- resp.end(JSON.stringify({foo:'bar'}));
- });
-
- // test .del(string, function)
- request.defaults({headers:{foo:"bar"}, json:true}).del(s.url + '/del', function (e, r, b){
- if (e) throw e;
- assert.deepEqual('bar', b.foo);
- counter += 1;
- });
-
- s.on('/head', function (req, resp) {
- assert.equal(req.headers.foo, 'bar');
- assert.equal(req.method, 'HEAD')
- resp.writeHead(200, {'Content-Type': 'text/plain'});
- resp.end();
- });
-
- // test head.(object, function)
- request.defaults({headers:{foo:"bar"}}).head({uri: s.url + '/head'}, function (e, r, b){
- if (e) throw e;
- counter += 1;
- console.log(counter.toString() + " tests passed.")
- s.close()
- });
+var request = require('../index')
+var qs = require('qs')
+var tape = require('tape')
+
+var s = server.createServer()
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ s.on('/', function (req, res) {
+ res.writeHead(200, {'content-type': 'application/json'})
+ res.end(JSON.stringify({
+ method: req.method,
+ headers: req.headers,
+ qs: qs.parse(req.url.replace(/.*\?(.*)/, '$1'))
+ }))
+ })
+
+ s.on('/head', function (req, res) {
+ res.writeHead(200, {'x-data': JSON.stringify({method: req.method, headers: req.headers})})
+ res.end()
+ })
+
+ s.on('/set-undefined', function (req, res) {
+ var data = ''
+ req.on('data', function (d) {
+ data += d
+ })
+ req.on('end', function () {
+ res.writeHead(200, {'Content-Type': 'application/json'})
+ res.end(JSON.stringify({
+ method: req.method, headers: req.headers, data: JSON.parse(data)
+ }))
+ })
+ })
+
+ t.end()
+ })
+})
+
+tape('get(string, function)', function (t) {
+ request.defaults({
+ headers: { foo: 'bar' }
+ })(s.url + '/', function (e, r, b) {
+ b = JSON.parse(b)
+ t.equal(b.method, 'GET')
+ t.equal(b.headers.foo, 'bar')
+ t.end()
+ })
+})
+
+tape('merge headers', function (t) {
+ request.defaults({
+ headers: { foo: 'bar', merged: 'no' }
+ })(s.url + '/', {
+ headers: { merged: 'yes' }, json: true
+ }, function (e, r, b) {
+ t.equal(b.headers.foo, 'bar')
+ t.equal(b.headers.merged, 'yes')
+ t.end()
+ })
+})
+
+tape('deep extend', function (t) {
+ request.defaults({
+ headers: { a: 1, b: 2 },
+ qs: { a: 1, b: 2 }
+ })(s.url + '/', {
+ headers: { b: 3, c: 4 },
+ qs: { b: 3, c: 4 },
+ json: true
+ }, function (e, r, b) {
+ delete b.headers.host
+ delete b.headers.accept
+ delete b.headers.connection
+ t.deepEqual(b.headers, { a: '1', b: '3', c: '4' })
+ t.deepEqual(b.qs, { a: '1', b: '3', c: '4' })
+ t.end()
+ })
+})
+
+tape('default undefined header', function (t) {
+ request.defaults({
+ headers: { foo: 'bar', test: undefined }, json: true
+ })(s.url + '/', function (e, r, b) {
+ t.equal(b.method, 'GET')
+ t.equal(b.headers.foo, 'bar')
+ t.equal(b.headers.test, undefined)
+ t.end()
+ })
+})
+
+tape('post(string, object, function)', function (t) {
+ request.defaults({
+ headers: { foo: 'bar' }
+ }).post(s.url + '/', { json: true }, function (e, r, b) {
+ t.equal(b.method, 'POST')
+ t.equal(b.headers.foo, 'bar')
+ t.equal(b.headers['content-type'], undefined)
+ t.end()
+ })
+})
+
+tape('patch(string, object, function)', function (t) {
+ request.defaults({
+ headers: { foo: 'bar' }
+ }).patch(s.url + '/', { json: true }, function (e, r, b) {
+ t.equal(b.method, 'PATCH')
+ t.equal(b.headers.foo, 'bar')
+ t.equal(b.headers['content-type'], undefined)
+ t.end()
+ })
+})
+
+tape('post(string, object, function) with body', function (t) {
+ request.defaults({
+ headers: { foo: 'bar' }
+ }).post(s.url + '/', {
+ json: true,
+ body: { bar: 'baz' }
+ }, function (e, r, b) {
+ t.equal(b.method, 'POST')
+ t.equal(b.headers.foo, 'bar')
+ t.equal(b.headers['content-type'], 'application/json')
+ t.end()
+ })
+})
+
+tape('del(string, function)', function (t) {
+ request.defaults({
+ headers: {foo: 'bar'},
+ json: true
+ }).del(s.url + '/', function (e, r, b) {
+ t.equal(b.method, 'DELETE')
+ t.equal(b.headers.foo, 'bar')
+ t.end()
+ })
+})
+
+tape('delete(string, function)', function (t) {
+ request.defaults({
+ headers: {foo: 'bar'},
+ json: true
+ }).delete(s.url + '/', function (e, r, b) {
+ t.equal(b.method, 'DELETE')
+ t.equal(b.headers.foo, 'bar')
+ t.end()
+ })
+})
+
+tape('head(object, function)', function (t) {
+ request.defaults({
+ headers: { foo: 'bar' }
+ }).head({ uri: s.url + '/head' }, function (e, r, b) {
+ b = JSON.parse(r.headers['x-data'])
+ t.equal(b.method, 'HEAD')
+ t.equal(b.headers.foo, 'bar')
+ t.end()
+ })
+})
+
+tape('recursive defaults', function (t) {
+ t.plan(11)
+
+ var defaultsOne = request.defaults({ headers: { foo: 'bar1' } })
+ var defaultsTwo = defaultsOne.defaults({ headers: { baz: 'bar2' } })
+ var defaultsThree = defaultsTwo.defaults({}, function (options, callback) {
+ options.headers = {
+ foo: 'bar3'
+ }
+ defaultsTwo(options, callback)
+ })
+
+ defaultsOne(s.url + '/', {json: true}, function (e, r, b) {
+ t.equal(b.method, 'GET')
+ t.equal(b.headers.foo, 'bar1')
+ })
+
+ defaultsTwo(s.url + '/', {json: true}, function (e, r, b) {
+ t.equal(b.method, 'GET')
+ t.equal(b.headers.foo, 'bar1')
+ t.equal(b.headers.baz, 'bar2')
+ })
+
+ // requester function on recursive defaults
+ defaultsThree(s.url + '/', {json: true}, function (e, r, b) {
+ t.equal(b.method, 'GET')
+ t.equal(b.headers.foo, 'bar3')
+ t.equal(b.headers.baz, 'bar2')
+ })
+
+ defaultsTwo.get(s.url + '/', {json: true}, function (e, r, b) {
+ t.equal(b.method, 'GET')
+ t.equal(b.headers.foo, 'bar1')
+ t.equal(b.headers.baz, 'bar2')
+ })
+})
+
+tape('recursive defaults requester', function (t) {
+ t.plan(5)
+
+ var defaultsOne = request.defaults({}, function (options, callback) {
+ var headers = options.headers || {}
+ headers.foo = 'bar1'
+ options.headers = headers
+
+ request(options, callback)
+ })
+
+ var defaultsTwo = defaultsOne.defaults({}, function (options, callback) {
+ var headers = options.headers || {}
+ headers.baz = 'bar2'
+ options.headers = headers
+
+ defaultsOne(options, callback)
+ })
+
+ defaultsOne.get(s.url + '/', {json: true}, function (e, r, b) {
+ t.equal(b.method, 'GET')
+ t.equal(b.headers.foo, 'bar1')
+ })
+
+ defaultsTwo.get(s.url + '/', {json: true}, function (e, r, b) {
+ t.equal(b.method, 'GET')
+ t.equal(b.headers.foo, 'bar1')
+ t.equal(b.headers.baz, 'bar2')
+ })
+})
+
+tape('test custom request handler function', function (t) {
+ t.plan(3)
+
+ var requestWithCustomHandler = request.defaults({
+ headers: { foo: 'bar' },
+ body: 'TESTING!'
+ }, function (uri, options, callback) {
+ var params = request.initParams(uri, options, callback)
+ params.headers.x = 'y'
+ return request(params.uri, params, params.callback)
+ })
+
+ t.throws(function () {
+ requestWithCustomHandler.head(s.url + '/', function (e, r, b) {
+ throw new Error('We should never get here')
+ })
+ }, /HTTP HEAD requests MUST NOT include a request body/)
+
+ requestWithCustomHandler.get(s.url + '/', function (e, r, b) {
+ b = JSON.parse(b)
+ t.equal(b.headers.foo, 'bar')
+ t.equal(b.headers.x, 'y')
+ })
+})
+
+tape('test custom request handler function without options', function (t) {
+ t.plan(2)
+
+ var customHandlerWithoutOptions = request.defaults(function (uri, options, callback) {
+ var params = request.initParams(uri, options, callback)
+ var headers = params.headers || {}
+ headers.x = 'y'
+ headers.foo = 'bar'
+ params.headers = headers
+ return request(params.uri, params, params.callback)
+ })
+
+ customHandlerWithoutOptions.get(s.url + '/', function (e, r, b) {
+ b = JSON.parse(b)
+ t.equal(b.headers.foo, 'bar')
+ t.equal(b.headers.x, 'y')
+ })
+})
+
+tape('test only setting undefined properties', function (t) {
+ request.defaults({
+ method: 'post',
+ json: true,
+ headers: { 'x-foo': 'bar' }
+ })({
+ uri: s.url + '/set-undefined',
+ json: {foo: 'bar'},
+ headers: {'x-foo': 'baz'}
+ }, function (e, r, b) {
+ t.equal(b.method, 'POST')
+ t.equal(b.headers['content-type'], 'application/json')
+ t.equal(b.headers['x-foo'], 'baz')
+ t.deepEqual(b.data, { foo: 'bar' })
+ t.end()
+ })
+})
+
+tape('test only function', function (t) {
+ var post = request.post
+ t.doesNotThrow(function () {
+ post(s.url + '/', function (e, r, b) {
+ t.equal(r.statusCode, 200)
+ t.end()
+ })
+ })
+})
+
+tape('invoke defaults', function (t) {
+ var d = request.defaults({
+ uri: s.url + '/',
+ headers: { foo: 'bar' }
+ })
+ d({json: true}, function (e, r, b) {
+ t.equal(b.method, 'GET')
+ t.equal(b.headers.foo, 'bar')
+ t.end()
+ })
+})
+
+tape('invoke convenience method from defaults', function (t) {
+ var d = request.defaults({
+ uri: s.url + '/',
+ headers: { foo: 'bar' }
+ })
+ d.get({json: true}, function (e, r, b) {
+ t.equal(b.method, 'GET')
+ t.equal(b.headers.foo, 'bar')
+ t.end()
+ })
+})
+
+tape('defaults without options', function (t) {
+ var d = request.defaults()
+ d.get(s.url + '/', {json: true}, function (e, r, b) {
+ t.equal(r.statusCode, 200)
+ t.end()
+ })
+})
+tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
})
diff --git a/tests/test-digest-auth.js b/tests/test-digest-auth.js
new file mode 100644
index 000000000..d5c3c0ee5
--- /dev/null
+++ b/tests/test-digest-auth.js
@@ -0,0 +1,232 @@
+'use strict'
+
+var http = require('http')
+var request = require('../index')
+var tape = require('tape')
+var crypto = require('crypto')
+
+function makeHeader () {
+ return [].join.call(arguments, ', ')
+}
+
+function makeHeaderRegex () {
+ return new RegExp('^' + makeHeader.apply(null, arguments) + '$')
+}
+
+function md5 (str) {
+ return crypto.createHash('md5').update(str).digest('hex')
+}
+
+var digestServer = http.createServer(function (req, res) {
+ var ok,
+ testHeader
+
+ if (req.url === '/test/') {
+ if (req.headers.authorization) {
+ testHeader = makeHeaderRegex(
+ 'Digest username="test"',
+ 'realm="Private"',
+ 'nonce="WpcHS2/TBAA=dffcc0dbd5f96d49a5477166649b7c0ae3866a93"',
+ 'uri="/test/"',
+ 'qop=auth',
+ 'response="[a-f0-9]{32}"',
+ 'nc=00000001',
+ 'cnonce="[a-f0-9]{32}"',
+ 'algorithm=MD5',
+ 'opaque="5ccc069c403ebaf9f0171e9517f40e41"'
+ )
+ if (testHeader.test(req.headers.authorization)) {
+ ok = true
+ } else {
+ // Bad auth header, don't send back WWW-Authenticate header
+ ok = false
+ }
+ } else {
+ // No auth header, send back WWW-Authenticate header
+ ok = false
+ res.setHeader('www-authenticate', makeHeader(
+ 'Digest realm="Private"',
+ 'nonce="WpcHS2/TBAA=dffcc0dbd5f96d49a5477166649b7c0ae3866a93"',
+ 'algorithm=MD5',
+ 'qop="auth"',
+ 'opaque="5ccc069c403ebaf9f0171e9517f40e41"'
+ ))
+ }
+ } else if (req.url === '/test/md5-sess') { // RFC 2716 MD5-sess w/ qop=auth
+ var user = 'test'
+ var realm = 'Private'
+ var pass = 'testing'
+ var nonce = 'WpcHS2/TBAA=dffcc0dbd5f96d49a5477166649b7c0ae3866a93'
+ var nonceCount = '00000001'
+ var qop = 'auth'
+ var algorithm = 'MD5-sess'
+ if (req.headers.authorization) {
+ // HA1=MD5(MD5(username:realm:password):nonce:cnonce)
+ // HA2=MD5(method:digestURI)
+ // response=MD5(HA1:nonce:nonceCount:clientNonce:qop:HA2)
+
+ var cnonce = /cnonce="(.*)"/.exec(req.headers.authorization)[1]
+ var ha1 = md5(md5(user + ':' + realm + ':' + pass) + ':' + nonce + ':' + cnonce)
+ var ha2 = md5('GET:/test/md5-sess')
+ var response = md5(ha1 + ':' + nonce + ':' + nonceCount + ':' + cnonce + ':' + qop + ':' + ha2)
+
+ testHeader = makeHeaderRegex(
+ 'Digest username="' + user + '"',
+ 'realm="' + realm + '"',
+ 'nonce="' + nonce + '"',
+ 'uri="/test/md5-sess"',
+ 'qop=' + qop,
+ 'response="' + response + '"',
+ 'nc=' + nonceCount,
+ 'cnonce="' + cnonce + '"',
+ 'algorithm=' + algorithm
+ )
+
+ ok = testHeader.test(req.headers.authorization)
+ } else {
+ // No auth header, send back WWW-Authenticate header
+ ok = false
+ res.setHeader('www-authenticate', makeHeader(
+ 'Digest realm="' + realm + '"',
+ 'nonce="' + nonce + '"',
+ 'algorithm=' + algorithm,
+ 'qop="' + qop + '"'
+ ))
+ }
+ } else if (req.url === '/dir/index.html') {
+ // RFC2069-compatible mode
+ // check: http://www.rfc-editor.org/errata_search.php?rfc=2069
+ if (req.headers.authorization) {
+ testHeader = makeHeaderRegex(
+ 'Digest username="Mufasa"',
+ 'realm="testrealm@host.com"',
+ 'nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093"',
+ 'uri="/dir/index.html"',
+ 'response="[a-f0-9]{32}"',
+ 'opaque="5ccc069c403ebaf9f0171e9517f40e41"'
+ )
+ if (testHeader.test(req.headers.authorization)) {
+ ok = true
+ } else {
+ // Bad auth header, don't send back WWW-Authenticate header
+ ok = false
+ }
+ } else {
+ // No auth header, send back WWW-Authenticate header
+ ok = false
+ res.setHeader('www-authenticate', makeHeader(
+ 'Digest realm="testrealm@host.com"',
+ 'nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093"',
+ 'opaque="5ccc069c403ebaf9f0171e9517f40e41"'
+ ))
+ }
+ }
+
+ if (ok) {
+ res.end('ok')
+ } else {
+ res.statusCode = 401
+ res.end('401')
+ }
+})
+
+tape('setup', function (t) {
+ digestServer.listen(0, function () {
+ digestServer.url = 'http://localhost:' + this.address().port
+ t.end()
+ })
+})
+
+tape('with sendImmediately = false', function (t) {
+ var numRedirects = 0
+
+ request({
+ method: 'GET',
+ uri: digestServer.url + '/test/',
+ auth: {
+ user: 'test',
+ pass: 'testing',
+ sendImmediately: false
+ }
+ }, function (error, response, body) {
+ t.equal(error, null)
+ t.equal(response.statusCode, 200)
+ t.equal(numRedirects, 1)
+ t.end()
+ }).on('redirect', function () {
+ t.equal(this.response.statusCode, 401)
+ numRedirects++
+ })
+})
+
+tape('with MD5-sess algorithm', function (t) {
+ var numRedirects = 0
+
+ request({
+ method: 'GET',
+ uri: digestServer.url + '/test/md5-sess',
+ auth: {
+ user: 'test',
+ pass: 'testing',
+ sendImmediately: false
+ }
+ }, function (error, response, body) {
+ t.equal(error, null)
+ t.equal(response.statusCode, 200)
+ t.equal(numRedirects, 1)
+ t.end()
+ }).on('redirect', function () {
+ t.equal(this.response.statusCode, 401)
+ numRedirects++
+ })
+})
+
+tape('without sendImmediately = false', function (t) {
+ var numRedirects = 0
+
+ // If we don't set sendImmediately = false, request will send basic auth
+ request({
+ method: 'GET',
+ uri: digestServer.url + '/test/',
+ auth: {
+ user: 'test',
+ pass: 'testing'
+ }
+ }, function (error, response, body) {
+ t.equal(error, null)
+ t.equal(response.statusCode, 401)
+ t.equal(numRedirects, 0)
+ t.end()
+ }).on('redirect', function () {
+ t.equal(this.response.statusCode, 401)
+ numRedirects++
+ })
+})
+
+tape('with different credentials', function (t) {
+ var numRedirects = 0
+
+ request({
+ method: 'GET',
+ uri: digestServer.url + '/dir/index.html',
+ auth: {
+ user: 'Mufasa',
+ pass: 'CircleOfLife',
+ sendImmediately: false
+ }
+ }, function (error, response, body) {
+ t.equal(error, null)
+ t.equal(response.statusCode, 200)
+ t.equal(numRedirects, 1)
+ t.end()
+ }).on('redirect', function () {
+ t.equal(this.response.statusCode, 401)
+ numRedirects++
+ })
+})
+
+tape('cleanup', function (t) {
+ digestServer.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-emptyBody.js b/tests/test-emptyBody.js
new file mode 100644
index 000000000..684d3d5ae
--- /dev/null
+++ b/tests/test-emptyBody.js
@@ -0,0 +1,56 @@
+'use strict'
+
+var request = require('../index')
+var http = require('http')
+var tape = require('tape')
+
+var s = http.createServer(function (req, resp) {
+ resp.statusCode = 200
+ resp.end('')
+})
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ s.url = 'http://localhost:' + this.address().port
+ t.end()
+ })
+})
+
+tape('empty body with encoding', function (t) {
+ request(s.url, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, '')
+ t.end()
+ })
+})
+
+tape('empty body without encoding', function (t) {
+ request({
+ url: s.url,
+ encoding: null
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.same(body, Buffer.alloc(0))
+ t.end()
+ })
+})
+
+tape('empty JSON body', function (t) {
+ request({
+ url: s.url,
+ json: {}
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, undefined)
+ t.end()
+ })
+})
+
+tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-errors.js b/tests/test-errors.js
index 1986a59ee..7060e9fca 100644
--- a/tests/test-errors.js
+++ b/tests/test-errors.js
@@ -1,37 +1,108 @@
-var server = require('./server')
- , events = require('events')
- , assert = require('assert')
- , request = require('../main.js')
- ;
-
-var local = 'http://localhost:8888/asdf'
-
-try {
- request({uri:local, body:{}})
- assert.fail("Should have throw")
-} catch(e) {
- assert.equal(e.message, 'Argument error, options.body.')
-}
-
-try {
- request({uri:local, multipart: 'foo'})
- assert.fail("Should have throw")
-} catch(e) {
- assert.equal(e.message, 'Argument error, options.multipart.')
-}
-
-try {
- request({uri:local, multipart: [{}]})
- assert.fail("Should have throw")
-} catch(e) {
- assert.equal(e.message, 'Body attribute missing in multipart.')
-}
-
-try {
- request(local, {multipart: [{}]})
- assert.fail("Should have throw")
-} catch(e) {
- assert.equal(e.message, 'Body attribute missing in multipart.')
-}
-
-console.log("All tests passed.")
+'use strict'
+
+var request = require('../index')
+var tape = require('tape')
+
+var local = 'http://localhost:0/asdf'
+
+tape('without uri', function (t) {
+ t.throws(function () {
+ request({})
+ }, /^Error: options\.uri is a required argument$/)
+ t.end()
+})
+
+tape('invalid uri 1', function (t) {
+ t.throws(function () {
+ request({
+ uri: 'this-is-not-a-valid-uri'
+ })
+ }, /^Error: Invalid URI/)
+ t.end()
+})
+
+tape('invalid uri 2', function (t) {
+ t.throws(function () {
+ request({
+ uri: 'github.com/uri-is-not-valid-without-protocol'
+ })
+ }, /^Error: Invalid URI/)
+ t.end()
+})
+
+tape('invalid uri + NO_PROXY', function (t) {
+ process.env.NO_PROXY = 'google.com'
+ t.throws(function () {
+ request({
+ uri: 'invalid'
+ })
+ }, /^Error: Invalid URI/)
+ delete process.env.NO_PROXY
+ t.end()
+})
+
+tape('deprecated unix URL', function (t) {
+ t.throws(function () {
+ request({
+ uri: 'unix://path/to/socket/and/then/request/path'
+ })
+ }, /^Error: `unix:\/\/` URL scheme is no longer supported/)
+ t.end()
+})
+
+tape('invalid body', function (t) {
+ t.throws(function () {
+ request({
+ uri: local, body: {}
+ })
+ }, /^Error: Argument error, options\.body\.$/)
+ t.end()
+})
+
+tape('invalid multipart', function (t) {
+ t.throws(function () {
+ request({
+ uri: local,
+ multipart: 'foo'
+ })
+ }, /^Error: Argument error, options\.multipart\.$/)
+ t.end()
+})
+
+tape('multipart without body 1', function (t) {
+ t.throws(function () {
+ request({
+ uri: local,
+ multipart: [ {} ]
+ })
+ }, /^Error: Body attribute missing in multipart\.$/)
+ t.end()
+})
+
+tape('multipart without body 2', function (t) {
+ t.throws(function () {
+ request(local, {
+ multipart: [ {} ]
+ })
+ }, /^Error: Body attribute missing in multipart\.$/)
+ t.end()
+})
+
+tape('head method with a body', function (t) {
+ t.throws(function () {
+ request(local, {
+ method: 'HEAD',
+ body: 'foo'
+ })
+ }, /HTTP HEAD requests MUST NOT include a request body/)
+ t.end()
+})
+
+tape('head method with a body 2', function (t) {
+ t.throws(function () {
+ request.head(local, {
+ body: 'foo'
+ })
+ }, /HTTP HEAD requests MUST NOT include a request body/)
+ t.end()
+})
diff --git a/tests/test-event-forwarding.js b/tests/test-event-forwarding.js
new file mode 100644
index 000000000..c057a0bb9
--- /dev/null
+++ b/tests/test-event-forwarding.js
@@ -0,0 +1,39 @@
+'use strict'
+
+var server = require('./server')
+var request = require('../index')
+var tape = require('tape')
+
+var s = server.createServer()
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ s.on('/', function (req, res) {
+ res.writeHead(200, { 'content-type': 'text/plain' })
+ res.write('waited')
+ res.end()
+ })
+ t.end()
+ })
+})
+
+tape('should emit socket event', function (t) {
+ t.plan(4)
+
+ var req = request(s.url, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'waited')
+ })
+
+ req.on('socket', function (socket) {
+ var requestSocket = req.req.socket
+ t.equal(requestSocket, socket)
+ })
+})
+
+tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-follow-all-303.js b/tests/test-follow-all-303.js
new file mode 100644
index 000000000..b40adf84b
--- /dev/null
+++ b/tests/test-follow-all-303.js
@@ -0,0 +1,45 @@
+'use strict'
+
+var http = require('http')
+var request = require('../index')
+var tape = require('tape')
+
+var server = http.createServer(function (req, res) {
+ if (req.method === 'POST') {
+ res.setHeader('location', req.url)
+ res.statusCode = 303
+ res.end('try again')
+ } else {
+ res.end('ok')
+ }
+})
+
+tape('setup', function (t) {
+ server.listen(0, function () {
+ server.url = 'http://localhost:' + this.address().port
+ t.end()
+ })
+})
+
+tape('followAllRedirects with 303', function (t) {
+ var redirects = 0
+
+ request.post({
+ url: server.url + '/foo',
+ followAllRedirects: true,
+ form: { foo: 'bar' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(body, 'ok')
+ t.equal(redirects, 1)
+ t.end()
+ }).on('redirect', function () {
+ redirects++
+ })
+})
+
+tape('cleanup', function (t) {
+ server.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-follow-all.js b/tests/test-follow-all.js
new file mode 100644
index 000000000..c35d74b3e
--- /dev/null
+++ b/tests/test-follow-all.js
@@ -0,0 +1,57 @@
+'use strict'
+
+var http = require('http')
+var request = require('../index')
+var tape = require('tape')
+
+var server = http.createServer(function (req, res) {
+ // redirect everything 3 times, no matter what.
+ var c = req.headers.cookie
+
+ if (!c) {
+ c = 0
+ } else {
+ c = +c.split('=')[1] || 0
+ }
+
+ if (c > 3) {
+ res.end('ok')
+ return
+ }
+
+ res.setHeader('set-cookie', 'c=' + (c + 1))
+ res.setHeader('location', req.url)
+ res.statusCode = 302
+ res.end('try again')
+})
+
+tape('setup', function (t) {
+ server.listen(0, function () {
+ server.url = 'http://localhost:' + this.address().port
+ t.end()
+ })
+})
+
+tape('followAllRedirects', function (t) {
+ var redirects = 0
+
+ request.post({
+ url: server.url + '/foo',
+ followAllRedirects: true,
+ jar: true,
+ form: { foo: 'bar' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(body, 'ok')
+ t.equal(redirects, 4)
+ t.end()
+ }).on('redirect', function () {
+ redirects++
+ })
+})
+
+tape('cleanup', function (t) {
+ server.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-form-data-error.js b/tests/test-form-data-error.js
new file mode 100644
index 000000000..d6ee25d1b
--- /dev/null
+++ b/tests/test-form-data-error.js
@@ -0,0 +1,85 @@
+'use strict'
+
+var request = require('../index')
+var server = require('./server')
+var tape = require('tape')
+
+var s = server.createServer()
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ t.end()
+ })
+})
+
+tape('re-emit formData errors', function (t) {
+ s.on('/', function (req, res) {
+ res.writeHead(400)
+ res.end()
+ t.fail('The form-data error did not abort the request.')
+ })
+
+ request.post(s.url, function (err, res, body) {
+ t.equal(err.message, 'form-data: Arrays are not supported.')
+ setTimeout(function () {
+ t.end()
+ }, 10)
+ }).form().append('field', ['value1', 'value2'])
+})
+
+tape('omit content-length header if the value is set to NaN', function (t) {
+ // returns chunked HTTP response which is streamed to the 2nd HTTP request in the form data
+ s.on('/chunky', server.createChunkResponse(
+ ['some string',
+ 'some other string'
+ ]))
+
+ // accepts form data request
+ s.on('/stream', function (req, resp) {
+ req.on('data', function (chunk) {
+ // consume the request body
+ })
+ req.on('end', function () {
+ resp.writeHead(200)
+ resp.end()
+ })
+ })
+
+ var sendStreamRequest = function (stream) {
+ request.post({
+ uri: s.url + '/stream',
+ formData: {
+ param: stream
+ }
+ }, function (err, res) {
+ t.error(err, 'request failed')
+ t.end()
+ })
+ }
+
+ request.get({
+ uri: s.url + '/chunky'
+ }).on('response', function (res) {
+ sendStreamRequest(res)
+ })
+})
+
+// TODO: remove this test after form-data@2.0 starts stringifying null values
+tape('form-data should throw on null value', function (t) {
+ t.throws(function () {
+ request({
+ method: 'POST',
+ url: s.url,
+ formData: {
+ key: null
+ }
+ })
+ }, TypeError)
+ t.end()
+})
+
+tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-form-data.js b/tests/test-form-data.js
new file mode 100644
index 000000000..990562be5
--- /dev/null
+++ b/tests/test-form-data.js
@@ -0,0 +1,133 @@
+'use strict'
+
+var http = require('http')
+var path = require('path')
+var mime = require('mime-types')
+var request = require('../index')
+var fs = require('fs')
+var tape = require('tape')
+
+function runTest (t, options) {
+ var remoteFile = path.join(__dirname, 'googledoodle.jpg')
+ var localFile = path.join(__dirname, 'unicycle.jpg')
+ var multipartFormData = {}
+
+ var server = http.createServer(function (req, res) {
+ if (req.url === '/file') {
+ res.writeHead(200, {'content-type': 'image/jpg', 'content-length': 7187})
+ res.end(fs.readFileSync(remoteFile), 'binary')
+ return
+ }
+
+ if (options.auth) {
+ if (!req.headers.authorization) {
+ res.writeHead(401, {'www-authenticate': 'Basic realm="Private"'})
+ res.end()
+ return
+ } else {
+ t.ok(req.headers.authorization === 'Basic ' + Buffer.from('user:pass').toString('base64'))
+ }
+ }
+
+ t.ok(/multipart\/form-data; boundary=--------------------------\d+/
+ .test(req.headers['content-type']))
+
+ // temp workaround
+ var data = ''
+ req.setEncoding('utf8')
+
+ req.on('data', function (d) {
+ data += d
+ })
+
+ req.on('end', function () {
+ // check for the fields' traces
+
+ // 1st field : my_field
+ t.ok(data.indexOf('form-data; name="my_field"') !== -1)
+ t.ok(data.indexOf(multipartFormData.my_field) !== -1)
+
+ // 2nd field : my_buffer
+ t.ok(data.indexOf('form-data; name="my_buffer"') !== -1)
+ t.ok(data.indexOf(multipartFormData.my_buffer) !== -1)
+
+ // 3rd field : my_file
+ t.ok(data.indexOf('form-data; name="my_file"') !== -1)
+ t.ok(data.indexOf('; filename="' + path.basename(multipartFormData.my_file.path) + '"') !== -1)
+ // check for unicycle.jpg traces
+ t.ok(data.indexOf('2005:06:21 01:44:12') !== -1)
+ t.ok(data.indexOf('Content-Type: ' + mime.lookup(multipartFormData.my_file.path)) !== -1)
+
+ // 4th field : remote_file
+ t.ok(data.indexOf('form-data; name="remote_file"') !== -1)
+ t.ok(data.indexOf('; filename="' + path.basename(multipartFormData.remote_file.path) + '"') !== -1)
+
+ // 5th field : file with metadata
+ t.ok(data.indexOf('form-data; name="secret_file"') !== -1)
+ t.ok(data.indexOf('Content-Disposition: form-data; name="secret_file"; filename="topsecret.jpg"') !== -1)
+ t.ok(data.indexOf('Content-Type: image/custom') !== -1)
+
+ // 6th field : batch of files
+ t.ok(data.indexOf('form-data; name="batch"') !== -1)
+ t.ok(data.match(/form-data; name="batch"/g).length === 2)
+
+ // check for http://localhost:nnnn/file traces
+ t.ok(data.indexOf('Photoshop ICC') !== -1)
+ t.ok(data.indexOf('Content-Type: ' + mime.lookup(remoteFile)) !== -1)
+
+ res.writeHead(200)
+ res.end(options.json ? JSON.stringify({status: 'done'}) : 'done')
+ })
+ })
+
+ server.listen(0, function () {
+ var url = 'http://localhost:' + this.address().port
+ // @NOTE: multipartFormData properties must be set here so that my_file read stream does not leak in node v0.8
+ multipartFormData.my_field = 'my_value'
+ multipartFormData.my_buffer = Buffer.from([1, 2, 3])
+ multipartFormData.my_file = fs.createReadStream(localFile)
+ multipartFormData.remote_file = request(url + '/file')
+ multipartFormData.secret_file = {
+ value: fs.createReadStream(localFile),
+ options: {
+ filename: 'topsecret.jpg',
+ contentType: 'image/custom'
+ }
+ }
+ multipartFormData.batch = [
+ fs.createReadStream(localFile),
+ fs.createReadStream(localFile)
+ ]
+
+ var reqOptions = {
+ url: url + '/upload',
+ formData: multipartFormData
+ }
+ if (options.json) {
+ reqOptions.json = true
+ }
+ if (options.auth) {
+ reqOptions.auth = {user: 'user', pass: 'pass', sendImmediately: false}
+ }
+ request.post(reqOptions, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.deepEqual(body, options.json ? {status: 'done'} : 'done')
+ server.close(function () {
+ t.end()
+ })
+ })
+ })
+}
+
+tape('multipart formData', function (t) {
+ runTest(t, {json: false})
+})
+
+tape('multipart formData + JSON', function (t) {
+ runTest(t, {json: true})
+})
+
+tape('multipart formData + basic auth', function (t) {
+ runTest(t, {json: false, auth: true})
+})
diff --git a/tests/test-form-urlencoded.js b/tests/test-form-urlencoded.js
new file mode 100644
index 000000000..5e46917bb
--- /dev/null
+++ b/tests/test-form-urlencoded.js
@@ -0,0 +1,73 @@
+'use strict'
+
+var http = require('http')
+var request = require('../index')
+var tape = require('tape')
+
+function runTest (t, options, index) {
+ var server = http.createServer(function (req, res) {
+ if (index === 0 || index === 3) {
+ t.equal(req.headers['content-type'], 'application/x-www-form-urlencoded')
+ } else {
+ t.equal(req.headers['content-type'], 'application/x-www-form-urlencoded; charset=UTF-8')
+ }
+ t.equal(req.headers['content-length'], '21')
+ t.equal(req.headers.accept, 'application/json')
+
+ var data = ''
+ req.setEncoding('utf8')
+
+ req.on('data', function (d) {
+ data += d
+ })
+
+ req.on('end', function () {
+ t.equal(data, 'some=url&encoded=data')
+
+ res.writeHead(200)
+ res.end('done')
+ })
+ })
+
+ server.listen(0, function () {
+ var url = 'http://localhost:' + this.address().port
+ var r = request.post(url, options, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'done')
+ server.close(function () {
+ t.end()
+ })
+ })
+ if (!options.form && !options.body) {
+ r.form({some: 'url', encoded: 'data'})
+ }
+ })
+}
+
+var cases = [
+ {
+ form: {some: 'url', encoded: 'data'},
+ json: true
+ },
+ {
+ headers: {'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'},
+ form: {some: 'url', encoded: 'data'},
+ json: true
+ },
+ {
+ headers: {'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'},
+ body: 'some=url&encoded=data',
+ json: true
+ },
+ {
+ // body set via .form() method
+ json: true
+ }
+]
+
+cases.forEach(function (options, index) {
+ tape('application/x-www-form-urlencoded ' + index, function (t) {
+ runTest(t, options, index)
+ })
+})
diff --git a/tests/test-form.js b/tests/test-form.js
new file mode 100644
index 000000000..5f262f204
--- /dev/null
+++ b/tests/test-form.js
@@ -0,0 +1,101 @@
+'use strict'
+
+var http = require('http')
+var path = require('path')
+var mime = require('mime-types')
+var request = require('../index')
+var fs = require('fs')
+var tape = require('tape')
+
+tape('multipart form append', function (t) {
+ var remoteFile = path.join(__dirname, 'googledoodle.jpg')
+ var localFile = path.join(__dirname, 'unicycle.jpg')
+ var totalLength = null
+ var FIELDS = []
+
+ var server = http.createServer(function (req, res) {
+ if (req.url === '/file') {
+ res.writeHead(200, {'content-type': 'image/jpg', 'content-length': 7187})
+ res.end(fs.readFileSync(remoteFile), 'binary')
+ return
+ }
+
+ t.ok(/multipart\/form-data; boundary=--------------------------\d+/
+ .test(req.headers['content-type']))
+
+ // temp workaround
+ var data = ''
+ req.setEncoding('utf8')
+
+ req.on('data', function (d) {
+ data += d
+ })
+
+ req.on('end', function () {
+ var field
+ // check for the fields' traces
+
+ // 1st field : my_field
+ field = FIELDS.shift()
+ t.ok(data.indexOf('form-data; name="' + field.name + '"') !== -1)
+ t.ok(data.indexOf(field.value) !== -1)
+
+ // 2nd field : my_buffer
+ field = FIELDS.shift()
+ t.ok(data.indexOf('form-data; name="' + field.name + '"') !== -1)
+ t.ok(data.indexOf(field.value) !== -1)
+
+ // 3rd field : my_file
+ field = FIELDS.shift()
+ t.ok(data.indexOf('form-data; name="' + field.name + '"') !== -1)
+ t.ok(data.indexOf('; filename="' + path.basename(field.value.path) + '"') !== -1)
+ // check for unicycle.jpg traces
+ t.ok(data.indexOf('2005:06:21 01:44:12') !== -1)
+ t.ok(data.indexOf('Content-Type: ' + mime.lookup(field.value.path)) !== -1)
+
+ // 4th field : remote_file
+ field = FIELDS.shift()
+ t.ok(data.indexOf('form-data; name="' + field.name + '"') !== -1)
+ t.ok(data.indexOf('; filename="' + path.basename(field.value.path) + '"') !== -1)
+ // check for http://localhost:nnnn/file traces
+ t.ok(data.indexOf('Photoshop ICC') !== -1)
+ t.ok(data.indexOf('Content-Type: ' + mime.lookup(remoteFile)) !== -1)
+
+ t.ok(+req.headers['content-length'] === totalLength)
+
+ res.writeHead(200)
+ res.end('done')
+
+ t.equal(FIELDS.length, 0)
+ })
+ })
+
+ server.listen(0, function () {
+ var url = 'http://localhost:' + this.address().port
+ FIELDS = [
+ { name: 'my_field', value: 'my_value' },
+ { name: 'my_buffer', value: Buffer.from([1, 2, 3]) },
+ { name: 'my_file', value: fs.createReadStream(localFile) },
+ { name: 'remote_file', value: request(url + '/file') }
+ ]
+
+ var req = request.post(url + '/upload', function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'done')
+ server.close(function () {
+ t.end()
+ })
+ })
+ var form = req.form()
+
+ FIELDS.forEach(function (field) {
+ form.append(field.name, field.value)
+ })
+
+ form.getLength(function (err, length) {
+ t.equal(err, null)
+ totalLength = length
+ })
+ })
+})
diff --git a/tests/test-gzip.js b/tests/test-gzip.js
new file mode 100644
index 000000000..933b7bae0
--- /dev/null
+++ b/tests/test-gzip.js
@@ -0,0 +1,296 @@
+'use strict'
+
+var request = require('../index')
+var http = require('http')
+var zlib = require('zlib')
+var assert = require('assert')
+var bufferEqual = require('buffer-equal')
+var tape = require('tape')
+
+var testContent = 'Compressible response content.\n'
+var testContentBig
+var testContentBigGzip
+var testContentGzip
+
+var server = http.createServer(function (req, res) {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/plain')
+
+ if (req.method === 'HEAD') {
+ res.setHeader('Content-Encoding', 'gzip')
+ res.end()
+ return
+ }
+ if (req.headers.code) {
+ res.writeHead(req.headers.code, {
+ 'Content-Encoding': 'gzip',
+ code: req.headers.code
+ })
+ res.end()
+ return
+ }
+
+ if (/\bgzip\b/i.test(req.headers['accept-encoding'])) {
+ res.setHeader('Content-Encoding', 'gzip')
+ if (req.url === '/error') {
+ // send plaintext instead of gzip (should cause an error for the client)
+ res.end(testContent)
+ } else if (req.url === '/chunks') {
+ res.writeHead(200)
+ res.write(testContentBigGzip.slice(0, 4096))
+ setTimeout(function () { res.end(testContentBigGzip.slice(4096)) }, 10)
+ } else if (req.url === '/just-slightly-truncated') {
+ zlib.gzip(testContent, function (err, data) {
+ assert.equal(err, null)
+ // truncate the CRC checksum and size check at the end of the stream
+ res.end(data.slice(0, data.length - 8))
+ })
+ } else {
+ zlib.gzip(testContent, function (err, data) {
+ assert.equal(err, null)
+ res.end(data)
+ })
+ }
+ } else if (/\bdeflate\b/i.test(req.headers['accept-encoding'])) {
+ res.setHeader('Content-Encoding', 'deflate')
+ zlib.deflate(testContent, function (err, data) {
+ assert.equal(err, null)
+ res.end(data)
+ })
+ } else {
+ res.end(testContent)
+ }
+})
+
+tape('setup', function (t) {
+ // Need big compressed content to be large enough to chunk into gzip blocks.
+ // Want it to be deterministic to ensure test is reliable.
+ // Generate pseudo-random printable ASCII characters using MINSTD
+ var a = 48271
+ var m = 0x7FFFFFFF
+ var x = 1
+ testContentBig = Buffer.alloc(10240)
+ for (var i = 0; i < testContentBig.length; ++i) {
+ x = (a * x) & m
+ // Printable ASCII range from 32-126, inclusive
+ testContentBig[i] = (x % 95) + 32
+ }
+
+ zlib.gzip(testContent, function (err, data) {
+ t.equal(err, null)
+ testContentGzip = data
+
+ zlib.gzip(testContentBig, function (err, data2) {
+ t.equal(err, null)
+ testContentBigGzip = data2
+
+ server.listen(0, function () {
+ server.url = 'http://localhost:' + this.address().port
+ t.end()
+ })
+ })
+ })
+})
+
+tape('transparently supports gzip decoding to callbacks', function (t) {
+ var options = { url: server.url + '/foo', gzip: true }
+ request.get(options, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.headers['content-encoding'], 'gzip')
+ t.equal(body, testContent)
+ t.end()
+ })
+})
+
+tape('supports slightly invalid gzip content', function (t) {
+ var options = { url: server.url + '/just-slightly-truncated', gzip: true }
+ request.get(options, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.headers['content-encoding'], 'gzip')
+ t.equal(body, testContent)
+ t.end()
+ })
+})
+
+tape('transparently supports gzip decoding to pipes', function (t) {
+ var options = { url: server.url + '/foo', gzip: true }
+ var chunks = []
+ request.get(options)
+ .on('data', function (chunk) {
+ chunks.push(chunk)
+ })
+ .on('end', function () {
+ t.equal(Buffer.concat(chunks).toString(), testContent)
+ t.end()
+ })
+ .on('error', function (err) {
+ t.fail(err)
+ })
+})
+
+tape('does not request gzip if user specifies Accepted-Encodings', function (t) {
+ var headers = { 'Accept-Encoding': null }
+ var options = {
+ url: server.url + '/foo',
+ headers: headers,
+ gzip: true
+ }
+ request.get(options, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.headers['content-encoding'], undefined)
+ t.equal(body, testContent)
+ t.end()
+ })
+})
+
+tape('does not decode user-requested encoding by default', function (t) {
+ var headers = { 'Accept-Encoding': 'gzip' }
+ var options = { url: server.url + '/foo', headers: headers }
+ request.get(options, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.headers['content-encoding'], 'gzip')
+ t.equal(body, testContentGzip.toString())
+ t.end()
+ })
+})
+
+tape('supports character encoding with gzip encoding', function (t) {
+ var headers = { 'Accept-Encoding': 'gzip' }
+ var options = {
+ url: server.url + '/foo',
+ headers: headers,
+ gzip: true,
+ encoding: 'utf8'
+ }
+ var strings = []
+ request.get(options)
+ .on('data', function (string) {
+ t.equal(typeof string, 'string')
+ strings.push(string)
+ })
+ .on('end', function () {
+ t.equal(strings.join(''), testContent)
+ t.end()
+ })
+ .on('error', function (err) {
+ t.fail(err)
+ })
+})
+
+tape('transparently supports gzip error to callbacks', function (t) {
+ var options = { url: server.url + '/error', gzip: true }
+ request.get(options, function (err, res, body) {
+ t.equal(err.code, 'Z_DATA_ERROR')
+ t.equal(res, undefined)
+ t.equal(body, undefined)
+ t.end()
+ })
+})
+
+tape('transparently supports gzip error to pipes', function (t) {
+ var options = { url: server.url + '/error', gzip: true }
+ request.get(options)
+ .on('data', function (chunk) {
+ t.fail('Should not receive data event')
+ })
+ .on('end', function () {
+ t.fail('Should not receive end event')
+ })
+ .on('error', function (err) {
+ t.equal(err.code, 'Z_DATA_ERROR')
+ t.end()
+ })
+})
+
+tape('pause when streaming from a gzip request object', function (t) {
+ var chunks = []
+ var paused = false
+ var options = { url: server.url + '/chunks', gzip: true }
+ request.get(options)
+ .on('data', function (chunk) {
+ var self = this
+
+ t.notOk(paused, 'Only receive data when not paused')
+
+ chunks.push(chunk)
+ if (chunks.length === 1) {
+ self.pause()
+ paused = true
+ setTimeout(function () {
+ paused = false
+ self.resume()
+ }, 100)
+ }
+ })
+ .on('end', function () {
+ t.ok(chunks.length > 1, 'Received multiple chunks')
+ t.ok(bufferEqual(Buffer.concat(chunks), testContentBig), 'Expected content')
+ t.end()
+ })
+})
+
+tape('pause before streaming from a gzip request object', function (t) {
+ var paused = true
+ var options = { url: server.url + '/foo', gzip: true }
+ var r = request.get(options)
+ r.pause()
+ r.on('data', function (data) {
+ t.notOk(paused, 'Only receive data when not paused')
+ t.equal(data.toString(), testContent)
+ })
+ r.on('end', t.end.bind(t))
+
+ setTimeout(function () {
+ paused = false
+ r.resume()
+ }, 100)
+})
+
+tape('transparently supports deflate decoding to callbacks', function (t) {
+ var options = { url: server.url + '/foo', gzip: true, headers: { 'Accept-Encoding': 'deflate' } }
+
+ request.get(options, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.headers['content-encoding'], 'deflate')
+ t.equal(body, testContent)
+ t.end()
+ })
+})
+
+tape('do not try to pipe HEAD request responses', function (t) {
+ var options = { method: 'HEAD', url: server.url + '/foo', gzip: true }
+
+ request(options, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(body, '')
+ t.end()
+ })
+})
+
+tape('do not try to pipe responses with no body', function (t) {
+ var options = { url: server.url + '/foo', gzip: true }
+
+ // skip 105 on Node >= v10
+ var statusCodes = process.version.split('.')[0].slice(1) >= 10
+ ? [204, 304] : [105, 204, 304]
+
+ ;(function next (index) {
+ if (index === statusCodes.length) {
+ t.end()
+ return
+ }
+ options.headers = {code: statusCodes[index]}
+ request.post(options, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.headers.code, statusCodes[index].toString())
+ t.equal(body, '')
+ next(++index)
+ })
+ })(0)
+})
+
+tape('cleanup', function (t) {
+ server.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-har.js b/tests/test-har.js
new file mode 100644
index 000000000..61f0d7d63
--- /dev/null
+++ b/tests/test-har.js
@@ -0,0 +1,175 @@
+'use strict'
+
+var path = require('path')
+var request = require('..')
+var tape = require('tape')
+var fixture = require('./fixtures/har.json')
+var server = require('./server')
+
+var s = server.createEchoServer()
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ t.end()
+ })
+})
+
+tape('application-form-encoded', function (t) {
+ var options = {
+ url: s.url,
+ har: fixture['application-form-encoded']
+ }
+
+ request(options, function (err, res, body) {
+ var json = JSON.parse(body)
+
+ t.equal(err, null)
+ t.equal(json.body, 'foo=bar&hello=world')
+ t.end()
+ })
+})
+
+tape('application-json', function (t) {
+ var options = {
+ url: s.url,
+ har: fixture['application-json']
+ }
+
+ request(options, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(body.body, fixture['application-json'].postData.text)
+ t.end()
+ })
+})
+
+tape('cookies', function (t) {
+ var options = {
+ url: s.url,
+ har: fixture.cookies
+ }
+
+ request(options, function (err, res, body) {
+ var json = JSON.parse(body)
+
+ t.equal(err, null)
+ t.equal(json.headers.cookie, 'foo=bar; bar=baz')
+ t.end()
+ })
+})
+
+tape('custom-method', function (t) {
+ var options = {
+ url: s.url,
+ har: fixture['custom-method']
+ }
+
+ request(options, function (err, res, body) {
+ var json = JSON.parse(body)
+
+ t.equal(err, null)
+ t.equal(json.method, fixture['custom-method'].method)
+ t.end()
+ })
+})
+
+tape('headers', function (t) {
+ var options = {
+ url: s.url,
+ har: fixture.headers
+ }
+
+ request(options, function (err, res, body) {
+ var json = JSON.parse(body)
+
+ t.equal(err, null)
+ t.equal(json.headers['x-foo'], 'Bar')
+ t.end()
+ })
+})
+
+tape('multipart-data', function (t) {
+ var options = {
+ url: s.url,
+ har: fixture['multipart-data']
+ }
+
+ request(options, function (err, res, body) {
+ var json = JSON.parse(body)
+
+ t.equal(err, null)
+ t.ok(~json.headers['content-type'].indexOf('multipart/form-data'))
+ t.ok(~json.body.indexOf('Content-Disposition: form-data; name="foo"; filename="hello.txt"\r\nContent-Type: text/plain\r\n\r\nHello World'))
+ t.end()
+ })
+})
+
+tape('multipart-file', function (t) {
+ var options = {
+ url: s.url,
+ har: fixture['multipart-file']
+ }
+ var absolutePath = path.resolve(__dirname, options.har.postData.params[0].fileName)
+ options.har.postData.params[0].fileName = absolutePath
+
+ request(options, function (err, res, body) {
+ var json = JSON.parse(body)
+
+ t.equal(err, null)
+ t.ok(~json.headers['content-type'].indexOf('multipart/form-data'))
+ t.ok(~json.body.indexOf('Content-Disposition: form-data; name="foo"; filename="unicycle.jpg"\r\nContent-Type: image/jpeg'))
+ t.end()
+ })
+})
+
+tape('multipart-form-data', function (t) {
+ var options = {
+ url: s.url,
+ har: fixture['multipart-form-data']
+ }
+
+ request(options, function (err, res, body) {
+ var json = JSON.parse(body)
+
+ t.equal(err, null)
+ t.ok(~json.headers['content-type'].indexOf('multipart/form-data'))
+ t.ok(~json.body.indexOf('Content-Disposition: form-data; name="foo"'))
+ t.end()
+ })
+})
+
+tape('query', function (t) {
+ var options = {
+ url: s.url + '/?fff=sss',
+ har: fixture.query
+ }
+
+ request(options, function (err, res, body) {
+ var json = JSON.parse(body)
+
+ t.equal(err, null)
+ t.equal(json.url, '/?fff=sss&foo%5B0%5D=bar&foo%5B1%5D=baz&baz=abc')
+ t.end()
+ })
+})
+
+tape('text/plain', function (t) {
+ var options = {
+ url: s.url,
+ har: fixture['text-plain']
+ }
+
+ request(options, function (err, res, body) {
+ var json = JSON.parse(body)
+
+ t.equal(err, null)
+ t.equal(json.headers['content-type'], 'text/plain')
+ t.equal(json.body, 'Hello World')
+ t.end()
+ })
+})
+
+tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-hawk.js b/tests/test-hawk.js
new file mode 100644
index 000000000..3765908cf
--- /dev/null
+++ b/tests/test-hawk.js
@@ -0,0 +1,187 @@
+'use strict'
+
+var http = require('http')
+var request = require('../index')
+var hawk = require('../lib/hawk')
+var tape = require('tape')
+var assert = require('assert')
+
+var server = http.createServer(function (req, res) {
+ res.writeHead(200, {
+ 'Content-Type': 'text/plain'
+ })
+ res.end(authenticate(req))
+})
+
+tape('setup', function (t) {
+ server.listen(0, function () {
+ server.url = 'http://localhost:' + this.address().port
+ t.end()
+ })
+})
+
+var creds = {
+ key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn',
+ algorithm: 'sha256',
+ id: 'dh37fgj492je'
+}
+
+tape('hawk-get', function (t) {
+ request(server.url, {
+ hawk: { credentials: creds }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'OK')
+ t.end()
+ })
+})
+
+tape('hawk-post', function (t) {
+ request.post({ url: server.url, body: 'hello', hawk: { credentials: creds, payload: 'hello' } }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'OK')
+ t.end()
+ })
+})
+
+tape('hawk-ext', function (t) {
+ request(server.url, {
+ hawk: { credentials: creds, ext: 'test' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'OK')
+ t.end()
+ })
+})
+
+tape('hawk-app', function (t) {
+ request(server.url, {
+ hawk: { credentials: creds, app: 'test' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'OK')
+ t.end()
+ })
+})
+
+tape('hawk-app+dlg', function (t) {
+ request(server.url, {
+ hawk: { credentials: creds, app: 'test', dlg: 'asd' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'OK')
+ t.end()
+ })
+})
+
+tape('hawk-missing-creds', function (t) {
+ request(server.url, {
+ hawk: {}
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'FAIL')
+ t.end()
+ })
+})
+
+tape('hawk-missing-creds-id', function (t) {
+ request(server.url, {
+ hawk: {
+ credentials: {}
+ }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'FAIL')
+ t.end()
+ })
+})
+
+tape('hawk-missing-creds-key', function (t) {
+ request(server.url, {
+ hawk: {
+ credentials: { id: 'asd' }
+ }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'FAIL')
+ t.end()
+ })
+})
+
+tape('hawk-missing-creds-algo', function (t) {
+ request(server.url, {
+ hawk: {
+ credentials: { key: '123', id: '123' }
+ }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'FAIL')
+ t.end()
+ })
+})
+
+tape('hawk-invalid-creds-algo', function (t) {
+ request(server.url, {
+ hawk: {
+ credentials: { key: '123', id: '123', algorithm: 'xx' }
+ }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'FAIL')
+ t.end()
+ })
+})
+
+tape('cleanup', function (t) {
+ server.close(function () {
+ t.end()
+ })
+})
+
+function authenticate (req) {
+ if (!req.headers.authorization) {
+ return 'FAIL'
+ }
+
+ var headerParts = req.headers.authorization.match(/^(\w+)(?:\s+(.*))?$/)
+ assert.equal(headerParts[1], 'Hawk')
+ var attributes = {}
+ headerParts[2].replace(/(\w+)="([^"\\]*)"\s*(?:,\s*|$)/g, function ($0, $1, $2) { attributes[$1] = $2 })
+ var hostParts = req.headers.host.split(':')
+
+ const artifacts = {
+ method: req.method,
+ host: hostParts[0],
+ port: (hostParts[1] ? hostParts[1] : (req.connection && req.connection.encrypted ? 443 : 80)),
+ resource: req.url,
+ ts: attributes.ts,
+ nonce: attributes.nonce,
+ hash: attributes.hash,
+ ext: attributes.ext,
+ app: attributes.app,
+ dlg: attributes.dlg,
+ mac: attributes.mac,
+ id: attributes.id
+ }
+
+ assert.equal(attributes.id, 'dh37fgj492je')
+ var credentials = {
+ key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn',
+ algorithm: 'sha256',
+ user: 'Steve'
+ }
+
+ const mac = hawk.calculateMac(credentials, artifacts)
+ assert.equal(mac, attributes.mac)
+ return 'OK'
+}
diff --git a/tests/test-headers.js b/tests/test-headers.js
index 31fe3f4e8..68b748691 100644
--- a/tests/test-headers.js
+++ b/tests/test-headers.js
@@ -1,52 +1,305 @@
+'use strict'
+
var server = require('./server')
- , assert = require('assert')
- , request = require('../main.js')
- , Cookie = require('../vendor/cookie')
- , Jar = require('../vendor/cookie/jar')
- , s = server.createServer()
-
-s.listen(s.port, function () {
- var serverUri = 'http://localhost:' + s.port
- , numTests = 0
- , numOutstandingTests = 0
-
- function createTest(requestObj, serverAssertFn) {
- var testNumber = numTests;
- numTests += 1;
- numOutstandingTests += 1;
- s.on('/' + testNumber, function (req, res) {
- serverAssertFn(req, res);
- res.writeHead(200);
- res.end();
- });
- requestObj.url = serverUri + '/' + testNumber
+var request = require('../index')
+var util = require('util')
+var tape = require('tape')
+var url = require('url')
+var os = require('os')
+
+var interfaces = os.networkInterfaces()
+var loopbackKeyTest = os.platform() === 'win32' ? /Loopback Pseudo-Interface/ : /lo/
+var hasIPv6interface = Object.keys(interfaces).some(function (name) {
+ return loopbackKeyTest.test(name) && interfaces[name].some(function (info) {
+ return info.family === 'IPv6'
+ })
+})
+
+var s = server.createServer()
+
+s.on('/redirect/from', function (req, res) {
+ res.writeHead(301, {
+ location: '/redirect/to'
+ })
+ res.end()
+})
+
+s.on('/redirect/to', function (req, res) {
+ res.end('ok')
+})
+
+s.on('/headers.json', function (req, res) {
+ res.writeHead(200, {
+ 'Content-Type': 'application/json'
+ })
+
+ res.end(JSON.stringify(req.headers))
+})
+
+function runTest (name, path, requestObj, serverAssertFn) {
+ tape(name, function (t) {
+ s.on('/' + path, function (req, res) {
+ serverAssertFn(t, req, res)
+ res.writeHead(200)
+ res.end()
+ })
+ requestObj.url = s.url + '/' + path
request(requestObj, function (err, res, body) {
- assert.ok(!err)
- assert.equal(res.statusCode, 200)
- numOutstandingTests -= 1
- if (numOutstandingTests === 0) {
- console.log(numTests + ' tests passed.')
- s.close()
- }
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.end()
})
+ })
+}
+
+function addTests () {
+ runTest(
+ '#125: headers.cookie with no cookie jar',
+ 'no-jar',
+ {headers: {cookie: 'foo=bar'}},
+ function (t, req, res) {
+ t.equal(req.headers.cookie, 'foo=bar')
+ })
+
+ var jar = request.jar()
+ jar.setCookie('quux=baz', s.url)
+ runTest(
+ '#125: headers.cookie + cookie jar',
+ 'header-and-jar',
+ {jar: jar, headers: {cookie: 'foo=bar'}},
+ function (t, req, res) {
+ t.equal(req.headers.cookie, 'foo=bar; quux=baz')
+ })
+
+ var jar2 = request.jar()
+ jar2.setCookie('quux=baz; Domain=foo.bar.com', s.url, {ignoreError: true})
+ runTest(
+ '#794: ignore cookie parsing and domain errors',
+ 'ignore-errors',
+ {jar: jar2, headers: {cookie: 'foo=bar'}},
+ function (t, req, res) {
+ t.equal(req.headers.cookie, 'foo=bar')
+ })
+
+ runTest(
+ '#784: override content-type when json is used',
+ 'json',
+ {
+ json: true,
+ method: 'POST',
+ headers: { 'content-type': 'application/json; charset=UTF-8' },
+ body: { hello: 'my friend' }},
+ function (t, req, res) {
+ t.equal(req.headers['content-type'], 'application/json; charset=UTF-8')
+ }
+ )
+
+ runTest(
+ 'neither headers.cookie nor a cookie jar is specified',
+ 'no-cookie',
+ {},
+ function (t, req, res) {
+ t.equal(req.headers.cookie, undefined)
+ })
+}
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ addTests()
+ tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
+ })
+ t.end()
+ })
+})
+
+tape('upper-case Host header and redirect', function (t) {
+ // Horrible hack to observe the raw data coming to the server (before Node
+ // core lower-cases the headers)
+ var rawData = ''
+
+ s.on('connection', function (socket) {
+ if (socket.ondata) {
+ var ondata = socket.ondata
+ }
+ function handledata (d, start, end) {
+ if (ondata) {
+ rawData += d.slice(start, end).toString()
+ return ondata.apply(this, arguments)
+ } else {
+ rawData += d
+ }
+ }
+ socket.on('data', handledata)
+ socket.ondata = handledata
+ })
+
+ function checkHostHeader (host) {
+ t.ok(
+ new RegExp('^Host: ' + host + '$', 'm').test(rawData),
+ util.format(
+ 'Expected "Host: %s" in data "%s"',
+ host, rawData.trim().replace(/\r?\n/g, '\\n')))
+ rawData = ''
}
- // Issue #125: headers.cookie shouldn't be replaced when a cookie jar isn't specified
- createTest({headers: {cookie: 'foo=bar'}}, function (req, res) {
- assert.ok(req.headers.cookie)
- assert.equal(req.headers.cookie, 'foo=bar')
+ var redirects = 0
+ request({
+ url: s.url + '/redirect/from',
+ headers: { Host: '127.0.0.1' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'ok')
+ t.equal(redirects, 1)
+ // XXX should the host header change like this after a redirect?
+ checkHostHeader('localhost:' + s.port)
+ t.end()
+ }).on('redirect', function () {
+ redirects++
+ t.equal(this.uri.href, s.url + '/redirect/to')
+ checkHostHeader('127.0.0.1')
+ })
+})
+
+tape('undefined headers', function (t) {
+ request({
+ url: s.url + '/headers.json',
+ headers: {
+ 'X-TEST-1': 'test1',
+ 'X-TEST-2': undefined
+ },
+ json: true
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(body['x-test-1'], 'test1')
+ t.equal(typeof body['x-test-2'], 'undefined')
+ t.end()
+ })
+})
+
+tape('preserve port in host header if non-standard port', function (t) {
+ var r = request({
+ url: s.url + '/headers.json'
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(r.originalHost, 'localhost:' + s.port)
+ t.end()
+ })
+})
+
+tape('strip port in host header if explicit standard port (:80) & protocol (HTTP)', function (t) {
+ var r = request({
+ url: 'http://localhost:80/headers.json'
+ }, function (_err, res, body) {
+ t.equal(r.req.socket._host, 'localhost')
+ t.end()
+ })
+})
+
+tape('strip port in host header if explicit standard port (:443) & protocol (HTTPS)', function (t) {
+ var r = request({
+ url: 'https://localhost:443/headers.json'
+ }, function (_err, res, body) {
+ t.equal(r.req.socket._host, 'localhost')
+ t.end()
+ })
+})
+
+tape('strip port in host header if implicit standard port & protocol (HTTP)', function (t) {
+ var r = request({
+ url: 'http://localhost/headers.json'
+ }, function (_err, res, body) {
+ t.equal(r.req.socket._host, 'localhost')
+ t.end()
})
+})
- // Issue #125: headers.cookie + cookie jar
- var jar = new Jar()
- jar.add(new Cookie('quux=baz'));
- createTest({jar: jar, headers: {cookie: 'foo=bar'}}, function (req, res) {
- assert.ok(req.headers.cookie)
- assert.equal(req.headers.cookie, 'foo=bar; quux=baz')
+tape('strip port in host header if implicit standard port & protocol (HTTPS)', function (t) {
+ var r = request({
+ url: 'https://localhost/headers.json'
+ }, function (_err, res, body) {
+ t.equal(r.req.socket._host, 'localhost')
+ t.end()
})
+})
+
+var isExpectedHeaderCharacterError = function (headerName, err) {
+ return err.message === 'The header content contains invalid characters' ||
+ err.message === ('Invalid character in header content ["' + headerName + '"]')
+}
- // There should be no cookie header when neither headers.cookie nor a cookie jar is specified
- createTest({}, function (req, res) {
- assert.ok(!req.headers.cookie)
+tape('catch invalid characters error - GET', function (t) {
+ request({
+ url: s.url + '/headers.json',
+ headers: {
+ 'test': 'אבגד'
+ }
+ }, function (err, res, body) {
+ t.true(isExpectedHeaderCharacterError('test', err))
+ })
+ .on('error', function (err) {
+ t.true(isExpectedHeaderCharacterError('test', err))
+ t.end()
})
})
+
+tape('catch invalid characters error - POST', function (t) {
+ request({
+ method: 'POST',
+ url: s.url + '/headers.json',
+ headers: {
+ 'test': 'אבגד'
+ },
+ body: 'beep'
+ }, function (err, res, body) {
+ t.true(isExpectedHeaderCharacterError('test', err))
+ })
+ .on('error', function (err) {
+ t.true(isExpectedHeaderCharacterError('test', err))
+ t.end()
+ })
+})
+
+if (hasIPv6interface) {
+ tape('IPv6 Host header', function (t) {
+ // Horrible hack to observe the raw data coming to the server
+ var rawData = ''
+
+ s.on('connection', function (socket) {
+ if (socket.ondata) {
+ var ondata = socket.ondata
+ }
+ function handledata (d, start, end) {
+ if (ondata) {
+ rawData += d.slice(start, end).toString()
+ return ondata.apply(this, arguments)
+ } else {
+ rawData += d
+ }
+ }
+ socket.on('data', handledata)
+ socket.ondata = handledata
+ })
+
+ function checkHostHeader (host) {
+ t.ok(
+ new RegExp('^Host: ' + host + '$', 'im').test(rawData),
+ util.format(
+ 'Expected "Host: %s" in data "%s"',
+ host, rawData.trim().replace(/\r?\n/g, '\\n')))
+ rawData = ''
+ }
+
+ request({
+ url: s.url.replace(url.parse(s.url).hostname, '[::1]') + '/headers.json'
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ checkHostHeader('\\[::1\\]:' + s.port)
+ t.end()
+ })
+ })
+}
diff --git a/tests/test-http-signature.js b/tests/test-http-signature.js
new file mode 100644
index 000000000..5ce015cba
--- /dev/null
+++ b/tests/test-http-signature.js
@@ -0,0 +1,110 @@
+'use strict'
+
+var http = require('http')
+var request = require('../index')
+var httpSignature = require('http-signature')
+var tape = require('tape')
+
+var privateKeyPEMs = {}
+
+privateKeyPEMs['key-1'] =
+ '-----BEGIN RSA PRIVATE KEY-----\n' +
+ 'MIIEpAIBAAKCAQEAzWSJl+Z9Bqv00FVL5N3+JCUoqmQPjIlya1BbeqQroNQ5yG1i\n' +
+ 'VbYTTnMRa1zQtR6r2fNvWeg94DvxivxIG9diDMnrzijAnYlTLOl84CK2vOxkj5b6\n' +
+ '8zrLH9b/Gd6NOHsywo8IjvXvCeTfca5WUHcuVi2lT9VjygFs1ILG4RyeX1BXUumu\n' +
+ 'Y8fzmposxLYdMxCqUTzAn0u9Saq2H2OVj5u114wS7OQPigu6G99dpn/iPHa3zBm8\n' +
+ '7baBWDbqZWRW0BP3K6eqq8sut1+NLhNW8ADPTdnO/SO+kvXy7fqd8atSn+HlQcx6\n' +
+ 'tW42dhXf3E9uE7K78eZtW0KvfyNGAjsI1Fft2QIDAQABAoIBAG1exe3/LEBrPLfb\n' +
+ 'U8iRdY0lxFvHYIhDgIwohC3wUdMYb5SMurpNdEZn+7Sh/fkUVgp/GKJViu1mvh52\n' +
+ 'bKd2r52DwG9NQBQjVgkqY/auRYSglIPpr8PpYNSZlcneunCDGeqEY9hMmXc5Ssqs\n' +
+ 'PQYoEKKPN+IlDTg6PguDgAfLR4IUvt9KXVvmB/SSgV9tSeTy35LECt1Lq3ozbUgu\n' +
+ '30HZI3U6/7H+X22Pxxf8vzBtzkg5rRCLgv+OeNPo16xMnqbutt4TeqEkxRv5rtOo\n' +
+ '/A1i9khBeki0OJAFJsE82qnaSZodaRsxic59VnN8sWBwEKAt87tEu5A3K3j4XSDU\n' +
+ '/avZxAECgYEA+pS3DvpiQLtHlaO3nAH6MxHRrREOARXWRDe5nUQuUNpS1xq9wte6\n' +
+ 'DkFtba0UCvDLic08xvReTCbo9kH0y6zEy3zMpZuJlKbcWCkZf4S5miYPI0RTZtF8\n' +
+ 'yps6hWqzYFSiO9hMYws9k4OJLxX0x3sLK7iNZ32ujcSrkPBSiBr0gxkCgYEA0dWl\n' +
+ '637K41AJ/zy0FP0syq+r4eIkfqv+/t6y2aQVUBvxJYrj9ci6XHBqoxpDV8lufVYj\n' +
+ 'fUAfeI9/MZaWvQJRbnYLre0I6PJfLuCBIL5eflO77BGso165AF7QJZ+fwtgKv3zv\n' +
+ 'ZX75eudCSS/cFo0po9hlbcLMT4B82zEkgT8E2MECgYEAnz+3/wrdOmpLGiyL2dff\n' +
+ '3GjsqmJ2VfY8z+niSrI0BSpbD11tT9Ct67VlCBjA7hsOH6uRfpd6/kaUMzzDiFVq\n' +
+ 'VDAiFvV8QD6zNkwYalQ9aFvbrvwTTPrBpjl0vamMCiJ/YC0cjq1sGr2zh3sar1Ph\n' +
+ 'S43kP+s97dcZeelhaiJHVrECgYEAsx61q/loJ/LDFeYzs1cLTVn4V7I7hQY9fkOM\n' +
+ 'WM0AhInVqD6PqdfXfeFYpjJdGisQ7l0BnoGGW9vir+nkcyPvb2PFRIr6+B8tsU5j\n' +
+ '7BeVgjDoUfQkcrEBK5fEBtnj/ud9BUkY8oMZZBjVNLRuI7IMwZiPvMp0rcj4zAN/\n' +
+ 'LfUlpgECgYArBvFcBxSkNAzR3Rtteud1YDboSKluRM37Ey5plrn4BS0DD0jm++aD\n' +
+ '0pG2Hsik000hibw92lCkzvvBVAqF8BuAcnPlAeYfsOaa97PGEjSKEN5bJVWZ9/om\n' +
+ '9FV1axotRN2XWlwrhixZLEaagkREXhgQc540FS5O8IaI2Vpa80Atzg==\n' +
+ '-----END RSA PRIVATE KEY-----'
+
+var publicKeyPEMs = {}
+
+publicKeyPEMs['key-1'] =
+ '-----BEGIN PUBLIC KEY-----\n' +
+ 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzWSJl+Z9Bqv00FVL5N3+\n' +
+ 'JCUoqmQPjIlya1BbeqQroNQ5yG1iVbYTTnMRa1zQtR6r2fNvWeg94DvxivxIG9di\n' +
+ 'DMnrzijAnYlTLOl84CK2vOxkj5b68zrLH9b/Gd6NOHsywo8IjvXvCeTfca5WUHcu\n' +
+ 'Vi2lT9VjygFs1ILG4RyeX1BXUumuY8fzmposxLYdMxCqUTzAn0u9Saq2H2OVj5u1\n' +
+ '14wS7OQPigu6G99dpn/iPHa3zBm87baBWDbqZWRW0BP3K6eqq8sut1+NLhNW8ADP\n' +
+ 'TdnO/SO+kvXy7fqd8atSn+HlQcx6tW42dhXf3E9uE7K78eZtW0KvfyNGAjsI1Fft\n' +
+ '2QIDAQAB\n' +
+ '-----END PUBLIC KEY-----'
+
+publicKeyPEMs['key-2'] =
+ '-----BEGIN PUBLIC KEY-----\n' +
+ 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqp04VVr9OThli9b35Omz\n' +
+ 'VqSfWbsoQuRrgyWsrNRn3XkFmbWw4FzZwQ42OgGMzQ84Ta4d9zGKKQyFriTiPjPf\n' +
+ 'xhhrsaJnDuybcpVhcr7UNKjSZ0S59tU3hpRiEz6hO+Nc/OSSLkvalG0VKrxOln7J\n' +
+ 'LK/h3rNS/l6wDZ5S/KqsI6CYtV2ZLpn3ahLrizvEYNY038Qcm38qMWx+VJAvZ4di\n' +
+ 'qqmW7RLIsLT59SWmpXdhFKnkYYGhxrk1Mwl22dBTJNY5SbriU5G3gWgzYkm8pgHr\n' +
+ '6CtrXch9ciJAcDJehPrKXNvNDOdUh8EW3fekNJerF1lWcwQg44/12v8sDPyfbaKB\n' +
+ 'dQIDAQAB\n' +
+ '-----END PUBLIC KEY-----'
+
+var server = http.createServer(function (req, res) {
+ var parsed = httpSignature.parseRequest(req)
+ var publicKeyPEM = publicKeyPEMs[parsed.keyId]
+ var verified = httpSignature.verifySignature(parsed, publicKeyPEM)
+ res.writeHead(verified ? 200 : 400)
+ res.end()
+})
+
+tape('setup', function (t) {
+ server.listen(0, function () {
+ server.url = 'http://localhost:' + this.address().port
+ t.end()
+ })
+})
+
+tape('correct key', function (t) {
+ var options = {
+ httpSignature: {
+ keyId: 'key-1',
+ key: privateKeyPEMs['key-1']
+ }
+ }
+ request(server.url, options, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(200, res.statusCode)
+ t.end()
+ })
+})
+
+tape('incorrect key', function (t) {
+ var options = {
+ httpSignature: {
+ keyId: 'key-2',
+ key: privateKeyPEMs['key-1']
+ }
+ }
+ request(server.url, options, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(400, res.statusCode)
+ t.end()
+ })
+})
+
+tape('cleanup', function (t) {
+ server.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-httpModule.js b/tests/test-httpModule.js
index 1866de2fa..4d4e236bf 100644
--- a/tests/test-httpModule.js
+++ b/tests/test-httpModule.js
@@ -1,94 +1,112 @@
+'use strict'
+
var http = require('http')
- , https = require('https')
- , server = require('./server')
- , assert = require('assert')
- , request = require('../main.js')
+var https = require('https')
+var destroyable = require('server-destroy')
+var server = require('./server')
+var request = require('../index')
+var tape = require('tape')
+
+var fauxRequestsMade
+function clearFauxRequests () {
+ fauxRequestsMade = { http: 0, https: 0 }
+}
-var faux_requests_made = {'http':0, 'https':0}
-function wrap_request(name, module) {
+function wrapRequest (name, module) {
// Just like the http or https module, but note when a request is made.
var wrapped = {}
- Object.keys(module).forEach(function(key) {
- var value = module[key];
-
- if(key != 'request')
- wrapped[key] = value;
- else
- wrapped[key] = function(options, callback) {
- faux_requests_made[name] += 1
+ Object.keys(module).forEach(function (key) {
+ var value = module[key]
+
+ if (key === 'request') {
+ wrapped[key] = function (/* options, callback */) {
+ fauxRequestsMade[name] += 1
return value.apply(this, arguments)
}
+ } else {
+ wrapped[key] = value
+ }
})
- return wrapped;
+ return wrapped
}
+var fauxHTTP = wrapRequest('http', http)
+var fauxHTTPS = wrapRequest('https', https)
+var plainServer = server.createServer()
+var httpsServer = server.createSSLServer()
-var faux_http = wrap_request('http', http)
- , faux_https = wrap_request('https', https)
- , plain_server = server.createServer()
- , https_server = server.createSSLServer()
-
+destroyable(plainServer)
+destroyable(httpsServer)
-plain_server.listen(plain_server.port, function() {
- plain_server.on('/plain', function (req, res) {
- res.writeHead(200)
- res.end('plain')
- })
- plain_server.on('/to_https', function (req, res) {
- res.writeHead(301, {'location':'https://localhost:'+https_server.port + '/https'})
- res.end()
- })
-
- https_server.listen(https_server.port, function() {
- https_server.on('/https', function (req, res) {
+tape('setup', function (t) {
+ plainServer.listen(0, function () {
+ plainServer.on('/plain', function (req, res) {
res.writeHead(200)
- res.end('https')
+ res.end('plain')
})
- https_server.on('/to_plain', function (req, res) {
- res.writeHead(302, {'location':'http://localhost:'+plain_server.port + '/plain'})
+ plainServer.on('/to_https', function (req, res) {
+ res.writeHead(301, { 'location': 'https://localhost:' + httpsServer.port + '/https' })
res.end()
})
- run_tests()
- run_tests({})
- run_tests({'http:':faux_http})
- run_tests({'https:':faux_https})
- run_tests({'http:':faux_http, 'https:':faux_https})
+ httpsServer.listen(0, function () {
+ httpsServer.on('/https', function (req, res) {
+ res.writeHead(200)
+ res.end('https')
+ })
+ httpsServer.on('/to_plain', function (req, res) {
+ res.writeHead(302, { 'location': 'http://localhost:' + plainServer.port + '/plain' })
+ res.end()
+ })
+
+ t.end()
+ })
})
})
-function run_tests(httpModules) {
- var to_https = 'http://localhost:'+plain_server.port+'/to_https'
- var to_plain = 'https://localhost:'+https_server.port+'/to_plain'
-
- request(to_https, {'httpModules':httpModules}, function (er, res, body) {
- assert.ok(!er, 'Bounce to SSL worked')
- assert.equal(body, 'https', 'Received HTTPS server body')
- done()
- })
+function runTests (name, httpModules) {
+ tape(name, function (t) {
+ var toHttps = 'http://localhost:' + plainServer.port + '/to_https'
+ var toPlain = 'https://localhost:' + httpsServer.port + '/to_plain'
+ var options = { httpModules: httpModules, strictSSL: false }
+ var modulesTest = httpModules || {}
- request(to_plain, {'httpModules':httpModules}, function (er, res, body) {
- assert.ok(!er, 'Bounce to plaintext server worked')
- assert.equal(body, 'plain', 'Received HTTPS server body')
- done()
- })
-}
+ clearFauxRequests()
+ request(toHttps, options, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'https', 'Received HTTPS server body')
-var passed = 0;
-function done() {
- passed += 1
- var expected = 10
+ t.equal(fauxRequestsMade.http, modulesTest['http:'] ? 1 : 0)
+ t.equal(fauxRequestsMade.https, modulesTest['https:'] ? 1 : 0)
- if(passed == expected) {
- plain_server.close()
- https_server.close()
+ request(toPlain, options, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'plain', 'Received HTTPS server body')
- assert.equal(faux_requests_made.http, 4, 'Wrapped http module called appropriately')
- assert.equal(faux_requests_made.https, 4, 'Wrapped https module called appropriately')
+ t.equal(fauxRequestsMade.http, modulesTest['http:'] ? 2 : 0)
+ t.equal(fauxRequestsMade.https, modulesTest['https:'] ? 2 : 0)
- console.log((expected+2) + ' tests passed.')
- }
+ t.end()
+ })
+ })
+ })
}
+
+runTests('undefined')
+runTests('empty', {})
+runTests('http only', { 'http:': fauxHTTP })
+runTests('https only', { 'https:': fauxHTTPS })
+runTests('http and https', { 'http:': fauxHTTP, 'https:': fauxHTTPS })
+
+tape('cleanup', function (t) {
+ plainServer.destroy(function () {
+ httpsServer.destroy(function () {
+ t.end()
+ })
+ })
+})
diff --git a/tests/test-https-strict.js b/tests/test-https-strict.js
deleted file mode 100644
index f53fc14a8..000000000
--- a/tests/test-https-strict.js
+++ /dev/null
@@ -1,97 +0,0 @@
-// a test where we validate the siguature of the keys
-// otherwise exactly the same as the ssl test
-
-var server = require('./server')
- , assert = require('assert')
- , request = require('../main.js')
- , fs = require('fs')
- , path = require('path')
- , opts = { key: path.resolve(__dirname, 'ssl/ca/server.key')
- , cert: path.resolve(__dirname, 'ssl/ca/server.crt') }
- , s = server.createSSLServer(null, opts)
- , caFile = path.resolve(__dirname, 'ssl/ca/ca.crt')
- , ca = fs.readFileSync(caFile)
-
-var tests =
- { testGet :
- { resp : server.createGetResponse("TESTING!")
- , expectBody: "TESTING!"
- }
- , testGetChunkBreak :
- { resp : server.createChunkResponse(
- [ new Buffer([239])
- , new Buffer([163])
- , new Buffer([191])
- , new Buffer([206])
- , new Buffer([169])
- , new Buffer([226])
- , new Buffer([152])
- , new Buffer([131])
- ])
- , expectBody: "Ω☃"
- }
- , testGetJSON :
- { resp : server.createGetResponse('{"test":true}', 'application/json')
- , json : true
- , expectBody: {"test":true}
- }
- , testPutString :
- { resp : server.createPostValidator("PUTTINGDATA")
- , method : "PUT"
- , body : "PUTTINGDATA"
- }
- , testPutBuffer :
- { resp : server.createPostValidator("PUTTINGDATA")
- , method : "PUT"
- , body : new Buffer("PUTTINGDATA")
- }
- , testPutJSON :
- { resp : server.createPostValidator(JSON.stringify({foo: 'bar'}))
- , method: "PUT"
- , json: {foo: 'bar'}
- }
- , testPutMultipart :
- { resp: server.createPostValidator(
- '--frontier\r\n' +
- 'content-type: text/html\r\n' +
- '\r\n' +
- 'Oh hi.' +
- '\r\n--frontier\r\n\r\n' +
- 'Oh hi.' +
- '\r\n--frontier--'
- )
- , method: "PUT"
- , multipart:
- [ {'content-type': 'text/html', 'body': 'Oh hi.'}
- , {'body': 'Oh hi.'}
- ]
- }
- }
-
-s.listen(s.port, function () {
-
- var counter = 0
-
- for (i in tests) {
- (function () {
- var test = tests[i]
- s.on('/'+i, test.resp)
- test.uri = s.url + '/' + i
- test.strictSSL = true
- test.ca = ca
- test.headers = { host: 'testing.request.mikealrogers.com' }
- request(test, function (err, resp, body) {
- if (err) throw err
- if (test.expectBody) {
- assert.deepEqual(test.expectBody, body)
- }
- counter = counter - 1;
- if (counter === 0) {
- console.log(Object.keys(tests).length+" tests passed.")
- s.close()
- }
- })
- counter++
- })()
- }
-})
diff --git a/tests/test-https.js b/tests/test-https.js
index df7330b39..028bc775b 100644
--- a/tests/test-https.js
+++ b/tests/test-https.js
@@ -1,86 +1,116 @@
+'use strict'
+
+// a test where we validate the siguature of the keys
+// otherwise exactly the same as the ssl test
+
var server = require('./server')
- , assert = require('assert')
- , request = require('../main.js')
-
-var s = server.createSSLServer();
-
-var tests =
- { testGet :
- { resp : server.createGetResponse("TESTING!")
- , expectBody: "TESTING!"
- }
- , testGetChunkBreak :
- { resp : server.createChunkResponse(
- [ new Buffer([239])
- , new Buffer([163])
- , new Buffer([191])
- , new Buffer([206])
- , new Buffer([169])
- , new Buffer([226])
- , new Buffer([152])
- , new Buffer([131])
- ])
- , expectBody: "Ω☃"
- }
- , testGetJSON :
- { resp : server.createGetResponse('{"test":true}', 'application/json')
- , json : true
- , expectBody: {"test":true}
- }
- , testPutString :
- { resp : server.createPostValidator("PUTTINGDATA")
- , method : "PUT"
- , body : "PUTTINGDATA"
- }
- , testPutBuffer :
- { resp : server.createPostValidator("PUTTINGDATA")
- , method : "PUT"
- , body : new Buffer("PUTTINGDATA")
- }
- , testPutJSON :
- { resp : server.createPostValidator(JSON.stringify({foo: 'bar'}))
- , method: "PUT"
- , json: {foo: 'bar'}
- }
- , testPutMultipart :
- { resp: server.createPostValidator(
- '--frontier\r\n' +
- 'content-type: text/html\r\n' +
- '\r\n' +
- 'Oh hi.' +
- '\r\n--frontier\r\n\r\n' +
- 'Oh hi.' +
- '\r\n--frontier--'
- )
- , method: "PUT"
- , multipart:
- [ {'content-type': 'text/html', 'body': 'Oh hi.'}
- , {'body': 'Oh hi.'}
- ]
- }
- }
+var request = require('../index')
+var fs = require('fs')
+var path = require('path')
+var tape = require('tape')
+
+var s = server.createSSLServer()
+var caFile = path.resolve(__dirname, 'ssl/ca/ca.crt')
+var ca = fs.readFileSync(caFile)
+var opts = {
+ ciphers: 'AES256-SHA',
+ key: path.resolve(__dirname, 'ssl/ca/server.key'),
+ cert: path.resolve(__dirname, 'ssl/ca/server.crt')
+}
+var sStrict = server.createSSLServer(opts)
-s.listen(s.port, function () {
+function runAllTests (strict, s) {
+ var strictMsg = (strict ? 'strict ' : 'relaxed ')
- var counter = 0
+ tape(strictMsg + 'setup', function (t) {
+ s.listen(0, function () {
+ t.end()
+ })
+ })
- for (i in tests) {
- (function () {
- var test = tests[i]
- s.on('/'+i, test.resp)
- test.uri = s.url + '/' + i
+ function runTest (name, test) {
+ tape(strictMsg + name, function (t) {
+ s.on('/' + name, test.resp)
+ test.uri = s.url + '/' + name
+ if (strict) {
+ test.strictSSL = true
+ test.ca = ca
+ test.headers = { host: 'testing.request.mikealrogers.com' }
+ } else {
+ test.rejectUnauthorized = false
+ }
request(test, function (err, resp, body) {
- if (err) throw err
+ t.equal(err, null)
if (test.expectBody) {
- assert.deepEqual(test.expectBody, body)
- }
- counter = counter - 1;
- if (counter === 0) {
- console.log(Object.keys(tests).length+" tests passed.")
- s.close()
+ t.deepEqual(test.expectBody, body)
}
+ t.end()
})
- counter++
- })()
+ })
}
-})
+
+ runTest('testGet', {
+ resp: server.createGetResponse('TESTING!'), expectBody: 'TESTING!'
+ })
+
+ runTest('testGetChunkBreak', {
+ resp: server.createChunkResponse(
+ [ Buffer.from([239]),
+ Buffer.from([163]),
+ Buffer.from([191]),
+ Buffer.from([206]),
+ Buffer.from([169]),
+ Buffer.from([226]),
+ Buffer.from([152]),
+ Buffer.from([131])
+ ]),
+ expectBody: '\uf8ff\u03a9\u2603'
+ })
+
+ runTest('testGetJSON', {
+ resp: server.createGetResponse('{"test":true}', 'application/json'), json: true, expectBody: {'test': true}
+ })
+
+ runTest('testPutString', {
+ resp: server.createPostValidator('PUTTINGDATA'), method: 'PUT', body: 'PUTTINGDATA'
+ })
+
+ runTest('testPutBuffer', {
+ resp: server.createPostValidator('PUTTINGDATA'), method: 'PUT', body: Buffer.from('PUTTINGDATA')
+ })
+
+ runTest('testPutJSON', {
+ resp: server.createPostValidator(JSON.stringify({foo: 'bar'})), method: 'PUT', json: {foo: 'bar'}
+ })
+
+ runTest('testPutMultipart', {
+ resp: server.createPostValidator(
+ '--__BOUNDARY__\r\n' +
+ 'content-type: text/html\r\n' +
+ '\r\n' +
+ 'Oh hi.' +
+ '\r\n--__BOUNDARY__\r\n\r\n' +
+ 'Oh hi.' +
+ '\r\n--__BOUNDARY__--'
+ ),
+ method: 'PUT',
+ multipart: [ {'content-type': 'text/html', 'body': 'Oh hi.'},
+ {'body': 'Oh hi.'}
+ ]
+ })
+
+ tape(strictMsg + 'cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
+ })
+}
+
+runAllTests(false, s)
+
+if (!process.env.running_under_istanbul) {
+ // somehow this test modifies the process state
+ // test coverage runs all tests in a single process via tape
+ // executing this test causes one of the tests in test-tunnel.js to throw
+ runAllTests(true, sStrict)
+}
diff --git a/tests/test-isUrl.js b/tests/test-isUrl.js
new file mode 100644
index 000000000..ae7f3ba11
--- /dev/null
+++ b/tests/test-isUrl.js
@@ -0,0 +1,120 @@
+'use strict'
+
+var http = require('http')
+var request = require('../index')
+var tape = require('tape')
+
+var s = http.createServer(function (req, res) {
+ res.statusCode = 200
+ res.end('ok')
+})
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ s.port = this.address().port
+ s.url = 'http://localhost:' + s.port
+ t.end()
+ })
+})
+
+tape('lowercase', function (t) {
+ request(s.url, function (err, resp, body) {
+ t.equal(err, null)
+ t.equal(body, 'ok')
+ t.end()
+ })
+})
+
+tape('uppercase', function (t) {
+ request(s.url.replace('http', 'HTTP'), function (err, resp, body) {
+ t.equal(err, null)
+ t.equal(body, 'ok')
+ t.end()
+ })
+})
+
+tape('mixedcase', function (t) {
+ request(s.url.replace('http', 'HtTp'), function (err, resp, body) {
+ t.equal(err, null)
+ t.equal(body, 'ok')
+ t.end()
+ })
+})
+
+tape('hostname and port', function (t) {
+ request({
+ uri: {
+ protocol: 'http:',
+ hostname: 'localhost',
+ port: s.port
+ }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(body, 'ok')
+ t.end()
+ })
+})
+
+tape('hostname and port 1', function (t) {
+ request({
+ uri: {
+ protocol: 'http:',
+ hostname: 'localhost',
+ port: s.port
+ }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(body, 'ok')
+ t.end()
+ })
+})
+
+tape('hostname and port 2', function (t) {
+ request({
+ protocol: 'http:',
+ hostname: 'localhost',
+ port: s.port
+ }, {
+ // need this empty options object, otherwise request thinks no uri was set
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(body, 'ok')
+ t.end()
+ })
+})
+
+tape('hostname and port 3', function (t) {
+ request({
+ protocol: 'http:',
+ hostname: 'localhost',
+ port: s.port
+ }, function (err, res, body) {
+ t.notEqual(err, null)
+ t.equal(err.message, 'options.uri is a required argument')
+ t.equal(body, undefined)
+ t.end()
+ })
+})
+
+tape('hostname and query string', function (t) {
+ request({
+ uri: {
+ protocol: 'http:',
+ hostname: 'localhost',
+ port: s.port
+ },
+ qs: {
+ test: 'test'
+ }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(body, 'ok')
+ t.end()
+ })
+})
+
+tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-json-request.js b/tests/test-json-request.js
new file mode 100644
index 000000000..af82f15b5
--- /dev/null
+++ b/tests/test-json-request.js
@@ -0,0 +1,117 @@
+'use strict'
+
+var server = require('./server')
+var request = require('../index')
+var tape = require('tape')
+
+var s = server.createServer()
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ t.end()
+ })
+})
+
+function testJSONValue (testId, value) {
+ tape('test ' + testId, function (t) {
+ var testUrl = '/' + testId
+ s.on(testUrl, server.createPostJSONValidator(value, 'application/json'))
+ var opts = {
+ method: 'PUT',
+ uri: s.url + testUrl,
+ json: true,
+ body: value
+ }
+ request(opts, function (err, resp, body) {
+ t.equal(err, null)
+ t.equal(resp.statusCode, 200)
+ t.deepEqual(body, value)
+ t.end()
+ })
+ })
+}
+
+function testJSONValueReviver (testId, value, reviver, revivedValue) {
+ tape('test ' + testId, function (t) {
+ var testUrl = '/' + testId
+ s.on(testUrl, server.createPostJSONValidator(value, 'application/json'))
+ var opts = {
+ method: 'PUT',
+ uri: s.url + testUrl,
+ json: true,
+ jsonReviver: reviver,
+ body: value
+ }
+ request(opts, function (err, resp, body) {
+ t.equal(err, null)
+ t.equal(resp.statusCode, 200)
+ t.deepEqual(body, revivedValue)
+ t.end()
+ })
+ })
+}
+
+function testJSONValueReplacer (testId, value, replacer, replacedValue) {
+ tape('test ' + testId, function (t) {
+ var testUrl = '/' + testId
+ s.on(testUrl, server.createPostJSONValidator(replacedValue, 'application/json'))
+ var opts = {
+ method: 'PUT',
+ uri: s.url + testUrl,
+ json: true,
+ jsonReplacer: replacer,
+ body: value
+ }
+ request(opts, function (err, resp, body) {
+ t.equal(err, null)
+ t.equal(resp.statusCode, 200)
+ t.deepEqual(body, replacedValue)
+ t.end()
+ })
+ })
+}
+
+testJSONValue('jsonNull', null)
+testJSONValue('jsonTrue', true)
+testJSONValue('jsonFalse', false)
+testJSONValue('jsonNumber', -289365.2938)
+testJSONValue('jsonString', 'some string')
+testJSONValue('jsonArray', ['value1', 2, null, 8925.53289, true, false, ['array'], { object: 'property' }])
+testJSONValue('jsonObject', {
+ trueProperty: true,
+ falseProperty: false,
+ numberProperty: -98346.34698,
+ stringProperty: 'string',
+ nullProperty: null,
+ arrayProperty: ['array'],
+ objectProperty: { object: 'property' }
+})
+
+testJSONValueReviver('jsonReviver', -48269.592, function (k, v) {
+ return v * -1
+}, 48269.592)
+testJSONValueReviver('jsonReviverInvalid', -48269.592, 'invalid reviver', -48269.592)
+
+testJSONValueReplacer('jsonReplacer', -48269.592, function (k, v) {
+ return v * -1
+}, 48269.592)
+testJSONValueReplacer('jsonReplacerInvalid', -48269.592, 'invalid replacer', -48269.592)
+testJSONValueReplacer('jsonReplacerObject', {foo: 'bar'}, function (k, v) {
+ return v.toUpperCase ? v.toUpperCase() : v
+}, {foo: 'BAR'})
+
+tape('missing body', function (t) {
+ s.on('/missing-body', function (req, res) {
+ t.equal(req.headers['content-type'], undefined)
+ res.end()
+ })
+ request({url: s.url + '/missing-body', json: true}, function () {
+ t.end()
+ })
+})
+
+tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-localAddress.js b/tests/test-localAddress.js
new file mode 100644
index 000000000..88a335326
--- /dev/null
+++ b/tests/test-localAddress.js
@@ -0,0 +1,49 @@
+'use strict'
+var request = require('../index')
+var tape = require('tape')
+
+tape('bind to invalid address', function (t) {
+ request.get({
+ uri: 'http://www.google.com',
+ localAddress: '1.2.3.4'
+ }, function (err, res) {
+ t.notEqual(err, null)
+ t.equal(true, /bind EADDRNOTAVAIL/.test(err.message))
+ t.equal(res, undefined)
+ t.end()
+ })
+})
+
+tape('bind to local address', function (t) {
+ request.get({
+ uri: 'http://www.google.com',
+ localAddress: '127.0.0.1'
+ }, function (err, res) {
+ t.notEqual(err, null)
+ t.equal(res, undefined)
+ t.end()
+ })
+})
+
+tape('bind to local address on redirect', function (t) {
+ var os = require('os')
+ var localInterfaces = os.networkInterfaces()
+ var localIPS = []
+ Object.keys(localInterfaces).forEach(function (ifname) {
+ localInterfaces[ifname].forEach(function (iface) {
+ if (iface.family !== 'IPv4' || iface.internal !== false) {
+ // skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses
+ return
+ }
+ localIPS.push(iface.address)
+ })
+ })
+ request.get({
+ uri: 'http://google.com', // redirects to 'http://google.com'
+ localAddress: localIPS[0]
+ }, function (err, res) {
+ t.equal(err, null)
+ t.equal(res.request.localAddress, localIPS[0])
+ t.end()
+ })
+})
diff --git a/tests/test-multipart-encoding.js b/tests/test-multipart-encoding.js
new file mode 100644
index 000000000..c88a6f143
--- /dev/null
+++ b/tests/test-multipart-encoding.js
@@ -0,0 +1,147 @@
+'use strict'
+
+var http = require('http')
+var path = require('path')
+var request = require('../index')
+var fs = require('fs')
+var tape = require('tape')
+
+var localFile = path.join(__dirname, 'unicycle.jpg')
+var cases = {
+ // based on body type
+ '+array -stream': {
+ options: {
+ multipart: [{name: 'field', body: 'value'}]
+ },
+ expected: {chunked: false}
+ },
+ '+array +stream': {
+ options: {
+ multipart: [{name: 'file', body: null}]
+ },
+ expected: {chunked: true}
+ },
+ // encoding overrides body value
+ '+array +encoding': {
+ options: {
+ headers: {'transfer-encoding': 'chunked'},
+ multipart: [{name: 'field', body: 'value'}]
+ },
+ expected: {chunked: true}
+ },
+
+ // based on body type
+ '+object -stream': {
+ options: {
+ multipart: {data: [{name: 'field', body: 'value'}]}
+ },
+ expected: {chunked: false}
+ },
+ '+object +stream': {
+ options: {
+ multipart: {data: [{name: 'file', body: null}]}
+ },
+ expected: {chunked: true}
+ },
+ // encoding overrides body value
+ '+object +encoding': {
+ options: {
+ headers: {'transfer-encoding': 'chunked'},
+ multipart: {data: [{name: 'field', body: 'value'}]}
+ },
+ expected: {chunked: true}
+ },
+
+ // based on body type
+ '+object -chunked -stream': {
+ options: {
+ multipart: {chunked: false, data: [{name: 'field', body: 'value'}]}
+ },
+ expected: {chunked: false}
+ },
+ '+object -chunked +stream': {
+ options: {
+ multipart: {chunked: false, data: [{name: 'file', body: null}]}
+ },
+ expected: {chunked: true}
+ },
+ // chunked overrides body value
+ '+object +chunked -stream': {
+ options: {
+ multipart: {chunked: true, data: [{name: 'field', body: 'value'}]}
+ },
+ expected: {chunked: true}
+ },
+ // encoding overrides chunked
+ '+object +encoding -chunked': {
+ options: {
+ headers: {'transfer-encoding': 'chunked'},
+ multipart: {chunked: false, data: [{name: 'field', body: 'value'}]}
+ },
+ expected: {chunked: true}
+ }
+}
+
+function runTest (t, test) {
+ var server = http.createServer(function (req, res) {
+ t.ok(req.headers['content-type'].match(/^multipart\/related; boundary=[^\s;]+$/))
+
+ if (test.expected.chunked) {
+ t.ok(req.headers['transfer-encoding'] === 'chunked')
+ t.notOk(req.headers['content-length'])
+ } else {
+ t.ok(req.headers['content-length'])
+ t.notOk(req.headers['transfer-encoding'])
+ }
+
+ // temp workaround
+ var data = ''
+ req.setEncoding('utf8')
+
+ req.on('data', function (d) {
+ data += d
+ })
+
+ req.on('end', function () {
+ // check for the fields traces
+ if (test.expected.chunked && data.indexOf('name: file') !== -1) {
+ // file
+ t.ok(data.indexOf('name: file') !== -1)
+ // check for unicycle.jpg traces
+ t.ok(data.indexOf('2005:06:21 01:44:12') !== -1)
+ } else {
+ // field
+ t.ok(data.indexOf('name: field') !== -1)
+ var parts = test.options.multipart.data || test.options.multipart
+ t.ok(data.indexOf(parts[0].body) !== -1)
+ }
+
+ res.writeHead(200)
+ res.end()
+ })
+ })
+
+ server.listen(0, function () {
+ var url = 'http://localhost:' + this.address().port
+ // @NOTE: multipartData properties must be set here
+ // so that file read stream does not leak in node v0.8
+ var parts = test.options.multipart.data || test.options.multipart
+ if (parts[0].name === 'file') {
+ parts[0].body = fs.createReadStream(localFile)
+ }
+
+ request.post(url, test.options, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ server.close(function () {
+ t.end()
+ })
+ })
+ })
+}
+
+Object.keys(cases).forEach(function (name) {
+ tape('multipart-encoding ' + name, function (t) {
+ runTest(t, cases[name])
+ })
+})
diff --git a/tests/test-multipart.js b/tests/test-multipart.js
new file mode 100644
index 000000000..8eff422e2
--- /dev/null
+++ b/tests/test-multipart.js
@@ -0,0 +1,129 @@
+'use strict'
+
+var http = require('http')
+var path = require('path')
+var request = require('../index')
+var fs = require('fs')
+var tape = require('tape')
+
+function runTest (t, a) {
+ var remoteFile = path.join(__dirname, 'googledoodle.jpg')
+ var localFile = path.join(__dirname, 'unicycle.jpg')
+ var multipartData = []
+
+ var server = http.createServer(function (req, res) {
+ if (req.url === '/file') {
+ res.writeHead(200, {'content-type': 'image/jpg'})
+ res.end(fs.readFileSync(remoteFile), 'binary')
+ return
+ }
+
+ if (a.header) {
+ if (a.header.indexOf('mixed') !== -1) {
+ t.ok(req.headers['content-type'].match(/^multipart\/mixed; boundary=[^\s;]+$/))
+ } else {
+ t.ok(req.headers['content-type']
+ .match(/^multipart\/related; boundary=XXX; type=text\/xml; start=""$/))
+ }
+ } else {
+ t.ok(req.headers['content-type'].match(/^multipart\/related; boundary=[^\s;]+$/))
+ }
+
+ // temp workaround
+ var data = ''
+ req.setEncoding('utf8')
+
+ req.on('data', function (d) {
+ data += d
+ })
+
+ req.on('end', function () {
+ // check for the fields traces
+
+ // my_field
+ t.ok(data.indexOf('name: my_field') !== -1)
+ t.ok(data.indexOf(multipartData[0].body) !== -1)
+
+ // my_number
+ t.ok(data.indexOf('name: my_number') !== -1)
+ t.ok(data.indexOf(multipartData[1].body) !== -1)
+
+ // my_buffer
+ t.ok(data.indexOf('name: my_buffer') !== -1)
+ t.ok(data.indexOf(multipartData[2].body) !== -1)
+
+ // my_file
+ t.ok(data.indexOf('name: my_file') !== -1)
+ // check for unicycle.jpg traces
+ t.ok(data.indexOf('2005:06:21 01:44:12') !== -1)
+
+ // remote_file
+ t.ok(data.indexOf('name: remote_file') !== -1)
+ // check for http://localhost:nnnn/file traces
+ t.ok(data.indexOf('Photoshop ICC') !== -1)
+
+ if (a.header && a.header.indexOf('boundary=XXX') !== -1) {
+ t.ok(data.indexOf('--XXX') !== -1)
+ }
+
+ res.writeHead(200)
+ res.end(a.json ? JSON.stringify({status: 'done'}) : 'done')
+ })
+ })
+
+ server.listen(0, function () {
+ var url = 'http://localhost:' + this.address().port
+ // @NOTE: multipartData properties must be set here so that my_file read stream does not leak in node v0.8
+ multipartData = [
+ {name: 'my_field', body: 'my_value'},
+ {name: 'my_number', body: 1000},
+ {name: 'my_buffer', body: Buffer.from([1, 2, 3])},
+ {name: 'my_file', body: fs.createReadStream(localFile)},
+ {name: 'remote_file', body: request(url + '/file')}
+ ]
+
+ var reqOptions = {
+ url: url + '/upload',
+ multipart: multipartData
+ }
+ if (a.header) {
+ reqOptions.headers = {
+ 'content-type': a.header
+ }
+ }
+ if (a.json) {
+ reqOptions.json = true
+ }
+ request[a.method](reqOptions, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.deepEqual(body, a.json ? {status: 'done'} : 'done')
+ server.close(function () {
+ t.end()
+ })
+ })
+ })
+}
+
+var testHeaders = [
+ null,
+ 'multipart/mixed',
+ 'multipart/related; boundary=XXX; type=text/xml; start=""'
+]
+
+var methods = ['post', 'get']
+methods.forEach(function (method) {
+ testHeaders.forEach(function (header) {
+ [true, false].forEach(function (json) {
+ var name = [
+ 'multipart-related', method.toUpperCase(),
+ (header || 'default'),
+ (json ? '+' : '-') + 'json'
+ ].join(' ')
+
+ tape(name, function (t) {
+ runTest(t, {method: method, header: header, json: json})
+ })
+ })
+ })
+})
diff --git a/tests/test-node-debug.js b/tests/test-node-debug.js
new file mode 100644
index 000000000..bcc6a401d
--- /dev/null
+++ b/tests/test-node-debug.js
@@ -0,0 +1,95 @@
+'use strict'
+
+var request = require('../index')
+var http = require('http')
+var tape = require('tape')
+
+var s = http.createServer(function (req, res) {
+ res.statusCode = 200
+ res.end('')
+})
+
+var stderr = []
+var prevStderrLen = 0
+
+tape('setup', function (t) {
+ process.stderr._oldWrite = process.stderr.write
+ process.stderr.write = function (string, encoding, fd) {
+ stderr.push(string)
+ }
+
+ s.listen(0, function () {
+ s.url = 'http://localhost:' + this.address().port
+ t.end()
+ })
+})
+
+tape('a simple request should not fail with debugging enabled', function (t) {
+ request.debug = true
+ t.equal(request.Request.debug, true, 'request.debug sets request.Request.debug')
+ t.equal(request.debug, true, 'request.debug gets request.Request.debug')
+ stderr = []
+
+ request(s.url, function (err, res, body) {
+ t.ifError(err, 'the request did not fail')
+ t.ok(res, 'the request did not fail')
+
+ t.ok(stderr.length, 'stderr has some messages')
+ var url = s.url.replace(/\//g, '\\/')
+ var patterns = [
+ /^REQUEST { uri: /,
+ new RegExp('^REQUEST make request ' + url + '/\n$'),
+ /^REQUEST onRequestResponse /,
+ /^REQUEST finish init /,
+ /^REQUEST response end /,
+ /^REQUEST end event /,
+ /^REQUEST emitting complete /
+ ]
+ patterns.forEach(function (pattern) {
+ var found = false
+ stderr.forEach(function (msg) {
+ if (pattern.test(msg)) {
+ found = true
+ }
+ })
+ t.ok(found, 'a log message matches ' + pattern)
+ })
+ prevStderrLen = stderr.length
+ t.end()
+ })
+})
+
+tape('there should be no further lookups on process.env', function (t) {
+ process.env.NODE_DEBUG = ''
+ stderr = []
+
+ request(s.url, function (err, res, body) {
+ t.ifError(err, 'the request did not fail')
+ t.ok(res, 'the request did not fail')
+ t.equal(stderr.length, prevStderrLen, 'env.NODE_DEBUG is not retested')
+ t.end()
+ })
+})
+
+tape('it should be possible to disable debugging at runtime', function (t) {
+ request.debug = false
+ t.equal(request.Request.debug, false, 'request.debug sets request.Request.debug')
+ t.equal(request.debug, false, 'request.debug gets request.Request.debug')
+ stderr = []
+
+ request(s.url, function (err, res, body) {
+ t.ifError(err, 'the request did not fail')
+ t.ok(res, 'the request did not fail')
+ t.equal(stderr.length, 0, 'debugging can be disabled')
+ t.end()
+ })
+})
+
+tape('cleanup', function (t) {
+ process.stderr.write = process.stderr._oldWrite
+ delete process.stderr._oldWrite
+
+ s.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-oauth.js b/tests/test-oauth.js
index 72ca92333..0358375ed 100644
--- a/tests/test-oauth.js
+++ b/tests/test-oauth.js
@@ -1,117 +1,721 @@
-var hmacsign = require('../oauth').hmacsign
- , assert = require('assert')
- , qs = require('querystring')
- , request = require('../main')
- ;
+'use strict'
-function getsignature (r) {
+var oauth = require('oauth-sign')
+var qs = require('querystring')
+var fs = require('fs')
+var path = require('path')
+var request = require('../index')
+var tape = require('tape')
+var http = require('http')
+
+function getSignature (r) {
var sign
- r.headers.Authorization.slice('OAuth '.length).replace(/,\ /g, ',').split(',').forEach(function (v) {
- if (v.slice(0, 'oauth_signature="'.length) === 'oauth_signature="') sign = v.slice('oauth_signature="'.length, -1)
+ r.headers.Authorization.slice('OAuth '.length).replace(/, /g, ',').split(',').forEach(function (v) {
+ if (v.slice(0, 'oauth_signature="'.length) === 'oauth_signature="') {
+ sign = v.slice('oauth_signature="'.length, -1)
+ }
})
return decodeURIComponent(sign)
}
// Tests from Twitter documentation https://dev.twitter.com/docs/auth/oauth
-var reqsign = hmacsign('POST', 'https://api.twitter.com/oauth/request_token',
- { oauth_callback: 'http://localhost:3005/the_dance/process_callback?service_provider_id=11'
- , oauth_consumer_key: 'GDdmIQH6jhtmLUypg82g'
- , oauth_nonce: 'QP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk'
- , oauth_signature_method: 'HMAC-SHA1'
- , oauth_timestamp: '1272323042'
- , oauth_version: '1.0'
- }, "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98")
-
-console.log(reqsign)
-console.log('8wUi7m5HFQy76nowoCThusfgB+Q=')
-assert.equal(reqsign, '8wUi7m5HFQy76nowoCThusfgB+Q=')
-
-var accsign = hmacsign('POST', 'https://api.twitter.com/oauth/access_token',
- { oauth_consumer_key: 'GDdmIQH6jhtmLUypg82g'
- , oauth_nonce: '9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8'
- , oauth_signature_method: 'HMAC-SHA1'
- , oauth_token: '8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc'
- , oauth_timestamp: '1272323047'
- , oauth_verifier: 'pDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY'
- , oauth_version: '1.0'
- }, "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98", "x6qpRnlEmW9JbQn4PQVVeVG8ZLPEx6A0TOebgwcuA")
-
-console.log(accsign)
-console.log('PUw/dHA4fnlJYM6RhXk5IU/0fCc=')
-assert.equal(accsign, 'PUw/dHA4fnlJYM6RhXk5IU/0fCc=')
-
-var upsign = hmacsign('POST', 'http://api.twitter.com/1/statuses/update.json',
- { oauth_consumer_key: "GDdmIQH6jhtmLUypg82g"
- , oauth_nonce: "oElnnMTQIZvqvlfXM56aBLAf5noGD0AQR3Fmi7Q6Y"
- , oauth_signature_method: "HMAC-SHA1"
- , oauth_token: "819797-Jxq8aYUDRmykzVKrgoLhXSq67TEa5ruc4GJC2rWimw"
- , oauth_timestamp: "1272325550"
- , oauth_version: "1.0"
- , status: 'setting up my twitter 私のさえずりを設定する'
- }, "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98", "J6zix3FfA9LofH0awS24M3HcBYXO5nI1iYe8EfBA")
-
-console.log(upsign)
-console.log('yOahq5m0YjDDjfjxHaXEsW9D+X0=')
-assert.equal(upsign, 'yOahq5m0YjDDjfjxHaXEsW9D+X0=')
-
-
-var rsign = request.post(
- { url: 'https://api.twitter.com/oauth/request_token'
- , oauth:
- { callback: 'http://localhost:3005/the_dance/process_callback?service_provider_id=11'
- , consumer_key: 'GDdmIQH6jhtmLUypg82g'
- , nonce: 'QP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk'
- , timestamp: '1272323042'
- , version: '1.0'
- , consumer_secret: "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98"
- }
+var hmacsign = oauth.hmacsign
+var hmacsign256 = oauth.hmacsign256
+var rsasign = oauth.rsasign
+var rsaPrivatePEM = fs.readFileSync(path.join(__dirname, 'ssl', 'test.key'))
+var reqsign
+var reqsign256
+var reqsignRSA
+var accsign
+var accsign256
+var accsignRSA
+var upsign
+var upsign256
+var upsignRSA
+
+tape('reqsign', function (t) {
+ reqsign = hmacsign('POST', 'https://api.twitter.com/oauth/request_token',
+ { oauth_callback: 'http://localhost:3005/the_dance/process_callback?service_provider_id=11',
+ oauth_consumer_key: 'GDdmIQH6jhtmLUypg82g',
+ oauth_nonce: 'QP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk',
+ oauth_signature_method: 'HMAC-SHA1',
+ oauth_timestamp: '1272323042',
+ oauth_version: '1.0'
+ }, 'MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98')
+
+ t.equal(reqsign, '8wUi7m5HFQy76nowoCThusfgB+Q=')
+ t.end()
+})
+
+tape('reqsign256', function (t) {
+ reqsign256 = hmacsign256('POST', 'https://api.twitter.com/oauth/request_token',
+ { oauth_callback: 'http://localhost:3005/the_dance/process_callback?service_provider_id=11',
+ oauth_consumer_key: 'GDdmIQH6jhtmLUypg82g',
+ oauth_nonce: 'QP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk',
+ oauth_signature_method: 'HMAC-SHA256',
+ oauth_timestamp: '1272323042',
+ oauth_version: '1.0'
+ }, 'MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98')
+
+ t.equal(reqsign256, 'N0KBpiPbuPIMx2B77eIg7tNfGNF81iq3bcO9RO6lH+k=')
+ t.end()
+})
+
+tape('reqsignRSA', function (t) {
+ reqsignRSA = rsasign('POST', 'https://api.twitter.com/oauth/request_token',
+ { oauth_callback: 'http://localhost:3005/the_dance/process_callback?service_provider_id=11',
+ oauth_consumer_key: 'GDdmIQH6jhtmLUypg82g',
+ oauth_nonce: 'QP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk',
+ oauth_signature_method: 'RSA-SHA1',
+ oauth_timestamp: '1272323042',
+ oauth_version: '1.0'
+ }, rsaPrivatePEM, 'this parameter is not used for RSA signing')
+
+ t.equal(reqsignRSA, 'MXdzEnIrQco3ACPoVWxCwv5pxYrm5MFRXbsP3LfRV+zfcRr+WMp/dOPS/3r+Wcb+17Z2IK3uJ8dMHfzb5LiDNCTUIj7SWBrbxOpy3Y6SA6z3vcrtjSekkTHLek1j+mzxOi3r3fkbYaNwjHx3PyoFSazbEstnkQQotbITeFt5FBE=')
+ t.end()
+})
+
+tape('accsign', function (t) {
+ accsign = hmacsign('POST', 'https://api.twitter.com/oauth/access_token',
+ { oauth_consumer_key: 'GDdmIQH6jhtmLUypg82g',
+ oauth_nonce: '9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8',
+ oauth_signature_method: 'HMAC-SHA1',
+ oauth_token: '8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc',
+ oauth_timestamp: '1272323047',
+ oauth_verifier: 'pDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY',
+ oauth_version: '1.0'
+ }, 'MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98', 'x6qpRnlEmW9JbQn4PQVVeVG8ZLPEx6A0TOebgwcuA')
+
+ t.equal(accsign, 'PUw/dHA4fnlJYM6RhXk5IU/0fCc=')
+ t.end()
+})
+
+tape('accsign256', function (t) {
+ accsign256 = hmacsign256('POST', 'https://api.twitter.com/oauth/access_token',
+ { oauth_consumer_key: 'GDdmIQH6jhtmLUypg82g',
+ oauth_nonce: '9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8',
+ oauth_signature_method: 'HMAC-SHA256',
+ oauth_token: '8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc',
+ oauth_timestamp: '1272323047',
+ oauth_verifier: 'pDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY',
+ oauth_version: '1.0'
+ }, 'MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98', 'x6qpRnlEmW9JbQn4PQVVeVG8ZLPEx6A0TOebgwcuA')
+
+ t.equal(accsign256, 'y7S9eUhA0tC9/YfRzCPqkg3/bUdYRDpZ93Xi51AvhjQ=')
+ t.end()
+})
+
+tape('accsignRSA', function (t) {
+ accsignRSA = rsasign('POST', 'https://api.twitter.com/oauth/access_token',
+ { oauth_consumer_key: 'GDdmIQH6jhtmLUypg82g',
+ oauth_nonce: '9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8',
+ oauth_signature_method: 'RSA-SHA1',
+ oauth_token: '8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc',
+ oauth_timestamp: '1272323047',
+ oauth_verifier: 'pDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY',
+ oauth_version: '1.0'
+ }, rsaPrivatePEM)
+
+ t.equal(accsignRSA, 'gZrMPexdgGMVUl9H6RxK0MbR6Db8tzf2kIIj52kOrDFcMgh4BunMBgUZAO1msUwz6oqZIvkVqyfyDAOP2wIrpYem0mBg3vqwPIroSE1AlUWo+TtQxOTuqrU+3kDcXpdvJe7CAX5hUx9Np/iGRqaCcgByqb9DaCcQ9ViQ+0wJiXI=')
+ t.end()
+})
+
+tape('upsign', function (t) {
+ upsign = hmacsign('POST', 'http://api.twitter.com/1/statuses/update.json',
+ { oauth_consumer_key: 'GDdmIQH6jhtmLUypg82g',
+ oauth_nonce: 'oElnnMTQIZvqvlfXM56aBLAf5noGD0AQR3Fmi7Q6Y',
+ oauth_signature_method: 'HMAC-SHA1',
+ oauth_token: '819797-Jxq8aYUDRmykzVKrgoLhXSq67TEa5ruc4GJC2rWimw',
+ oauth_timestamp: '1272325550',
+ oauth_version: '1.0',
+ status: 'setting up my twitter 私のさえずりを設定する'
+ }, 'MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98', 'J6zix3FfA9LofH0awS24M3HcBYXO5nI1iYe8EfBA')
+
+ t.equal(upsign, 'yOahq5m0YjDDjfjxHaXEsW9D+X0=')
+ t.end()
+})
+
+tape('upsign256', function (t) {
+ upsign256 = hmacsign256('POST', 'http://api.twitter.com/1/statuses/update.json',
+ { oauth_consumer_key: 'GDdmIQH6jhtmLUypg82g',
+ oauth_nonce: 'oElnnMTQIZvqvlfXM56aBLAf5noGD0AQR3Fmi7Q6Y',
+ oauth_signature_method: 'HMAC-SHA256',
+ oauth_token: '819797-Jxq8aYUDRmykzVKrgoLhXSq67TEa5ruc4GJC2rWimw',
+ oauth_timestamp: '1272325550',
+ oauth_version: '1.0',
+ status: 'setting up my twitter 私のさえずりを設定する'
+ }, 'MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98', 'J6zix3FfA9LofH0awS24M3HcBYXO5nI1iYe8EfBA')
+
+ t.equal(upsign256, 'xYhKjozxc3NYef7C26WU+gORdhEURdZRxSDzRttEKH0=')
+ t.end()
+})
+
+tape('upsignRSA', function (t) {
+ upsignRSA = rsasign('POST', 'http://api.twitter.com/1/statuses/update.json',
+ { oauth_consumer_key: 'GDdmIQH6jhtmLUypg82g',
+ oauth_nonce: 'oElnnMTQIZvqvlfXM56aBLAf5noGD0AQR3Fmi7Q6Y',
+ oauth_signature_method: 'RSA-SHA1',
+ oauth_token: '819797-Jxq8aYUDRmykzVKrgoLhXSq67TEa5ruc4GJC2rWimw',
+ oauth_timestamp: '1272325550',
+ oauth_version: '1.0',
+ status: 'setting up my twitter 私のさえずりを設定する'
+ }, rsaPrivatePEM)
+
+ t.equal(upsignRSA, 'fF4G9BNzDxPu/htctzh9CWzGhtXo9DYYl+ZyRO1/QNOhOZPqnWVUOT+CGUKxmAeJSzLKMAH8y/MFSHI0lzihqwgfZr7nUhTx6kH7lUChcVasr+TZ4qPqvGGEhfJ8Av8D5dF5fytfCSzct62uONU0iHYVqainP+zefk1K7Ptb6hI=')
+ t.end()
+})
+
+tape('rsign', function (t) {
+ var rsign = request.post(
+ { url: 'https://api.twitter.com/oauth/request_token',
+ oauth: { callback: 'http://localhost:3005/the_dance/process_callback?service_provider_id=11',
+ consumer_key: 'GDdmIQH6jhtmLUypg82g',
+ nonce: 'QP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk',
+ timestamp: '1272323042',
+ version: '1.0',
+ consumer_secret: 'MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98'
+ }
+ })
+
+ process.nextTick(function () {
+ t.equal(reqsign, getSignature(rsign))
+ rsign.abort()
+ t.end()
})
+})
+
+tape('rsign_rsa', function (t) {
+ var rsignRSA = request.post(
+ { url: 'https://api.twitter.com/oauth/request_token',
+ oauth: { callback: 'http://localhost:3005/the_dance/process_callback?service_provider_id=11',
+ consumer_key: 'GDdmIQH6jhtmLUypg82g',
+ nonce: 'QP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk',
+ timestamp: '1272323042',
+ version: '1.0',
+ private_key: rsaPrivatePEM,
+ signature_method: 'RSA-SHA1'
+ }
+ })
-setTimeout(function () {
- console.log(getsignature(rsign))
- assert.equal(reqsign, getsignature(rsign))
+ process.nextTick(function () {
+ t.equal(reqsignRSA, getSignature(rsignRSA))
+ rsignRSA.abort()
+ t.end()
+ })
})
-var raccsign = request.post(
- { url: 'https://api.twitter.com/oauth/access_token'
- , oauth:
- { consumer_key: 'GDdmIQH6jhtmLUypg82g'
- , nonce: '9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8'
- , signature_method: 'HMAC-SHA1'
- , token: '8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc'
- , timestamp: '1272323047'
- , verifier: 'pDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY'
- , version: '1.0'
- , consumer_secret: "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98"
- , token_secret: "x6qpRnlEmW9JbQn4PQVVeVG8ZLPEx6A0TOebgwcuA"
- }
+tape('raccsign', function (t) {
+ var raccsign = request.post(
+ { url: 'https://api.twitter.com/oauth/access_token',
+ oauth: { consumer_key: 'GDdmIQH6jhtmLUypg82g',
+ nonce: '9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8',
+ signature_method: 'HMAC-SHA1',
+ token: '8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc',
+ timestamp: '1272323047',
+ verifier: 'pDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY',
+ version: '1.0',
+ consumer_secret: 'MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98',
+ token_secret: 'x6qpRnlEmW9JbQn4PQVVeVG8ZLPEx6A0TOebgwcuA'
+ }
+ })
+
+ process.nextTick(function () {
+ t.equal(accsign, getSignature(raccsign))
+ raccsign.abort()
+ t.end()
})
+})
-setTimeout(function () {
- console.log(getsignature(raccsign))
- assert.equal(accsign, getsignature(raccsign))
-}, 1)
-
-var rupsign = request.post(
- { url: 'http://api.twitter.com/1/statuses/update.json'
- , oauth:
- { consumer_key: "GDdmIQH6jhtmLUypg82g"
- , nonce: "oElnnMTQIZvqvlfXM56aBLAf5noGD0AQR3Fmi7Q6Y"
- , signature_method: "HMAC-SHA1"
- , token: "819797-Jxq8aYUDRmykzVKrgoLhXSq67TEa5ruc4GJC2rWimw"
- , timestamp: "1272325550"
- , version: "1.0"
- , consumer_secret: "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98"
- , token_secret: "J6zix3FfA9LofH0awS24M3HcBYXO5nI1iYe8EfBA"
- }
- , form: {status: 'setting up my twitter 私のさえずりを設定する'}
+tape('raccsignRSA', function (t) {
+ var raccsignRSA = request.post(
+ { url: 'https://api.twitter.com/oauth/access_token',
+ oauth: { consumer_key: 'GDdmIQH6jhtmLUypg82g',
+ nonce: '9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8',
+ signature_method: 'RSA-SHA1',
+ token: '8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc',
+ timestamp: '1272323047',
+ verifier: 'pDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY',
+ version: '1.0',
+ private_key: rsaPrivatePEM,
+ token_secret: 'x6qpRnlEmW9JbQn4PQVVeVG8ZLPEx6A0TOebgwcuA'
+ }
+ })
+
+ process.nextTick(function () {
+ t.equal(accsignRSA, getSignature(raccsignRSA))
+ raccsignRSA.abort()
+ t.end()
})
-setTimeout(function () {
- console.log(getsignature(rupsign))
- assert.equal(upsign, getsignature(rupsign))
-}, 1)
+})
+
+tape('rupsign', function (t) {
+ var rupsign = request.post(
+ { url: 'http://api.twitter.com/1/statuses/update.json',
+ oauth: { consumer_key: 'GDdmIQH6jhtmLUypg82g',
+ nonce: 'oElnnMTQIZvqvlfXM56aBLAf5noGD0AQR3Fmi7Q6Y',
+ signature_method: 'HMAC-SHA1',
+ token: '819797-Jxq8aYUDRmykzVKrgoLhXSq67TEa5ruc4GJC2rWimw',
+ timestamp: '1272325550',
+ version: '1.0',
+ consumer_secret: 'MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98',
+ token_secret: 'J6zix3FfA9LofH0awS24M3HcBYXO5nI1iYe8EfBA'
+ },
+ form: {status: 'setting up my twitter 私のさえずりを設定する'}
+ })
+ process.nextTick(function () {
+ t.equal(upsign, getSignature(rupsign))
+ rupsign.abort()
+ t.end()
+ })
+})
+
+tape('rupsignRSA', function (t) {
+ var rupsignRSA = request.post(
+ { url: 'http://api.twitter.com/1/statuses/update.json',
+ oauth: { consumer_key: 'GDdmIQH6jhtmLUypg82g',
+ nonce: 'oElnnMTQIZvqvlfXM56aBLAf5noGD0AQR3Fmi7Q6Y',
+ signature_method: 'RSA-SHA1',
+ token: '819797-Jxq8aYUDRmykzVKrgoLhXSq67TEa5ruc4GJC2rWimw',
+ timestamp: '1272325550',
+ version: '1.0',
+ private_key: rsaPrivatePEM,
+ token_secret: 'J6zix3FfA9LofH0awS24M3HcBYXO5nI1iYe8EfBA'
+ },
+ form: {status: 'setting up my twitter 私のさえずりを設定する'}
+ })
+ process.nextTick(function () {
+ t.equal(upsignRSA, getSignature(rupsignRSA))
+ rupsignRSA.abort()
+ t.end()
+ })
+})
+tape('rfc5849 example', function (t) {
+ var rfc5849 = request.post(
+ { url: 'http://example.com/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b',
+ oauth: { consumer_key: '9djdj82h48djs9d2',
+ nonce: '7d8f3e4a',
+ signature_method: 'HMAC-SHA1',
+ token: 'kkk9d7dh3k39sjv7',
+ timestamp: '137131201',
+ consumer_secret: 'j49sk3j29djd',
+ token_secret: 'dh893hdasih9',
+ realm: 'Example'
+ },
+ form: {
+ c2: '',
+ a3: '2 q'
+ }
+ })
+ process.nextTick(function () {
+ // different signature in rfc5849 because request sets oauth_version by default
+ t.equal('OB33pYjWAnf+xtOHN4Gmbdil168=', getSignature(rfc5849))
+ rfc5849.abort()
+ t.end()
+ })
+})
+
+tape('rfc5849 RSA example', function (t) {
+ var rfc5849RSA = request.post(
+ { url: 'http://example.com/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b',
+ oauth: { consumer_key: '9djdj82h48djs9d2',
+ nonce: '7d8f3e4a',
+ signature_method: 'RSA-SHA1',
+ token: 'kkk9d7dh3k39sjv7',
+ timestamp: '137131201',
+ private_key: rsaPrivatePEM,
+ token_secret: 'dh893hdasih9',
+ realm: 'Example'
+ },
+ form: {
+ c2: '',
+ a3: '2 q'
+ }
+ })
+
+ process.nextTick(function () {
+ // different signature in rfc5849 because request sets oauth_version by default
+ t.equal('ThNYfZhYogcAU6rWgI3ZFlPEhoIXHMZcuMzl+jykJZW/ab+AxyefS03dyd64CclIZ0u8JEW64TQ5SHthoQS8aM8qir4t+t88lRF3LDkD2KtS1krgCZTUQxkDL5BO5pxsqAQ2Zfdcrzaxb6VMGD1Hf+Pno+fsHQo/UUKjq4V3RMo=', getSignature(rfc5849RSA))
+ rfc5849RSA.abort()
+ t.end()
+ })
+})
+tape('plaintext signature method', function (t) {
+ var plaintext = request.post(
+ { url: 'https://dummy.com',
+ oauth: { consumer_secret: 'consumer_secret',
+ token_secret: 'token_secret',
+ signature_method: 'PLAINTEXT'
+ }
+ })
+ process.nextTick(function () {
+ t.equal('consumer_secret&token_secret', getSignature(plaintext))
+ plaintext.abort()
+ t.end()
+ })
+})
+
+tape('invalid transport_method', function (t) {
+ t.throws(
+ function () {
+ request.post(
+ { url: 'http://example.com/',
+ oauth: { transport_method: 'headerquery'
+ }
+ })
+ }, /transport_method invalid/)
+ t.end()
+})
+
+tape("invalid method while using transport_method 'body'", function (t) {
+ t.throws(
+ function () {
+ request.get(
+ { url: 'http://example.com/',
+ headers: { 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' },
+ oauth: { transport_method: 'body'
+ }
+ })
+ }, /requires POST/)
+ t.end()
+})
+
+tape("invalid content-type while using transport_method 'body'", function (t) {
+ t.throws(
+ function () {
+ request.post(
+ { url: 'http://example.com/',
+ headers: { 'content-type': 'application/json; charset=UTF-8' },
+ oauth: { transport_method: 'body'
+ }
+ })
+ }, /requires POST/)
+ t.end()
+})
+
+tape('query transport_method', function (t) {
+ var r = request.post(
+ { url: 'https://api.twitter.com/oauth/access_token',
+ oauth: { consumer_key: 'GDdmIQH6jhtmLUypg82g',
+ nonce: '9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8',
+ signature_method: 'HMAC-SHA1',
+ token: '8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc',
+ timestamp: '1272323047',
+ verifier: 'pDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY',
+ version: '1.0',
+ consumer_secret: 'MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98',
+ token_secret: 'x6qpRnlEmW9JbQn4PQVVeVG8ZLPEx6A0TOebgwcuA',
+ transport_method: 'query'
+ }
+ })
+
+ process.nextTick(function () {
+ t.notOk(r.headers.Authorization, "oauth Authorization header should not be present with transport_method 'query'")
+ t.equal(r.uri.path, r.path, 'r.uri.path should equal r.path')
+ t.ok(r.path.match(/^\/oauth\/access_token\?/), 'path should contain path + query')
+ t.deepEqual(qs.parse(r.uri.query),
+ { oauth_consumer_key: 'GDdmIQH6jhtmLUypg82g',
+ oauth_nonce: '9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8',
+ oauth_signature_method: 'HMAC-SHA1',
+ oauth_timestamp: '1272323047',
+ oauth_token: '8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc',
+ oauth_verifier: 'pDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY',
+ oauth_version: '1.0',
+ oauth_signature: accsign })
+ r.abort()
+ t.end()
+ })
+})
+
+tape('query transport_method + form option + url params', function (t) {
+ var r = request.post(
+ { url: 'http://example.com/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b',
+ oauth: { consumer_key: '9djdj82h48djs9d2',
+ nonce: '7d8f3e4a',
+ signature_method: 'HMAC-SHA1',
+ token: 'kkk9d7dh3k39sjv7',
+ timestamp: '137131201',
+ consumer_secret: 'j49sk3j29djd',
+ token_secret: 'dh893hdasih9',
+ realm: 'Example',
+ transport_method: 'query'
+ },
+ form: {
+ c2: '',
+ a3: '2 q'
+ }
+ })
+
+ process.nextTick(function () {
+ t.notOk(r.headers.Authorization, "oauth Authorization header should not be present with transport_method 'query'")
+ t.equal(r.uri.path, r.path, 'r.uri.path should equal r.path')
+ t.ok(r.path.match(/^\/request\?/), 'path should contain path + query')
+ t.deepEqual(qs.parse(r.uri.query),
+ { b5: '=%3D',
+ a3: 'a',
+ 'c@': '',
+ a2: 'r b',
+ realm: 'Example',
+ oauth_consumer_key: '9djdj82h48djs9d2',
+ oauth_nonce: '7d8f3e4a',
+ oauth_signature_method: 'HMAC-SHA1',
+ oauth_timestamp: '137131201',
+ oauth_token: 'kkk9d7dh3k39sjv7',
+ oauth_version: '1.0',
+ oauth_signature: 'OB33pYjWAnf+xtOHN4Gmbdil168=' })
+ r.abort()
+ t.end()
+ })
+})
+
+tape('query transport_method + qs option + url params', function (t) {
+ var r = request.post(
+ { url: 'http://example.com/request?a2=r%20b',
+ oauth: { consumer_key: '9djdj82h48djs9d2',
+ nonce: '7d8f3e4a',
+ signature_method: 'HMAC-SHA1',
+ token: 'kkk9d7dh3k39sjv7',
+ timestamp: '137131201',
+ consumer_secret: 'j49sk3j29djd',
+ token_secret: 'dh893hdasih9',
+ realm: 'Example',
+ transport_method: 'query'
+ },
+ qs: {
+ b5: '=%3D',
+ a3: ['a', '2 q'],
+ 'c@': '',
+ c2: ''
+ }
+ })
+
+ process.nextTick(function () {
+ t.notOk(r.headers.Authorization, "oauth Authorization header should not be present with transport_method 'query'")
+ t.equal(r.uri.path, r.path, 'r.uri.path should equal r.path')
+ t.ok(r.path.match(/^\/request\?/), 'path should contain path + query')
+ t.deepEqual(qs.parse(r.uri.query),
+ { a2: 'r b',
+ b5: '=%3D',
+ 'a3[0]': 'a',
+ 'a3[1]': '2 q',
+ 'c@': '',
+ c2: '',
+ realm: 'Example',
+ oauth_consumer_key: '9djdj82h48djs9d2',
+ oauth_nonce: '7d8f3e4a',
+ oauth_signature_method: 'HMAC-SHA1',
+ oauth_timestamp: '137131201',
+ oauth_token: 'kkk9d7dh3k39sjv7',
+ oauth_version: '1.0',
+ oauth_signature: 'OB33pYjWAnf+xtOHN4Gmbdil168=' })
+ r.abort()
+ t.end()
+ })
+})
+
+tape('body transport_method', function (t) {
+ var r = request.post(
+ { url: 'https://api.twitter.com/oauth/access_token',
+ headers: { 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' },
+ oauth: { consumer_key: 'GDdmIQH6jhtmLUypg82g',
+ nonce: '9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8',
+ signature_method: 'HMAC-SHA1',
+ token: '8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc',
+ timestamp: '1272323047',
+ verifier: 'pDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY',
+ version: '1.0',
+ consumer_secret: 'MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98',
+ token_secret: 'x6qpRnlEmW9JbQn4PQVVeVG8ZLPEx6A0TOebgwcuA',
+ transport_method: 'body'
+ }
+ })
+
+ process.nextTick(function () {
+ t.notOk(r.headers.Authorization, "oauth Authorization header should not be present with transport_method 'body'")
+ t.deepEqual(qs.parse(r.body),
+ { oauth_consumer_key: 'GDdmIQH6jhtmLUypg82g',
+ oauth_nonce: '9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8',
+ oauth_signature_method: 'HMAC-SHA1',
+ oauth_timestamp: '1272323047',
+ oauth_token: '8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc',
+ oauth_verifier: 'pDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY',
+ oauth_version: '1.0',
+ oauth_signature: accsign })
+ r.abort()
+ t.end()
+ })
+})
+
+tape('body transport_method + form option + url params', function (t) {
+ var r = request.post(
+ { url: 'http://example.com/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b',
+ oauth: { consumer_key: '9djdj82h48djs9d2',
+ nonce: '7d8f3e4a',
+ signature_method: 'HMAC-SHA1',
+ token: 'kkk9d7dh3k39sjv7',
+ timestamp: '137131201',
+ consumer_secret: 'j49sk3j29djd',
+ token_secret: 'dh893hdasih9',
+ realm: 'Example',
+ transport_method: 'body'
+ },
+ form: {
+ c2: '',
+ a3: '2 q'
+ }
+ })
+
+ process.nextTick(function () {
+ t.notOk(r.headers.Authorization, "oauth Authorization header should not be present with transport_method 'body'")
+ t.deepEqual(qs.parse(r.body),
+ { c2: '',
+ a3: '2 q',
+ realm: 'Example',
+ oauth_consumer_key: '9djdj82h48djs9d2',
+ oauth_nonce: '7d8f3e4a',
+ oauth_signature_method: 'HMAC-SHA1',
+ oauth_timestamp: '137131201',
+ oauth_token: 'kkk9d7dh3k39sjv7',
+ oauth_version: '1.0',
+ oauth_signature: 'OB33pYjWAnf+xtOHN4Gmbdil168=' })
+ r.abort()
+ t.end()
+ })
+})
+
+tape('body_hash manually set', function (t) {
+ var r = request.post(
+ { url: 'http://example.com',
+ oauth: { consumer_secret: 'consumer_secret',
+ body_hash: 'ManuallySetHash'
+ },
+ json: {foo: 'bar'}
+ })
+
+ process.nextTick(function () {
+ var hash = r.headers.Authorization.replace(/.*oauth_body_hash="([^"]+)".*/, '$1')
+ t.equal('ManuallySetHash', hash)
+ r.abort()
+ t.end()
+ })
+})
+
+tape('body_hash automatically built for string', function (t) {
+ var r = request.post(
+ { url: 'http://example.com',
+ oauth: { consumer_secret: 'consumer_secret',
+ body_hash: true
+ },
+ body: 'Hello World!'
+ })
+
+ process.nextTick(function () {
+ var hash = r.headers.Authorization.replace(/.*oauth_body_hash="([^"]+)".*/, '$1')
+ // from https://tools.ietf.org/id/draft-eaton-oauth-bodyhash-00.html#anchor15
+ t.equal('Lve95gjOVATpfV8EL5X4nxwjKHE%3D', hash)
+ r.abort()
+ t.end()
+ })
+})
+
+tape('body_hash automatically built for JSON', function (t) {
+ var r = request.post(
+ { url: 'http://example.com',
+ oauth: { consumer_secret: 'consumer_secret',
+ body_hash: true
+ },
+ json: {foo: 'bar'}
+ })
+
+ process.nextTick(function () {
+ var hash = r.headers.Authorization.replace(/.*oauth_body_hash="([^"]+)".*/, '$1')
+ t.equal('pedE0BZFQNM7HX6mFsKPL6l%2BdUo%3D', hash)
+ r.abort()
+ t.end()
+ })
+})
+
+tape('body_hash PLAINTEXT signature_method', function (t) {
+ t.throws(function () {
+ request.post(
+ { url: 'http://example.com',
+ oauth: { consumer_secret: 'consumer_secret',
+ body_hash: true,
+ signature_method: 'PLAINTEXT'
+ },
+ json: {foo: 'bar'}
+ })
+ }, /oauth: PLAINTEXT signature_method not supported with body_hash signing/)
+ t.end()
+})
+
+tape('refresh oauth_nonce on redirect', function (t) {
+ var oauthNonce1
+ var oauthNonce2
+ var url
+ var s = http.createServer(function (req, res) {
+ if (req.url === '/redirect') {
+ oauthNonce1 = req.headers.authorization.replace(/.*oauth_nonce="([^"]+)".*/, '$1')
+ res.writeHead(302, {location: url + '/response'})
+ res.end()
+ } else if (req.url === '/response') {
+ oauthNonce2 = req.headers.authorization.replace(/.*oauth_nonce="([^"]+)".*/, '$1')
+ res.writeHead(200, {'content-type': 'text/plain'})
+ res.end()
+ }
+ })
+ s.listen(0, function () {
+ url = 'http://localhost:' + this.address().port
+ request.get(
+ { url: url + '/redirect',
+ oauth: { consumer_key: 'consumer_key',
+ consumer_secret: 'consumer_secret',
+ token: 'token',
+ token_secret: 'token_secret'
+ }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.notEqual(oauthNonce1, oauthNonce2)
+ s.close(function () {
+ t.end()
+ })
+ })
+ })
+})
+
+tape('no credentials on external redirect', function (t) {
+ var s2 = http.createServer(function (req, res) {
+ res.writeHead(200, {'content-type': 'text/plain'})
+ res.end()
+ })
+ var s1 = http.createServer(function (req, res) {
+ res.writeHead(302, {location: s2.url})
+ res.end()
+ })
+ s1.listen(0, function () {
+ s1.url = 'http://localhost:' + this.address().port
+ s2.listen(0, function () {
+ s2.url = 'http://127.0.0.1:' + this.address().port
+ request.get(
+ { url: s1.url,
+ oauth: { consumer_key: 'consumer_key',
+ consumer_secret: 'consumer_secret',
+ token: 'token',
+ token_secret: 'token_secret'
+ }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.request.headers.Authorization, undefined)
+ s1.close(function () {
+ s2.close(function () {
+ t.end()
+ })
+ })
+ })
+ })
+ })
+})
diff --git a/tests/test-onelineproxy.js b/tests/test-onelineproxy.js
new file mode 100644
index 000000000..b2219f246
--- /dev/null
+++ b/tests/test-onelineproxy.js
@@ -0,0 +1,61 @@
+'use strict'
+
+var http = require('http')
+var assert = require('assert')
+var request = require('../index')
+var tape = require('tape')
+
+var server = http.createServer(function (req, resp) {
+ resp.statusCode = 200
+ if (req.url === '/get') {
+ assert.equal(req.method, 'GET')
+ resp.write('content')
+ resp.end()
+ return
+ }
+ if (req.url === '/put') {
+ var x = ''
+ assert.equal(req.method, 'PUT')
+ req.on('data', function (chunk) {
+ x += chunk
+ })
+ req.on('end', function () {
+ assert.equal(x, 'content')
+ resp.write('success')
+ resp.end()
+ })
+ return
+ }
+ if (req.url === '/proxy') {
+ assert.equal(req.method, 'PUT')
+ req.pipe(request(server.url + '/put')).pipe(resp)
+ return
+ }
+ if (req.url === '/test') {
+ request(server.url + '/get').pipe(request.put(server.url + '/proxy')).pipe(resp)
+ return
+ }
+ throw new Error('Unknown url', req.url)
+})
+
+tape('setup', function (t) {
+ server.listen(0, function () {
+ server.url = 'http://localhost:' + this.address().port
+ t.end()
+ })
+})
+
+tape('chained one-line proxying', function (t) {
+ request(server.url + '/test', function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'success')
+ t.end()
+ })
+})
+
+tape('cleanup', function (t) {
+ server.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-option-reuse.js b/tests/test-option-reuse.js
new file mode 100644
index 000000000..1c9b09d64
--- /dev/null
+++ b/tests/test-option-reuse.js
@@ -0,0 +1,54 @@
+'use strict'
+
+var request = require('../index')
+var http = require('http')
+var tape = require('tape')
+
+var methodsSeen = {
+ head: 0,
+ get: 0
+}
+
+var s = http.createServer(function (req, res) {
+ res.statusCode = 200
+ res.end('ok')
+
+ methodsSeen[req.method.toLowerCase()]++
+})
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ s.url = 'http://localhost:' + this.address().port
+ t.end()
+ })
+})
+
+tape('options object is not mutated', function (t) {
+ var url = s.url
+ var options = { url: url }
+
+ request.head(options, function (err, resp, body) {
+ t.equal(err, null)
+ t.equal(body, '')
+ t.equal(Object.keys(options).length, 1)
+ t.equal(options.url, url)
+
+ request.get(options, function (err, resp, body) {
+ t.equal(err, null)
+ t.equal(body, 'ok')
+ t.equal(Object.keys(options).length, 1)
+ t.equal(options.url, url)
+
+ t.equal(methodsSeen.head, 1)
+ t.equal(methodsSeen.get, 1)
+
+ t.end()
+ })
+ })
+})
+
+tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-options-convenience-method.js b/tests/test-options-convenience-method.js
new file mode 100644
index 000000000..43231f27d
--- /dev/null
+++ b/tests/test-options-convenience-method.js
@@ -0,0 +1,52 @@
+'use strict'
+
+var server = require('./server')
+var request = require('../index')
+var tape = require('tape')
+var destroyable = require('server-destroy')
+
+var s = server.createServer()
+
+destroyable(s)
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ s.on('/options', function (req, res) {
+ res.writeHead(200, {
+ 'x-original-method': req.method,
+ 'allow': 'OPTIONS, GET, HEAD'
+ })
+
+ res.end()
+ })
+
+ t.end()
+ })
+})
+
+tape('options(string, function)', function (t) {
+ request.options(s.url + '/options', function (err, res) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(res.headers['x-original-method'], 'OPTIONS')
+ t.end()
+ })
+})
+
+tape('options(object, function)', function (t) {
+ request.options({
+ url: s.url + '/options',
+ headers: { foo: 'bar' }
+ }, function (err, res) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(res.headers['x-original-method'], 'OPTIONS')
+ t.end()
+ })
+})
+
+tape('cleanup', function (t) {
+ s.destroy(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-params.js b/tests/test-params.js
index 8354f6d8d..4659aa70f 100644
--- a/tests/test-params.js
+++ b/tests/test-params.js
@@ -1,92 +1,101 @@
+'use strict'
+
var server = require('./server')
- , assert = require('assert')
- , request = require('../main.js')
- ;
+var request = require('../index')
+var tape = require('tape')
-var s = server.createServer();
+var s = server.createServer()
-var tests =
- { testGet :
- { resp : server.createGetResponse("TESTING!")
- , expectBody: "TESTING!"
- }
- , testGetChunkBreak :
- { resp : server.createChunkResponse(
- [ new Buffer([239])
- , new Buffer([163])
- , new Buffer([191])
- , new Buffer([206])
- , new Buffer([169])
- , new Buffer([226])
- , new Buffer([152])
- , new Buffer([131])
- ])
- , expectBody: "Ω☃"
- }
- , testGetBuffer :
- { resp : server.createGetResponse(new Buffer("TESTING!"))
- , encoding: null
- , expectBody: new Buffer("TESTING!")
- }
- , testGetJSON :
- { resp : server.createGetResponse('{"test":true}', 'application/json')
- , json : true
- , expectBody: {"test":true}
- }
- , testPutString :
- { resp : server.createPostValidator("PUTTINGDATA")
- , method : "PUT"
- , body : "PUTTINGDATA"
- }
- , testPutBuffer :
- { resp : server.createPostValidator("PUTTINGDATA")
- , method : "PUT"
- , body : new Buffer("PUTTINGDATA")
- }
- , testPutJSON :
- { resp : server.createPostValidator(JSON.stringify({foo: 'bar'}))
- , method: "PUT"
- , json: {foo: 'bar'}
- }
- , testPutMultipart :
- { resp: server.createPostValidator(
- '--frontier\r\n' +
- 'content-type: text/html\r\n' +
- '\r\n' +
- 'Oh hi.' +
- '\r\n--frontier\r\n\r\n' +
- 'Oh hi.' +
- '\r\n--frontier--'
- )
- , method: "PUT"
- , multipart:
- [ {'content-type': 'text/html', 'body': 'Oh hi.'}
- , {'body': 'Oh hi.'}
- ]
+function runTest (name, test) {
+ tape(name, function (t) {
+ s.on('/' + name, test.resp)
+ request(s.url + '/' + name, test, function (err, resp, body) {
+ t.equal(err, null)
+ if (test.expectBody) {
+ if (Buffer.isBuffer(test.expectBody)) {
+ t.equal(test.expectBody.toString(), body.toString())
+ } else {
+ t.deepEqual(test.expectBody, body)
+ }
}
- }
+ t.end()
+ })
+ })
+}
-s.listen(s.port, function () {
+tape('setup', function (t) {
+ s.listen(0, function () {
+ t.end()
+ })
+})
- var counter = 0
+runTest('testGet', {
+ resp: server.createGetResponse('TESTING!'),
+ expectBody: 'TESTING!'
+})
- for (i in tests) {
- (function () {
- var test = tests[i]
- s.on('/'+i, test.resp)
- //test.uri = s.url + '/' + i
- request(s.url + '/' + i, test, function (err, resp, body) {
- if (err) throw err
- if (test.expectBody) {
- assert.deepEqual(test.expectBody, body)
- }
- counter = counter - 1;
- if (counter === 0) {
- console.log(Object.keys(tests).length+" tests passed.")
- s.close()
- }
- })
- counter++
- })()
- }
+runTest('testGetChunkBreak', {
+ resp: server.createChunkResponse(
+ [ Buffer.from([239]),
+ Buffer.from([163]),
+ Buffer.from([191]),
+ Buffer.from([206]),
+ Buffer.from([169]),
+ Buffer.from([226]),
+ Buffer.from([152]),
+ Buffer.from([131])
+ ]),
+ expectBody: '\uf8ff\u03a9\u2603'
+})
+
+runTest('testGetBuffer', {
+ resp: server.createGetResponse(Buffer.from('TESTING!')),
+ encoding: null,
+ expectBody: Buffer.from('TESTING!')
+})
+
+runTest('testGetJSON', {
+ resp: server.createGetResponse('{"test":true}', 'application/json'),
+ json: true,
+ expectBody: {'test': true}
+})
+
+runTest('testPutString', {
+ resp: server.createPostValidator('PUTTINGDATA'),
+ method: 'PUT',
+ body: 'PUTTINGDATA'
+})
+
+runTest('testPutBuffer', {
+ resp: server.createPostValidator('PUTTINGDATA'),
+ method: 'PUT',
+ body: Buffer.from('PUTTINGDATA')
+})
+
+runTest('testPutJSON', {
+ resp: server.createPostValidator(JSON.stringify({foo: 'bar'})),
+ method: 'PUT',
+ json: {foo: 'bar'}
+})
+
+runTest('testPutMultipart', {
+ resp: server.createPostValidator(
+ '--__BOUNDARY__\r\n' +
+ 'content-type: text/html\r\n' +
+ '\r\n' +
+ 'Oh hi.' +
+ '\r\n--__BOUNDARY__\r\n\r\n' +
+ 'Oh hi.' +
+ '\r\n--__BOUNDARY__--'
+ ),
+ method: 'PUT',
+ multipart: [ {'content-type': 'text/html', 'body': 'Oh hi.'},
+ {'body': 'Oh hi.'}
+ ]
+})
+
+tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
})
diff --git a/tests/test-piped-redirect.js b/tests/test-piped-redirect.js
new file mode 100644
index 000000000..77135d4d1
--- /dev/null
+++ b/tests/test-piped-redirect.js
@@ -0,0 +1,55 @@
+'use strict'
+
+var http = require('http')
+var request = require('../index')
+var tape = require('tape')
+
+var port1
+var port2
+
+var s1 = http.createServer(function (req, resp) {
+ if (req.url === '/original') {
+ resp.writeHeader(302, {
+ 'location': '/redirected'
+ })
+ resp.end()
+ } else if (req.url === '/redirected') {
+ resp.writeHeader(200, {
+ 'content-type': 'text/plain'
+ })
+ resp.write('OK')
+ resp.end()
+ }
+})
+
+var s2 = http.createServer(function (req, resp) {
+ var x = request('http://localhost:' + port1 + '/original')
+ req.pipe(x)
+ x.pipe(resp)
+})
+
+tape('setup', function (t) {
+ s1.listen(0, function () {
+ port1 = this.address().port
+ s2.listen(0, function () {
+ port2 = this.address().port
+ t.end()
+ })
+ })
+})
+
+tape('piped redirect', function (t) {
+ request('http://localhost:' + port2 + '/original', function (err, res, body) {
+ t.equal(err, null)
+ t.equal(body, 'OK')
+ t.end()
+ })
+})
+
+tape('cleanup', function (t) {
+ s1.close(function () {
+ s2.close(function () {
+ t.end()
+ })
+ })
+})
diff --git a/tests/test-pipes.js b/tests/test-pipes.js
index 186987445..dab7cb311 100644
--- a/tests/test-pipes.js
+++ b/tests/test-pipes.js
@@ -1,202 +1,383 @@
+'use strict'
+
var server = require('./server')
- , events = require('events')
- , stream = require('stream')
- , assert = require('assert')
- , fs = require('fs')
- , request = require('../main.js')
- , path = require('path')
- , util = require('util')
- ;
-
-var s = server.createServer(3453);
-
-function ValidationStream(str) {
+var stream = require('stream')
+var fs = require('fs')
+var request = require('../index')
+var path = require('path')
+var util = require('util')
+var tape = require('tape')
+
+var s = server.createServer()
+
+s.on('/cat', function (req, res) {
+ if (req.method === 'GET') {
+ res.writeHead(200, {
+ 'content-type': 'text/plain-test',
+ 'content-length': 4
+ })
+ res.end('asdf')
+ } else if (req.method === 'PUT') {
+ var body = ''
+ req.on('data', function (chunk) {
+ body += chunk
+ }).on('end', function () {
+ res.writeHead(201)
+ res.end()
+ s.emit('catDone', req, res, body)
+ })
+ }
+})
+
+s.on('/doodle', function (req, res) {
+ if (req.headers['x-oneline-proxy']) {
+ res.setHeader('x-oneline-proxy', 'yup')
+ }
+ res.writeHead('200', { 'content-type': 'image/jpeg' })
+ fs.createReadStream(path.join(__dirname, 'googledoodle.jpg')).pipe(res)
+})
+
+function ValidationStream (t, str) {
this.str = str
this.buf = ''
this.on('data', function (data) {
this.buf += data
})
this.on('end', function () {
- assert.equal(this.str, this.buf)
+ t.equal(this.str, this.buf)
})
this.writable = true
}
+
util.inherits(ValidationStream, stream.Stream)
+
ValidationStream.prototype.write = function (chunk) {
this.emit('data', chunk)
}
+
ValidationStream.prototype.end = function (chunk) {
- if (chunk) emit('data', chunk)
+ if (chunk) {
+ this.emit('data', chunk)
+ }
this.emit('end')
}
-s.listen(s.port, function () {
- counter = 0;
-
- var check = function () {
- counter = counter - 1
- if (counter === 0) {
- console.log('All tests passed.')
- setTimeout(function () {
- process.exit();
- }, 500)
- }
- }
+tape('setup', function (t) {
+ s.listen(0, function () {
+ t.end()
+ })
+})
- // Test pipeing to a request object
- s.once('/push', server.createPostValidator("mydata"));
+tape('piping to a request object', function (t) {
+ s.once('/push', server.createPostValidator('mydata'))
- var mydata = new stream.Stream();
+ var mydata = new stream.Stream()
mydata.readable = true
- counter++
- var r1 = request.put({url:'http://localhost:3453/push'}, function () {
- check();
+ var r1 = request.put({
+ url: s.url + '/push'
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'mydata')
+ t.end()
})
mydata.pipe(r1)
- mydata.emit('data', 'mydata');
- mydata.emit('end');
+ mydata.emit('data', 'mydata')
+ mydata.emit('end')
+})
+
+tape('piping to a request object with invalid uri', function (t) {
+ var mybodydata = new stream.Stream()
+ mybodydata.readable = true
+
+ var r2 = request.put({
+ url: '/bad-uri',
+ json: true
+ }, function (err, res, body) {
+ t.ok(err instanceof Error)
+ t.equal(err.message, 'Invalid URI "/bad-uri"')
+ t.end()
+ })
+ mybodydata.pipe(r2)
+
+ mybodydata.emit('data', JSON.stringify({ foo: 'bar' }))
+ mybodydata.emit('end')
+})
+
+tape('piping to a request object with a json body', function (t) {
+ var obj = {foo: 'bar'}
+ var json = JSON.stringify(obj)
+ s.once('/push-json', server.createPostValidator(json, 'application/json'))
+ var mybodydata = new stream.Stream()
+ mybodydata.readable = true
+ var r2 = request.put({
+ url: s.url + '/push-json',
+ json: true
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.deepEqual(body, obj)
+ t.end()
+ })
+ mybodydata.pipe(r2)
+
+ mybodydata.emit('data', JSON.stringify({ foo: 'bar' }))
+ mybodydata.emit('end')
+})
- // Test pipeing from a request object.
- s.once('/pull', server.createGetResponse("mypulldata"));
+tape('piping from a request object', function (t) {
+ s.once('/pull', server.createGetResponse('mypulldata'))
- var mypulldata = new stream.Stream();
+ var mypulldata = new stream.Stream()
mypulldata.writable = true
- counter++
- request({url:'http://localhost:3453/pull'}).pipe(mypulldata)
+ request({
+ url: s.url + '/pull'
+ }).pipe(mypulldata)
- var d = '';
+ var d = ''
mypulldata.write = function (chunk) {
- d += chunk;
+ d += chunk
}
mypulldata.end = function () {
- assert.equal(d, 'mypulldata');
- check();
- };
-
-
- s.on('/cat', function (req, resp) {
- if (req.method === "GET") {
- resp.writeHead(200, {'content-type':'text/plain-test', 'content-length':4});
- resp.end('asdf')
- } else if (req.method === "PUT") {
- assert.equal(req.headers['content-type'], 'text/plain-test');
- assert.equal(req.headers['content-length'], 4)
- var validate = '';
-
- req.on('data', function (chunk) {validate += chunk})
- req.on('end', function () {
- resp.writeHead(201);
- resp.end();
- assert.equal(validate, 'asdf');
- check();
- })
- }
+ t.equal(d, 'mypulldata')
+ t.end()
+ }
+})
+
+tape('pause when piping from a request object', function (t) {
+ s.once('/chunks', function (req, res) {
+ res.writeHead(200, {
+ 'content-type': 'text/plain'
+ })
+ res.write('Chunk 1')
+ setTimeout(function () { res.end('Chunk 2') }, 10)
})
- s.on('/pushjs', function (req, resp) {
- if (req.method === "PUT") {
- assert.equal(req.headers['content-type'], 'text/javascript');
- check();
- }
+
+ var chunkNum = 0
+ var paused = false
+ request({
+ url: s.url + '/chunks'
+ })
+ .on('data', function (chunk) {
+ var self = this
+
+ t.notOk(paused, 'Only receive data when not paused')
+
+ ++chunkNum
+ if (chunkNum === 1) {
+ t.equal(chunk.toString(), 'Chunk 1')
+ self.pause()
+ paused = true
+ setTimeout(function () {
+ paused = false
+ self.resume()
+ }, 100)
+ } else {
+ t.equal(chunk.toString(), 'Chunk 2')
+ }
+ })
+ .on('end', t.end.bind(t))
+})
+
+tape('pause before piping from a request object', function (t) {
+ s.once('/pause-before', function (req, res) {
+ res.writeHead(200, {
+ 'content-type': 'text/plain'
+ })
+ res.end('Data')
})
- s.on('/catresp', function (req, resp) {
- request.get('http://localhost:3453/cat').pipe(resp)
+
+ var paused = true
+ var r = request({
+ url: s.url + '/pause-before'
})
- s.on('/doodle', function (req, resp) {
- if (req.headers['x-oneline-proxy']) {
- resp.setHeader('x-oneline-proxy', 'yup')
- }
- resp.writeHead('200', {'content-type':'image/png'})
- fs.createReadStream(path.join(__dirname, 'googledoodle.png')).pipe(resp)
+ r.pause()
+ r.on('data', function (data) {
+ t.notOk(paused, 'Only receive data when not paused')
+ t.equal(data.toString(), 'Data')
})
- s.on('/onelineproxy', function (req, resp) {
- var x = request('http://localhost:3453/doodle')
- req.pipe(x)
- x.pipe(resp)
+ r.on('end', t.end.bind(t))
+
+ setTimeout(function () {
+ paused = false
+ r.resume()
+ }, 100)
+})
+
+var fileContents = fs.readFileSync(__filename)
+function testPipeFromFile (testName, hasContentLength) {
+ tape(testName, function (t) {
+ s.once('/pushjs', function (req, res) {
+ if (req.method === 'PUT') {
+ t.equal(req.headers['content-type'], 'application/javascript')
+ t.equal(
+ req.headers['content-length'],
+ (hasContentLength ? '' + fileContents.length : undefined))
+ var body = ''
+ req.setEncoding('utf8')
+ req.on('data', function (data) {
+ body += data
+ })
+ req.on('end', function () {
+ res.end()
+ t.equal(body, fileContents.toString())
+ t.end()
+ })
+ } else {
+ res.end()
+ }
+ })
+ var r = request.put(s.url + '/pushjs')
+ fs.createReadStream(__filename).pipe(r)
+ if (hasContentLength) {
+ r.setHeader('content-length', fileContents.length)
+ }
})
+}
- counter++
- fs.createReadStream(__filename).pipe(request.put('http://localhost:3453/pushjs'))
+// TODO Piping from a file does not send content-length header
+testPipeFromFile('piping from a file', false)
+testPipeFromFile('piping from a file with content-length', true)
- counter++
- request.get('http://localhost:3453/cat').pipe(request.put('http://localhost:3453/cat'))
+tape('piping to and from same URL', function (t) {
+ s.once('catDone', function (req, res, body) {
+ t.equal(req.headers['content-type'], 'text/plain-test')
+ t.equal(req.headers['content-length'], '4')
+ t.equal(body, 'asdf')
+ t.end()
+ })
+ request.get(s.url + '/cat')
+ .pipe(request.put(s.url + '/cat'))
+})
- counter++
- request.get('http://localhost:3453/catresp', function (e, resp, body) {
- assert.equal(resp.headers['content-type'], 'text/plain-test');
- assert.equal(resp.headers['content-length'], 4)
- check();
+tape('piping between urls', function (t) {
+ s.once('/catresp', function (req, res) {
+ request.get(s.url + '/cat').pipe(res)
})
- var doodleWrite = fs.createWriteStream(path.join(__dirname, 'test.png'))
+ request.get(s.url + '/catresp', function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.headers['content-type'], 'text/plain-test')
+ t.equal(res.headers['content-length'], '4')
+ t.end()
+ })
+})
+
+tape('writing to file', function (t) {
+ var doodleWrite = fs.createWriteStream(path.join(__dirname, 'test.jpg'))
- counter++
- request.get('http://localhost:3453/doodle').pipe(doodleWrite)
+ request.get(s.url + '/doodle').pipe(doodleWrite)
doodleWrite.on('close', function () {
- assert.deepEqual(fs.readFileSync(path.join(__dirname, 'googledoodle.png')), fs.readFileSync(path.join(__dirname, 'test.png')))
- check()
+ t.deepEqual(
+ fs.readFileSync(path.join(__dirname, 'googledoodle.jpg')),
+ fs.readFileSync(path.join(__dirname, 'test.jpg')))
+ fs.unlinkSync(path.join(__dirname, 'test.jpg'))
+ t.end()
+ })
+})
+
+tape('one-line proxy', function (t) {
+ s.once('/onelineproxy', function (req, res) {
+ var x = request(s.url + '/doodle')
+ req.pipe(x)
+ x.pipe(res)
})
- process.on('exit', function () {
- fs.unlinkSync(path.join(__dirname, 'test.png'))
+ request.get({
+ uri: s.url + '/onelineproxy',
+ headers: { 'x-oneline-proxy': 'nope' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.headers['x-oneline-proxy'], 'yup')
+ t.equal(body, fs.readFileSync(path.join(__dirname, 'googledoodle.jpg')).toString())
+ t.end()
})
+})
- counter++
- request.get({uri:'http://localhost:3453/onelineproxy', headers:{'x-oneline-proxy':'nope'}}, function (err, resp, body) {
- assert.equal(resp.headers['x-oneline-proxy'], 'yup')
- check()
+tape('piping after response', function (t) {
+ s.once('/afterresponse', function (req, res) {
+ res.write('d')
+ res.end()
})
- s.on('/afterresponse', function (req, resp) {
- resp.write('d')
- resp.end()
+ var rAfterRes = request.post(s.url + '/afterresponse')
+
+ rAfterRes.on('response', function () {
+ var v = new ValidationStream(t, 'd')
+ rAfterRes.pipe(v)
+ v.on('end', function () {
+ t.end()
+ })
})
+})
- counter++
- var afterresp = request.post('http://localhost:3453/afterresponse').on('response', function () {
- var v = new ValidationStream('d')
- afterresp.pipe(v)
- v.on('end', check)
+tape('piping through a redirect', function (t) {
+ s.once('/forward1', function (req, res) {
+ res.writeHead(302, { location: '/forward2' })
+ res.end()
})
-
- s.on('/forward1', function (req, resp) {
- resp.writeHead(302, {location:'/forward2'})
- resp.end()
+ s.once('/forward2', function (req, res) {
+ res.writeHead('200', { 'content-type': 'image/png' })
+ res.write('d')
+ res.end()
})
- s.on('/forward2', function (req, resp) {
- resp.writeHead('200', {'content-type':'image/png'})
- resp.write('d')
- resp.end()
+
+ var validateForward = new ValidationStream(t, 'd')
+
+ request.get(s.url + '/forward1').pipe(validateForward)
+
+ validateForward.on('end', function () {
+ t.end()
})
-
- counter++
- var validateForward = new ValidationStream('d')
- validateForward.on('end', check)
- request.get('http://localhost:3453/forward1').pipe(validateForward)
+})
+
+tape('pipe options', function (t) {
+ s.once('/opts', server.createGetResponse('opts response'))
- // Test pipe options
- s.once('/opts', server.createGetResponse('opts response'));
+ var optsStream = new stream.Stream()
+ var optsData = ''
- var optsStream = new stream.Stream();
optsStream.writable = true
-
- var optsData = '';
optsStream.write = function (buf) {
- optsData += buf;
+ optsData += buf
if (optsData === 'opts response') {
- setTimeout(check, 10);
+ setTimeout(function () {
+ t.end()
+ }, 10)
}
}
-
optsStream.end = function () {
- assert.fail('end called')
- };
+ t.fail('end called')
+ }
+
+ request({
+ url: s.url + '/opts'
+ }).pipe(optsStream, { end: false })
+})
+
+tape('request.pipefilter is called correctly', function (t) {
+ s.once('/pipefilter', function (req, res) {
+ res.end('d')
+ })
+ var validatePipeFilter = new ValidationStream(t, 'd')
- counter++
- request({url:'http://localhost:3453/opts'}).pipe(optsStream, { end : false })
+ var r3 = request.get(s.url + '/pipefilter')
+ r3.pipe(validatePipeFilter)
+ r3.pipefilter = function (res, dest) {
+ t.equal(res, r3.response)
+ t.equal(dest, validatePipeFilter)
+ t.end()
+ }
+})
+
+tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
})
diff --git a/tests/test-pool.js b/tests/test-pool.js
new file mode 100644
index 000000000..f2d96bd1f
--- /dev/null
+++ b/tests/test-pool.js
@@ -0,0 +1,148 @@
+'use strict'
+
+var request = require('../index')
+var http = require('http')
+var tape = require('tape')
+
+var s = http.createServer(function (req, res) {
+ res.statusCode = 200
+ res.end('asdf')
+})
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ s.url = 'http://localhost:' + this.address().port
+ t.end()
+ })
+})
+
+tape('pool', function (t) {
+ request({
+ url: s.url,
+ pool: false
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'asdf')
+
+ var agent = res.request.agent
+ t.equal(agent, false)
+ t.end()
+ })
+})
+
+tape('forever', function (t) {
+ var r = request({
+ url: s.url,
+ forever: true,
+ pool: {maxSockets: 1024}
+ }, function (err, res, body) {
+ // explicitly shut down the agent
+ if (typeof r.agent.destroy === 'function') {
+ r.agent.destroy()
+ } else {
+ // node < 0.12
+ Object.keys(r.agent.sockets).forEach(function (name) {
+ r.agent.sockets[name].forEach(function (socket) {
+ socket.end()
+ })
+ })
+ }
+
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'asdf')
+
+ var agent = res.request.agent
+ t.equal(agent.maxSockets, 1024)
+ t.end()
+ })
+})
+
+tape('forever, should use same agent in sequential requests', function (t) {
+ var r = request.defaults({
+ forever: true
+ })
+ var req1 = r(s.url)
+ var req2 = r(s.url + '/somepath')
+ req1.abort()
+ req2.abort()
+ if (typeof req1.agent.destroy === 'function') {
+ req1.agent.destroy()
+ }
+ if (typeof req2.agent.destroy === 'function') {
+ req2.agent.destroy()
+ }
+ t.equal(req1.agent, req2.agent)
+ t.end()
+})
+
+tape('forever, should use same agent in sequential requests(with pool.maxSockets)', function (t) {
+ var r = request.defaults({
+ forever: true,
+ pool: {maxSockets: 1024}
+ })
+ var req1 = r(s.url)
+ var req2 = r(s.url + '/somepath')
+ req1.abort()
+ req2.abort()
+ if (typeof req1.agent.destroy === 'function') {
+ req1.agent.destroy()
+ }
+ if (typeof req2.agent.destroy === 'function') {
+ req2.agent.destroy()
+ }
+ t.equal(req1.agent.maxSockets, 1024)
+ t.equal(req1.agent, req2.agent)
+ t.end()
+})
+
+tape('forever, should use same agent in request() and request.verb', function (t) {
+ var r = request.defaults({
+ forever: true,
+ pool: {maxSockets: 1024}
+ })
+ var req1 = r(s.url)
+ var req2 = r.get(s.url)
+ req1.abort()
+ req2.abort()
+ if (typeof req1.agent.destroy === 'function') {
+ req1.agent.destroy()
+ }
+ if (typeof req2.agent.destroy === 'function') {
+ req2.agent.destroy()
+ }
+ t.equal(req1.agent.maxSockets, 1024)
+ t.equal(req1.agent, req2.agent)
+ t.end()
+})
+
+tape('should use different agent if pool option specified', function (t) {
+ var r = request.defaults({
+ forever: true,
+ pool: {maxSockets: 1024}
+ })
+ var req1 = r(s.url)
+ var req2 = r.get({
+ url: s.url,
+ pool: {maxSockets: 20}
+ })
+ req1.abort()
+ req2.abort()
+ if (typeof req1.agent.destroy === 'function') {
+ req1.agent.destroy()
+ }
+ if (typeof req2.agent.destroy === 'function') {
+ req2.agent.destroy()
+ }
+ t.equal(req1.agent.maxSockets, 1024)
+ t.equal(req2.agent.maxSockets, 20)
+ t.notEqual(req1.agent, req2.agent)
+ t.end()
+})
+
+tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-promise.js b/tests/test-promise.js
new file mode 100644
index 000000000..028d15fbb
--- /dev/null
+++ b/tests/test-promise.js
@@ -0,0 +1,53 @@
+'use strict'
+
+var http = require('http')
+var request = require('../index')
+var tape = require('tape')
+var Promise = require('bluebird')
+
+var s = http.createServer(function (req, res) {
+ res.writeHead(200, {})
+ res.end('ok')
+})
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ s.url = 'http://localhost:' + this.address().port
+ t.end()
+ })
+})
+
+tape('promisify convenience method', function (t) {
+ var get = request.get
+ var p = Promise.promisify(get, {multiArgs: true})
+ p(s.url)
+ .then(function (results) {
+ var res = results[0]
+ t.equal(res.statusCode, 200)
+ t.end()
+ })
+})
+
+tape('promisify request function', function (t) {
+ var p = Promise.promisify(request, {multiArgs: true})
+ p(s.url)
+ .spread(function (res, body) {
+ t.equal(res.statusCode, 200)
+ t.end()
+ })
+})
+
+tape('promisify all methods', function (t) {
+ Promise.promisifyAll(request, {multiArgs: true})
+ request.getAsync(s.url)
+ .spread(function (res, body) {
+ t.equal(res.statusCode, 200)
+ t.end()
+ })
+})
+
+tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-proxy-connect.js b/tests/test-proxy-connect.js
new file mode 100644
index 000000000..06800d00e
--- /dev/null
+++ b/tests/test-proxy-connect.js
@@ -0,0 +1,80 @@
+'use strict'
+
+var request = require('../index')
+var tape = require('tape')
+
+var called = false
+var proxiedHost = 'google.com'
+var data = ''
+
+var s = require('net').createServer(function (sock) {
+ called = true
+ sock.once('data', function (c) {
+ data += c
+
+ sock.write('HTTP/1.1 200 OK\r\n\r\n')
+
+ sock.once('data', function (c) {
+ data += c
+
+ sock.write('HTTP/1.1 200 OK\r\n')
+ sock.write('content-type: text/plain\r\n')
+ sock.write('content-length: 5\r\n')
+ sock.write('\r\n')
+ sock.end('derp\n')
+ })
+ })
+})
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ s.url = 'http://localhost:' + this.address().port
+ t.end()
+ })
+})
+
+tape('proxy', function (t) {
+ request({
+ tunnel: true,
+ url: 'http://' + proxiedHost,
+ proxy: s.url,
+ headers: {
+ 'Proxy-Authorization': 'Basic dXNlcjpwYXNz',
+ 'authorization': 'Token deadbeef',
+ 'dont-send-to-proxy': 'ok',
+ 'dont-send-to-dest': 'ok',
+ 'accept': 'yo',
+ 'user-agent': 'just another foobar'
+ },
+ proxyHeaderExclusiveList: ['Dont-send-to-dest']
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'derp\n')
+ var re = new RegExp([
+ 'CONNECT google.com:80 HTTP/1.1',
+ 'Proxy-Authorization: Basic dXNlcjpwYXNz',
+ 'dont-send-to-dest: ok',
+ 'accept: yo',
+ 'user-agent: just another foobar',
+ 'host: google.com:80',
+ 'Connection: close',
+ '',
+ 'GET / HTTP/1.1',
+ 'authorization: Token deadbeef',
+ 'dont-send-to-proxy: ok',
+ 'accept: yo',
+ 'user-agent: just another foobar',
+ 'host: google.com'
+ ].join('\r\n'))
+ t.equal(true, re.test(data))
+ t.equal(called, true, 'the request must be made to the proxy server')
+ t.end()
+ })
+})
+
+tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-proxy.js b/tests/test-proxy.js
index 647157cae..77cb7a831 100644
--- a/tests/test-proxy.js
+++ b/tests/test-proxy.js
@@ -1,39 +1,304 @@
+'use strict'
+
var server = require('./server')
- , events = require('events')
- , stream = require('stream')
- , assert = require('assert')
- , fs = require('fs')
- , request = require('../main.js')
- , path = require('path')
- , util = require('util')
- ;
-
-var port = 6768
- , called = false
- , proxiedHost = 'google.com'
- ;
-
-var s = server.createServer(port)
-s.listen(port, function () {
- s.on('http://google.com/', function (req, res) {
- called = true
- assert.equal(req.headers.host, proxiedHost)
+var request = require('../index')
+var tape = require('tape')
+
+var s = server.createServer()
+var currResponseHandler
+
+['http://google.com/', 'https://google.com/'].forEach(function (url) {
+ s.on(url, function (req, res) {
+ currResponseHandler(req, res)
res.writeHeader(200)
- res.end()
- })
- request ({
- url: 'http://'+proxiedHost,
- proxy: 'http://localhost:'+port
- /*
- //should behave as if these arguments where passed:
- url: 'http://localhost:'+port,
- headers: {host: proxiedHost}
- //*/
- }, function (err, res, body) {
- s.close()
+ res.end('ok')
})
})
-process.on('exit', function () {
- assert.ok(called, 'the request must be made to the proxy server')
+var proxyEnvVars = [
+ 'http_proxy',
+ 'HTTP_PROXY',
+ 'https_proxy',
+ 'HTTPS_PROXY',
+ 'no_proxy',
+ 'NO_PROXY'
+]
+
+// Set up and run a proxy test. All environment variables pertaining to
+// proxies will be deleted before each test. Specify environment variables as
+// `options.env`; all other keys on `options` will be passed as additional
+// options to `request`.
+//
+// If `responseHandler` is a function, it should perform asserts on the server
+// response. It will be called with parameters (t, req, res). Otherwise,
+// `responseHandler` should be truthy to indicate that the proxy should be used
+// for this request, or falsy to indicate that the proxy should not be used for
+// this request.
+function runTest (name, options, responseHandler) {
+ tape(name, function (t) {
+ proxyEnvVars.forEach(function (v) {
+ delete process.env[v]
+ })
+ if (options.env) {
+ for (var v in options.env) {
+ process.env[v] = options.env[v]
+ }
+ delete options.env
+ }
+
+ var called = false
+ currResponseHandler = function (req, res) {
+ if (responseHandler) {
+ called = true
+ t.equal(req.headers.host, 'google.com')
+ if (typeof responseHandler === 'function') {
+ responseHandler(t, req, res)
+ }
+ } else {
+ t.fail('proxy response should not be called')
+ }
+ }
+
+ options.url = options.url || 'http://google.com'
+ request(options, function (err, res, body) {
+ if (responseHandler && !called) {
+ t.fail('proxy response should be called')
+ }
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ if (responseHandler) {
+ if (body.length > 100) {
+ body = body.substring(0, 100)
+ }
+ t.equal(body, 'ok')
+ } else {
+ t.equal(/^/i.test(body), true)
+ }
+ t.end()
+ })
+ })
+}
+
+function addTests () {
+ // If the `runTest` function is changed, run the following command and make
+ // sure both of these tests fail:
+ //
+ // TEST_PROXY_HARNESS=y node tests/test-proxy.js
+
+ if (process.env.TEST_PROXY_HARNESS) {
+ runTest('should fail with "proxy response should not be called"', {
+ proxy: s.url
+ }, false)
+
+ runTest('should fail with "proxy response should be called"', {
+ proxy: null
+ }, true)
+ } else {
+ // Run the real tests
+
+ runTest('basic proxy', {
+ proxy: s.url,
+ headers: {
+ 'proxy-authorization': 'Token Fooblez'
+ }
+ }, function (t, req, res) {
+ t.equal(req.headers['proxy-authorization'], 'Token Fooblez')
+ })
+
+ runTest('proxy auth without uri auth', {
+ proxy: 'http://user:pass@localhost:' + s.port
+ }, function (t, req, res) {
+ t.equal(req.headers['proxy-authorization'], 'Basic dXNlcjpwYXNz')
+ })
+
+ // http: urls and basic proxy settings
+
+ runTest('HTTP_PROXY environment variable and http: url', {
+ env: { HTTP_PROXY: s.url }
+ }, true)
+
+ runTest('http_proxy environment variable and http: url', {
+ env: { http_proxy: s.url }
+ }, true)
+
+ runTest('HTTPS_PROXY environment variable and http: url', {
+ env: { HTTPS_PROXY: s.url }
+ }, false)
+
+ runTest('https_proxy environment variable and http: url', {
+ env: { https_proxy: s.url }
+ }, false)
+
+ // https: urls and basic proxy settings
+
+ runTest('HTTP_PROXY environment variable and https: url', {
+ env: { HTTP_PROXY: s.url },
+ url: 'https://google.com',
+ tunnel: false,
+ pool: false
+ }, true)
+
+ runTest('http_proxy environment variable and https: url', {
+ env: { http_proxy: s.url },
+ url: 'https://google.com',
+ tunnel: false
+ }, true)
+
+ runTest('HTTPS_PROXY environment variable and https: url', {
+ env: { HTTPS_PROXY: s.url },
+ url: 'https://google.com',
+ tunnel: false
+ }, true)
+
+ runTest('https_proxy environment variable and https: url', {
+ env: { https_proxy: s.url },
+ url: 'https://google.com',
+ tunnel: false
+ }, true)
+
+ runTest('multiple environment variables and https: url', {
+ env: {
+ HTTPS_PROXY: s.url,
+ HTTP_PROXY: 'http://localhost:0/'
+ },
+ url: 'https://google.com',
+ tunnel: false
+ }, true)
+
+ // no_proxy logic
+
+ runTest('NO_PROXY hostnames are case insensitive', {
+ env: {
+ HTTP_PROXY: s.url,
+ NO_PROXY: 'GOOGLE.COM'
+ }
+ }, false)
+
+ runTest('NO_PROXY hostnames are case insensitive 2', {
+ env: {
+ http_proxy: s.url,
+ NO_PROXY: 'GOOGLE.COM'
+ }
+ }, false)
+
+ runTest('NO_PROXY hostnames are case insensitive 3', {
+ env: {
+ HTTP_PROXY: s.url,
+ no_proxy: 'GOOGLE.COM'
+ }
+ }, false)
+
+ runTest('NO_PROXY ignored with explicit proxy passed', {
+ env: { NO_PROXY: '*' },
+ proxy: s.url
+ }, true)
+
+ runTest('NO_PROXY overrides HTTP_PROXY for specific hostname', {
+ env: {
+ HTTP_PROXY: s.url,
+ NO_PROXY: 'google.com'
+ }
+ }, false)
+
+ runTest('no_proxy overrides HTTP_PROXY for specific hostname', {
+ env: {
+ HTTP_PROXY: s.url,
+ no_proxy: 'google.com'
+ }
+ }, false)
+
+ runTest('NO_PROXY does not override HTTP_PROXY if no hostnames match', {
+ env: {
+ HTTP_PROXY: s.url,
+ NO_PROXY: 'foo.bar,bar.foo'
+ }
+ }, true)
+
+ runTest('NO_PROXY overrides HTTP_PROXY if a hostname matches', {
+ env: {
+ HTTP_PROXY: s.url,
+ NO_PROXY: 'foo.bar,google.com'
+ }
+ }, false)
+
+ runTest('NO_PROXY allows an explicit port', {
+ env: {
+ HTTP_PROXY: s.url,
+ NO_PROXY: 'google.com:80'
+ }
+ }, false)
+
+ runTest('NO_PROXY only overrides HTTP_PROXY if the port matches', {
+ env: {
+ HTTP_PROXY: s.url,
+ NO_PROXY: 'google.com:1234'
+ }
+ }, true)
+
+ runTest('NO_PROXY=* should override HTTP_PROXY for all hosts', {
+ env: {
+ HTTP_PROXY: s.url,
+ NO_PROXY: '*'
+ }
+ }, false)
+
+ runTest('NO_PROXY should override HTTP_PROXY for all subdomains', {
+ env: {
+ HTTP_PROXY: s.url,
+ NO_PROXY: 'google.com'
+ },
+ headers: { host: 'www.google.com' }
+ }, false)
+
+ runTest('NO_PROXY should not override HTTP_PROXY for partial domain matches', {
+ env: {
+ HTTP_PROXY: s.url,
+ NO_PROXY: 'oogle.com'
+ }
+ }, true)
+
+ runTest('NO_PROXY with port should not override HTTP_PROXY for partial domain matches', {
+ env: {
+ HTTP_PROXY: s.url,
+ NO_PROXY: 'oogle.com:80'
+ }
+ }, true)
+
+ // misc
+
+ // this fails if the check 'isMatchedAt > -1' in lib/getProxyFromURI.js is
+ // missing or broken
+ runTest('http_proxy with length of one more than the URL', {
+ env: {
+ HTTP_PROXY: s.url,
+ NO_PROXY: 'elgoog1.com' // one more char than google.com
+ }
+ }, true)
+
+ runTest('proxy: null should override HTTP_PROXY', {
+ env: { HTTP_PROXY: s.url },
+ proxy: null,
+ timeout: 500
+ }, false)
+
+ runTest('uri auth without proxy auth', {
+ url: 'http://user:pass@google.com',
+ proxy: s.url
+ }, function (t, req, res) {
+ t.equal(req.headers['proxy-authorization'], undefined)
+ t.equal(req.headers.authorization, 'Basic dXNlcjpwYXNz')
+ })
+ }
+}
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ addTests()
+ tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
+ })
+ t.end()
+ })
})
diff --git a/tests/test-qs.js b/tests/test-qs.js
index 1aac22bc9..f70c685db 100644
--- a/tests/test-qs.js
+++ b/tests/test-qs.js
@@ -1,28 +1,135 @@
-var request = request = require('../main.js')
- , assert = require('assert')
- ;
-
-
-// Test adding a querystring
-var req1 = request.get({ uri: 'http://www.google.com', qs: { q : 'search' }})
-setTimeout(function() {
- assert.equal('/?q=search', req1.path)
-}, 1)
-
-// Test replacing a querystring value
-var req2 = request.get({ uri: 'http://www.google.com?q=abc', qs: { q : 'search' }})
-setTimeout(function() {
- assert.equal('/?q=search', req2.path)
-}, 1)
-
-// Test appending a querystring value to the ones present in the uri
-var req3 = request.get({ uri: 'http://www.google.com?x=y', qs: { q : 'search' }})
-setTimeout(function() {
- assert.equal('/?x=y&q=search', req3.path)
-}, 1)
-
-// Test leaving a querystring alone
-var req4 = request.get({ uri: 'http://www.google.com?x=y'})
-setTimeout(function() {
- assert.equal('/?x=y', req4.path)
-}, 1)
+'use strict'
+
+var request = require('../index')
+var tape = require('tape')
+
+// Run a querystring test. `options` can have the following keys:
+// - suffix : a string to be added to the URL
+// - qs : an object to be passed to request's `qs` option
+// - qsParseOptions : an object to be passed to request's `qsParseOptions` option
+// - qsStringifyOptions : an object to be passed to request's `qsStringifyOptions` option
+// - afterRequest : a function to execute after creating the request
+// - expected : the expected path of the request
+// - expectedQuerystring : expected path when using the querystring library
+function runTest (name, options) {
+ var uri = 'http://www.google.com' + (options.suffix || '')
+ var opts = {
+ uri: uri,
+ qsParseOptions: options.qsParseOptions,
+ qsStringifyOptions: options.qsStringifyOptions
+ }
+
+ if (options.qs) {
+ opts.qs = options.qs
+ }
+
+ tape(name + ' - using qs', function (t) {
+ var r = request.get(opts)
+ if (typeof options.afterRequest === 'function') {
+ options.afterRequest(r)
+ }
+ process.nextTick(function () {
+ t.equal(r.path, options.expected)
+ r.abort()
+ t.end()
+ })
+ })
+
+ tape(name + ' - using querystring', function (t) {
+ opts.useQuerystring = true
+ var r = request.get(opts)
+ if (typeof options.afterRequest === 'function') {
+ options.afterRequest(r)
+ }
+ process.nextTick(function () {
+ t.equal(r.path, options.expectedQuerystring || options.expected)
+ r.abort()
+ t.end()
+ })
+ })
+}
+
+function esc (str) {
+ return str
+ .replace(/\[/g, '%5B')
+ .replace(/\]/g, '%5D')
+}
+
+runTest('adding a querystring', {
+ qs: { q: 'search' },
+ expected: '/?q=search'
+})
+
+runTest('replacing a querystring value', {
+ suffix: '?q=abc',
+ qs: { q: 'search' },
+ expected: '/?q=search'
+})
+
+runTest('appending a querystring value to the ones present in the uri', {
+ suffix: '?x=y',
+ qs: { q: 'search' },
+ expected: '/?x=y&q=search'
+})
+
+runTest('leaving a querystring alone', {
+ suffix: '?x=y',
+ expected: '/?x=y'
+})
+
+runTest('giving empty qs property', {
+ qs: {},
+ expected: '/'
+})
+
+runTest('modifying the qs after creating the request', {
+ qs: {},
+ afterRequest: function (r) {
+ r.qs({ q: 'test' })
+ },
+ expected: '/?q=test'
+})
+
+runTest('a query with an object for a value', {
+ qs: { where: { foo: 'bar' } },
+ expected: esc('/?where[foo]=bar'),
+ expectedQuerystring: '/?where='
+})
+
+runTest('a query with an array for a value', {
+ qs: { order: ['bar', 'desc'] },
+ expected: esc('/?order[0]=bar&order[1]=desc'),
+ expectedQuerystring: '/?order=bar&order=desc'
+})
+
+runTest('pass options to the qs module via the qsParseOptions key', {
+ suffix: '?a=1;b=2',
+ qs: {},
+ qsParseOptions: { delimiter: ';' },
+ qsStringifyOptions: { delimiter: ';' },
+ expected: esc('/?a=1;b=2'),
+ expectedQuerystring: '/?a=1%3Bb%3D2'
+})
+
+runTest('pass options to the qs module via the qsStringifyOptions key', {
+ qs: { order: ['bar', 'desc'] },
+ qsStringifyOptions: { arrayFormat: 'brackets' },
+ expected: esc('/?order[]=bar&order[]=desc'),
+ expectedQuerystring: '/?order=bar&order=desc'
+})
+
+runTest('pass options to the querystring module via the qsParseOptions key', {
+ suffix: '?a=1;b=2',
+ qs: {},
+ qsParseOptions: { sep: ';' },
+ qsStringifyOptions: { sep: ';' },
+ expected: esc('/?a=1%3Bb%3D2'),
+ expectedQuerystring: '/?a=1;b=2'
+})
+
+runTest('pass options to the querystring module via the qsStringifyOptions key', {
+ qs: { order: ['bar', 'desc'] },
+ qsStringifyOptions: { sep: ';' },
+ expected: esc('/?order[0]=bar&order[1]=desc'),
+ expectedQuerystring: '/?order=bar;order=desc'
+})
diff --git a/tests/test-redirect-auth.js b/tests/test-redirect-auth.js
new file mode 100644
index 000000000..7aef6edcc
--- /dev/null
+++ b/tests/test-redirect-auth.js
@@ -0,0 +1,131 @@
+'use strict'
+
+var server = require('./server')
+var request = require('../index')
+var util = require('util')
+var tape = require('tape')
+var destroyable = require('server-destroy')
+
+var s = server.createServer()
+var ss = server.createSSLServer()
+
+destroyable(s)
+destroyable(ss)
+
+// always send basic auth and allow non-strict SSL
+request = request.defaults({
+ auth: {
+ user: 'test',
+ pass: 'testing'
+ },
+ rejectUnauthorized: false
+})
+
+// redirect.from(proto, host).to(proto, host) returns an object with keys:
+// src : source URL
+// dst : destination URL
+var redirect = {
+ from: function (fromProto, fromHost) {
+ return {
+ to: function (toProto, toHost) {
+ var fromPort = (fromProto === 'http' ? s.port : ss.port)
+ var toPort = (toProto === 'http' ? s.port : ss.port)
+ return {
+ src: util.format(
+ '%s://%s:%d/to/%s/%s',
+ fromProto, fromHost, fromPort, toProto, toHost),
+ dst: util.format(
+ '%s://%s:%d/from/%s/%s',
+ toProto, toHost, toPort, fromProto, fromHost)
+ }
+ }
+ }
+ }
+}
+
+function handleRequests (srv) {
+ ['http', 'https'].forEach(function (proto) {
+ ['localhost', '127.0.0.1'].forEach(function (host) {
+ srv.on(util.format('/to/%s/%s', proto, host), function (req, res) {
+ var r = redirect
+ .from(srv.protocol, req.headers.host.split(':')[0])
+ .to(proto, host)
+ res.writeHead(301, {
+ location: r.dst
+ })
+ res.end()
+ })
+
+ srv.on(util.format('/from/%s/%s', proto, host), function (req, res) {
+ res.end('auth: ' + (req.headers.authorization || '(nothing)'))
+ })
+ })
+ })
+}
+
+handleRequests(s)
+handleRequests(ss)
+
+function runTest (name, redir, expectAuth) {
+ tape('redirect to ' + name, function (t) {
+ request(redir.src, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.request.uri.href, redir.dst)
+ t.equal(res.statusCode, 200)
+ t.equal(body, expectAuth
+ ? 'auth: Basic dGVzdDp0ZXN0aW5n'
+ : 'auth: (nothing)')
+ t.end()
+ })
+ })
+}
+
+function addTests () {
+ runTest('same host and protocol',
+ redirect.from('http', 'localhost').to('http', 'localhost'),
+ true)
+
+ runTest('same host different protocol',
+ redirect.from('http', 'localhost').to('https', 'localhost'),
+ true)
+
+ runTest('different host same protocol',
+ redirect.from('https', '127.0.0.1').to('https', 'localhost'),
+ false)
+
+ runTest('different host and protocol',
+ redirect.from('http', 'localhost').to('https', '127.0.0.1'),
+ false)
+}
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ ss.listen(0, function () {
+ addTests()
+ tape('cleanup', function (t) {
+ s.destroy(function () {
+ ss.destroy(function () {
+ t.end()
+ })
+ })
+ })
+ t.end()
+ })
+ })
+})
+
+tape('redirect URL helper', function (t) {
+ t.deepEqual(
+ redirect.from('http', 'localhost').to('https', '127.0.0.1'),
+ {
+ src: util.format('http://localhost:%d/to/https/127.0.0.1', s.port),
+ dst: util.format('https://127.0.0.1:%d/from/http/localhost', ss.port)
+ })
+ t.deepEqual(
+ redirect.from('https', 'localhost').to('http', 'localhost'),
+ {
+ src: util.format('https://localhost:%d/to/http/localhost', ss.port),
+ dst: util.format('http://localhost:%d/from/https/localhost', s.port)
+ })
+ t.end()
+})
diff --git a/tests/test-redirect-complex.js b/tests/test-redirect-complex.js
new file mode 100644
index 000000000..072b5986c
--- /dev/null
+++ b/tests/test-redirect-complex.js
@@ -0,0 +1,93 @@
+'use strict'
+
+var server = require('./server')
+var request = require('../index')
+var events = require('events')
+var tape = require('tape')
+var destroyable = require('server-destroy')
+
+var s = server.createServer()
+var ss = server.createSSLServer()
+var e = new events.EventEmitter()
+
+destroyable(s)
+destroyable(ss)
+
+function bouncy (s, serverUrl) {
+ var redirs = {
+ a: 'b',
+ b: 'c',
+ c: 'd',
+ d: 'e',
+ e: 'f',
+ f: 'g',
+ g: 'h',
+ h: 'end'
+ }
+
+ var perm = true
+ Object.keys(redirs).forEach(function (p) {
+ var t = redirs[p]
+
+ // switch type each time
+ var type = perm ? 301 : 302
+ perm = !perm
+ s.on('/' + p, function (req, res) {
+ setTimeout(function () {
+ res.writeHead(type, { location: serverUrl + '/' + t })
+ res.end()
+ }, Math.round(Math.random() * 25))
+ })
+ })
+
+ s.on('/end', function (req, res) {
+ var key = req.headers['x-test-key']
+ e.emit('hit-' + key, key)
+ res.writeHead(200)
+ res.end(key)
+ })
+}
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ ss.listen(0, function () {
+ bouncy(s, ss.url)
+ bouncy(ss, s.url)
+ t.end()
+ })
+ })
+})
+
+tape('lots of redirects', function (t) {
+ var n = 10
+ t.plan(n * 4)
+
+ function doRedirect (i) {
+ var key = 'test_' + i
+ request({
+ url: (i % 2 ? s.url : ss.url) + '/a',
+ headers: { 'x-test-key': key },
+ rejectUnauthorized: false
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, key)
+ })
+
+ e.once('hit-' + key, function (v) {
+ t.equal(v, key)
+ })
+ }
+
+ for (var i = 0; i < n; i++) {
+ doRedirect(i)
+ }
+})
+
+tape('cleanup', function (t) {
+ s.destroy(function () {
+ ss.destroy(function () {
+ t.end()
+ })
+ })
+})
diff --git a/tests/test-redirect.js b/tests/test-redirect.js
index b84844a79..b7b5ca676 100644
--- a/tests/test-redirect.js
+++ b/tests/test-redirect.js
@@ -1,154 +1,449 @@
+'use strict'
+
var server = require('./server')
- , assert = require('assert')
- , request = require('../main.js')
- , Cookie = require('../vendor/cookie')
- , Jar = require('../vendor/cookie/jar')
+var assert = require('assert')
+var request = require('../index')
+var tape = require('tape')
+var http = require('http')
+var destroyable = require('server-destroy')
var s = server.createServer()
+var ss = server.createSSLServer()
+var hits = {}
+var jar = request.jar()
+
+destroyable(s)
+destroyable(ss)
+
+s.on('/ssl', function (req, res) {
+ res.writeHead(302, {
+ location: ss.url + '/'
+ })
+ res.end()
+})
+
+ss.on('/', function (req, res) {
+ res.writeHead(200)
+ res.end('SSL')
+})
-s.listen(s.port, function () {
- var server = 'http://localhost:' + s.port;
- var hits = {}
- var passed = 0;
-
- bouncer(301, 'temp')
- bouncer(302, 'perm')
- bouncer(302, 'nope')
-
- function bouncer(code, label) {
- var landing = label+'_landing';
-
- s.on('/'+label, function (req, res) {
- hits[label] = true;
- res.writeHead(code, {
- 'location':server + '/'+landing,
- 'set-cookie': 'ham=eggs'
- })
- res.end()
+function createRedirectEndpoint (code, label, landing) {
+ s.on('/' + label, function (req, res) {
+ hits[label] = true
+ res.writeHead(code, {
+ 'location': s.url + '/' + landing,
+ 'set-cookie': 'ham=eggs'
})
+ res.end()
+ })
+}
+
+function createLandingEndpoint (landing) {
+ s.on('/' + landing, function (req, res) {
+ // Make sure the cookie doesn't get included twice, see #139:
+ // Make sure cookies are set properly after redirect
+ assert.equal(req.headers.cookie, 'foo=bar; quux=baz; ham=eggs')
+ hits[landing] = true
+ res.writeHead(200, {'x-response': req.method.toUpperCase() + ' ' + landing})
+ res.end(req.method.toUpperCase() + ' ' + landing)
+ })
+}
+
+function bouncer (code, label, hops) {
+ var hop
+ var landing = label + '_landing'
+ var currentLabel
+ var currentLanding
+
+ hops = hops || 1
- s.on('/'+landing, function (req, res) {
- if (req.method !== 'GET') { // We should only accept GET redirects
- console.error("Got a non-GET request to the redirect destination URL");
- res.writeHead(400);
- res.end();
- return;
- }
- // Make sure the cookie doesn't get included twice, see #139:
- // Make sure cookies are set properly after redirect
- assert.equal(req.headers.cookie, 'foo=bar; quux=baz; ham=eggs');
- hits[landing] = true;
- res.writeHead(200)
- res.end(landing)
+ if (hops === 1) {
+ createRedirectEndpoint(code, label, landing)
+ } else {
+ for (hop = 0; hop < hops; hop++) {
+ currentLabel = (hop === 0) ? label : label + '_' + (hop + 1)
+ currentLanding = (hop === hops - 1) ? landing : label + '_' + (hop + 2)
+
+ createRedirectEndpoint(code, currentLabel, currentLanding)
+ }
+ }
+
+ createLandingEndpoint(landing)
+}
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ ss.listen(0, function () {
+ bouncer(301, 'temp')
+ bouncer(301, 'double', 2)
+ bouncer(301, 'treble', 3)
+ bouncer(302, 'perm')
+ bouncer(302, 'nope')
+ bouncer(307, 'fwd')
+ t.end()
})
+ })
+})
+
+tape('permanent bounce', function (t) {
+ jar.setCookie('quux=baz', s.url)
+ hits = {}
+ request({
+ uri: s.url + '/perm',
+ jar: jar,
+ headers: { cookie: 'foo=bar' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.ok(hits.perm, 'Original request is to /perm')
+ t.ok(hits.perm_landing, 'Forward to permanent landing URL')
+ t.equal(body, 'GET perm_landing', 'Got permanent landing content')
+ t.end()
+ })
+})
+
+tape('preserve HEAD method when using followAllRedirects', function (t) {
+ jar.setCookie('quux=baz', s.url)
+ hits = {}
+ request({
+ method: 'HEAD',
+ uri: s.url + '/perm',
+ followAllRedirects: true,
+ jar: jar,
+ headers: { cookie: 'foo=bar' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.ok(hits.perm, 'Original request is to /perm')
+ t.ok(hits.perm_landing, 'Forward to permanent landing URL')
+ t.equal(res.headers['x-response'], 'HEAD perm_landing', 'Got permanent landing content')
+ t.end()
+ })
+})
+
+tape('temporary bounce', function (t) {
+ hits = {}
+ request({
+ uri: s.url + '/temp',
+ jar: jar,
+ headers: { cookie: 'foo=bar' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.ok(hits.temp, 'Original request is to /temp')
+ t.ok(hits.temp_landing, 'Forward to temporary landing URL')
+ t.equal(body, 'GET temp_landing', 'Got temporary landing content')
+ t.end()
+ })
+})
+
+tape('prevent bouncing', function (t) {
+ hits = {}
+ request({
+ uri: s.url + '/nope',
+ jar: jar,
+ headers: { cookie: 'foo=bar' },
+ followRedirect: false
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 302)
+ t.ok(hits.nope, 'Original request to /nope')
+ t.ok(!hits.nope_landing, 'No chasing the redirect')
+ t.equal(res.statusCode, 302, 'Response is the bounce itself')
+ t.end()
+ })
+})
+
+tape('should not follow post redirects by default', function (t) {
+ hits = {}
+ request.post(s.url + '/temp', {
+ jar: jar,
+ headers: { cookie: 'foo=bar' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 301)
+ t.ok(hits.temp, 'Original request is to /temp')
+ t.ok(!hits.temp_landing, 'No chasing the redirect when post')
+ t.equal(res.statusCode, 301, 'Response is the bounce itself')
+ t.end()
+ })
+})
+
+tape('should follow post redirects when followallredirects true', function (t) {
+ hits = {}
+ request.post({
+ uri: s.url + '/temp',
+ followAllRedirects: true,
+ jar: jar,
+ headers: { cookie: 'foo=bar' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.ok(hits.temp, 'Original request is to /temp')
+ t.ok(hits.temp_landing, 'Forward to temporary landing URL')
+ t.equal(body, 'GET temp_landing', 'Got temporary landing content')
+ t.end()
+ })
+})
+
+tape('should follow post redirects when followallredirects true and followOriginalHttpMethod is enabled', function (t) {
+ hits = {}
+ request.post({
+ uri: s.url + '/temp',
+ followAllRedirects: true,
+ followOriginalHttpMethod: true,
+ jar: jar,
+ headers: { cookie: 'foo=bar' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.ok(hits.temp, 'Original request is to /temp')
+ t.ok(hits.temp_landing, 'Forward to temporary landing URL')
+ t.equal(body, 'POST temp_landing', 'Got temporary landing content')
+ t.end()
+ })
+})
+
+tape('should not follow post redirects when followallredirects false', function (t) {
+ hits = {}
+ request.post({
+ uri: s.url + '/temp',
+ followAllRedirects: false,
+ jar: jar,
+ headers: { cookie: 'foo=bar' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 301)
+ t.ok(hits.temp, 'Original request is to /temp')
+ t.ok(!hits.temp_landing, 'No chasing the redirect')
+ t.equal(res.statusCode, 301, 'Response is the bounce itself')
+ t.end()
+ })
+})
+
+tape('should not follow delete redirects by default', function (t) {
+ hits = {}
+ request.del(s.url + '/temp', {
+ jar: jar,
+ headers: { cookie: 'foo=bar' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.ok(res.statusCode >= 301 && res.statusCode < 400, 'Status is a redirect')
+ t.ok(hits.temp, 'Original request is to /temp')
+ t.ok(!hits.temp_landing, 'No chasing the redirect when delete')
+ t.equal(res.statusCode, 301, 'Response is the bounce itself')
+ t.end()
+ })
+})
+
+tape('should not follow delete redirects even if followredirect is set to true', function (t) {
+ hits = {}
+ request.del(s.url + '/temp', {
+ followRedirect: true,
+ jar: jar,
+ headers: { cookie: 'foo=bar' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 301)
+ t.ok(hits.temp, 'Original request is to /temp')
+ t.ok(!hits.temp_landing, 'No chasing the redirect when delete')
+ t.equal(res.statusCode, 301, 'Response is the bounce itself')
+ t.end()
+ })
+})
+
+tape('should follow delete redirects when followallredirects true', function (t) {
+ hits = {}
+ request.del(s.url + '/temp', {
+ followAllRedirects: true,
+ jar: jar,
+ headers: { cookie: 'foo=bar' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.ok(hits.temp, 'Original request is to /temp')
+ t.ok(hits.temp_landing, 'Forward to temporary landing URL')
+ t.equal(body, 'GET temp_landing', 'Got temporary landing content')
+ t.end()
+ })
+})
+
+tape('should follow 307 delete redirects when followallredirects true', function (t) {
+ hits = {}
+ request.del(s.url + '/fwd', {
+ followAllRedirects: true,
+ jar: jar,
+ headers: { cookie: 'foo=bar' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.ok(hits.fwd, 'Original request is to /fwd')
+ t.ok(hits.fwd_landing, 'Forward to temporary landing URL')
+ t.equal(body, 'DELETE fwd_landing', 'Got temporary landing content')
+ t.end()
+ })
+})
+
+tape('double bounce', function (t) {
+ hits = {}
+ request({
+ uri: s.url + '/double',
+ jar: jar,
+ headers: { cookie: 'foo=bar' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.ok(hits.double, 'Original request is to /double')
+ t.ok(hits.double_2, 'Forward to temporary landing URL')
+ t.ok(hits.double_landing, 'Forward to landing URL')
+ t.equal(body, 'GET double_landing', 'Got temporary landing content')
+ t.end()
+ })
+})
+
+tape('double bounce terminated after first redirect', function (t) {
+ function filterDouble (response) {
+ return (response.headers.location || '').indexOf('double_2') === -1
+ }
+
+ hits = {}
+ request({
+ uri: s.url + '/double',
+ jar: jar,
+ headers: { cookie: 'foo=bar' },
+ followRedirect: filterDouble
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 301)
+ t.ok(hits.double, 'Original request is to /double')
+ t.equal(res.headers.location, s.url + '/double_2', 'Current location should be ' + s.url + '/double_2')
+ t.end()
+ })
+})
+
+tape('triple bounce terminated after second redirect', function (t) {
+ function filterTreble (response) {
+ return (response.headers.location || '').indexOf('treble_3') === -1
}
- // Permanent bounce
- var jar = new Jar()
- jar.add(new Cookie('quux=baz'))
- request({uri: server+'/perm', jar: jar, headers: {cookie: 'foo=bar'}}, function (er, res, body) {
- if (er) throw er
- if (res.statusCode !== 200) throw new Error('Status is not 200: '+res.statusCode)
- assert.ok(hits.perm, 'Original request is to /perm')
- assert.ok(hits.perm_landing, 'Forward to permanent landing URL')
- assert.equal(body, 'perm_landing', 'Got permanent landing content')
- passed += 1
- done()
- })
-
- // Temporary bounce
- request({uri: server+'/temp', jar: jar, headers: {cookie: 'foo=bar'}}, function (er, res, body) {
- if (er) throw er
- if (res.statusCode !== 200) throw new Error('Status is not 200: '+res.statusCode)
- assert.ok(hits.temp, 'Original request is to /temp')
- assert.ok(hits.temp_landing, 'Forward to temporary landing URL')
- assert.equal(body, 'temp_landing', 'Got temporary landing content')
- passed += 1
- done()
- })
-
- // Prevent bouncing.
- request({uri:server+'/nope', jar: jar, headers: {cookie: 'foo=bar'}, followRedirect:false}, function (er, res, body) {
- if (er) throw er
- if (res.statusCode !== 302) throw new Error('Status is not 302: '+res.statusCode)
- assert.ok(hits.nope, 'Original request to /nope')
- assert.ok(!hits.nope_landing, 'No chasing the redirect')
- assert.equal(res.statusCode, 302, 'Response is the bounce itself')
- passed += 1
- done()
- })
-
- // Should not follow post redirects by default
- request.post(server+'/temp', { jar: jar, headers: {cookie: 'foo=bar'}}, function (er, res, body) {
- if (er) throw er
- if (res.statusCode !== 301) throw new Error('Status is not 301: '+res.statusCode)
- assert.ok(hits.temp, 'Original request is to /temp')
- assert.ok(!hits.temp_landing, 'No chasing the redirect when post')
- assert.equal(res.statusCode, 301, 'Response is the bounce itself')
- passed += 1
- done()
- })
-
- // Should follow post redirects when followAllRedirects true
- request.post({uri:server+'/temp', followAllRedirects:true, jar: jar, headers: {cookie: 'foo=bar'}}, function (er, res, body) {
- if (er) throw er
- if (res.statusCode !== 200) throw new Error('Status is not 200: '+res.statusCode)
- assert.ok(hits.temp, 'Original request is to /temp')
- assert.ok(hits.temp_landing, 'Forward to temporary landing URL')
- assert.equal(body, 'temp_landing', 'Got temporary landing content')
- passed += 1
- done()
- })
-
- request.post({uri:server+'/temp', followAllRedirects:false, jar: jar, headers: {cookie: 'foo=bar'}}, function (er, res, body) {
- if (er) throw er
- if (res.statusCode !== 301) throw new Error('Status is not 301: '+res.statusCode)
- assert.ok(hits.temp, 'Original request is to /temp')
- assert.ok(!hits.temp_landing, 'No chasing the redirect')
- assert.equal(res.statusCode, 301, 'Response is the bounce itself')
- passed += 1
- done()
- })
-
- // Should not follow delete redirects by default
- request.del(server+'/temp', { jar: jar, headers: {cookie: 'foo=bar'}}, function (er, res, body) {
- if (er) throw er
- if (res.statusCode < 301) throw new Error('Status is not a redirect.')
- assert.ok(hits.temp, 'Original request is to /temp')
- assert.ok(!hits.temp_landing, 'No chasing the redirect when delete')
- assert.equal(res.statusCode, 301, 'Response is the bounce itself')
- passed += 1
- done()
- })
-
- // Should not follow delete redirects even if followRedirect is set to true
- request.del(server+'/temp', { followRedirect: true, jar: jar, headers: {cookie: 'foo=bar'}}, function (er, res, body) {
- if (er) throw er
- if (res.statusCode !== 301) throw new Error('Status is not 301: '+res.statusCode)
- assert.ok(hits.temp, 'Original request is to /temp')
- assert.ok(!hits.temp_landing, 'No chasing the redirect when delete')
- assert.equal(res.statusCode, 301, 'Response is the bounce itself')
- passed += 1
- done()
- })
-
- // Should follow delete redirects when followAllRedirects true
- request.del(server+'/temp', {followAllRedirects:true, jar: jar, headers: {cookie: 'foo=bar'}}, function (er, res, body) {
- if (er) throw er
- if (res.statusCode !== 200) throw new Error('Status is not 200: '+res.statusCode)
- assert.ok(hits.temp, 'Original request is to /temp')
- assert.ok(hits.temp_landing, 'Forward to temporary landing URL')
- assert.equal(body, 'temp_landing', 'Got temporary landing content')
- passed += 1
- done()
- })
-
- var reqs_done = 0;
- function done() {
- reqs_done += 1;
- if(reqs_done == 9) {
- console.log(passed + ' tests passed.')
- s.close()
+ hits = {}
+ request({
+ uri: s.url + '/treble',
+ jar: jar,
+ headers: { cookie: 'foo=bar' },
+ followRedirect: filterTreble
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 301)
+ t.ok(hits.treble, 'Original request is to /treble')
+ t.equal(res.headers.location, s.url + '/treble_3', 'Current location should be ' + s.url + '/treble_3')
+ t.end()
+ })
+})
+
+tape('http to https redirect', function (t) {
+ hits = {}
+ request.get({
+ uri: require('url').parse(s.url + '/ssl'),
+ rejectUnauthorized: false
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'SSL', 'Got SSL redirect')
+ t.end()
+ })
+})
+
+tape('should have referer header by default when following redirect', function (t) {
+ request.post({
+ uri: s.url + '/temp',
+ jar: jar,
+ followAllRedirects: true,
+ headers: { cookie: 'foo=bar' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.end()
+ })
+ .on('redirect', function () {
+ t.equal(this.headers.referer, s.url + '/temp')
+ })
+})
+
+tape('should not have referer header when removeRefererHeader is true', function (t) {
+ request.post({
+ uri: s.url + '/temp',
+ jar: jar,
+ followAllRedirects: true,
+ removeRefererHeader: true,
+ headers: { cookie: 'foo=bar' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.end()
+ })
+ .on('redirect', function () {
+ t.equal(this.headers.referer, undefined)
+ })
+})
+
+tape('should preserve referer header set in the initial request when removeRefererHeader is true', function (t) {
+ request.post({
+ uri: s.url + '/temp',
+ jar: jar,
+ followAllRedirects: true,
+ removeRefererHeader: true,
+ headers: { cookie: 'foo=bar', referer: 'http://awesome.com' }
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.end()
+ })
+ .on('redirect', function () {
+ t.equal(this.headers.referer, 'http://awesome.com')
+ })
+})
+
+tape('should use same agent class on redirect', function (t) {
+ var agent
+ var calls = 0
+ var agentOptions = {}
+
+ function FakeAgent (agentOptions) {
+ var createConnection
+
+ agent = new http.Agent(agentOptions)
+ createConnection = agent.createConnection
+ agent.createConnection = function () {
+ calls++
+ return createConnection.apply(agent, arguments)
}
+
+ return agent
}
+
+ hits = {}
+ request.get({
+ uri: s.url + '/temp',
+ jar: jar,
+ headers: { cookie: 'foo=bar' },
+ agentOptions: agentOptions,
+ agentClass: FakeAgent
+ }, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(res.statusCode, 200)
+ t.equal(body, 'GET temp_landing', 'Got temporary landing content')
+ t.equal(calls, 2)
+ t.ok(this.agent === agent, 'Reinstantiated the user-specified agent')
+ t.ok(this.agentOptions === agentOptions, 'Reused agent options')
+ t.end()
+ })
+})
+
+tape('cleanup', function (t) {
+ s.destroy(function () {
+ ss.destroy(function () {
+ t.end()
+ })
+ })
})
diff --git a/tests/test-rfc3986.js b/tests/test-rfc3986.js
new file mode 100644
index 000000000..a3918628d
--- /dev/null
+++ b/tests/test-rfc3986.js
@@ -0,0 +1,106 @@
+'use strict'
+
+var http = require('http')
+var request = require('../index')
+var tape = require('tape')
+
+function runTest (t, options) {
+ var server = http.createServer(function (req, res) {
+ var data = ''
+ req.setEncoding('utf8')
+
+ req.on('data', function (d) {
+ data += d
+ })
+
+ req.on('end', function () {
+ if (options.qs) {
+ t.equal(req.url, '/?rfc3986=%21%2A%28%29%27')
+ }
+ t.equal(data, options._expectBody)
+
+ res.writeHead(200)
+ res.end('done')
+ })
+ })
+
+ server.listen(0, function () {
+ var port = this.address().port
+ request.post('http://localhost:' + port, options, function (err, res, body) {
+ t.equal(err, null)
+ server.close(function () {
+ t.end()
+ })
+ })
+ })
+}
+
+var bodyEscaped = 'rfc3986=%21%2A%28%29%27'
+var bodyJson = '{"rfc3986":"!*()\'"}'
+
+var cases = [
+ {
+ _name: 'qs',
+ qs: {rfc3986: "!*()'"},
+ _expectBody: ''
+ },
+ {
+ _name: 'qs + json',
+ qs: {rfc3986: "!*()'"},
+ json: true,
+ _expectBody: ''
+ },
+ {
+ _name: 'form',
+ form: {rfc3986: "!*()'"},
+ _expectBody: bodyEscaped
+ },
+ {
+ _name: 'form + json',
+ form: {rfc3986: "!*()'"},
+ json: true,
+ _expectBody: bodyEscaped
+ },
+ {
+ _name: 'qs + form',
+ qs: {rfc3986: "!*()'"},
+ form: {rfc3986: "!*()'"},
+ _expectBody: bodyEscaped
+ },
+ {
+ _name: 'qs + form + json',
+ qs: {rfc3986: "!*()'"},
+ form: {rfc3986: "!*()'"},
+ json: true,
+ _expectBody: bodyEscaped
+ },
+ {
+ _name: 'body + header + json',
+ headers: {'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'},
+ body: "rfc3986=!*()'",
+ json: true,
+ _expectBody: bodyEscaped
+ },
+ {
+ _name: 'body + json',
+ body: {rfc3986: "!*()'"},
+ json: true,
+ _expectBody: bodyJson
+ },
+ {
+ _name: 'json object',
+ json: {rfc3986: "!*()'"},
+ _expectBody: bodyJson
+ }
+]
+
+var libs = ['qs', 'querystring']
+
+libs.forEach(function (lib) {
+ cases.forEach(function (options) {
+ options.useQuerystring = (lib === 'querystring')
+ tape(lib + ' rfc3986 ' + options._name, function (t) {
+ runTest(t, options)
+ })
+ })
+})
diff --git a/tests/test-stream.js b/tests/test-stream.js
new file mode 100644
index 000000000..1d7bf3de0
--- /dev/null
+++ b/tests/test-stream.js
@@ -0,0 +1,36 @@
+var fs = require('fs')
+var path = require('path')
+var http = require('http')
+var tape = require('tape')
+var request = require('../')
+var server
+
+tape('before', function (t) {
+ server = http.createServer()
+ server.on('request', function (req, res) {
+ req.pipe(res)
+ })
+ server.listen(0, function () {
+ server.url = 'http://localhost:' + this.address().port
+ t.end()
+ })
+})
+
+tape('request body stream', function (t) {
+ var fpath = path.join(__dirname, 'unicycle.jpg')
+ var input = fs.createReadStream(fpath, {highWaterMark: 1000})
+ request({
+ uri: server.url,
+ method: 'POST',
+ body: input,
+ encoding: null
+ }, function (err, res, body) {
+ t.error(err)
+ t.equal(body.length, fs.statSync(fpath).size)
+ t.end()
+ })
+})
+
+tape('after', function (t) {
+ server.close(t.end)
+})
diff --git a/tests/test-timeout.js b/tests/test-timeout.js
index 673f8ad86..c87775d3c 100644
--- a/tests/test-timeout.js
+++ b/tests/test-timeout.js
@@ -1,87 +1,260 @@
+'use strict'
+
+function checkErrCode (t, err) {
+ t.notEqual(err, null)
+ t.ok(err.code === 'ETIMEDOUT' || err.code === 'ESOCKETTIMEDOUT',
+ 'Error ETIMEDOUT or ESOCKETTIMEDOUT')
+}
+
+function checkEventHandlers (t, socket) {
+ var connectListeners = socket.listeners('connect')
+ var found = false
+ for (var i = 0; i < connectListeners.length; ++i) {
+ var fn = connectListeners[i]
+ if (typeof fn === 'function' && fn.name === 'onReqSockConnect') {
+ found = true
+ break
+ }
+ }
+ t.ok(!found, 'Connect listener should not exist')
+}
+
var server = require('./server')
- , events = require('events')
- , stream = require('stream')
- , assert = require('assert')
- , request = require('../main.js')
- ;
-
-var s = server.createServer();
-var expectedBody = "waited";
-var remainingTests = 5;
-
-s.listen(s.port, function () {
- // Request that waits for 200ms
- s.on('/timeout', function (req, resp) {
- setTimeout(function(){
- resp.writeHead(200, {'content-type':'text/plain'})
- resp.write(expectedBody)
- resp.end()
- }, 200);
- });
-
- // Scenario that should timeout
+var request = require('../index')
+var tape = require('tape')
+
+var s = server.createServer()
+
+// Request that waits for 200ms
+s.on('/timeout', function (req, res) {
+ setTimeout(function () {
+ res.writeHead(200, {'content-type': 'text/plain'})
+ res.write('waited')
+ res.end()
+ }, 200)
+})
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ t.end()
+ })
+})
+
+tape('should timeout', function (t) {
var shouldTimeout = {
- url: s.url + "/timeout",
- timeout:100
+ url: s.url + '/timeout',
+ timeout: 100
}
+ request(shouldTimeout, function (err, res, body) {
+ checkErrCode(t, err)
+ t.end()
+ })
+})
+
+tape('should set connect to false', function (t) {
+ var shouldTimeout = {
+ url: s.url + '/timeout',
+ timeout: 100
+ }
- request(shouldTimeout, function (err, resp, body) {
- assert.equal(err.code, "ETIMEDOUT");
- checkDone();
+ request(shouldTimeout, function (err, res, body) {
+ checkErrCode(t, err)
+ t.ok(err.connect === false, 'Read Timeout Error should set \'connect\' property to false')
+ t.end()
})
+})
+
+tape('should timeout with events', function (t) {
+ t.plan(3)
+ var shouldTimeoutWithEvents = {
+ url: s.url + '/timeout',
+ timeout: 100
+ }
+
+ var eventsEmitted = 0
+ request(shouldTimeoutWithEvents)
+ .on('error', function (err) {
+ eventsEmitted++
+ t.equal(1, eventsEmitted)
+ checkErrCode(t, err)
+ })
+})
- // Scenario that shouldn't timeout
+tape('should not timeout', function (t) {
var shouldntTimeout = {
- url: s.url + "/timeout",
- timeout:300
+ url: s.url + '/timeout',
+ timeout: 1200
}
- request(shouldntTimeout, function (err, resp, body) {
- assert.equal(err, null);
- assert.equal(expectedBody, body)
- checkDone();
+ var socket
+ request(shouldntTimeout, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(body, 'waited')
+ checkEventHandlers(t, socket)
+ t.end()
+ }).on('socket', function (socket_) {
+ socket = socket_
})
+})
- // Scenario with no timeout set, so shouldn't timeout
+tape('no timeout', function (t) {
var noTimeout = {
- url: s.url + "/timeout"
+ url: s.url + '/timeout'
}
- request(noTimeout, function (err, resp, body) {
- assert.equal(err);
- assert.equal(expectedBody, body)
- checkDone();
+ request(noTimeout, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(body, 'waited')
+ t.end()
})
+})
- // Scenario with a negative timeout value, should be treated a zero or the minimum delay
+tape('negative timeout', function (t) { // should be treated a zero or the minimum delay
var negativeTimeout = {
- url: s.url + "/timeout",
- timeout:-1000
+ url: s.url + '/timeout',
+ timeout: -1000
}
- request(negativeTimeout, function (err, resp, body) {
- assert.equal(err.code, "ETIMEDOUT");
- checkDone();
+ request(negativeTimeout, function (err, res, body) {
+ // Only verify error if it is set, since using a timeout value of 0 can lead
+ // to inconsistent results, depending on a variety of factors
+ if (err) {
+ checkErrCode(t, err)
+ }
+ t.end()
})
+})
- // Scenario with a float timeout value, should be rounded by setTimeout anyway
+tape('float timeout', function (t) { // should be rounded by setTimeout anyway
var floatTimeout = {
- url: s.url + "/timeout",
+ url: s.url + '/timeout',
timeout: 100.76
}
- request(floatTimeout, function (err, resp, body) {
- assert.equal(err.code, "ETIMEDOUT");
- checkDone();
+ request(floatTimeout, function (err, res, body) {
+ checkErrCode(t, err)
+ t.end()
})
+})
- function checkDone() {
- if(--remainingTests == 0) {
- s.close();
- console.log("All tests passed.");
+// We need a destination that will not immediately return a TCP Reset
+// packet. StackOverflow suggests these hosts:
+// (https://stackoverflow.com/a/904609/329700)
+var nonRoutable = [
+ '10.255.255.1',
+ '10.0.0.0',
+ '192.168.0.0',
+ '192.168.255.255',
+ '172.16.0.0',
+ '172.31.255.255'
+]
+var nrIndex = 0
+function getNonRoutable () {
+ var ip = nonRoutable[nrIndex]
+ if (!ip) {
+ throw new Error('No more non-routable addresses')
+ }
+ ++nrIndex
+ return ip
+}
+tape('connect timeout', function tryConnect (t) {
+ var tarpitHost = 'http://' + getNonRoutable()
+ var shouldConnectTimeout = {
+ url: tarpitHost + '/timeout',
+ timeout: 100
+ }
+ var socket
+ request(shouldConnectTimeout, function (err) {
+ t.notEqual(err, null)
+ if (err.code === 'ENETUNREACH' && nrIndex < nonRoutable.length) {
+ // With some network configurations, some addresses will be reported as
+ // unreachable immediately (before the timeout occurs). In those cases,
+ // try other non-routable addresses before giving up.
+ return tryConnect(t)
}
+ checkErrCode(t, err)
+ t.ok(err.connect === true, 'Connect Timeout Error should set \'connect\' property to true')
+ checkEventHandlers(t, socket)
+ nrIndex = 0
+ t.end()
+ }).on('socket', function (socket_) {
+ socket = socket_
+ })
+})
+
+tape('connect timeout with non-timeout error', function tryConnect (t) {
+ var tarpitHost = 'http://' + getNonRoutable()
+ var shouldConnectTimeout = {
+ url: tarpitHost + '/timeout',
+ timeout: 1000
}
+ var socket
+ request(shouldConnectTimeout, function (err) {
+ t.notEqual(err, null)
+ if (err.code === 'ENETUNREACH' && nrIndex < nonRoutable.length) {
+ // With some network configurations, some addresses will be reported as
+ // unreachable immediately (before the timeout occurs). In those cases,
+ // try other non-routable addresses before giving up.
+ return tryConnect(t)
+ }
+ // Delay the check since the 'connect' handler is removed in a separate
+ // 'error' handler which gets triggered after this callback
+ setImmediate(function () {
+ checkEventHandlers(t, socket)
+ nrIndex = 0
+ t.end()
+ })
+ }).on('socket', function (socket_) {
+ socket = socket_
+ setImmediate(function () {
+ socket.emit('error', new Error('Fake Error'))
+ })
+ })
+})
+
+tape('request timeout with keep-alive connection', function (t) {
+ var Agent = require('http').Agent
+ var agent = new Agent({ keepAlive: true })
+ var firstReq = {
+ url: s.url + '/timeout',
+ agent: agent
+ }
+ request(firstReq, function (err) {
+ // We should now still have a socket open. For the second request we should
+ // see a request timeout on the active socket ...
+ t.equal(err, null)
+ var shouldReqTimeout = {
+ url: s.url + '/timeout',
+ timeout: 100,
+ agent: agent
+ }
+ request(shouldReqTimeout, function (err) {
+ checkErrCode(t, err)
+ t.ok(err.connect === false, 'Error should have been a request timeout error')
+ t.end()
+ }).on('socket', function (socket) {
+ var isConnecting = socket._connecting || socket.connecting
+ t.ok(isConnecting !== true, 'Socket should already be connected')
+ })
+ }).on('socket', function (socket) {
+ var isConnecting = socket._connecting || socket.connecting
+ t.ok(isConnecting === true, 'Socket should be new')
+ })
+})
+
+tape('calling abort clears the timeout', function (t) {
+ const req = request({ url: s.url + '/timeout', timeout: 2500 })
+ setTimeout(function () {
+ req.abort()
+ t.equal(req.timeoutTimer, null)
+ t.end()
+ }, 5)
})
+tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-timing.js b/tests/test-timing.js
new file mode 100644
index 000000000..f3e77f929
--- /dev/null
+++ b/tests/test-timing.js
@@ -0,0 +1,147 @@
+'use strict'
+
+var server = require('./server')
+var request = require('../index')
+var tape = require('tape')
+var http = require('http')
+
+var plainServer = server.createServer()
+var redirectMockTime = 10
+
+tape('setup', function (t) {
+ plainServer.listen(0, function () {
+ plainServer.on('/', function (req, res) {
+ res.writeHead(200)
+ res.end('plain')
+ })
+ plainServer.on('/redir', function (req, res) {
+ // fake redirect delay to ensure strong signal for rollup check
+ setTimeout(function () {
+ res.writeHead(301, { 'location': 'http://localhost:' + plainServer.port + '/' })
+ res.end()
+ }, redirectMockTime)
+ })
+
+ t.end()
+ })
+})
+
+tape('non-redirected request is timed', function (t) {
+ var options = {time: true}
+
+ var start = new Date().getTime()
+ var r = request('http://localhost:' + plainServer.port + '/', options, function (err, res, body) {
+ var end = new Date().getTime()
+
+ t.equal(err, null)
+ t.equal(typeof res.elapsedTime, 'number')
+ t.equal(typeof res.responseStartTime, 'number')
+ t.equal(typeof res.timingStart, 'number')
+ t.equal((res.timingStart >= start), true)
+ t.equal(typeof res.timings, 'object')
+ t.equal((res.elapsedTime > 0), true)
+ t.equal((res.elapsedTime <= (end - start)), true)
+ t.equal((res.responseStartTime > r.startTime), true)
+ t.equal((res.timings.socket >= 0), true)
+ t.equal((res.timings.lookup >= res.timings.socket), true)
+ t.equal((res.timings.connect >= res.timings.lookup), true)
+ t.equal((res.timings.response >= res.timings.connect), true)
+ t.equal((res.timings.end >= res.timings.response), true)
+ t.equal(typeof res.timingPhases, 'object')
+ t.equal((res.timingPhases.wait >= 0), true)
+ t.equal((res.timingPhases.dns >= 0), true)
+ t.equal((res.timingPhases.tcp >= 0), true)
+ t.equal((res.timingPhases.firstByte > 0), true)
+ t.equal((res.timingPhases.download > 0), true)
+ t.equal((res.timingPhases.total > 0), true)
+ t.equal((res.timingPhases.total <= (end - start)), true)
+
+ // validate there are no unexpected properties
+ var propNames = []
+ for (var propName in res.timings) {
+ if (res.timings.hasOwnProperty(propName)) {
+ propNames.push(propName)
+ }
+ }
+ t.deepEqual(propNames, ['socket', 'lookup', 'connect', 'response', 'end'])
+
+ propNames = []
+ for (propName in res.timingPhases) {
+ if (res.timingPhases.hasOwnProperty(propName)) {
+ propNames.push(propName)
+ }
+ }
+ t.deepEqual(propNames, ['wait', 'dns', 'tcp', 'firstByte', 'download', 'total'])
+
+ t.end()
+ })
+})
+
+tape('redirected request is timed with rollup', function (t) {
+ var options = {time: true}
+ var r = request('http://localhost:' + plainServer.port + '/redir', options, function (err, res, body) {
+ t.equal(err, null)
+ t.equal(typeof res.elapsedTime, 'number')
+ t.equal(typeof res.responseStartTime, 'number')
+ t.equal((res.elapsedTime > 0), true)
+ t.equal((res.responseStartTime > 0), true)
+ t.equal((res.elapsedTime > redirectMockTime), true)
+ t.equal((res.responseStartTime > r.startTime), true)
+ t.end()
+ })
+})
+
+tape('keepAlive is timed', function (t) {
+ var agent = new http.Agent({ keepAlive: true })
+ var options = { time: true, agent: agent }
+ var start1 = new Date().getTime()
+
+ request('http://localhost:' + plainServer.port + '/', options, function (err1, res1, body1) {
+ var end1 = new Date().getTime()
+
+ // ensure the first request's timestamps look ok
+ t.equal((res1.timingStart >= start1), true)
+ t.equal((start1 <= end1), true)
+
+ t.equal((res1.timings.socket >= 0), true)
+ t.equal((res1.timings.lookup >= res1.timings.socket), true)
+ t.equal((res1.timings.connect >= res1.timings.lookup), true)
+ t.equal((res1.timings.response >= res1.timings.connect), true)
+
+ // open a second request with the same agent so we re-use the same connection
+ var start2 = new Date().getTime()
+ request('http://localhost:' + plainServer.port + '/', options, function (err2, res2, body2) {
+ var end2 = new Date().getTime()
+
+ // ensure the second request's timestamps look ok
+ t.equal((res2.timingStart >= start2), true)
+ t.equal((start2 <= end2), true)
+
+ // ensure socket==lookup==connect for the second request
+ t.equal((res2.timings.socket >= 0), true)
+ t.equal((res2.timings.lookup === res2.timings.socket), true)
+ t.equal((res2.timings.connect === res2.timings.lookup), true)
+ t.equal((res2.timings.response >= res2.timings.connect), true)
+
+ // explicitly shut down the agent
+ if (typeof agent.destroy === 'function') {
+ agent.destroy()
+ } else {
+ // node < 0.12
+ Object.keys(agent.sockets).forEach(function (name) {
+ agent.sockets[name].forEach(function (socket) {
+ socket.end()
+ })
+ })
+ }
+
+ t.end()
+ })
+ })
+})
+
+tape('cleanup', function (t) {
+ plainServer.close(function () {
+ t.end()
+ })
+})
diff --git a/tests/test-toJSON.js b/tests/test-toJSON.js
index c81dfb568..43fa79169 100644
--- a/tests/test-toJSON.js
+++ b/tests/test-toJSON.js
@@ -1,14 +1,45 @@
-var request = require('../main')
- , http = require('http')
- , assert = require('assert')
- ;
+'use strict'
+
+var request = require('../index')
+var http = require('http')
+var tape = require('tape')
var s = http.createServer(function (req, resp) {
resp.statusCode = 200
resp.end('asdf')
-}).listen(8080, function () {
- var r = request('http://localhost:8080', function (e, resp) {
- assert(JSON.parse(JSON.stringify(r)).response.statusCode, 200)
- s.close()
+})
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ s.url = 'http://localhost:' + this.address().port
+ t.end()
+ })
+})
+
+tape('request().toJSON()', function (t) {
+ var r = request({
+ url: s.url,
+ headers: { foo: 'bar' }
+ }, function (err, res) {
+ var jsonR = JSON.parse(JSON.stringify(r))
+ var jsonRes = JSON.parse(JSON.stringify(res))
+
+ t.equal(err, null)
+
+ t.equal(jsonR.uri.href, r.uri.href)
+ t.equal(jsonR.method, r.method)
+ t.equal(jsonR.headers.foo, r.headers.foo)
+
+ t.equal(jsonRes.statusCode, res.statusCode)
+ t.equal(jsonRes.body, res.body)
+ t.equal(jsonRes.headers.date, res.headers.date)
+
+ t.end()
+ })
+})
+
+tape('cleanup', function (t) {
+ s.close(function () {
+ t.end()
})
-})
\ No newline at end of file
+})
diff --git a/tests/test-tunnel.js b/tests/test-tunnel.js
index 58131b9bb..fa2ebce33 100644
--- a/tests/test-tunnel.js
+++ b/tests/test-tunnel.js
@@ -1,61 +1,466 @@
-// test that we can tunnel a https request over an http proxy
-// keeping all the CA and whatnot intact.
-//
-// Note: this requires that squid is installed.
-// If the proxy fails to start, we'll just log a warning and assume success.
+'use strict'
var server = require('./server')
- , assert = require('assert')
- , request = require('../main.js')
- , fs = require('fs')
- , path = require('path')
- , caFile = path.resolve(__dirname, 'ssl/npm-ca.crt')
- , ca = fs.readFileSync(caFile)
- , child_process = require('child_process')
- , sqConf = path.resolve(__dirname, 'squid.conf')
- , sqArgs = ['-f', sqConf, '-N', '-d', '5']
- , proxy = 'http://localhost:3128'
- , hadError = null
-
-var squid = child_process.spawn('squid', sqArgs);
-var ready = false
-
-squid.stderr.on('data', function (c) {
- console.error('SQUIDERR ' + c.toString().trim().split('\n')
- .join('\nSQUIDERR '))
- ready = c.toString().match(/ready to serve requests/i)
-})
+var tape = require('tape')
+var request = require('../index')
+var https = require('https')
+var net = require('net')
+var fs = require('fs')
+var path = require('path')
+var util = require('util')
+var url = require('url')
+var destroyable = require('server-destroy')
-squid.stdout.on('data', function (c) {
- console.error('SQUIDOUT ' + c.toString().trim().split('\n')
- .join('\nSQUIDOUT '))
-})
+var events = []
+var caFile = path.resolve(__dirname, 'ssl/ca/ca.crt')
+var ca = fs.readFileSync(caFile)
+var clientCert = fs.readFileSync(path.resolve(__dirname, 'ssl/ca/client.crt'))
+var clientKey = fs.readFileSync(path.resolve(__dirname, 'ssl/ca/client-enc.key'))
+var clientPassword = 'password'
+var sslOpts = {
+ key: path.resolve(__dirname, 'ssl/ca/localhost.key'),
+ cert: path.resolve(__dirname, 'ssl/ca/localhost.crt')
+}
-squid.on('exit', function (c) {
- console.error('exit '+c)
- if (c && !ready) {
- console.error('squid must be installed to run this test.')
- c = null
- hadError = null
- process.exit(0)
- return
- }
+var mutualSSLOpts = {
+ key: path.resolve(__dirname, 'ssl/ca/localhost.key'),
+ cert: path.resolve(__dirname, 'ssl/ca/localhost.crt'),
+ ca: caFile,
+ requestCert: true,
+ rejectUnauthorized: true
+}
+
+// this is needed for 'https over http, tunnel=false' test
+// from https://github.com/coolaj86/node-ssl-root-cas/blob/v1.1.9-beta/ssl-root-cas.js#L4267-L4281
+var httpsOpts = https.globalAgent.options
+httpsOpts.ca = httpsOpts.ca || []
+httpsOpts.ca.push(ca)
+
+var s = server.createServer()
+var ss = server.createSSLServer(sslOpts)
+var ss2 = server.createSSLServer(mutualSSLOpts)
+
+// XXX when tunneling https over https, connections get left open so the server
+// doesn't want to close normally (and same issue with http server on v0.8.x)
+destroyable(s)
+destroyable(ss)
+destroyable(ss2)
+
+function event () {
+ events.push(util.format.apply(null, arguments))
+}
+
+function setListeners (server, type) {
+ server.on('/', function (req, res) {
+ event('%s response', type)
+ res.end(type + ' ok')
+ })
+
+ server.on('request', function (req, res) {
+ if (/^https?:/.test(req.url)) {
+ // This is a proxy request
+ var dest = req.url.split(':')[0]
+ // Is it a redirect?
+ var match = req.url.match(/\/redirect\/(https?)$/)
+ if (match) {
+ dest += '->' + match[1]
+ }
+ event('%s proxy to %s', type, dest)
+ request(req.url, { followRedirect: false }).pipe(res)
+ }
+ })
+
+ server.on('/redirect/http', function (req, res) {
+ event('%s redirect to http', type)
+ res.writeHead(301, {
+ location: s.url
+ })
+ res.end()
+ })
+
+ server.on('/redirect/https', function (req, res) {
+ event('%s redirect to https', type)
+ res.writeHead(301, {
+ location: ss.url
+ })
+ res.end()
+ })
- if (c) {
- hadError = hadError || new Error('Squid exited with '+c)
+ server.on('connect', function (req, client, head) {
+ var u = url.parse(req.url)
+ var server = net.connect(u.host, u.port, function () {
+ event('%s connect to %s', type, req.url)
+ client.write('HTTP/1.1 200 Connection established\r\n\r\n')
+ client.pipe(server)
+ server.write(head)
+ server.pipe(client)
+ })
+ })
+}
+
+setListeners(s, 'http')
+setListeners(ss, 'https')
+setListeners(ss2, 'https')
+
+// monkey-patch since you can't set a custom certificate authority for the
+// proxy in tunnel-agent (this is necessary for "* over https" tests)
+var customCaCount = 0
+var httpsRequestOld = https.request
+https.request = function (options) {
+ if (customCaCount) {
+ options.ca = ca
+ customCaCount--
}
- if (hadError) throw hadError
-})
+ return httpsRequestOld.apply(this, arguments)
+}
+
+function runTest (name, opts, expected) {
+ tape(name, function (t) {
+ opts.ca = ca
+ if (opts.proxy === ss.url) {
+ customCaCount = (opts.url === ss.url ? 2 : 1)
+ }
+ request(opts, function (err, res, body) {
+ event(err ? 'err ' + err.message : res.statusCode + ' ' + body)
+ t.deepEqual(events, expected)
+ events = []
+ t.end()
+ })
+ })
+}
+
+function addTests () {
+ // HTTP OVER HTTP
+
+ runTest('http over http, tunnel=true', {
+ url: s.url,
+ proxy: s.url,
+ tunnel: true
+ }, [
+ 'http connect to localhost:' + s.port,
+ 'http response',
+ '200 http ok'
+ ])
+
+ runTest('http over http, tunnel=false', {
+ url: s.url,
+ proxy: s.url,
+ tunnel: false
+ }, [
+ 'http proxy to http',
+ 'http response',
+ '200 http ok'
+ ])
+
+ runTest('http over http, tunnel=default', {
+ url: s.url,
+ proxy: s.url
+ }, [
+ 'http proxy to http',
+ 'http response',
+ '200 http ok'
+ ])
+
+ // HTTP OVER HTTPS
+
+ runTest('http over https, tunnel=true', {
+ url: s.url,
+ proxy: ss.url,
+ tunnel: true
+ }, [
+ 'https connect to localhost:' + s.port,
+ 'http response',
+ '200 http ok'
+ ])
+
+ runTest('http over https, tunnel=false', {
+ url: s.url,
+ proxy: ss.url,
+ tunnel: false
+ }, [
+ 'https proxy to http',
+ 'http response',
+ '200 http ok'
+ ])
+
+ runTest('http over https, tunnel=default', {
+ url: s.url,
+ proxy: ss.url
+ }, [
+ 'https proxy to http',
+ 'http response',
+ '200 http ok'
+ ])
-setTimeout(function F () {
- if (!ready) return setTimeout(F, 100)
- request({ uri: 'https://registry.npmjs.org/request/'
- , proxy: 'http://localhost:3128'
- , ca: ca
- , json: true }, function (er, body) {
- hadError = er
- console.log(er || typeof body)
- if (!er) console.log("ok")
- squid.kill('SIGKILL')
+ // HTTPS OVER HTTP
+
+ runTest('https over http, tunnel=true', {
+ url: ss.url,
+ proxy: s.url,
+ tunnel: true
+ }, [
+ 'http connect to localhost:' + ss.port,
+ 'https response',
+ '200 https ok'
+ ])
+
+ runTest('https over http, tunnel=false', {
+ url: ss.url,
+ proxy: s.url,
+ tunnel: false
+ }, [
+ 'http proxy to https',
+ 'https response',
+ '200 https ok'
+ ])
+
+ runTest('https over http, tunnel=default', {
+ url: ss.url,
+ proxy: s.url
+ }, [
+ 'http connect to localhost:' + ss.port,
+ 'https response',
+ '200 https ok'
+ ])
+
+ // HTTPS OVER HTTPS
+
+ runTest('https over https, tunnel=true', {
+ url: ss.url,
+ proxy: ss.url,
+ tunnel: true
+ }, [
+ 'https connect to localhost:' + ss.port,
+ 'https response',
+ '200 https ok'
+ ])
+
+ runTest('https over https, tunnel=false', {
+ url: ss.url,
+ proxy: ss.url,
+ tunnel: false,
+ pool: false // must disable pooling here or Node.js hangs
+ }, [
+ 'https proxy to https',
+ 'https response',
+ '200 https ok'
+ ])
+
+ runTest('https over https, tunnel=default', {
+ url: ss.url,
+ proxy: ss.url
+ }, [
+ 'https connect to localhost:' + ss.port,
+ 'https response',
+ '200 https ok'
+ ])
+
+ // HTTP->HTTP OVER HTTP
+
+ runTest('http->http over http, tunnel=true', {
+ url: s.url + '/redirect/http',
+ proxy: s.url,
+ tunnel: true
+ }, [
+ 'http connect to localhost:' + s.port,
+ 'http redirect to http',
+ 'http connect to localhost:' + s.port,
+ 'http response',
+ '200 http ok'
+ ])
+
+ runTest('http->http over http, tunnel=false', {
+ url: s.url + '/redirect/http',
+ proxy: s.url,
+ tunnel: false
+ }, [
+ 'http proxy to http->http',
+ 'http redirect to http',
+ 'http proxy to http',
+ 'http response',
+ '200 http ok'
+ ])
+
+ runTest('http->http over http, tunnel=default', {
+ url: s.url + '/redirect/http',
+ proxy: s.url
+ }, [
+ 'http proxy to http->http',
+ 'http redirect to http',
+ 'http proxy to http',
+ 'http response',
+ '200 http ok'
+ ])
+
+ // HTTP->HTTPS OVER HTTP
+
+ runTest('http->https over http, tunnel=true', {
+ url: s.url + '/redirect/https',
+ proxy: s.url,
+ tunnel: true
+ }, [
+ 'http connect to localhost:' + s.port,
+ 'http redirect to https',
+ 'http connect to localhost:' + ss.port,
+ 'https response',
+ '200 https ok'
+ ])
+
+ runTest('http->https over http, tunnel=false', {
+ url: s.url + '/redirect/https',
+ proxy: s.url,
+ tunnel: false
+ }, [
+ 'http proxy to http->https',
+ 'http redirect to https',
+ 'http proxy to https',
+ 'https response',
+ '200 https ok'
+ ])
+
+ runTest('http->https over http, tunnel=default', {
+ url: s.url + '/redirect/https',
+ proxy: s.url
+ }, [
+ 'http proxy to http->https',
+ 'http redirect to https',
+ 'http connect to localhost:' + ss.port,
+ 'https response',
+ '200 https ok'
+ ])
+
+ // HTTPS->HTTP OVER HTTP
+
+ runTest('https->http over http, tunnel=true', {
+ url: ss.url + '/redirect/http',
+ proxy: s.url,
+ tunnel: true
+ }, [
+ 'http connect to localhost:' + ss.port,
+ 'https redirect to http',
+ 'http connect to localhost:' + s.port,
+ 'http response',
+ '200 http ok'
+ ])
+
+ runTest('https->http over http, tunnel=false', {
+ url: ss.url + '/redirect/http',
+ proxy: s.url,
+ tunnel: false
+ }, [
+ 'http proxy to https->http',
+ 'https redirect to http',
+ 'http proxy to http',
+ 'http response',
+ '200 http ok'
+ ])
+
+ runTest('https->http over http, tunnel=default', {
+ url: ss.url + '/redirect/http',
+ proxy: s.url
+ }, [
+ 'http connect to localhost:' + ss.port,
+ 'https redirect to http',
+ 'http proxy to http',
+ 'http response',
+ '200 http ok'
+ ])
+
+ // HTTPS->HTTPS OVER HTTP
+
+ runTest('https->https over http, tunnel=true', {
+ url: ss.url + '/redirect/https',
+ proxy: s.url,
+ tunnel: true
+ }, [
+ 'http connect to localhost:' + ss.port,
+ 'https redirect to https',
+ 'http connect to localhost:' + ss.port,
+ 'https response',
+ '200 https ok'
+ ])
+
+ runTest('https->https over http, tunnel=false', {
+ url: ss.url + '/redirect/https',
+ proxy: s.url,
+ tunnel: false
+ }, [
+ 'http proxy to https->https',
+ 'https redirect to https',
+ 'http proxy to https',
+ 'https response',
+ '200 https ok'
+ ])
+
+ runTest('https->https over http, tunnel=default', {
+ url: ss.url + '/redirect/https',
+ proxy: s.url
+ }, [
+ 'http connect to localhost:' + ss.port,
+ 'https redirect to https',
+ 'http connect to localhost:' + ss.port,
+ 'https response',
+ '200 https ok'
+ ])
+
+ // MUTUAL HTTPS OVER HTTP
+
+ runTest('mutual https over http, tunnel=true', {
+ url: ss2.url,
+ proxy: s.url,
+ tunnel: true,
+ cert: clientCert,
+ key: clientKey,
+ passphrase: clientPassword
+ }, [
+ 'http connect to localhost:' + ss2.port,
+ 'https response',
+ '200 https ok'
+ ])
+
+ // XXX causes 'Error: socket hang up'
+ // runTest('mutual https over http, tunnel=false', {
+ // url : ss2.url,
+ // proxy : s.url,
+ // tunnel : false,
+ // cert : clientCert,
+ // key : clientKey,
+ // passphrase : clientPassword
+ // }, [
+ // 'http connect to localhost:' + ss2.port,
+ // 'https response',
+ // '200 https ok'
+ // ])
+
+ runTest('mutual https over http, tunnel=default', {
+ url: ss2.url,
+ proxy: s.url,
+ cert: clientCert,
+ key: clientKey,
+ passphrase: clientPassword
+ }, [
+ 'http connect to localhost:' + ss2.port,
+ 'https response',
+ '200 https ok'
+ ])
+}
+
+tape('setup', function (t) {
+ s.listen(0, function () {
+ ss.listen(0, function () {
+ ss2.listen(0, 'localhost', function () {
+ addTests()
+ tape('cleanup', function (t) {
+ s.destroy(function () {
+ ss.destroy(function () {
+ ss2.destroy(function () {
+ t.end()
+ })
+ })
+ })
+ })
+ t.end()
+ })
+ })
})
-}, 100)
+})
diff --git a/tests/test-unix.js b/tests/test-unix.js
new file mode 100644
index 000000000..acf883273
--- /dev/null
+++ b/tests/test-unix.js
@@ -0,0 +1,74 @@
+'use strict'
+
+var request = require('../index')
+var http = require('http')
+var fs = require('fs')
+var rimraf = require('rimraf')
+var assert = require('assert')
+var tape = require('tape')
+var url = require('url')
+
+var rawPath = [null, 'raw', 'path'].join('/')
+var queryPath = [null, 'query', 'path'].join('/')
+var searchString = '?foo=bar'
+var socket = [__dirname, 'tmp-socket'].join('/')
+var expectedBody = 'connected'
+var statusCode = 200
+
+rimraf.sync(socket)
+
+var s = http.createServer(function (req, res) {
+ var incomingUrl = url.parse(req.url)
+ switch (incomingUrl.pathname) {
+ case rawPath:
+ assert.equal(incomingUrl.pathname, rawPath, 'requested path is sent to server')
+ break
+
+ case queryPath:
+ assert.equal(incomingUrl.pathname, queryPath, 'requested path is sent to server')
+ assert.equal(incomingUrl.search, searchString, 'query string is sent to server')
+ break
+
+ default:
+ assert(false, 'A valid path was requested')
+ }
+ res.statusCode = statusCode
+ res.end(expectedBody)
+})
+
+tape('setup', function (t) {
+ s.listen(socket, function () {
+ t.end()
+ })
+})
+
+tape('unix socket connection', function (t) {
+ request('http://unix:' + socket + ':' + rawPath, function (err, res, body) {
+ t.equal(err, null, 'no error in connection')
+ t.equal(res.statusCode, statusCode, 'got HTTP 200 OK response')
+ t.equal(body, expectedBody, 'expected response body is received')
+ t.end()
+ })
+})
+
+tape('unix socket connection with qs', function (t) {
+ request({
+ uri: 'http://unix:' + socket + ':' + queryPath,
+ qs: {
+ foo: 'bar'
+ }
+ }, function (err, res, body) {
+ t.equal(err, null, 'no error in connection')
+ t.equal(res.statusCode, statusCode, 'got HTTP 200 OK response')
+ t.equal(body, expectedBody, 'expected response body is received')
+ t.end()
+ })
+})
+
+tape('cleanup', function (t) {
+ s.close(function () {
+ fs.unlink(socket, function () {
+ t.end()
+ })
+ })
+})
diff --git a/tests/unicycle.jpg b/tests/unicycle.jpg
new file mode 100644
index 000000000..7cea4dd71
Binary files /dev/null and b/tests/unicycle.jpg differ
diff --git a/tunnel.js b/tunnel.js
deleted file mode 100644
index 453786c5e..000000000
--- a/tunnel.js
+++ /dev/null
@@ -1,229 +0,0 @@
-'use strict';
-
-var net = require('net');
-var tls = require('tls');
-var http = require('http');
-var https = require('https');
-var events = require('events');
-var assert = require('assert');
-var util = require('util');
-
-
-exports.httpOverHttp = httpOverHttp;
-exports.httpsOverHttp = httpsOverHttp;
-exports.httpOverHttps = httpOverHttps;
-exports.httpsOverHttps = httpsOverHttps;
-
-
-function httpOverHttp(options) {
- var agent = new TunnelingAgent(options);
- agent.request = http.request;
- return agent;
-}
-
-function httpsOverHttp(options) {
- var agent = new TunnelingAgent(options);
- agent.request = http.request;
- agent.createSocket = createSecureSocket;
- return agent;
-}
-
-function httpOverHttps(options) {
- var agent = new TunnelingAgent(options);
- agent.request = https.request;
- return agent;
-}
-
-function httpsOverHttps(options) {
- var agent = new TunnelingAgent(options);
- agent.request = https.request;
- agent.createSocket = createSecureSocket;
- return agent;
-}
-
-
-function TunnelingAgent(options) {
- var self = this;
- self.options = options || {};
- self.proxyOptions = self.options.proxy || {};
- self.maxSockets = self.options.maxSockets || http.Agent.defaultMaxSockets;
- self.requests = [];
- self.sockets = [];
-
- self.on('free', function onFree(socket, host, port) {
- for (var i = 0, len = self.requests.length; i < len; ++i) {
- var pending = self.requests[i];
- if (pending.host === host && pending.port === port) {
- // Detect the request to connect same origin server,
- // reuse the connection.
- self.requests.splice(i, 1);
- pending.request.onSocket(socket);
- return;
- }
- }
- socket.destroy();
- self.removeSocket(socket);
- });
-}
-util.inherits(TunnelingAgent, events.EventEmitter);
-
-TunnelingAgent.prototype.addRequest = function addRequest(req, host, port) {
- var self = this;
-
- if (self.sockets.length >= this.maxSockets) {
- // We are over limit so we'll add it to the queue.
- self.requests.push({host: host, port: port, request: req});
- return;
- }
-
- // If we are under maxSockets create a new one.
- self.createSocket({host: host, port: port, request: req}, function(socket) {
- socket.on('free', onFree);
- socket.on('close', onCloseOrRemove);
- socket.on('agentRemove', onCloseOrRemove);
- req.onSocket(socket);
-
- function onFree() {
- self.emit('free', socket, host, port);
- }
-
- function onCloseOrRemove(err) {
- self.removeSocket();
- socket.removeListener('free', onFree);
- socket.removeListener('close', onCloseOrRemove);
- socket.removeListener('agentRemove', onCloseOrRemove);
- }
- });
-};
-
-TunnelingAgent.prototype.createSocket = function createSocket(options, cb) {
- var self = this;
- var placeholder = {};
- self.sockets.push(placeholder);
-
- var connectOptions = mergeOptions({}, self.proxyOptions, {
- method: 'CONNECT',
- path: options.host + ':' + options.port,
- agent: false
- });
- if (connectOptions.proxyAuth) {
- connectOptions.headers = connectOptions.headers || {};
- connectOptions.headers['Proxy-Authorization'] = 'Basic ' +
- new Buffer(connectOptions.proxyAuth).toString('base64');
- }
-
- debug('making CONNECT request');
- var connectReq = self.request(connectOptions);
- connectReq.useChunkedEncodingByDefault = false; // for v0.6
- connectReq.once('response', onResponse); // for v0.6
- connectReq.once('upgrade', onUpgrade); // for v0.6
- connectReq.once('connect', onConnect); // for v0.7 or later
- connectReq.once('error', onError);
- connectReq.end();
-
- function onResponse(res) {
- // Very hacky. This is necessary to avoid http-parser leaks.
- res.upgrade = true;
- }
-
- function onUpgrade(res, socket, head) {
- // Hacky.
- process.nextTick(function() {
- onConnect(res, socket, head);
- });
- }
-
- function onConnect(res, socket, head) {
- connectReq.removeAllListeners();
- socket.removeAllListeners();
-
- if (res.statusCode === 200) {
- assert.equal(head.length, 0);
- debug('tunneling connection has established');
- self.sockets[self.sockets.indexOf(placeholder)] = socket;
- cb(socket);
- } else {
- debug('tunneling socket could not be established, statusCode=%d',
- res.statusCode);
- var error = new Error('tunneling socket could not be established, ' +
- 'sutatusCode=' + res.statusCode);
- error.code = 'ECONNRESET';
- options.request.emit('error', error);
- self.removeSocket(placeholder);
- }
- }
-
- function onError(cause) {
- connectReq.removeAllListeners();
-
- debug('tunneling socket could not be established, cause=%s\n',
- cause.message, cause.stack);
- var error = new Error('tunneling socket could not be established, ' +
- 'cause=' + cause.message);
- error.code = 'ECONNRESET';
- options.request.emit('error', error);
- self.removeSocket(placeholder);
- }
-};
-
-TunnelingAgent.prototype.removeSocket = function removeSocket(socket) {
- var pos = this.sockets.indexOf(socket)
- if (pos === -1) {
- return;
- }
- this.sockets.splice(pos, 1);
-
- var pending = this.requests.shift();
- if (pending) {
- // If we have pending requests and a socket gets closed a new one
- // needs to be created to take over in the pool for the one that closed.
- this.createSocket(pending, function(socket) {
- pending.request.onSocket(socket);
- });
- }
-};
-
-function createSecureSocket(options, cb) {
- var self = this;
- TunnelingAgent.prototype.createSocket.call(self, options, function(socket) {
- // 0 is dummy port for v0.6
- var secureSocket = tls.connect(0, mergeOptions({}, self.options, {
- socket: socket
- }));
- cb(secureSocket);
- });
-}
-
-
-function mergeOptions(target) {
- for (var i = 1, len = arguments.length; i < len; ++i) {
- var overrides = arguments[i];
- if (typeof overrides === 'object') {
- var keys = Object.keys(overrides);
- for (var j = 0, keyLen = keys.length; j < keyLen; ++j) {
- var k = keys[j];
- if (overrides[k] !== undefined) {
- target[k] = overrides[k];
- }
- }
- }
- }
- return target;
-}
-
-
-var debug;
-if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) {
- debug = function() {
- var args = Array.prototype.slice.call(arguments);
- if (typeof args[0] === 'string') {
- args[0] = 'TUNNEL: ' + args[0];
- } else {
- args.unshift('TUNNEL:');
- }
- console.error.apply(console, args);
- }
-} else {
- debug = function() {};
-}
-exports.debug = debug; // for test
diff --git a/uuid.js b/uuid.js
deleted file mode 100644
index 1d83bd50a..000000000
--- a/uuid.js
+++ /dev/null
@@ -1,19 +0,0 @@
-module.exports = function () {
- var s = [], itoh = '0123456789ABCDEF';
-
- // Make array of random hex digits. The UUID only has 32 digits in it, but we
- // allocate an extra items to make room for the '-'s we'll be inserting.
- for (var i = 0; i <36; i++) s[i] = Math.floor(Math.random()*0x10);
-
- // Conform to RFC-4122, section 4.4
- s[14] = 4; // Set 4 high bits of time_high field to version
- s[19] = (s[19] & 0x3) | 0x8; // Specify 2 high bits of clock sequence
-
- // Convert to hex chars
- for (var i = 0; i <36; i++) s[i] = itoh[s[i]];
-
- // Insert '-'s
- s[8] = s[13] = s[18] = s[23] = '-';
-
- return s.join('');
-}
diff --git a/vendor/cookie/index.js b/vendor/cookie/index.js
deleted file mode 100644
index ff44b3e62..000000000
--- a/vendor/cookie/index.js
+++ /dev/null
@@ -1,65 +0,0 @@
-/*!
- * Tobi - Cookie
- * Copyright(c) 2010 LearnBoost
- * MIT Licensed
- */
-
-/**
- * Module dependencies.
- */
-
-var url = require('url');
-
-/**
- * Initialize a new `Cookie` with the given cookie `str` and `req`.
- *
- * @param {String} str
- * @param {IncomingRequest} req
- * @api private
- */
-
-var Cookie = exports = module.exports = function Cookie(str, req) {
- this.str = str;
-
- // Map the key/val pairs
- str.split(/ *; */).reduce(function(obj, pair){
- var p = pair.indexOf('=');
- var key = p > 0 ? pair.substring(0, p).trim() : pair.trim();
- var lowerCasedKey = key.toLowerCase();
- var value = p > 0 ? pair.substring(p + 1).trim() : true;
-
- if (!obj.name) {
- // First key is the name
- obj.name = key;
- obj.value = value;
- }
- else if (lowerCasedKey === 'httponly') {
- obj.httpOnly = value;
- }
- else {
- obj[lowerCasedKey] = value;
- }
- return obj;
- }, this);
-
- // Expires
- this.expires = this.expires
- ? new Date(this.expires)
- : Infinity;
-
- // Default or trim path
- this.path = this.path
- ? this.path.trim(): req
- ? url.parse(req.url).pathname: '/';
-};
-
-/**
- * Return the original cookie string.
- *
- * @return {String}
- * @api public
- */
-
-Cookie.prototype.toString = function(){
- return this.str;
-};
diff --git a/vendor/cookie/jar.js b/vendor/cookie/jar.js
deleted file mode 100644
index 34920e062..000000000
--- a/vendor/cookie/jar.js
+++ /dev/null
@@ -1,72 +0,0 @@
-/*!
-* Tobi - CookieJar
-* Copyright(c) 2010 LearnBoost
-* MIT Licensed
-*/
-
-/**
-* Module dependencies.
-*/
-
-var url = require('url');
-
-/**
-* Initialize a new `CookieJar`.
-*
-* @api private
-*/
-
-var CookieJar = exports = module.exports = function CookieJar() {
- this.cookies = [];
-};
-
-/**
-* Add the given `cookie` to the jar.
-*
-* @param {Cookie} cookie
-* @api private
-*/
-
-CookieJar.prototype.add = function(cookie){
- this.cookies = this.cookies.filter(function(c){
- // Avoid duplication (same path, same name)
- return !(c.name == cookie.name && c.path == cookie.path);
- });
- this.cookies.push(cookie);
-};
-
-/**
-* Get cookies for the given `req`.
-*
-* @param {IncomingRequest} req
-* @return {Array}
-* @api private
-*/
-
-CookieJar.prototype.get = function(req){
- var path = url.parse(req.url).pathname
- , now = new Date
- , specificity = {};
- return this.cookies.filter(function(cookie){
- if (0 == path.indexOf(cookie.path) && now < cookie.expires
- && cookie.path.length > (specificity[cookie.name] || 0))
- return specificity[cookie.name] = cookie.path.length;
- });
-};
-
-/**
-* Return Cookie string for the given `req`.
-*
-* @param {IncomingRequest} req
-* @return {String}
-* @api private
-*/
-
-CookieJar.prototype.cookieString = function(req){
- var cookies = this.get(req);
- if (cookies.length) {
- return cookies.map(function(cookie){
- return cookie.name + '=' + cookie.value;
- }).join('; ');
- }
-};