diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 9c3350d6b..000000000 --- a/.eslintrc +++ /dev/null @@ -1,22 +0,0 @@ -{ - "env": { - "node": true - }, - "rules": { - // Disallow semi-colons, unless needed to disambiguate statement - "semi": [2, "never"], - // Require strings to use single quotes - "quotes": [2, "single"], - // Require curly braces for all control statements - "curly": 2, - // Disallow using variables and functions before they've been defined - "no-use-before-define": 2, - // Allow any case for variable naming - "camelcase": 0, - // Disallow unused variables, except as function arguments - "no-unused-vars": [2, {"args":"none"}], - // Allow leading underscores for method names - // REASON: we use underscores to denote private methods - "no-underscore-dangle": 0 - } -} 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 index 98f9955c6..214a2ec7c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ node_modules coverage .idea +npm-debug.log +package-lock.json +.nyc_output \ No newline at end of file diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 53fc9efa9..000000000 --- a/.npmignore +++ /dev/null @@ -1,3 +0,0 @@ -coverage -tests -node_modules diff --git a/.travis.yml b/.travis.yml index 0988483f3..9c9940a2b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,21 @@ + language: node_js + node_js: - - "0.8" - - "0.10" -before_install: npm install -g npm@~1.4.6 -after_script: ./node_modules/.bin/istanbul cover ./node_modules/tape/bin/tape tests/test-*.js --report lcovonly && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js --verbose + - 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 index cfaf17384..d3ffcd00d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,318 @@ ## 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, @brichard19) +- [#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) @@ -32,7 +339,7 @@ - [#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) +- [#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) @@ -177,7 +484,7 @@ - [#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) +- [#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) @@ -249,18 +556,12 @@ - [#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) - -### v2.30.0 (2013/12/13) - [#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) - -### v2.29.0 (2013/12/06) - [#727](https://github.com/request/request/pull/727) fix requester bug (@jchris) - -### v2.28.0 (2013/12/04) - [#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. (@oztu) +- [#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) @@ -274,42 +575,28 @@ - [#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) - -### v2.27.0 (2013/08/15) - [#619](https://github.com/request/request/pull/619) decouple things a bit (@joaojeronimo) - -### v2.26.0 (2013/08/07) - [#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) - -### v2.24.0 (2013/07/23) - [#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) - -### v2.23.0 (2013/07/23) - [#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) - -### v2.22.0 (2013/07/05) - [#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) - -### v2.21.0 (2013/04/30) - [#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 (@noway421) +- [#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) - -### v2.17.0 (2013/04/22) -- [#523](https://github.com/request/request/pull/523) Updating dependencies (@noway421) -- [#520](https://github.com/request/request/pull/520) Fixing test-tunnel.js (@noway421) +- [#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) @@ -326,10 +613,10 @@ - [#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) +- [#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) -- [#413](https://github.com/request/request/pull/413) rename googledoodle.png to .jpg (@nfriedly) +- [#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) @@ -356,15 +643,15 @@ - [#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 (@strk) +- [#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) -- [#272](https://github.com/request/request/pull/272) Boundary begins with CRLF? (@elspoono) +- [#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) @@ -375,16 +662,16 @@ - [#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) +- [#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) +- [#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 (@milewise) +- [#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) @@ -411,7 +698,7 @@ - [#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 (@developmentseed) +- [#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) @@ -419,7 +706,7 @@ - [#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) +- [#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) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06b1968d9..8aa6999ac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,19 +1,57 @@ -# This is an OPEN Open Source Project + +# 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. + ----------------------------------------- -## What? + +## 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 @@ -35,10 +73,9 @@ There are a few basic ground-rules for contributors: 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 8b668f99f..42290d5ce 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,82 @@ -# Request — Simplified HTTP client +# Deprecated! + +As of Feb 11th 2020, request is fully deprecated. No new changes are expected to land. In fact, none have landed for some time. + +For more information about why request is deprecated and possible alternatives refer to +[this issue](https://github.com/request/request/issues/3142). + +# Request - Simplified HTTP client + [![npm package](https://nodei.co/npm/request.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/request/) -[![Build status](https://img.shields.io/travis/request/request.svg?style=flat)](https://travis-ci.org/request/request) -[![Coverage](https://img.shields.io/coveralls/request/request.svg?style=flat)](https://coveralls.io/r/request/request) -[![Gitter](https://img.shields.io/badge/gitter-join_chat-blue.svg?style=flat)](https://gitter.im/request/request?utm_source=badge) +[![Build status](https://img.shields.io/travis/request/request/master.svg?style=flat-square)](https://travis-ci.org/request/request) +[![Coverage](https://img.shields.io/codecov/c/github/request/request.svg?style=flat-square)](https://codecov.io/github/request/request?branch=master) +[![Coverage](https://img.shields.io/coveralls/request/request.svg?style=flat-square)](https://coveralls.io/r/request/request) +[![Dependency Status](https://img.shields.io/david/request/request.svg?style=flat-square)](https://david-dm.org/request/request) +[![Known Vulnerabilities](https://snyk.io/test/npm/request/badge.svg?style=flat-square)](https://snyk.io/test/npm/request) +[![Gitter](https://img.shields.io/badge/gitter-join_chat-blue.svg?style=flat-square)](https://gitter.im/request/request?utm_source=badge) + ## Super simple to use 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) // Show the HTML for the Google homepage. - } -}) + console.error('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 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, `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')) ``` -Request emits a "response" event when a response is received. The `response` argument will be an instance of [http.IncomingMessage](http://nodejs.org/api/http.html#http_http_incomingmessage). +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). -```javascript +```js request .get('http://google.com/img.png') .on('response', function(response) { @@ -52,18 +88,18 @@ request To easily handle errors when streaming requests, listen to the `error` event before piping: -```javascript +```js request .get('http://mysite.com/doodle.png') .on('error', function(err) { - console.log(err) + console.error(err) }) .pipe(fs.createWriteStream('doodle.png')) ``` Now let’s get fancy. -```javascript +```js http.createServer(function (req, resp) { if (req.url === '/doodle.png') { if (req.method === 'PUT') { @@ -77,10 +113,10 @@ http.createServer(function (req, resp) { 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) } @@ -89,14 +125,14 @@ http.createServer(function (req, resp) { 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') { @@ -107,139 +143,40 @@ http.createServer(function (req, resp) { You can still use intermediate proxies, the requests will still follow HTTP forwards, etc. -## 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](http://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. +[back to top](#table-of-contents) -### 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` -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. +## Promises & Async/Await -`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. +`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. -Here's some examples of valid `no_proxy` values: +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) - * `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. +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. -## UNIX Socket -`request` supports making requests to [UNIX Domain Sockets](http://en.wikipedia.org/wiki/Unix_domain_socket). To make one, use the following URL scheme: +[back to top](#table-of-contents) -```javascript -/* Pattern */ 'http://unix:SOCKET:PATH' -/* Example */ request.get('http://unix:/absolute/path/to/unix.socket:/request/path') -``` -Note: The `SOCKET` path is assumed to be absolute to the root of the host file system. +--- ## 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. -```javascript +```js request.post('http://service.com/upload', {form:{key:'value'}}) // or request.post('http://service.com/upload').form({key:'value'}) @@ -247,17 +184,18 @@ request.post('http://service.com/upload').form({key:'value'}) 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/felixge/node-form-data) library by [@felixge](https://github.com/felixge). For the most cases, you can pass your upload form data via the `formData` option. +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. -```javascript -var formData = { +```js +const formData = { // Pass a simple key-value pair my_field: 'my_value', // Pass data via Buffers - my_buffer: new Buffer([1, 2, 3]), + my_buffer: Buffer.from([1, 2, 3]), // Pass data via Streams my_file: fs.createReadStream(__dirname + '/unicycle.jpg'), // Pass multiple values /w an Array @@ -267,12 +205,12 @@ var formData = { ], // 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/felixge/node-form-data + // 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/jpg' + contentType: 'image/jpeg' } } }; @@ -286,22 +224,22 @@ request.post({url:'http://service.com/upload', formData: formData}, function opt 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.) -```javascript +```js // NOTE: Advanced use-case, for normal use see 'formData' usage above -var r = request.post('http://service.com/upload', function optionalCallback(err, httpResponse, body) { // ... - -var form = r.form(); +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', new Buffer([1, 2, 3])); +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/felixge/node-form-data) for more information & examples. +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. -```javascript +```js request({ method: 'PUT', preambleCRLF: true, @@ -309,7 +247,7 @@ Some variations in different HTTP implementations require a newline/CRLF before, uri: 'http://service.com/upload', multipart: [ { - 'content-type': 'application/json' + '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' }, @@ -335,10 +273,15 @@ Some variations in different HTTP implementations require a newline/CRLF before, }) ``` +[back to top](#table-of-contents) + + +--- + ## HTTP Authentication -```javascript +```js request.get('http://some.server.com/').auth('username', 'password', false); // or request.get('http://some.server.com/', { @@ -369,21 +312,21 @@ 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 +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 +detailed in [RFC 1738](http://www.ietf.org/rfc/rfc1738.txt). Simply pass the `user:password` before the host with an `@` sign: -```javascript -var username = 'username', +```js +const username = 'username', password = 'password', url = 'http://' + username + ':' + password + '@some.server.com'; -request({url: url}, function (error, response, body) { +request({url}, function (error, response, body) { // Do more stuff with 'body' here }); ``` @@ -395,19 +338,59 @@ 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 conjuction with `defaults` to allow a single function to supply the +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 -[OAuth version 1.0](https://tools.ietf.org/html/rfc5849) is supported. The +[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): -```javascript +```js // OAuth1.0 - 3-legged server side flow (Twitter example) // step 1 -var qs = require('querystring') +const qs = require('querystring') , oauth = { callback: 'http://mysite.com/callback/' , consumer_key: CONSUMER_KEY @@ -422,14 +405,14 @@ request.post({url:url, oauth:oauth}, function (e, r, body) { // verified with twitter that they are authorizing your app. // step 2 - var req_data = qs.parse(body) - var uri = 'https://api.twitter.com/oauth/authenticate' + 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 - var auth_data = qs.parse(body) + const auth_data = qs.parse(body) , oauth = { consumer_key: CONSUMER_KEY , consumer_secret: CONSUMER_SECRET @@ -441,7 +424,7 @@ request.post({url:url, oauth:oauth}, function (e, r, body) { ; request.post({url:url, oauth:oauth}, function (e, r, body) { // ready to make signed requests on behalf of the user - var perm_data = qs.parse(body) + const perm_data = qs.parse(body) , oauth = { consumer_key: CONSUMER_KEY , consumer_secret: CONSUMER_SECRET @@ -454,7 +437,7 @@ request.post({url:url, oauth:oauth}, function (e, r, body) { , user_id: perm_data.user_id } ; - 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) }) }) @@ -478,49 +461,189 @@ section of the oauth1 spec: options object. * `transport_method` defaults to `'header'` -## Custom HTTP Headers +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` -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. +[back to top](#table-of-contents) -```javascript -var request = require('request'); -var options = { - url: 'https://api.github.com/repos/request/request', - headers: { - 'User-Agent': 'request' - } -}; +--- -function callback(error, response, body) { - if (!error && response.statusCode == 200) { - var info = JSON.parse(body); - console.log(info.stargazers_count + " Stars"); - console.log(info.forks_count + " Forks"); - } -} -request(options, callback); +## 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` + +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` 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. + +Here's some examples of valid `no_proxy` values: + + * `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') +``` + +Note: The `SOCKET` path is assumed to be absolute to the root of the host file system. + +[back to top](#table-of-contents) + + +--- + + ## TLS/SSL Protocol TLS/SSL Protocol options, such as `cert`, `key` and `passphrase`, can be -set in the `agentOptions` property of the `options` object. -In the example below, we call an API requires client side SSL certificate +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); +``` + +### 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: -```javascript -var fs = require('fs') +```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'); -var options = { +const options = { url: 'https://api.some-server.com/', agentOptions: { cert: fs.readFileSync(certFile), @@ -537,7 +660,7 @@ request.get(options); It is able to force using SSLv3 only by specifying `secureProtocol`: -```javascript +```js request.get({ url: 'https://api.some-server.com/', agentOptions: { @@ -548,9 +671,10 @@ request.get({ 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 allow a different certificate, you can specify the signing CA by adding the contents of the CA's certificate file to the `agentOptions`: +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: -```javascript +```js request.get({ url: 'https://api.some-server.com/', agentOptions: { @@ -559,84 +683,214 @@ request.get({ }); ``` +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 + +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`. + +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 +``` + +[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()` -* `qs` - object containing querystring values to be appended to the `uri` -* `useQuerystring` - If true, use `querystring` to stringify and parse - querystrings, otherwise use `qs` (default: `false`). Set this option to +- `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`. -* `method` - http method (default: `"GET"`) -* `headers` - http headers (default: `{}`) -* `body` - entity body for PATCH, POST and PUT requests. Must be a `Buffer` or `String`, unless `json` is `true`. 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 + +--- + +- `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` +- `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 + - 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. -* `auth` - A hash containing values `user` || `username`, `pass` || `password`, and `sendImmediately` (optional). See documentation above. -* `json` - sets `body` but 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. -* `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. -* `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`) -* `maxRedirects` - the maximum number of redirects to follow (default: `10`) -* `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). -* `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](request.js#L747)). 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. - * 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 +- `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 the number of milliseconds to wait for a - request to respond before aborting the request. Note that if the underlying - TCP connection cannot be established, the OS-wide TCP connection timeout will - overrule the `timeout` option ([the default in Linux is around 20 seconds](http://www.sekuda.com/overriding_the_default_linux_kernel_20_second_tcp_socket_connect_timeout)). -* `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`) -* `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). -* `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. -* `agentOptions` - Object containing user agent options. See documentation above. **Note:** [see tls API doc for TLS/SSL options](http://nodejs.org/api/tls.html#tls_tls_connect_options_callback). - -* `jar` - If `true` and `tough-cookie` is installed, remember cookies for future use (or define your custom cookie jar; see examples section) -* `aws` - `object` containing AWS signing information. Should have the properties `key`, `secret`. 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) -* `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. -* `localAddress` - Local interface to bind for network connections. -* `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. -* `tunnel` - controls the behavior of +- `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` or a previous - request in the redirect chain used a tunneling proxy, `false` otherwise - * `true` - always tunnel to the destination by making a `CONNECT` request to + - `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 + - `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 +- `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`](http://nodejs.org/api/http.html#http_http_incomingmessage) 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) + + +--- + ## 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 @@ -649,86 +903,115 @@ instead, it **returns a wrapper** that has your default settings applied to it. `request.defaults` to add/override defaults that were previously defaulted. For example: -```javascript +```js //requests using baseRequest() will set the 'x-token' header -var baseRequest = request.defaults({ - headers: {x-token: 'my-token'} +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 -var specialRequest = baseRequest.defaults({ +const specialRequest = baseRequest.defaults({ headers: {special: 'special value'} }) ``` -### request.put +### request.METHOD() -Same as `request()`, but defaults to `method: "PUT"`. +These HTTP method convenience functions act just like `request()` but with a default method already set for you: -```javascript -request.put(url) -``` +- *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.patch +### request.cookie() -Same as `request()`, but defaults to `method: "PATCH"`. +Function that creates a new cookie. -```javascript -request.patch(url) +```js +request.cookie('key1=value1') ``` +### request.jar() -### request.post - -Same as `request()`, but defaults to `method: "POST"`. +Function that creates a new cookie jar. -```javascript -request.post(url) +```js +request.jar() ``` -### request.head +### response.caseless.get('header-name') -Same as `request()`, but defaults to `method: "HEAD"`. +Function that returns the specified response header field using a [case-insensitive match](https://tools.ietf.org/html/rfc7230#section-3.2) -```javascript -request.head(url) +```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')); +}); ``` -### request.del +[back to top](#table-of-contents) -Same as `request()`, but defaults to `method: "DELETE"`. -```javascript -request.del(url) -``` +--- -### request.get -Same as `request()` (for uniformity). +## Debugging -```javascript -request.get(url) -``` -### request.cookie +There are at least three ways to debug the operation of `request`: -Function that creates a new cookie. +1. Launch the node process like `NODE_DEBUG=request node script.js` + (`lib,request,otherlib` works too). -```javascript -request.cookie('key1=value1') -``` -### request.jar() +2. Set `require('request').debug = true` at any time (this does the same thing + as #1). -Function that creates a new cookie jar. +3. Use the [request-debug module](https://github.com/request/request-debug) to + view request and response headers and bodies. -```javascript -request.jar() +[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( @@ -753,13 +1036,13 @@ request.jar() ``` For backwards-compatibility, response compression is not supported by default. -To accept gzip-compressed responses, set the `gzip` option to `true`. Note +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. -```javascript - var request = require('request') +```js + const request = require('request') request( { method: 'GET' , uri: 'http://www.google.com' @@ -770,7 +1053,8 @@ the server sent a compressed response. console.log('server encoded the data as: ' + (response.headers['content-encoding'] || 'identity')) console.log('the decoded data is: ' + body) } - ).on('data', function(data) { + ) + .on('data', function(data) { // decompressed data as it is received console.log('decoded chunk: ' + data) }) @@ -783,10 +1067,10 @@ the server sent a compressed response. }) ``` -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`) and install `tough-cookie`. +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`). -```javascript -var request = request.defaults({jar: true}) +```js +const request = request.defaults({jar: true}) request('http://www.google.com', function () { request('http://images.google.com') }) @@ -794,9 +1078,9 @@ request('http://www.google.com', function () { 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') }) @@ -804,10 +1088,10 @@ request('http://www.google.com', function () { OR -```javascript -var j = request.jar(); -var cookie = request.cookie('key1=value1'); -var url = 'http://www.google.com'; +```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') @@ -819,10 +1103,10 @@ To use a custom cookie store (such as a which supports saving to and restoring from JSON files), pass it as a parameter to `request.jar()`: -```javascript -var FileCookieStore = require('tough-cookie-filestore'); +```js +const FileCookieStore = require('tough-cookie-filestore'); // NOTE - currently the 'cookies.json' file must already exist! -var j = request.jar(new FileCookieStore('cookies.json')); +const j = request.jar(new FileCookieStore('cookies.json')); request = request.defaults({ jar : j }) request('http://www.google.com', function() { request('http://images.google.com') @@ -830,31 +1114,20 @@ request('http://www.google.com', function() { ``` The cookie store must be a -[`tough-cookie`](https://github.com/goinstant/tough-cookie) +[`tough-cookie`](https://github.com/SalesforceEng/tough-cookie) store and it must support synchronous operations; see the -[`CookieStore` API docs](https://github.com/goinstant/tough-cookie/#cookiestore-api) +[`CookieStore` API docs](https://github.com/SalesforceEng/tough-cookie#api) for details. To inspect your cookie jar after a request: -```javascript -var j = request.jar() +```js +const j = request.jar() request({url: 'http://www.google.com', jar: j}, function () { - var cookie_string = j.getCookieString(uri); // "key1=value1; key2=value2; ..." - var cookies = j.getCookies(uri); + const cookie_string = j.getCookieString(url); // "key1=value1; key2=value2; ..." + const cookies = j.getCookies(url); // [{key: 'key1', value: 'value1', domain: "www.google.com", ...}, ...] }) ``` -## 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/nylen/request-debug) to - view request and response headers and bodies. +[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/examples/README.md b/examples/README.md index 526d71bba..615a33da5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -113,3 +113,23 @@ 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/index.js b/index.js index 3581b83b4..d50f9917b 100755 --- a/index.js +++ b/index.js @@ -14,89 +14,63 @@ 'use strict' -var extend = require('util')._extend - , cookies = require('./lib/cookies') - , helpers = require('./lib/helpers') - -var isFunction = helpers.isFunction - , constructObject = helpers.constructObject - , filterForCallback = helpers.filterForCallback - , constructOptionsFrom = helpers.constructOptionsFrom - , paramsHaveRequestBody = helpers.paramsHaveRequestBody +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) { - callback = filterForCallback([options, callback]) - options = constructOptionsFrom(uri, options) - - return constructObject() - .extend({callback: callback}) - .extend({options: options}) - .extend({uri: options.uri}) - .done() -} - -function request (uri, options, callback) { - if (typeof uri === 'undefined') { - throw new Error('undefined is not a valid uri or options object.') +function initParams (uri, options, callback) { + if (typeof options === 'function') { + callback = options } - var params = initParams(uri, options, callback) - options = params.options - options.callback = params.callback - options.uri = params.uri + 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) + } - return new request.Request(options) + params.callback = callback || params.callback + return params } -function requester(params) { - if(typeof params.options._requester === 'function') { - return params.options._requester +function request (uri, options, callback) { + if (typeof uri === 'undefined') { + throw new Error('undefined is not a valid uri or options object.') } - return request -} - -request.get = function (uri, options, callback) { - var params = initParams(uri, options, callback) - params.options.method = 'GET' - return requester(params)(params.uri || null, params.options, params.callback) -} -request.head = function (uri, options, callback) { var params = initParams(uri, options, callback) - params.options.method = 'HEAD' - if (paramsHaveRequestBody(params)) { + if (params.method === 'HEAD' && paramsHaveRequestBody(params)) { throw new Error('HTTP HEAD requests MUST NOT include a request body.') } - return requester(params)(params.uri || null, params.options, params.callback) + return new request.Request(params) } -request.post = function (uri, options, callback) { - var params = initParams(uri, options, callback) - params.options.method = 'POST' - return requester(params)(params.uri || null, params.options, params.callback) -} - -request.put = function (uri, options, callback) { - var params = initParams(uri, options, callback) - params.options.method = 'PUT' - return requester(params)(params.uri || null, params.options, params.callback) +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) + } } -request.patch = function (uri, options, callback) { - var params = initParams(uri, options, callback) - params.options.method = 'PATCH' - return requester(params)(params.uri || null, params.options, params.callback) -} - -request.del = function (uri, options, callback) { - var params = initParams(uri, options, callback) - params.options.method = 'DELETE' - return requester(params)(params.uri || null, params.options, 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) @@ -106,66 +80,61 @@ request.cookie = function (str) { return cookies.parse(str) } -request.defaults = function (options, requester) { - var self = this - var wrap = function (method) { - var headerlessOptions = function (options) { - options = extend({}, options) - delete options.headers - return options +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() } - var getHeaders = function (params, options) { - return constructObject() - .extend(options.headers) - .extend(params.options.headers) - .done() + if (typeof requester === 'function') { + method = requester } - return function (uri, opts, callback) { - var params = initParams(uri, opts, callback) - params.options = extend(headerlessOptions(options), params.options) + return method(target, target.callback) + } +} - if (options.headers) { - params.options.headers = getHeaders(params, options) - } +request.defaults = function (options, requester) { + var self = this - if (isFunction(requester)) { - if (method === self) { - method = requester - } else { - params.options._requester = requester - } - } + options = options || {} - return method(params.options, params.callback) - } + if (typeof options === 'function') { + requester = options + options = {} } - var defaults = wrap(self) - defaults.get = wrap(self.get) - defaults.patch = wrap(self.patch) - defaults.post = wrap(self.post) - defaults.put = wrap(self.put) - defaults.head = wrap(self.head) - defaults.del = wrap(self.del) - defaults.cookie = wrap(self.cookie) - defaults.jar = self.jar + 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 = constructObject() + var options = {} if (optionsArg) { - options.extend(optionsArg) + extend(options, optionsArg) } if (agentOptions) { options.agentOptions = agentOptions } - options.extend({forever: true}) - return request.defaults(options.done()) + options.forever = true + return request.defaults(options) } // Exports @@ -176,11 +145,11 @@ request.initParams = initParams // Backwards compatibility for request.debug Object.defineProperty(request, 'debug', { - enumerable : true, - get : function() { + enumerable: true, + get: function () { return request.Request.debug }, - set : function(debug) { + set: function (debug) { request.Request.debug = debug } }) diff --git a/lib/auth.js b/lib/auth.js index 79f1ce3b4..02f203869 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -1,12 +1,11 @@ 'use strict' var caseless = require('caseless') - , uuid = require('node-uuid') - , helpers = require('./helpers') +var uuid = require('uuid/v4') +var helpers = require('./helpers') var md5 = helpers.md5 - , toBase64 = helpers.toBase64 - +var toBase64 = helpers.toBase64 function Auth (request) { // define all public properties here @@ -21,12 +20,12 @@ function Auth (request) { Auth.prototype.basic = function (user, pass, sendImmediately) { var self = this if (typeof user !== 'string' || (pass !== undefined && typeof pass !== 'string')) { - throw new Error('auth() received invalid user or password') + self.request.emit('error', new Error('auth() received invalid user or password')) } self.user = user self.pass = pass self.hasAuth = true - var header = typeof pass !== 'undefined' ? user + ':' + pass : user + var header = user + ':' + (pass || '') if (sendImmediately || typeof sendImmediately === 'undefined') { var authHeader = 'Basic ' + toBase64(header) self.sentAuth = true @@ -42,7 +41,7 @@ Auth.prototype.bearer = function (bearer, sendImmediately) { if (typeof bearer === 'function') { bearer = bearer() } - var authHeader = 'Bearer ' + bearer + var authHeader = 'Bearer ' + (bearer || '') self.sentAuth = true return authHeader } @@ -50,8 +49,6 @@ Auth.prototype.bearer = function (bearer, sendImmediately) { Auth.prototype.digest = function (method, path, authHeader) { // TODO: More complete implementation of RFC 2617. - // - check challenge.algorithm - // - support algorithm="MD5-sess" // - handle challenge.domain // - support qop="auth-int" only // - handle Authentication-Info (not necessarily?) @@ -65,7 +62,7 @@ Auth.prototype.digest = function (method, path, authHeader) { var challenge = {} var re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi - for (;;) { + while (true) { var match = re.exec(authHeader) if (!match) { break @@ -73,11 +70,28 @@ Auth.prototype.digest = function (method, path, authHeader) { challenge[match[1]] = match[2] || match[3] } - var ha1 = md5(self.user + ':' + challenge.realm + ':' + self.pass) - var ha2 = md5(method + ':' + path) + /** + * 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) @@ -111,10 +125,12 @@ Auth.prototype.digest = function (method, path, authHeader) { Auth.prototype.onRequest = function (user, pass, sendImmediately, bearer) { var self = this - , request = self.request + var request = self.request var authHeader - if (bearer !== undefined) { + 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) @@ -126,7 +142,7 @@ Auth.prototype.onRequest = function (user, pass, sendImmediately, bearer) { Auth.prototype.onResponse = function (response) { var self = this - , request = self.request + var request = self.request if (!self.hasAuth || self.sentAuth) { return null } @@ -134,7 +150,7 @@ Auth.prototype.onResponse = function (response) { var authHeader = c.get('www-authenticate') var authVerb = authHeader && authHeader.split(' ')[0].toLowerCase() - // debug('reauth', authVerb) + request.debug('reauth', authVerb) switch (authVerb) { case 'basic': diff --git a/lib/cookies.js b/lib/cookies.js index adde7c601..bd5d46bea 100644 --- a/lib/cookies.js +++ b/lib/cookies.js @@ -3,37 +3,36 @@ var tough = require('tough-cookie') var Cookie = tough.Cookie - , CookieJar = tough.CookieJar +var CookieJar = tough.CookieJar - -exports.parse = function(str) { +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) + return Cookie.parse(str, {loose: true}) } // Adapt the sometimes-Async api of tough.CookieJar to our requirements -function RequestJar(store) { +function RequestJar (store) { var self = this - self._jar = new CookieJar(store) + self._jar = new CookieJar(store, {looseMode: true}) } -RequestJar.prototype.setCookie = function(cookieOrStr, uri, options) { +RequestJar.prototype.setCookie = function (cookieOrStr, uri, options) { var self = this return self._jar.setCookieSync(cookieOrStr, uri, options || {}) } -RequestJar.prototype.getCookieString = function(uri) { +RequestJar.prototype.getCookieString = function (uri) { var self = this return self._jar.getCookieStringSync(uri) } -RequestJar.prototype.getCookies = function(uri) { +RequestJar.prototype.getCookies = function (uri) { var self = this return self._jar.getCookiesSync(uri) } -exports.jar = function(store) { +exports.jar = function (store) { return new RequestJar(store) } diff --git a/lib/copy.js b/lib/copy.js deleted file mode 100644 index ad162a508..000000000 --- a/lib/copy.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict' - -module.exports = -function copy (obj) { - var o = {} - Object.keys(obj).forEach(function (i) { - o[i] = obj[i] - }) - return o -} diff --git a/lib/getProxyFromURI.js b/lib/getProxyFromURI.js index 0e54767f5..0b9b18e5a 100644 --- a/lib/getProxyFromURI.js +++ b/lib/getProxyFromURI.js @@ -1,33 +1,33 @@ 'use strict' -function formatHostname(hostname) { +function formatHostname (hostname) { // canonicalize the hostname, so that 'oogle.com' won't match 'google.com' return hostname.replace(/^\.*/, '.').toLowerCase() } -function parseNoProxyZone(zone) { +function parseNoProxyZone (zone) { zone = zone.trim().toLowerCase() var zoneParts = zone.split(':', 2) - , zoneHost = formatHostname(zoneParts[0]) - , zonePort = zoneParts[1] - , hasPort = zone.indexOf(':') > -1 + 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) { +function uriInNoProxy (uri, noProxy) { var port = uri.port || (uri.protocol === 'https:' ? '443' : '80') - , hostname = formatHostname(uri.hostname) - , noProxyList = noProxy.split(',') + 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) { + return noProxyList.map(parseNoProxyZone).some(function (noProxyZone) { var isMatchedAt = hostname.indexOf(noProxyZone.hostname) - , hostnameMatched = ( - isMatchedAt > -1 && - (isMatchedAt === hostname.length - noProxyZone.hostname.length) - ) + var hostnameMatched = ( + isMatchedAt > -1 && + (isMatchedAt === hostname.length - noProxyZone.hostname.length) + ) if (noProxyZone.hasPort) { return (port === noProxyZone.port) && hostnameMatched @@ -37,10 +37,10 @@ function uriInNoProxy(uri, noProxy) { }) } -function getProxyFromURI(uri) { +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: http://lynx.isc.org/current/breakout/lynx_help/keystrokes/environments.html) + // 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 || '' @@ -49,7 +49,7 @@ function getProxyFromURI(uri) { if (noProxy === '*') { return null } - + // if the noProxy is not empty and the uri is found return null if (noProxy !== '' && uriInNoProxy(uri, noProxy)) { @@ -60,14 +60,14 @@ function getProxyFromURI(uri) { if (uri.protocol === 'http:') { return process.env.HTTP_PROXY || - process.env.http_proxy || null + 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 + process.env.https_proxy || + process.env.HTTP_PROXY || + process.env.http_proxy || null } // if none of that works, return null 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 index fa5712ffb..8b2a7e6eb 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -1,66 +1,28 @@ 'use strict' -var extend = require('util')._extend - , jsonSafeStringify = require('json-stringify-safe') - , crypto = require('crypto') +var jsonSafeStringify = require('json-stringify-safe') +var crypto = require('crypto') +var Buffer = require('safe-buffer').Buffer -function deferMethod() { - if(typeof setImmediate === 'undefined') { - return process.nextTick - } - - return setImmediate -} - -function constructObject(initialObject) { - initialObject = initialObject || {} - - return { - extend: function (object) { - return constructObject(extend(initialObject, object)) - }, - done: function () { - return initialObject - } - } -} - -function constructOptionsFrom(uri, options) { - var params = constructObject() - if (typeof options === 'object') { - params.extend(options).extend({uri: uri}) - } else if (typeof uri === 'string') { - params.extend({uri: uri}) - } else { - params.extend(uri) - } - return params.done() -} +var defer = typeof setImmediate === 'undefined' + ? process.nextTick + : setImmediate -function isFunction(value) { - return typeof value === 'function' -} - -function filterForCallback(values) { - var callbacks = values.filter(isFunction) - return callbacks[0] -} - -function paramsHaveRequestBody(params) { +function paramsHaveRequestBody (params) { return ( - params.options.body || - params.options.requestBodyStream || - (params.options.json && typeof params.options.json !== 'boolean') || - params.options.multipart + params.body || + params.requestBodyStream || + (params.json && typeof params.json !== 'boolean') || + params.multipart ) } -function safeStringify (obj) { +function safeStringify (obj, replacer) { var ret try { - ret = JSON.stringify(obj) + ret = JSON.stringify(obj, replacer) } catch (e) { - ret = jsonSafeStringify(obj) + ret = jsonSafeStringify(obj, replacer) } return ret } @@ -74,16 +36,31 @@ function isReadStream (rs) { } function toBase64 (str) { - return (new Buffer(str || '', 'ascii')).toString('base64') + 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.isFunction = isFunction -exports.constructObject = constructObject -exports.constructOptionsFrom = constructOptionsFrom -exports.filterForCallback = filterForCallback exports.paramsHaveRequestBody = paramsHaveRequestBody -exports.safeStringify = safeStringify -exports.md5 = md5 -exports.isReadStream = isReadStream -exports.toBase64 = toBase64 -exports.defer = deferMethod() +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 index cddd8d392..6a009bc13 100644 --- a/lib/multipart.js +++ b/lib/multipart.js @@ -1,9 +1,9 @@ 'use strict' -var uuid = require('node-uuid') - , CombinedStream = require('combined-stream') - , isstream = require('isstream') - +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 @@ -14,25 +14,25 @@ function Multipart (request) { Multipart.prototype.isChunked = function (options) { var self = this - , chunked = false - , parts = options.data || options + var chunked = false + var parts = options.data || options if (!parts.forEach) { - throw new Error('Argument error, options.multipart.') - } - - if (self.request.getHeader('transfer-encoding') === 'chunked') { - chunked = true + 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') { - throw new Error('Body attribute missing in multipart.') + if (typeof part.body === 'undefined') { + self.request.emit('error', new Error('Body attribute missing in multipart.')) } if (isstream(part.body)) { chunked = true @@ -51,11 +51,16 @@ Multipart.prototype.setHeaders = function (chunked) { } var header = self.request.getHeader('content-type') - var contentType = (!header || header.indexOf('multipart') === -1) - ? 'multipart/related' - : header.split(';')[0] - self.request.setHeader('content-type', contentType + '; boundary=' + self.boundary) + 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) { @@ -63,7 +68,10 @@ Multipart.prototype.build = function (parts, chunked) { var body = chunked ? new CombinedStream() : [] function add (part) { - return chunked ? body.append(part) : body.push(new Buffer(part)) + if (typeof part === 'number') { + part = part.toString() + } + return chunked ? body.append(part) : body.push(Buffer.from(part)) } if (self.request.preambleCRLF) { @@ -94,7 +102,7 @@ Multipart.prototype.onRequest = function (options) { var self = this var chunked = self.isChunked(options) - , parts = options.data || options + var parts = options.data || options self.setHeaders(chunked) self.chunked = chunked diff --git a/lib/oauth.js b/lib/oauth.js index e44263a00..96de72b8e 100644 --- a/lib/oauth.js +++ b/lib/oauth.js @@ -1,14 +1,16 @@ 'use strict' -var querystring = require('querystring') - , qs = require('qs') - , caseless = require('caseless') - , uuid = require('node-uuid') - , oauth = require('oauth-sign') - +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) { @@ -20,7 +22,7 @@ OAuth.prototype.buildParams = function (_oauth, uri, method, query, form, qsLib) oa.oauth_version = '1.0' } if (!oa.oauth_timestamp) { - oa.oauth_timestamp = Math.floor( Date.now() / 1000 ).toString() + oa.oauth_timestamp = Math.floor(Date.now() / 1000).toString() } if (!oa.oauth_nonce) { oa.oauth_nonce = uuid().replace(/-/g, '') @@ -29,11 +31,11 @@ OAuth.prototype.buildParams = function (_oauth, uri, method, query, form, qsLib) oa.oauth_signature_method = 'HMAC-SHA1' } - var consumer_secret_or_private_key = oa.oauth_consumer_secret || oa.oauth_private_key + 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 + var token_secret = oa.oauth_token_secret // eslint-disable-line camelcase delete oa.oauth_token_secret var realm = oa.oauth_realm @@ -48,8 +50,9 @@ OAuth.prototype.buildParams = function (_oauth, uri, method, query, form, qsLib) method, baseurl, params, - consumer_secret_or_private_key, - token_secret) + consumer_secret_or_private_key, // eslint-disable-line camelcase + token_secret // eslint-disable-line camelcase + ) if (realm) { oa.realm = realm @@ -58,6 +61,19 @@ OAuth.prototype.buildParams = function (_oauth, uri, method, query, form, qsLib) 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 || '' @@ -66,7 +82,7 @@ OAuth.prototype.concatParams = function (oa, sep, wrap) { }).sort() if (oa.realm) { - params.splice(0, 1, 'realm') + params.splice(0, 0, 'realm') } params.push('oauth_signature') @@ -77,19 +93,19 @@ OAuth.prototype.concatParams = function (oa, sep, wrap) { OAuth.prototype.onRequest = function (_oauth) { var self = this - , request = self.request + self.params = _oauth - var uri = request.uri || {} - , method = request.method || '' - , headers = caseless(request.headers) - , body = request.body || '' - , qsLib = request.qsLib || qs + 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 - , query - , contentType = headers.get('content-type') || '' - , formContentType = 'application/x-www-form-urlencoded' - , transport = _oauth.transport_method || 'header' + 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 @@ -99,27 +115,33 @@ OAuth.prototype.onRequest = function (_oauth) { query = uri.query } if (transport === 'body' && (method !== 'POST' || contentType !== formContentType)) { - throw new Error('oauth: transport_method of \'body\' requires \'POST\' ' + - 'and content-type \'' + 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 = this.buildParams(_oauth, uri, method, query, form, qsLib) + var oa = self.buildParams(_oauth, uri, method, query, form, qsLib) switch (transport) { case 'header': - request.setHeader('Authorization', 'OAuth ' + this.concatParams(oa, ',', '"')) + self.request.setHeader('Authorization', 'OAuth ' + self.concatParams(oa, ',', '"')) break case 'query': - request.path = (query ? '&' : '?') + this.concatParams(oa, '&') + 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': - request.body = (form ? form + '&' : '') + this.concatParams(oa, '&') + self.request.body = (form ? form + '&' : '') + self.concatParams(oa, '&') break default: - throw new Error('oauth: transport_method invalid') + self.request.emit('error', new Error('oauth: transport_method invalid')) } } 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 index 2d9a9d518..b9150e77c 100644 --- a/lib/redirect.js +++ b/lib/redirect.js @@ -8,41 +8,48 @@ function Redirect (request) { this.followRedirect = true this.followRedirects = true this.followAllRedirects = false - this.allowRedirect = function () {return true} + this.followOriginalHttpMethod = false + this.allowRedirect = function () { return true } this.maxRedirects = 10 this.redirects = [] this.redirectsFollowed = 0 + this.removeRefererHeader = false } -Redirect.prototype.onRequest = function () { +Redirect.prototype.onRequest = function (options) { var self = this - , request = self.request - if (request.maxRedirects !== undefined) { - self.maxRedirects = request.maxRedirects + if (options.maxRedirects !== undefined) { + self.maxRedirects = options.maxRedirects } - if (typeof request.followRedirect === 'function') { - self.allowRedirect = request.followRedirect + if (typeof options.followRedirect === 'function') { + self.allowRedirect = options.followRedirect } - if (request.followRedirect !== undefined) { - self.followRedirects = !!request.followRedirect + if (options.followRedirect !== undefined) { + self.followRedirects = !!options.followRedirect } - if (request.followAllRedirects !== undefined) { - self.followAllRedirects = request.followAllRedirects + 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 - , request = self.request + var request = self.request var redirectTo = null if (response.statusCode >= 300 && response.statusCode < 400 && response.caseless.has('location')) { var location = response.caseless.get('location') - // debug('redirect', location) + request.debug('redirect', location) if (self.followAllRedirects) { redirectTo = location @@ -71,19 +78,19 @@ Redirect.prototype.redirectTo = function (response) { Redirect.prototype.onResponse = function (response) { var self = this - , request = self.request + var request = self.request var redirectTo = self.redirectTo(response) if (!redirectTo || !self.allowRedirect.call(request, response)) { return false } - - // debug('redirect to', redirectTo) + request.debug('redirect to', redirectTo) // ignore any potential response body. it cannot possibly be useful // to us at this point. - if (request._paused) { + // response.resume should be defined, but check anyway before calling. Workaround for browserify. + if (response.resume) { response.resume() } @@ -102,21 +109,18 @@ Redirect.prototype.onResponse = function (response) { // handle the case where we change protocol from https to http or vice versa if (request.uri.protocol !== uriPrev.protocol) { - request._updateProtocol() + delete request.agent } - self.redirects.push( - { statusCode : response.statusCode - , redirectUri: redirectTo - } - ) - if (self.followAllRedirects && response.statusCode !== 401 && response.statusCode !== 307) { - request.method = 'GET' + 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.agent delete request._started if (response.statusCode !== 401 && response.statusCode !== 307) { // Remove parameters from the previous response, unless this is the second request @@ -136,6 +140,10 @@ Redirect.prototype.onResponse = function (response) { } } + if (!self.removeRefererHeader) { + request.setHeader('referer', uriPrev.href) + } + request.emit('redirect') request.init() 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/package.json b/package.json index accaafdd9..86e4266bf 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "request", "description": "Simplified HTTP request client.", - "tags": [ + "keywords": [ "http", "simple", "util", "utility" ], - "version": "2.53.1", + "version": "2.88.1", "author": "Mikeal Rogers ", "repository": { "type": "git", @@ -18,49 +18,69 @@ }, "license": "Apache-2.0", "engines": { - "node": ">=0.8.0" + "node": ">= 6" }, "main": "index.js", + "files": [ + "lib/", + "index.js", + "request.js" + ], "dependencies": { - "bl": "~0.9.0", - "caseless": "~0.9.0", - "forever-agent": "~0.5.0", - "form-data": "~0.2.0", - "json-stringify-safe": "~5.0.0", - "mime-types": "~2.0.1", - "node-uuid": "~1.4.0", - "qs": "~2.3.1", - "tunnel-agent": "~0.4.0", - "tough-cookie": ">=0.12.0", - "http-signature": "~0.10.0", - "oauth-sign": "~0.6.0", - "hawk": "~2.3.0", - "aws-sign2": "~0.5.0", - "stringstream": "~0.0.4", - "combined-stream": "~0.0.5", - "isstream": "~0.1.1" + "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 && node node_modules/.bin/taper tests/test-*.js && npm run test-browser && npm run clean", + "test": "npm run lint && npm run test-ci && npm run test-browser", + "test-ci": "taper tests/test-*.js", + "test-cov": "nyc --reporter=lcov tape tests/test-*.js", "test-browser": "node tests/browser/start.js", - "lint": "node node_modules/.bin/eslint lib/ *.js tests/ && echo Lint passed." + "lint": "standard" }, "devDependencies": { - "browserify": "~5.9.1", - "browserify-istanbul": "~0.1.3", - "coveralls": "~2.11.2", - "eslint": "0.5.1", - "function-bind": "~1.0.0", - "istanbul": "~0.3.2", - "karma": "~0.12.21", - "karma-browserify": "~3.0.1", - "karma-cli": "0.0.4", - "karma-coverage": "0.2.6", - "karma-phantomjs-launcher": "~0.1.4", - "karma-tap": "~1.0.1", - "rimraf": "~2.2.8", - "server-destroy": "~1.0.0", - "tape": "~3.0.0", - "taper": "~0.4.0" + "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", + "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", + "nyc": "^14.1.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" + ] } } diff --git a/request.js b/request.js index 6f1305a41..198b76093 100644 --- a/request.js +++ b/request.js @@ -1,74 +1,46 @@ 'use strict' var http = require('http') - , https = require('https') - , url = require('url') - , util = require('util') - , stream = require('stream') - , qs = require('qs') - , querystring = require('querystring') - , zlib = require('zlib') - , helpers = require('./lib/helpers') - , bl = require('bl') - , hawk = require('hawk') - , aws = require('aws-sign2') - , httpSignature = require('http-signature') - , mime = require('mime-types') - , tunnel = require('tunnel-agent') - , stringstream = require('stringstream') - , caseless = require('caseless') - , ForeverAgent = require('forever-agent') - , FormData = require('form-data') - , cookies = require('./lib/cookies') - , copy = require('./lib/copy') - , net = require('net') - , getProxyFromURI = require('./lib/getProxyFromURI') - , Auth = require('./lib/auth').Auth - , OAuth = require('./lib/oauth').OAuth - , Multipart = require('./lib/multipart').Multipart - , Redirect = require('./lib/redirect').Redirect +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 - , md5 = helpers.md5 - , isReadStream = helpers.isReadStream - , toBase64 = helpers.toBase64 - , defer = helpers.defer - , globalCookieJar = cookies.jar() - +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 = {} -var defaultProxyHeaderWhiteList = [ - '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', - 'referer', - 'te', - 'transfer-encoding', - 'user-agent', - 'via' -] - -var defaultProxyHeaderExclusiveList = [ - 'proxy-authorization' -] - -function filterForNonReserved(reserved, options) { +function filterForNonReserved (reserved, options) { // Filter out properties that are not reserved. // Reserved values are passed in at call site. @@ -82,7 +54,7 @@ function filterForNonReserved(reserved, options) { return object } -function filterOutReservedFunctions(reserved, options) { +function filterOutReservedFunctions (reserved, options) { // Filter out properties that are functions and are reserved. // Reserved values are passed in at call site. @@ -95,122 +67,10 @@ function filterOutReservedFunctions(reserved, options) { } } return object - -} - -function constructProxyHost(uriObject) { - var port = uriObject.portA - , protocol = uriObject.protocol - , 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 getTunnelOption(self, options) { - // Tunnel HTTPS by default, or if a previous request in the redirect chain - // was tunneled. Allow the user to override this setting. - - // If self.tunnel is already set (because this is a redirect), use the - // existing value. - if (typeof self.tunnel !== 'undefined') { - return self.tunnel - } - - // If options.tunnel is set (the user specified a value), use it. - if (typeof options.tunnel !== 'undefined') { - return options.tunnel - } - - // If the destination is HTTPS, tunnel. - if (self.uri.protocol === 'https:') { - return true - } - - // Otherwise, leave tunnel unset, because if a later request in the redirect - // chain is HTTPS then that request (and any subsequent ones) should be - // tunneled. - return undefined -} - -function constructTunnelOptions(request) { - var proxy = request.proxy - - var tunnelOptions = { - proxy : { - host : proxy.hostname, - port : +proxy.port, - proxyAuth : proxy.auth, - headers : request.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 for properly handling a connection error -function connectionErrorHandler(error) { - var socket = this - if (socket.res) { - if (socket.res.request) { - socket.res.request.emit('error', error) - } else { - socket.res.emit('error', error) - } - } else { - socket._httpMessage.emit('error', error) - } } // Return a simpler request object to allow serialization -function requestToJSON() { +function requestToJSON () { var self = this return { uri: self.uri, @@ -220,7 +80,7 @@ function requestToJSON() { } // Return a simpler response object to allow serialization -function responseToJSON() { +function responseToJSON () { var self = this return { statusCode: self.statusCode, @@ -230,13 +90,6 @@ function responseToJSON() { } } -// encode rfc3986 characters -function rfc3986 (str) { - return str.replace(/[!'()*]/g, function(c) { - return '%' + c.charCodeAt(0).toString(16).toUpperCase() - }) -} - function Request (options) { // if given the method property in options, set property explicitMethod to true @@ -246,12 +99,18 @@ function Request (options) { // 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) - stream.Stream.call(self) - util._extend(self, nonReserved) + extend(self, nonReserved) options = filterOutReservedFunctions(reserved, options) self.readable = true @@ -259,10 +118,12 @@ function Request (options) { 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) } @@ -270,42 +131,12 @@ util.inherits(Request, stream.Stream) // Debugging Request.debug = process.env.NODE_DEBUG && /\brequest\b/.test(process.env.NODE_DEBUG) -function debug() { +function debug () { if (Request.debug) { console.error('REQUEST %s', util.format.apply(util, arguments)) } } - -Request.prototype.setupTunnel = function () { - var self = this - - if (typeof self.proxy === 'string') { - self.proxy = url.parse(self.proxy) - } - - if (!self.proxy || !self.tunnel) { - return false - } - - // Setup Proxy Header Exclusive List and White List - self.proxyHeaderExclusiveList = self.proxyHeaderExclusiveList || [] - self.proxyHeaderWhiteList = self.proxyHeaderWhiteList || defaultProxyHeaderWhiteList - 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 - self.proxyHeaders = constructProxyHeaderWhiteList(self.headers, proxyHeaderWhiteList) - self.proxyHeaders.host = constructProxyHost(self.uri) - proxyHeaderExclusiveList.forEach(self.removeHeader, self) - - // Set Agent from Tunnel Data - var tunnelFn = getTunnelFn(self) - var tunnelOptions = constructTunnelOptions(self) - self.agent = tunnelFn(tunnelOptions) - - return true -} +Request.prototype.debug = debug Request.prototype.init = function (options) { // init() contains all the code to setup the request object. @@ -317,17 +148,25 @@ Request.prototype.init = function (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' } - self.localAddress = options.localAddress - - if (!self.qsLib) { - self.qsLib = (options.useQuerystring ? querystring : qs) + if (!self.localAddress) { + self.localAddress = options.localAddress } + self._qs.init(options) + debug(options) if (!self.pool && self.pool !== false) { self.pool = globalPool @@ -355,50 +194,68 @@ Request.prototype.init = function (options) { delete self.url } - // A URI is needed by this point, throw if we haven't been able to get one + // 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') { + 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') { - // Get the socket & request paths from the URL - var unixParts = self.uri.path.split(':') - , host = unixParts[0] - , path = unixParts[1] - // Apply unix properties to request - self.socketPath = host - self.uri.pathname = path - self.uri.path = path - self.uri.host = host - self.uri.hostname = host - self.uri.isUnix = true + if (self.uri.host === 'unix') { + self.enableUnixSocket() } if (self.strictSSL === false) { self.rejectUnauthorized = false } - if(!self.hasOwnProperty('proxy')) { - self.proxy = getProxyFromURI(self.uri) - } - - self.tunnel = getTunnelOption(self, options) - if (self.proxy) { - self.setupTunnel() - } - - if (!self.uri.pathname) {self.uri.pathname = '/'} + 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 @@ -412,19 +269,30 @@ Request.prototype.init = function (options) { message += '. This can be caused by a crappy redirection.' } // This error was fatal + self.abort() return self.emit('error', new Error(message)) } - self._redirect.onRequest() + 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.hostname) + 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.getHeader('host') + (':' + 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 @@ -433,8 +301,7 @@ Request.prototype.init = function (options) { 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.uri.protocol === 'http:') { self.uri.port = 80 } else if (self.uri.protocol === 'https:') { self.uri.port = 443 } } if (self.proxy && !self.tunnel) { @@ -453,7 +320,7 @@ Request.prototype.init = function (options) { var formData = options.formData var requestForm = self.form() var appendFormValue = function (key, value) { - if (value.hasOwnProperty('value') && value.hasOwnProperty('options')) { + if (value && value.hasOwnProperty('value') && value.hasOwnProperty('options')) { requestForm.append(key, value.value, value.options) } else { requestForm.append(key, value) @@ -488,10 +355,6 @@ Request.prototype.init = function (options) { } // Auth must happen last in case signing is dependent on other headers - if (options.oauth) { - self.oauth(options.oauth) - } - if (options.aws) { self.aws(options.aws) } @@ -521,18 +384,16 @@ Request.prototype.init = function (options) { } if (self.gzip && !self.hasHeader('accept-encoding')) { - self.setHeader('accept-encoding', 'gzip') + self.setHeader('accept-encoding', 'gzip, deflate') } if (self.uri.auth && !self.hasHeader('authorization')) { - var uriAuthPieces = self.uri.auth.split(':').map(function(item){ return querystring.unescape(item) }) + 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 querystring.unescape(item) - }) + 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) } @@ -548,32 +409,48 @@ Request.prototype.init = function (options) { 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 - } + 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 { - self.body = new Buffer(self.body) length = self.body.length } - } else { - length = self.body.length - } - if (length) { - if (!self.hasHeader('content-length')) { + + if (length) { self.setHeader('content-length', length) + } else { + self.emit('error', new Error('Argument error, options.body.')) } - } else { - throw 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 - , defaultModules = {'http:':http, 'https:':https} - , httpModules = self.httpModules || {} + var defaultModules = {'http:': http, 'https:': https} + var httpModules = self.httpModules || {} self.httpModule = httpModules[protocol] || defaultModules[protocol] @@ -593,7 +470,15 @@ Request.prototype.init = function (options) { if (options.agentClass) { self.agentClass = options.agentClass } else if (options.forever) { - self.agentClass = protocol === 'http:' ? ForeverAgent : ForeverAgent.SSL + 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 } @@ -607,7 +492,7 @@ Request.prototype.init = function (options) { self.on('pipe', function (src) { if (self.ntick && self._started) { - throw new Error('You cannot pipe to this stream after the outbound request has started.') + self.emit('error', new Error('You cannot pipe to this stream after the outbound request has started.')) } self.src = src if (isReadStream(src)) { @@ -630,9 +515,9 @@ Request.prototype.init = function (options) { } } - // self.on('pipe', function () { - // console.error('You have already piped to this stream. Pipeing twice is likely to break the request.') - // }) + // self.on('pipe', function () { + // console.error('You have already piped to this stream. Pipeing twice is likely to break the request.') + // }) }) defer(function () { @@ -642,24 +527,37 @@ Request.prototype.init = function (options) { var end = function () { if (self._form) { - self._form.pipe(self) + 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 (Array.isArray(self.body)) { - self.body.forEach(function (part) { - self.write(part) - }) + if (isstream(self.body)) { + self.body.pipe(self) } else { - self.write(self.body) + setContentLength() + if (Array.isArray(self.body)) { + self.body.forEach(function (part) { + self.write(part) + }) + } else { + self.write(self.body) + } + self.end() } - 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) } @@ -669,9 +567,9 @@ Request.prototype.init = function (options) { 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()) + self.setHeader(self._form.getHeaders(), true) self._form.getLength(function (err, length) { - if (!err) { + if (!err && !isNaN(length)) { self.setHeader('content-length', length) } end() @@ -682,64 +580,6 @@ Request.prototype.init = function (options) { self.ntick = true }) - -} - -// Must call this when following a redirect from https to http or vice versa -// Attempts to keep everything as identical as possible, but update the -// httpModule, Tunneling agent, and/or Forever Agent in use. -Request.prototype._updateProtocol = function () { - var self = this - var protocol = self.uri.protocol - - if (protocol === 'https:' || self.tunnel) { - // previously was doing http, now doing https - // if it's https, then we might need to tunnel now. - if (self.proxy) { - if (self.setupTunnel()) { - return - } - } - - self.httpModule = https - switch (self.agentClass) { - case ForeverAgent: - self.agentClass = ForeverAgent.SSL - break - case http.Agent: - self.agentClass = https.Agent - break - default: - // nothing we can do. Just hope for the best. - return - } - - // if there's an agent, we need to get a new one. - if (self.agent) { - self.agent = self.getNewAgent() - } - - } else { - // previously was doing https, now doing http - self.httpModule = http - switch (self.agentClass) { - case ForeverAgent.SSL: - self.agentClass = ForeverAgent - break - case https.Agent: - self.agentClass = http.Agent - break - default: - // nothing we can do. just hope for the best - return - } - - // if there's an agent, then get a new one. - if (self.agent) { - self.agent = null - self.agent = self.getNewAgent() - } - } } Request.prototype.getNewAgent = function () { @@ -870,6 +710,16 @@ Request.prototype.start = function () { // 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 } @@ -891,44 +741,123 @@ Request.prototype.start = function () { delete reqOptions.auth debug('make request', self.uri.href) - self.req = self.httpModule.request(reqOptions) + // 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) { - self.timeoutTimer = setTimeout(function () { - self.abort() - var e = new Error('ETIMEDOUT') - e.code = 'ETIMEDOUT' - self.emit('error', e) - }, self.timeout) + 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 + } - // 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 () { + 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.req.abort() + 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() + } - 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) { + 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.on('end', function() { - if ( self.req.connection ) { - self.req.connection.removeListener('error', connectionErrorHandler) - } - }) self.emit('request', self.req) } @@ -937,43 +866,74 @@ Request.prototype.onRequestError = function (error) { if (self._aborted) { return } - if (self.req && self.req._reusedSocket && error.code === 'ECONNRESET' - && self.agent.addRequestNoreuse) { + 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 } - if (self.timeout && self.timeoutTimer) { - clearTimeout(self.timeoutTimer) - self.timeoutTimer = null - } + 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() { + 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) }) - // The check on response.connection is a workaround for browserify. - if (response.connection && response.connection.listeners('error').indexOf(connectionErrorHandler) === -1) { - response.connection.setMaxListeners(0) - response.connection.once('error', connectionErrorHandler) - } if (self._aborted) { debug('aborted', self.uri.href) response.resume() return } - if (self._paused) { - response.pause() - } else if (response.resume) { - // response.resume should be defined, but check anyway before calling. Workaround for browserify. - response.resume() - } self.response = response response.request = self @@ -981,10 +941,10 @@ Request.prototype.onRequestResponse = function (response) { // XXX This is different on 0.10, because SSL is strict by default if (self.httpModule === https && - self.strictSSL && (!response.hasOwnProperty('client') || - !response.client.authorized)) { + self.strictSSL && (!response.hasOwnProperty('socket') || + !response.socket.authorized)) { debug('strict ssl error', self.uri.href) - var sslErr = response.hasOwnProperty('client') ? response.client.authorizationError : self.uri.href + ' does not support SSL' + var sslErr = response.hasOwnProperty('socket') ? response.socket.authorizationError : self.uri.href + ' does not support SSL' self.emit('error', new Error('SSL Error: ' + sslErr)) return } @@ -1000,14 +960,11 @@ Request.prototype.onRequestResponse = function (response) { if (self.setHost) { self.removeHeader('host') } - if (self.timeout && self.timeoutTimer) { - clearTimeout(self.timeoutTimer) - self.timeoutTimer = null - } + 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. + // set the cookie if it's domain in the href's domain. try { targetCookieJar.setCookie(cookie, self.uri.href, {ignoreError: true}) } catch (e) { @@ -1037,111 +994,95 @@ Request.prototype.onRequestResponse = function (response) { } }) - response.on('end', function () { + response.once('end', function () { self._ended = true }) - var dataStream - if (self.gzip) { + 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') { - dataStream = zlib.createGunzip() - response.pipe(dataStream) + 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) } - dataStream = response + responseContent = response } } else { - dataStream = response + 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 if (dataStream.setEncoding) { - dataStream.setEncoding(self.encoding) } else { - // Should only occur on node pre-v0.9.4 (joyent/node@9b5abe5) with - // zlib streams. - // If/When support for 0.9.4 is dropped, this should be unnecessary. - dataStream = dataStream.pipe(stringstream(self.encoding)) + responseContent.setEncoding(self.encoding) } } + if (self._paused) { + responseContent.pause() + } + + self.responseContent = responseContent + self.emit('response', response) self.dests.forEach(function (dest) { self.pipeDest(dest) }) - dataStream.on('data', function (chunk) { + 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) }) - dataStream.on('end', function (chunk) { + responseContent.once('end', function (chunk) { self.emit('end', chunk) }) - dataStream.on('error', function (error) { + responseContent.on('error', function (error) { self.emit('error', error) }) - dataStream.on('close', function () {self.emit('close')}) + responseContent.on('close', function () { self.emit('close') }) if (self.callback) { - var buffer = bl() - , strings = [] - - self.on('data', function (chunk) { - if (Buffer.isBuffer(chunk)) { - buffer.append(chunk) - } else { - strings.push(chunk) - } - }) - self.on('end', function () { - debug('end event', self.uri.href) - if (self._aborted) { - debug('aborted', self.uri.href) - return - } - - if (buffer.length) { - debug('has body', self.uri.href, buffer.length) - if (self.encoding === null) { - // response.body = buffer - // can't move to this until https://github.com/rvagg/bl/issues/13 - response.body = buffer.slice() - } else { - response.body = buffer.toString(self.encoding) - } - } 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('emitting complete', self.uri.href) - if(typeof response.body === 'undefined' && !self._json) { - response.body = self.encoding === null ? new Buffer(0) : '' - } - self.emit('complete', response, response.body) - }) - } - //if no callback - else{ + self.readResponseBody(response) + } else { // if no callback self.on('end', function () { if (self._aborted) { debug('aborted', self.uri.href) @@ -1154,17 +1095,77 @@ Request.prototype.onRequestResponse = function (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.abort() + } else if (self.response) { + self.response.destroy() } + self.clearTimeout() self.emit('abort') } @@ -1177,8 +1178,7 @@ Request.prototype.pipeDest = function (dest) { var ctname = response.caseless.has('content-type') if (dest.setHeader) { dest.setHeader(ctname, response.headers[ctname]) - } - else { + } else { dest.headers[ctname] = response.headers[ctname] } } @@ -1211,7 +1211,7 @@ Request.prototype.qs = function (q, clobber) { var self = this var base if (!clobber && self.uri.query) { - base = self.qsLib.parse(self.uri.query) + base = self._qs.parse(self.uri.query) } else { base = {} } @@ -1220,29 +1220,36 @@ Request.prototype.qs = function (q, clobber) { base[i] = q[i] } - if (self.qsLib.stringify(base) === ''){ + var qs = self._qs.stringify(base) + + if (qs === '') { return self } - var qs = self.qsLib.stringify(base) - - self.uri = url.parse(self.uri.href.split('?')[0] + '?' + rfc3986(qs)) + 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) { - self.setHeader('content-type', 'application/x-www-form-urlencoded') - self.body = (typeof form === 'string') ? form.toString('utf8') : self.qsLib.stringify(form).toString('utf8') - self.body = rfc3986(self.body) + 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) { + self._form.on('error', function (err) { err.message = 'form-data: ' + err.message self.emit('error', err) self.abort() @@ -1267,20 +1274,24 @@ Request.prototype.json = function (val) { 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.body = safeStringify(self.body, self._jsonReplacer) } else { - self.body = rfc3986(self.body) + self.body = self._qs.rfc3986(self.body) } if (!self.hasHeader('content-type')) { self.setHeader('content-type', 'application/json') } } } else { - self.body = safeStringify(val) + self.body = safeStringify(val, self._jsonReplacer) if (!self.hasHeader('content-type')) { self.setHeader('content-type', 'application/json') } @@ -1310,6 +1321,19 @@ Request.prototype.getHeader = function (name, headers) { }) 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 @@ -1325,39 +1349,65 @@ Request.prototype.aws = function (opts, now) { self._aws = opts return self } - 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: aws.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 = aws.canonicalizeResource(auth.resource) - self.setHeader('authorization', aws.authorization(auth)) + + 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) { + getHeader: function (header) { return self.getHeader(header, self.headers) }, - setHeader: function(header, value) { + setHeader: function (header, value) { self.setHeader(header, value) }, method: self.method, @@ -1369,7 +1419,7 @@ Request.prototype.httpSignature = function (opts) { } Request.prototype.hawk = function (opts) { var self = this - self.setHeader('Authorization', hawk.client.header(self.uri, self.method, opts).field) + self.setHeader('Authorization', hawk.header(self.uri, self.method, opts)) } Request.prototype.oauth = function (_oauth) { var self = this @@ -1392,15 +1442,15 @@ Request.prototype.jar = function (jar) { cookies = false self._disableCookies = true } else { - var targetCookieJar = (jar && jar.getCookieString) ? jar : globalCookieJar + var targetCookieJar = jar.getCookieString ? jar : globalCookieJar var urihref = self.uri.href - //fetch cookie in the Specified host + // fetch cookie in the Specified host if (targetCookieJar) { cookies = targetCookieJar.getCookieString(urihref) } } - //if need cookie and cookie is not empty + // if need cookie and cookie is not empty if (cookies && cookies.length) { if (self.originalCookieHeader) { // Don't overwrite existing Cookie header @@ -1413,16 +1463,15 @@ Request.prototype.jar = function (jar) { return self } - // Stream API Request.prototype.pipe = function (dest, opts) { var self = this if (self.response) { if (self._destdata) { - throw new Error('You cannot pipe after data has been emitted from the response.') + self.emit('error', new Error('You cannot pipe after data has been emitted from the response.')) } else if (self._ended) { - throw new Error('You cannot pipe after the response has been 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) @@ -1436,39 +1485,48 @@ Request.prototype.pipe = function (dest, opts) { } Request.prototype.write = function () { var self = this + if (self._aborted) { return } + if (!self._started) { self.start() } - return self.req.write.apply(self.req, arguments) + 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() } - self.req.end() + if (self.req) { + self.req.end() + } } Request.prototype.pause = function () { var self = this - if (!self.response) { + if (!self.responseContent) { self._paused = true } else { - self.response.pause.apply(self.response, arguments) + self.responseContent.pause.apply(self.responseContent, arguments) } } Request.prototype.resume = function () { var self = this - if (!self.response) { + if (!self.responseContent) { self._paused = false } else { - self.response.resume.apply(self.response, arguments) + 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) { @@ -1476,11 +1534,18 @@ Request.prototype.destroy = function () { } } +Request.prototype.clearTimeout = function () { + if (this.timeoutTimer) { + clearTimeout(this.timeoutTimer) + this.timeoutTimer = null + } +} + Request.defaultProxyHeaderWhiteList = - defaultProxyHeaderWhiteList.slice() + Tunnel.defaultProxyHeaderWhiteList.slice() Request.defaultProxyHeaderExclusiveList = - defaultProxyHeaderExclusiveList.slice() + Tunnel.defaultProxyHeaderExclusiveList.slice() // Exports diff --git a/tests/browser/karma.conf.js b/tests/browser/karma.conf.js index 6c9311bb0..5fbf31a45 100644 --- a/tests/browser/karma.conf.js +++ b/tests/browser/karma.conf.js @@ -1,8 +1,9 @@ 'use strict' var istanbul = require('browserify-istanbul') -module.exports = function(config) { +module.exports = function (config) { config.set({ + client: { requestTestUrl: process.argv[4] }, basePath: '../..', frameworks: ['tap', 'browserify'], preprocessors: { diff --git a/tests/browser/start.js b/tests/browser/start.js index c515f2be3..3ce0c6a81 100644 --- a/tests/browser/start.js +++ b/tests/browser/start.js @@ -4,30 +4,30 @@ var https = require('https') var fs = require('fs') var path = require('path') -var port = 6767 - 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') + 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(port, function() { +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') + path.join(__dirname, '/karma.conf.js'), + 'https://localhost:' + port ]) c.stdout.pipe(process.stdout) c.stderr.pipe(process.stderr) - c.on('exit', function(c) { + c.on('exit', function (c) { // Exit process with karma exit code. if (c !== 0) { throw new Error('Karma exited with status code ' + c) diff --git a/tests/browser/test.js b/tests/browser/test.js index 2ca07b712..34135a398 100644 --- a/tests/browser/test.js +++ b/tests/browser/test.js @@ -4,32 +4,30 @@ 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*/ + /* eslint no-extend-native:0 */ Function.prototype.bind = require('function-bind') } +var tape = require('tape') +var request = require('../../index') -var assert = require('assert') - , tape = require('tape') - , request = require('../../index') - -tape('returns on error', function(t) { +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(response.statusCode, 0) + t.equal(typeof error, 'object') t.end() }) }) -tape('succeeds on valid URLs (with https and CORS)', function(t) { +tape('succeeds on valid URLs (with https and CORS)', function (t) { t.plan(1) request({ - uri: 'https://localhost:6767', + uri: __karma__.config.requestTestUrl, // eslint-disable-line no-undef withCredentials: false - }, function (error, response) { + }, 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/server.js b/tests/server.js index e6f398dee..93a68913d 100644 --- a/tests/server.js +++ b/tests/server.js @@ -1,40 +1,60 @@ '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.port = 6767 -exports.portSSL = 16167 - -exports.createServer = function (port) { - port = port || exports.port +exports.createServer = function () { var s = http.createServer(function (req, resp) { - s.emit(req.url, req, resp) + s.emit(req.url.replace(/(\?.*)/, ''), req, resp) }) - s.port = port - s.url = 'http://localhost:' + port + s.on('listening', function () { + s.port = this.address().port + s.url = 'http://localhost:' + s.port + }) + s.port = 0 s.protocol = 'http' return s } -exports.createSSLServer = function(port, opts) { - port = port || exports.portSSL +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 +} +exports.createSSLServer = function (opts) { var i - , options = { 'key' : path.join(__dirname, 'ssl', 'test.key') - , 'cert': path.join(__dirname, 'ssl', 'test.crt') - } + var options = { 'key': path.join(__dirname, 'ssl', 'test.key'), 'cert': path.join(__dirname, 'ssl', 'test.crt') } if (opts) { - for (i in opts) options[i] = opts[i] + for (i in opts) { + options[i] = opts[i] + } } for (i in options) { - if (i !== 'requestCert' && i !== 'rejectUnauthorized') { + if (i !== 'requestCert' && i !== 'rejectUnauthorized' && i !== 'ciphers') { options[i] = fs.readFileSync(options[i]) } } @@ -42,8 +62,11 @@ exports.createSSLServer = function(port, opts) { var s = https.createServer(options, function (req, resp) { s.emit(req.url, req, resp) }) - s.port = port - s.url = 'https://localhost:' + port + s.on('listening', function () { + s.port = this.address().port + s.url = 'https://localhost:' + s.port + }) + s.port = 0 s.protocol = 'https' return s } @@ -52,8 +75,8 @@ exports.createPostStream = function (text) { var postStream = new stream.Stream() postStream.writeable = true postStream.readable = true - setTimeout(function() { - postStream.emit('data', new Buffer(text)) + setTimeout(function () { + postStream.emit('data', Buffer.from(text)) postStream.emit('end') }, 0) return postStream @@ -61,7 +84,7 @@ exports.createPostStream = function (text) { exports.createPostValidator = function (text, reqContentType) { var l = function (req, resp) { var r = '' - req.on('data', function (chunk) {r += chunk}) + req.on('data', function (chunk) { r += chunk }) req.on('end', function () { if (req.headers['content-type'] && req.headers['content-type'].indexOf('boundary=') >= 0) { var boundary = req.headers['content-type'].split('boundary=')[1] @@ -72,8 +95,8 @@ exports.createPostValidator = function (text, reqContentType) { assert.ok(req.headers['content-type']) assert.ok(~req.headers['content-type'].indexOf(reqContentType)) } - resp.writeHead(200, {'content-type':'text/plain'}) - resp.write('OK') + resp.writeHead(200, {'content-type': 'text/plain'}) + resp.write(r) resp.end() }) } @@ -82,7 +105,7 @@ exports.createPostValidator = function (text, reqContentType) { exports.createPostJSONValidator = function (value, reqContentType) { var l = function (req, resp) { var r = '' - req.on('data', function (chunk) {r += chunk}) + req.on('data', function (chunk) { r += chunk }) req.on('end', function () { var parsedValue = JSON.parse(r) assert.deepEqual(parsedValue, value) @@ -90,7 +113,7 @@ exports.createPostJSONValidator = function (value, reqContentType) { assert.ok(req.headers['content-type']) assert.ok(~req.headers['content-type'].indexOf(reqContentType)) } - resp.writeHead(200, {'content-type':'application/json'}) + resp.writeHead(200, {'content-type': 'application/json'}) resp.write(r) resp.end() }) @@ -100,7 +123,7 @@ exports.createPostJSONValidator = function (value, reqContentType) { 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() } @@ -109,7 +132,7 @@ exports.createGetResponse = function (text, contentType) { 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) }) 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 index 8875c9ffc..19fb73b2f 100644 --- a/tests/ssl/ca/client-enc.key +++ b/tests/ssl/ca/client-enc.key @@ -1,30 +1,30 @@ -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED -DEK-Info: AES-128-CBC,E95AEBB470632DA50EC1ABAB7F2D7A2A +DEK-Info: AES-128-CBC,F383CD86ECED9F72E8A8A5671B9B478F -FDVPpWyXyUn3f+mYpHyQIWa2bnqfnzffLjrZky938WSL43hHL5XmqXXcgemQsoeb -abz68jYpVfDicvcgHprv40c69ZAKv1lmN11P9mHzRmI6HHo1oaQ2Sn13UHxX425W -NZ8IBREbSxm65a5zBYFLQXamNVBkR1z5EIoU/1FJz5CY9TAlmWYrybtUVHbc7V4S -GiiyD0Cm5fyg/436p9bcZPvXxalep9903eWd5kl1A+WO05Sin1Ilk5OVCFLMIiuM -yq4MCtONumDdIWoiSGT488P+/eMe5gfxYGseNR0AEWHYIM+Fptd2qxj9QM6CO6Pc -8EDoPs5zUXitCsTKR1tsNI7WnldMnQGf5J4SG2X5jXX3fyX534bbEBkA/CtkkdCa -A4rjrbBpMpijnq6yEsOxMOE9pqvJsWtBVdVtFtw/bx+oBKD3S2IUn3DVhSFjs62R -OSsWCT8zY0B8+EwXBPQjmfjAU/dq21ot3jxvy5fl627wM1TuEnsrqMiGxZe5mkXJ -wg9Y0c6K4Qwr5bbYIf7WvsMoKj0PfU9NRBUymSdZnaFlNSe5RguBCokaYD9/Kn9E -wyf5AKlI3LrdUk+9AmOKKmejPCzPBaGnhZjpFLLbV2pCkT3pRlrnGk5zvEKLKTdx -MjYzBxxbbBqAk5VHwN5t7D0aPerVPHsyt2bfBC2FWQAbpil70O2KHe90UbAh2VpZ -ofcx0ta6nUGTz5htcLZXsE0TR6d1bcdtXhtyxONo3kNDfNsfYlHmzQJpf9FTOKVG -EKQZ5M9OqMTzzVHysWu3DfN4x4Z6nNoXjDckWyv5bhpazZU2Puq2zgliiXORd30D -xVSuCkNeaw0DbeHd0CDL3Hn3W3ElevS7yHwKgG8OOntb4iVOzwx8nw0B/8O4OBoh -xB3EKu8eCfenkX9+smAz6ZE+ahgT7Jc0wsknekyInhWBR6JmmROy+DrCxwrntq8X -7zcoMLNFx7k6XnXo3u1MTo5rWVtsVZki+O3vCnxep0txkDoVXSSMW7aQMaUG7w68 -SXav16s14DuKpQVA6pSQRWG1spdiR2leOxvvzaFaKcnTRWKDeyqJMRf2cKqtrl+0 -Qg21folVaQlWT24P1f0d9tXGCtVzhvSBvHkomrSnCiHYzXtQLA9Cacqf4FN+vFx1 -ZxZQtv/9tFFBh0PE3EtHPdLM5KZLQuqL3jMHIWGwl+U5hQ9SlNi5239W9NWrY9Iu -ZCf5QW39orW5sH9EOk4eFwrGovY1tCJlboj6sxOoNncW9Iz/Z1q0xm6J4FV94W7+ -vaz5EXSVr4egYnJHgxEvA3XGKO2s99DFU3A2PcwkCTHDgi/UatOE8IckgPseXCsQ -I8uRzwSZO/q+kWEfb3c6PJb/wxgKYbMGqaw4Qlyg3DwjKBUbAv3gXd9rv1JvVhrP -I1LTr2MLYsDErbaj2F0KUW/XTYEHWlAQ2X4pDcq9bRBosqnJhYclW56w9d0WS10H -71J5a4Bk5qVun2mkCQIGcsONQxmVkGUYuQ/7Dag7noN8B5qZV5HljJBMQSpmDS1v -QxkMI5pPM/AptRl2jRYd8laluxM33fqP0/nFBM9ghDn8mU1BAVfMqPPv0gz446Kl +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.crt b/tests/ssl/ca/client.crt index 338387b9e..ab641d916 100644 --- a/tests/ssl/ca/client.crt +++ b/tests/ssl/ca/client.crt @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDLjCCApcCCQCt9iAWqkDJwzANBgkqhkiG9w0BAQUFADCBojELMAkGA1UEBhMC +MIIDLjCCApcCCQCt9iAWqkDJxzANBgkqhkiG9w0BAQUFADCBojELMAkGA1UEBhMC VVMxCzAJBgNVBAgTAkNBMRAwDgYDVQQHEwdPYWtsYW5kMRAwDgYDVQQKEwdyZXF1 ZXN0MSYwJAYDVQQLEx1yZXF1ZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTESMBAG A1UEAxMJcmVxdWVzdENBMSYwJAYJKoZIhvcNAQkBFhdtaWtlYWxAbWlrZWFscm9n -ZXJzLmNvbTAeFw0xNTAxMjYyMTAxMjNaFw0xNTAyMjUyMTAxMjNaMIGPMQswCQYD -VQQGEwJVUzELMAkGA1UECBMCQ0ExEDAOBgNVBAcTB09ha2xhbmQxEDAOBgNVBAoT -B3JlcXVlc3QxGjAYBgNVBAsUEXJlcXVlc3RAbG9jYWxob3N0MRMwEQYDVQQDEwpU +ZXJzLmNvbTAeFw0xODExMjIxNTIzMjNaFw0yMTExMjExNTIzMjNaMIGPMQswCQYD +VQQGEwJVUzELMAkGA1UECAwCQ0ExEDAOBgNVBAcMB09ha2xhbmQxEDAOBgNVBAoM +B3JlcXVlc3QxGjAYBgNVBAsMEXJlcXVlc3RAbG9jYWxob3N0MRMwEQYDVQQDDApU ZXN0Q2xpZW50MR4wHAYJKoZIhvcNAQkBFg9kby5ub3RAZW1haWwubWUwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJDnEzgWpgsEyQcMQnkTTm7uFNKYOM -76RID4bdK2pHbFlW4ZEWC//RFee4XkAzI4Yqa0thSaQBlm/ZrQD5w82XlHNUMtAu -CJPfIMGKUYSBv8L26Bj23X8Hju1n8bdvyqAmLh0rvLvHF0g8AitpiQvn+DAhnV7A -st5a87mUbCoBtETmnOZ1HQk8eIJ+3kZ0WaTmJy3WY8lP+LA64qLOC7kpIKkeBJUT -ndnedd8gnq87Ale1XwLILUxN1hEfbBXLG2qxr8YDyUzntQMAEDNwJP8ikCcm8Yia -+Ftp7vuioVtwR9G4lTn+3LYwDV2mki2IAjZ0bTVowJXFu6Aw9uLmm9NpAgMBAAEw -DQYJKoZIhvcNAQEFBQADgYEAsmRaJf+axYufn9vLB5nh94vRsMSSLe5ARrWl0btT -BZil51V0zvBwU502AVjxhzYmWGwEJuuChsnOv3rf/km5EmDo/yYN1ZEIZZEKGPQ1 -U7GMMJkrT1aBplPI/97CjuZkJYhBKpXMvi0yb4leJfYIORSyegPsDOEpaKMKDcDO -QWw= +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 index 2186fe53d..6bb0449be 100644 --- a/tests/ssl/ca/client.csr +++ b/tests/ssl/ca/client.csr @@ -1,18 +1,18 @@ -----BEGIN CERTIFICATE REQUEST----- -MIIC+DCCAeACAQAwgY8xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEQMA4GA1UE -BxMHT2FrbGFuZDEQMA4GA1UEChMHcmVxdWVzdDEaMBgGA1UECxQRcmVxdWVzdEBs -b2NhbGhvc3QxEzARBgNVBAMTClRlc3RDbGllbnQxHjAcBgkqhkiG9w0BCQEWD2Rv -Lm5vdEBlbWFpbC5tZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMkO -cTOBamCwTJBwxCeRNObu4U0pg4zvpEgPht0rakdsWVbhkRYL/9EV57heQDMjhipr -S2FJpAGWb9mtAPnDzZeUc1Qy0C4Ik98gwYpRhIG/wvboGPbdfweO7Wfxt2/KoCYu -HSu8u8cXSDwCK2mJC+f4MCGdXsCy3lrzuZRsKgG0ROac5nUdCTx4gn7eRnRZpOYn -LdZjyU/4sDrios4LuSkgqR4ElROd2d513yCerzsCV7VfAsgtTE3WER9sFcsbarGv -xgPJTOe1AwAQM3Ak/yKQJybxiJr4W2nu+6KhW3BH0biVOf7ctjANXaaSLYgCNnRt -NWjAlcW7oDD24uab02kCAwEAAaAjMCEGCSqGSIb3DQEJBzEUExJwYXNzd29yZCBj -aGFsbGVuZ2UwDQYJKoZIhvcNAQELBQADggEBACEqC+TJTli7enf064IaIbaJvN2+ -KyHgOmWvjI3B/Sswb2E8gm2d5epPTH1BD62wv2TowHI9NvRwa6MVb1xrdDs5WAck -EDvw9hUlv+9t5OXQXd0LmAzFVga3EjYCSdECKjiyTP71irBjmnxAYI/2cqE39xfw -rGLXI1qOs+ivptaCAIJeHkNjf/z6EHQJE9F6OyGI6Mhg8GcoufI5xfV8FXjy8RBd -Cz7uLocFxiQk9lwNwfL0ki5nrSWJOaa/1rY0q/sK/yHFLfapXEcE70vVr/hf05B/ -w8q4LgqU2PCELrFb0JKpT1L3lSXe17+AeYk2fi/SbHyVe53VY6rep/Y9yQ8= +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 index a61e755cf..c21878c96 100644 --- a/tests/ssl/ca/client.key +++ b/tests/ssl/ca/client.key @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAyQ5xM4FqYLBMkHDEJ5E05u7hTSmDjO+kSA+G3StqR2xZVuGR -Fgv/0RXnuF5AMyOGKmtLYUmkAZZv2a0A+cPNl5RzVDLQLgiT3yDBilGEgb/C9ugY -9t1/B47tZ/G3b8qgJi4dK7y7xxdIPAIraYkL5/gwIZ1ewLLeWvO5lGwqAbRE5pzm -dR0JPHiCft5GdFmk5ict1mPJT/iwOuKizgu5KSCpHgSVE53Z3nXfIJ6vOwJXtV8C -yC1MTdYRH2wVyxtqsa/GA8lM57UDABAzcCT/IpAnJvGImvhbae77oqFbcEfRuJU5 -/ty2MA1dppItiAI2dG01aMCVxbugMPbi5pvTaQIDAQABAoIBAB27wQn47Z529Bu4 -UYn4c3ZjhXY/2XCSUB1IDo3OydzeLSgoG6jDBYYKU0Z0ydHGQyUq0O8GUPbbJJdw -emB1kIYGMjgVe6wTIKsy0Ox/ubTmgxK4qFh50Ttw67MfkB08PgrnbvD07GA5FTmq -qHjnB5e6oIOYHlcpHLEesic9B8lQeHWvCDdmoSS/qSw3t/GR02rlQjE/tEwMDGSl -sDbm/3SgWBFDiNuZsewP3rq+ZaK0BQ2g9BDZjTXQEdyuqEfqZg1idx5UIYUBlaHD -qoT6DxQdhZVk5+DIABHSRhK0InegxtQTVXOwR7HeJFtyu4QqLcfS3RVr8fiv3kx4 -8uoCngECgYEA4vMj6keOPkvHzWLCd8mzdHtQ8Iq+LstSqjYrw/Wu4vYNCbY2bHTD -ZTJYKRy7R81+PXTLZaOSPVVj/gGg/89dO/xX8RYIm/FuRMTqTA45x1dWbjyHAURS -HDlWhNb2ht853XZnrnvup8HH3cuFy9Ep6oF+ffje/Lw6mSrtftR3QnkCgYEA4srP -Jg7y1TR/vSSJk+OMzlnqEtyo8ZVwO+I3UrtvKIOXJM3gsqCwDFhkAL/ROLMZL94o -27+Ov4kNZRWJ3y/Cdlj82amFGzhVdwmP3hFlJDC+Rf4bkkgtkTwn1uo/qoXE/OZO -rhYPdZkeWT/43O+kXn2+ucD/F2H+XCv8hG20XHECgYEAqGmXmE4bTz06+r2z4+KI -ygKMsMO0l9MH+AmU9qkFa6T9TdyqjFclfJ4cb/3DOGhUqtRV74mvhtYsCp041TwT -SuVaeSxJnTdPBbc+ysuvsq6sE8fUw2rop8sg2hkO/kz+ispH7GJJWrHhWESkd/gy -a7RGosKg7tnbfjgt33VZPrkCgYBc5dBWeZcUqE2Oz5GfR31c5U3Rbhux4ZG4peAd -fnN49/YIeGCLKvESDX7hI7Fy9UHi7rBz2xKA+IXJGzp/dpPEYI0qJ5tDXB7+BKeu -whdY7LJz/zOSBwjLTgXPreJoWiUnprsh6h1pAVCCJIcvEOaWYhGnCxwymsxTOx1T -rZBMsQKBgQCnhZUI01EAMhMihdABW4WEGqLyQ0y395ebD7j5eq0RxTJs1151Skx2 -in8Ut0u9K0r4MUHh1tQU+JRbmWm/uFHI7uksW5e59mfFvGs0ioGuKecy60Giieod -BgqAfzyAmodwrvPgPaBOaPCYLVDnmeM1QYH6G4DiMzpc0dJOBneZ/Q== +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 index 953223ef4..1d5cfd203 100755 --- a/tests/ssl/ca/gen-client.sh +++ b/tests/ssl/ca/gen-client.sh @@ -1,4 +1,5 @@ #!/bin/sh +set -ex # Adapted from: # http://nodejs.org/api/tls.html @@ -8,7 +9,7 @@ openssl genrsa -out client.key 2048 # Create a certificate signing request -openssl req -new -sha256 -key client.key -out client.csr -config client.cnf +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 \ @@ -17,7 +18,8 @@ openssl x509 -req \ -CAkey ca.key \ -set_serial 0x`cat ca.srl` \ -passin 'pass:password' \ - -out client.crt + -out client.crt \ + -days 1095 # Encrypt with password -openssl rsa -aes128 -in client.key -out client-enc.key -passout '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 index 21a1f367b..5dbd04b53 100755 --- a/tests/ssl/ca/gen-localhost.sh +++ b/tests/ssl/ca/gen-localhost.sh @@ -1,4 +1,5 @@ #!/bin/sh +set -ex # Adapted from: # http://nodejs.org/api/tls.html @@ -8,7 +9,7 @@ openssl genrsa -out localhost.key 2048 # Create a certificate signing request -openssl req -new -sha256 -key localhost.key -out localhost.csr -config localhost.cnf +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 \ @@ -17,4 +18,5 @@ openssl x509 -req \ -CAkey ca.key \ -set_serial 0x`cat ca.srl` \ -passin 'pass:password' \ - -out localhost.crt + -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.crt b/tests/ssl/ca/localhost.crt index 7c8ad98c8..4a0fe1003 100644 --- a/tests/ssl/ca/localhost.crt +++ b/tests/ssl/ca/localhost.crt @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDLTCCApYCCQCt9iAWqkDJwzANBgkqhkiG9w0BAQsFADCBojELMAkGA1UEBhMC +MIIDLTCCApYCCQCt9iAWqkDJxzANBgkqhkiG9w0BAQUFADCBojELMAkGA1UEBhMC VVMxCzAJBgNVBAgTAkNBMRAwDgYDVQQHEwdPYWtsYW5kMRAwDgYDVQQKEwdyZXF1 ZXN0MSYwJAYDVQQLEx1yZXF1ZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTESMBAG A1UEAxMJcmVxdWVzdENBMSYwJAYJKoZIhvcNAQkBFhdtaWtlYWxAbWlrZWFscm9n -ZXJzLmNvbTAeFw0xNTAxMjQwNDEzMzVaFw0xNTAyMjMwNDEzMzVaMIGOMQswCQYD -VQQGEwJVUzELMAkGA1UECBMCQ0ExEDAOBgNVBAcTB09ha2xhbmQxEDAOBgNVBAoT -B3JlcXVlc3QxGjAYBgNVBAsUEXJlcXVlc3RAbG9jYWxob3N0MRIwEAYDVQQDEwls +ZXJzLmNvbTAeFw0xODExMjIxNTIzMjVaFw0yODExMTkxNTIzMjVaMIGOMQswCQYD +VQQGEwJVUzELMAkGA1UECAwCQ0ExEDAOBgNVBAcMB09ha2xhbmQxEDAOBgNVBAoM +B3JlcXVlc3QxGjAYBgNVBAsMEXJlcXVlc3RAbG9jYWxob3N0MRIwEAYDVQQDDAls b2NhbGhvc3QxHjAcBgkqhkiG9w0BCQEWD2RvLm5vdEBlbWFpbC5tZTCCASIwDQYJ -KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ8rQcGbUWXLZZ0XAq0A5OSG/yunu0D5 -x5GcgArmiWo2EwgkdGGd3DrECmsXAqg05LDTP8LjN5wdvtdEXc4R+vf54VN/CD31 -AtFXILfGEQZioWtdni+T9K0jEcVukdklAwCC1jjplJ8MxTXyJ9pEVoyv/tX4EFMf -+ayUsDUCSrJQLW069iV4GXQglZr6UVfSG3ip4+1JDvP0MKUhitfWkrAYtb8m30AS -fRj2Le/9HhhBWwxLDK1G23TqC86Sqe0Mhk5a1V5DKZPanDld5jVNKlrXTUMU4OcL -b3mdidAy5kSFmRSJJdficeXnp6eBGK5kOFoRIyjeJ0Ut/ntw2c7WcLsCAwEAATAN -BgkqhkiG9w0BAQsFAAOBgQAgie0OE8U3w4w2cGgCa8qqraOezz961/i/6zNLamMn -XSjoIpB8syOgXzPTwk/pR1OPOIfv2C06usqTR31r/zAN63Ev+wqBW4RIQ6mD1J0O -WxmuY7pYyISD+5CXGMoxmM4Mh78GBQaUWTwhbsZr+vNSgEWwJfEvoh2BAVUgqjHh -ug== +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 index a74907d20..44aed654b 100644 --- a/tests/ssl/ca/localhost.csr +++ b/tests/ssl/ca/localhost.csr @@ -1,18 +1,18 @@ -----BEGIN CERTIFICATE REQUEST----- -MIIC9zCCAd8CAQAwgY4xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEQMA4GA1UE -BxMHT2FrbGFuZDEQMA4GA1UEChMHcmVxdWVzdDEaMBgGA1UECxQRcmVxdWVzdEBs -b2NhbGhvc3QxEjAQBgNVBAMTCWxvY2FsaG9zdDEeMBwGCSqGSIb3DQEJARYPZG8u -bm90QGVtYWlsLm1lMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnytB -wZtRZctlnRcCrQDk5Ib/K6e7QPnHkZyACuaJajYTCCR0YZ3cOsQKaxcCqDTksNM/ -wuM3nB2+10RdzhH69/nhU38IPfUC0Vcgt8YRBmKha12eL5P0rSMRxW6R2SUDAILW -OOmUnwzFNfIn2kRWjK/+1fgQUx/5rJSwNQJKslAtbTr2JXgZdCCVmvpRV9IbeKnj -7UkO8/QwpSGK19aSsBi1vybfQBJ9GPYt7/0eGEFbDEsMrUbbdOoLzpKp7QyGTlrV -XkMpk9qcOV3mNU0qWtdNQxTg5wtveZ2J0DLmRIWZFIkl1+Jx5eenp4EYrmQ4WhEj -KN4nRS3+e3DZztZwuwIDAQABoCMwIQYJKoZIhvcNAQkHMRQTEnBhc3N3b3JkIGNo -YWxsZW5nZTANBgkqhkiG9w0BAQsFAAOCAQEAQBSAV6pyGnm1+EsDku9sKWy1ZhM8 -75+nQ2rJvAtmcLE7mAzJ5QEB8MfGELfPbpKJEHi/TUHvONyrIyml9zy1+0+fkxRx -5gXZ6Ggw64t5OpNgEc2EtJta+dua+W7gNeGFWPJ36iAHlkRIgK4PxttM7YV4hEwQ -kJ5jWmNPj/e033kPShBAnWPGFdFTG92oq9Xb0+yF4a1ff4PpQLVivj5tDzs80B5M -Khm38sQOK7qPR4IdugoJHkRtBcXQKNmeSXhYPl+0FYIFpvPd+E8DKWEOfR6LjQ9J -WBLLMvr4B8BXnoJu4uHzJln6uVWFxizfa+u9LRIrL7CjxgAupKQ6kRprgQ== +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 index 515700c01..567bdb8e6 100644 --- a/tests/ssl/ca/localhost.js +++ b/tests/ssl/ca/localhost.js @@ -2,28 +2,32 @@ var fs = require('fs') var https = require('https') -var options = { key: fs.readFileSync('./localhost.key') - , cert: fs.readFileSync('./localhost.crt') } +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(1337) +server.listen(0, function () { + var ca = fs.readFileSync('./ca.crt') + var agent = new https.Agent({ + host: 'localhost', + port: this.address().port, + ca: ca + }) -var ca = fs.readFileSync('./ca.crt') -var agent = new https.Agent({ host: 'localhost', port: 1337, ca: ca }) - -https.request({ host: 'localhost' - , method: 'HEAD' - , port: 1337 - , 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, + 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 index 2cfeaf458..211578ddb 100644 --- a/tests/ssl/ca/localhost.key +++ b/tests/ssl/ca/localhost.key @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAnytBwZtRZctlnRcCrQDk5Ib/K6e7QPnHkZyACuaJajYTCCR0 -YZ3cOsQKaxcCqDTksNM/wuM3nB2+10RdzhH69/nhU38IPfUC0Vcgt8YRBmKha12e -L5P0rSMRxW6R2SUDAILWOOmUnwzFNfIn2kRWjK/+1fgQUx/5rJSwNQJKslAtbTr2 -JXgZdCCVmvpRV9IbeKnj7UkO8/QwpSGK19aSsBi1vybfQBJ9GPYt7/0eGEFbDEsM -rUbbdOoLzpKp7QyGTlrVXkMpk9qcOV3mNU0qWtdNQxTg5wtveZ2J0DLmRIWZFIkl -1+Jx5eenp4EYrmQ4WhEjKN4nRS3+e3DZztZwuwIDAQABAoIBAE3YJgy+HY0fcM7n -VhOugEOUEnATVG1uu7/nPmgWX9ZmI+Czk4e6YN8MydueIVqKo94nMuPppGTh11gI -w6fo+0kUGLNxSWKj1YD0j7fRUrpAupl768VxIxUaNbLNZN9CTrmNQ6AJ/PnckQbV -K9B/46Ri3steyv0cgkt5XMRQHqAd+OAMiqiSD03gxgcpnyPCskzgk48GIM1NhjwW -Q6ia0uIPUnak7KxW13F6yH9ddnNpS1CJdcStaZeFWlZgDGbTDef9Op2+f42CU54/ -bXlnb6pm8ZHv7NxkMS3ncObv1d1TD3qfFOQpLiWu8EdyqVrCKFbToTnwG0XdYKuG -1+GEe4ECgYEAzSnTI+INAxADuqu/M9KXSKtj30YdAo52s5z8Ui0SWbUS9fxpxzAV -Kx00RKD4I9CwV8sq4IETPFd+x+ietcMVeLH7jkwoY7A8ntLKctgQvpdkOCgsd1+Y -g2H2ukKjsc0RH0QUaq8pSlrIzku09CKwAeQK7tBDUZ3wMH4Xc5o6M+sCgYEAxpvb -xXF7UW5+xt8hwxin0FhiaqJuJoCo0E6/JjXI5B6QJNxVfechvig2H2lcZ7HcGdO6 -r+CmpgIcoEtWTLunFM6JnrZnmQixoQCSyC4CbTfpUpDxr8/2cKDU6982eo0sG2Tu -I0CCDrqWMQFMBkeQBdQECBXi9rQs2hc7Ji29EnECgYBLp5uzhL01nucxI/oq+wJM -it8WS32RHsXI8B/fkb1NlUc7rGu5RxLXRjqrAAzg8CjHByV1ikN0ofMfdrln31uA -mWlhDNZsBGYmTybWeLScA6myR6Y2Eutjr3FTOBWzECK7O9inipYYVCfuYt6ElHIB -EH2zmNrqMuqKh0TQnVPPJwKBgCmYrxjVQby2ZbsFNK8F1O/f8wzeZC+QNssaExLP -pPmSJSJzOzyZUgnfpiZCDOZy6+RE4g7AAGc4fgJchQChNMc40r34+g2lMn7D/foL -GNsDIMz4KoZmCflg1fdo0qIsOxaptu6PLi4jih1NZjzSdCmkVAvVeamt5s7umqbO -YZEhAoGAeICIxtu1kx0LQxvQ3nfBv5aJwvksvTcAZvC02XpFIpL8l8WE1pUAWHMC -R4K4O8uzBH3ILAmnihG096lhTtnt9RiEtPzOPkAB/83nipa/NCLgOIPOVqTgnS1Z -2Zmckn2mbYTNxB8g+nQmeLeH6pM9+KhxHioQJIzPPpubfUTriY8= +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 2e216d5c6..ca2a00e77 100644 --- a/tests/ssl/ca/server.js +++ b/tests/ssl/ca/server.js @@ -2,29 +2,33 @@ var fs = require('fs') var https = require('https') -var options = { key: fs.readFileSync('./server.key') - , cert: fs.readFileSync('./server.crt') } +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) +server.listen(0, function () { + var ca = fs.readFileSync('./ca.crt') + var agent = new https.Agent({ + host: 'localhost', + port: this.address().port, + ca: ca + }) -var ca = fs.readFileSync('./ca.crt') -var agent = new https.Agent({ host: 'localhost', port: 1337, 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 index 665e7408c..4682cbbba 100644 --- a/tests/test-agentOptions.js +++ b/tests/test-agentOptions.js @@ -1,47 +1,51 @@ 'use strict' -var request = require('../index') - , http = require('http') - , server = require('./server') - , tape = require('tape') +// 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(function (req, resp) { - resp.statusCode = 200 - resp.end('') -}) + var s = server.createServer() -tape('setup', function(t) { - s.listen(s.port, function() { - t.end() + s.on('/', function (req, resp) { + resp.statusCode = 200 + resp.end('') }) -}) -tape('without agentOptions should use global agent', function(t) { - var r = request(s.url, function(/*err, res, body*/) { - // TODO: figure out why err.code === 'ECONNREFUSED' on Travis? - //if (err) console.log(err) - //t.equal(err, null) - t.deepEqual(r.agent, http.globalAgent) - t.equal(Object.keys(r.pool).length, 0) - t.end() + tape('setup', function (t) { + s.listen(0, function () { + 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*/) { - // TODO: figure out why err.code === 'ECONNREFUSED' on Travis? - //if (err) console.log(err) - //t.equal(err, null) - t.equal(r.agent.options.foo, 'bar') - t.equal(Object.keys(r.pool).length, 1) - 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('cleanup', function(t) { - s.close(function() { - 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 index 1ad452352..5368b0584 100644 --- a/tests/test-basic-auth.js +++ b/tests/test-basic-auth.js @@ -1,28 +1,27 @@ 'use strict' var assert = require('assert') - , http = require('http') - , request = require('../index') - , tape = require('tape') +var http = require('http') +var request = require('../index') +var tape = require('tape') var numBasicRequests = 0 - , basicServer - , port = 6767 +var basicServer -tape('setup', function(t) { +tape('setup', function (t) { basicServer = http.createServer(function (req, res) { numBasicRequests++ var ok if (req.headers.authorization) { - if (req.headers.authorization === 'Basic ' + new Buffer('user:pass').toString('base64')) { + if (req.headers.authorization === 'Basic ' + Buffer.from('user:pass').toString('base64')) { ok = true - } else if ( req.headers.authorization === 'Basic ' + new Buffer('user:').toString('base64')) { + } else if (req.headers.authorization === 'Basic ' + Buffer.from('user:').toString('base64')) { ok = true - } else if ( req.headers.authorization === 'Basic ' + new Buffer(':pass').toString('base64')) { + } else if (req.headers.authorization === 'Basic ' + Buffer.from(':pass').toString('base64')) { ok = true - } else if ( req.headers.authorization === 'Basic ' + new Buffer('user').toString('base64')) { + } 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 @@ -36,7 +35,7 @@ tape('setup', function(t) { if (req.url === '/post/') { var expectedContent = 'key=value' - req.on('data', function(data) { + req.on('data', function (data) { assert.equal(data, expectedContent) }) assert.equal(req.method, 'POST') @@ -50,21 +49,24 @@ tape('setup', function(t) { res.statusCode = 401 res.end('401') } - }).listen(port, function() { + }).listen(0, function () { + basicServer.port = this.address().port + basicServer.url = 'http://localhost:' + basicServer.port t.end() }) }) -tape('sendImmediately - false', function(t) { +tape('sendImmediately - false', function (t) { var r = request({ 'method': 'GET', - 'uri': 'http://localhost:6767/test/', + 'uri': basicServer.url + '/test/', 'auth': { 'user': 'user', 'pass': 'pass', 'sendImmediately': false } - }, function(error, res, body) { + }, function (error, res, body) { + t.error(error) t.equal(r._auth.user, 'user') t.equal(res.statusCode, 200) t.equal(numBasicRequests, 2) @@ -72,16 +74,17 @@ tape('sendImmediately - false', function(t) { }) }) -tape('sendImmediately - true', function(t) { +tape('sendImmediately - true', function (t) { // If we don't set sendImmediately = false, request will send basic auth var r = request({ 'method': 'GET', - 'uri': 'http://localhost:6767/test2/', + 'uri': basicServer.url + '/test2/', 'auth': { 'user': 'user', 'pass': 'pass' } - }, function(error, res, body) { + }, function (error, res, body) { + t.error(error) t.equal(r._auth.user, 'user') t.equal(res.statusCode, 200) t.equal(numBasicRequests, 3) @@ -89,11 +92,12 @@ tape('sendImmediately - true', function(t) { }) }) -tape('credentials in url', function(t) { +tape('credentials in url', function (t) { var r = request({ 'method': 'GET', - 'uri': 'http://user:pass@localhost:6767/test2/' - }, function(error, res, body) { + '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) @@ -101,17 +105,18 @@ tape('credentials in url', function(t) { }) }) -tape('POST request', function(t) { +tape('POST request', function (t) { var r = request({ 'method': 'POST', 'form': { 'key': 'value' }, - 'uri': 'http://localhost:6767/post/', + 'uri': basicServer.url + '/post/', 'auth': { 'user': 'user', 'pass': 'pass', 'sendImmediately': false } - }, function(error, res, body) { + }, function (error, res, body) { + t.error(error) t.equal(r._auth.user, 'user') t.equal(res.statusCode, 200) t.equal(numBasicRequests, 6) @@ -119,17 +124,18 @@ tape('POST request', function(t) { }) }) -tape('user - empty string', function(t) { - t.doesNotThrow( function() { +tape('user - empty string', function (t) { + t.doesNotThrow(function () { var r = request({ 'method': 'GET', - 'uri': 'http://localhost:6767/allow_empty_user/', + 'uri': basicServer.url + '/allow_empty_user/', 'auth': { 'user': '', 'pass': 'pass', 'sendImmediately': false } - }, function(error, res, body ) { + }, function (error, res, body) { + t.error(error) t.equal(r._auth.user, '') t.equal(res.statusCode, 200) t.equal(numBasicRequests, 8) @@ -138,17 +144,18 @@ tape('user - empty string', function(t) { }) }) -tape('pass - undefined', function(t) { - t.doesNotThrow( function() { +tape('pass - undefined', function (t) { + t.doesNotThrow(function () { var r = request({ 'method': 'GET', - 'uri': 'http://localhost:6767/allow_undefined_password/', + 'uri': basicServer.url + '/allow_undefined_password/', 'auth': { 'user': 'user', 'pass': undefined, 'sendImmediately': false } - }, function(error, res, body ) { + }, function (error, res, body) { + t.error(error) t.equal(r._auth.user, 'user') t.equal(res.statusCode, 200) t.equal(numBasicRequests, 10) @@ -157,20 +164,41 @@ tape('pass - undefined', function(t) { }) }) -tape('auth method', function(t) { +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('http://localhost:6767/test/') - .auth('user','',false) + .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, 12) + t.equal(numBasicRequests, 14) t.end() }) }) -tape('get method', function(t) { - var r = request.get('http://localhost:6767/test/', +tape('get method', function (t) { + var r = request.get(basicServer.url + '/test/', { auth: { user: 'user', @@ -181,13 +209,13 @@ tape('get method', function(t) { t.equal(r._auth.user, 'user') t.equal(err, null) t.equal(res.statusCode, 200) - t.equal(numBasicRequests, 14) + t.equal(numBasicRequests, 16) t.end() }) }) -tape('cleanup', function(t) { - basicServer.close(function() { +tape('cleanup', function (t) { + basicServer.close(function () { t.end() }) }) diff --git a/tests/test-bearer-auth.js b/tests/test-bearer-auth.js index 38c41f126..032ccc7ee 100644 --- a/tests/test-bearer-auth.js +++ b/tests/test-bearer-auth.js @@ -1,15 +1,14 @@ 'use strict' var assert = require('assert') - , http = require('http') - , request = require('../index') - , tape = require('tape') +var http = require('http') +var request = require('../index') +var tape = require('tape') var numBearerRequests = 0 - , bearerServer - , port = 6767 +var bearerServer -tape('setup', function(t) { +tape('setup', function (t) { bearerServer = http.createServer(function (req, res) { numBearerRequests++ @@ -30,7 +29,7 @@ tape('setup', function(t) { if (req.url === '/post/') { var expectedContent = 'data_key=data_value' - req.on('data', function(data) { + req.on('data', function (data) { assert.equal(data, expectedContent) }) assert.equal(req.method, 'POST') @@ -44,61 +43,65 @@ tape('setup', function(t) { res.statusCode = 401 res.end('401') } - }).listen(port, function() { + }).listen(0, function () { + bearerServer.url = 'http://localhost:' + this.address().port t.end() }) }) -tape('', function(t) { +tape('bearer auth', function (t) { request({ 'method': 'GET', - 'uri': 'http://localhost:6767/test/', + 'uri': bearerServer.url + '/test/', 'auth': { 'bearer': 'theToken', 'sendImmediately': false } - }, function(error, res, body) { + }, function (error, res, body) { + t.error(error) t.equal(res.statusCode, 200) t.equal(numBearerRequests, 2) t.end() }) }) -tape('', function(t) { +tape('bearer auth with default sendImmediately', function (t) { // If we don't set sendImmediately = false, request will send bearer auth request({ 'method': 'GET', - 'uri': 'http://localhost:6767/test2/', + 'uri': bearerServer.url + '/test2/', 'auth': { 'bearer': 'theToken' } - }, function(error, res, body) { + }, function (error, res, body) { + t.error(error) t.equal(res.statusCode, 200) t.equal(numBearerRequests, 3) t.end() }) }) -tape('', function(t) { +tape('', function (t) { request({ 'method': 'POST', 'form': { 'data_key': 'data_value' }, - 'uri': 'http://localhost:6767/post/', + 'uri': bearerServer.url + '/post/', 'auth': { 'bearer': 'theToken', 'sendImmediately': false } - }, function(error, res, body) { + }, function (error, res, body) { + t.error(error) t.equal(res.statusCode, 200) t.equal(numBearerRequests, 5) t.end() }) }) -tape('', function(t) { +tape('using .auth, sendImmediately = false', function (t) { request - .get('http://localhost:6767/test/') - .auth(null,null,false,'theToken') + .get(bearerServer.url + '/test/') + .auth(null, null, false, 'theToken') .on('response', function (res) { t.equal(res.statusCode, 200) t.equal(numBearerRequests, 7) @@ -106,10 +109,10 @@ tape('', function(t) { }) }) -tape('', function(t) { +tape('using .auth, sendImmediately = true', function (t) { request - .get('http://localhost:6767/test/') - .auth(null,null,true,'theToken') + .get(bearerServer.url + '/test/') + .auth(null, null, true, 'theToken') .on('response', function (res) { t.equal(res.statusCode, 200) t.equal(numBearerRequests, 8) @@ -117,38 +120,68 @@ tape('', function(t) { }) }) -tape('', function(t) { +tape('bearer is a function', function (t) { request({ 'method': 'GET', - 'uri': 'http://localhost:6767/test/', + 'uri': bearerServer.url + '/test/', 'auth': { - 'bearer': function() { return 'theToken' }, + 'bearer': function () { return 'theToken' }, 'sendImmediately': false } - }, function(error, res, body) { + }, function (error, res, body) { + t.error(error) t.equal(res.statusCode, 200) t.equal(numBearerRequests, 10) t.end() }) }) -tape('', function(t) { +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': 'http://localhost:6767/test2/', + 'uri': bearerServer.url + '/test2/', 'auth': { - 'bearer': function() { return 'theToken' } + 'bearer': function () { return 'theToken' } } - }, function(error, res, body) { + }, function (error, res, body) { + t.error(error) t.equal(res.statusCode, 200) t.equal(numBearerRequests, 11) t.end() }) }) -tape('cleanup', function(t) { - bearerServer.close(function() { +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 d605f574f..dc482125a 100644 --- a/tests/test-body.js +++ b/tests/test-body.js @@ -1,27 +1,28 @@ 'use strict' var server = require('./server') - , events = require('events') - , stream = require('stream') - , request = require('../index') - , tape = require('tape') +var request = require('../index') +var tape = require('tape') +var http = require('http') var s = server.createServer() -tape('setup', function(t) { - s.listen(s.port, function() { +tape('setup', function (t) { + s.listen(0, function () { t.end() }) }) -function addTest(name, data) { - tape('test ' + name, function(t) { +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() }) @@ -29,123 +30,125 @@ function addTest(name, data) { } addTest('testGet', { - resp : server.createGetResponse('TESTING!') - , expectBody: 'TESTING!' + resp: server.createGetResponse('TESTING!'), expectBody: 'TESTING!' }) addTest('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: '\uF8FF\u03A9\u2603' + 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(new Buffer('TESTING!')) - , encoding: null - , expectBody: new Buffer('TESTING!') + resp: server.createGetResponse(Buffer.from('TESTING!')), encoding: null, expectBody: Buffer.from('TESTING!') }) addTest('testGetEncoding', { - resp : server.createGetResponse(new Buffer('efa3bfcea9e29883', 'hex')) - , encoding: 'hex' - , expectBody: 'efa3bfcea9e29883' + resp: server.createGetResponse(Buffer.from('efa3bfcea9e29883', 'hex')), encoding: 'hex', expectBody: 'efa3bfcea9e29883' }) addTest('testGetUTF', { - resp: server.createGetResponse(new Buffer([0xEF, 0xBB, 0xBF, 226, 152, 131])) - , encoding: 'utf8' - , expectBody: '\u2603' + 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} + resp: server.createGetResponse('{"test":true}', 'application/json'), json: true, expectBody: {'test': true} }) addTest('testPutString', { - resp : server.createPostValidator('PUTTINGDATA') - , method : 'PUT' - , body : 'PUTTINGDATA' + resp: server.createPostValidator('PUTTINGDATA'), method: 'PUT', body: 'PUTTINGDATA' }) addTest('testPutBuffer', { - resp : server.createPostValidator('PUTTINGDATA') - , method : 'PUT' - , body : new Buffer('PUTTINGDATA') + resp: server.createPostValidator('PUTTINGDATA'), method: 'PUT', body: Buffer.from('PUTTINGDATA') }) addTest('testPutJSON', { - resp : server.createPostValidator(JSON.stringify({foo: 'bar'})) - , method: 'PUT' - , json: {foo: 'bar'} + 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.'} - ] + 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.'} - ] + 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.'} - ] + 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('cleanup', function(t) { - s.close(function() { - t.end() +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-cookies.js b/tests/test-cookies.js index cf8de5cf9..6bebcaf12 100644 --- a/tests/test-cookies.js +++ b/tests/test-cookies.js @@ -1,74 +1,107 @@ 'use strict' var http = require('http') - , request = require('../index') - , tape = require('tape') +var request = require('../index') +var tape = require('tape') - -var validUrl = 'http://localhost:6767/valid' - , invalidUrl = 'http://localhost:6767/invalid' +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(6767, function() { +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) { +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('after server sends a cookie', function(t) { +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') + 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() - }) + 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) { +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() - }) + 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) { +tape('make sure setCookie works', function (t) { var jar3 = request.jar() - , err = null + var err = null try { jar3.setCookie(request.cookie('foo=bar'), validUrl) } catch (e) { @@ -82,16 +115,16 @@ tape('make sure setCookie works', function(t) { t.end() }) -tape('custom store', function(t) { - var Store = function() {} +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() { +tape('cleanup', function (t) { + server.close(function () { t.end() }) }) diff --git a/tests/test-defaults.js b/tests/test-defaults.js index ec827be68..f75f5d7bc 100644 --- a/tests/test-defaults.js +++ b/tests/test-defaults.js @@ -1,107 +1,38 @@ 'use strict' var server = require('./server') - , assert = require('assert') - , request = require('../index') - , tape = require('tape') +var request = require('../index') +var qs = require('qs') +var tape = require('tape') var s = server.createServer() -tape('setup', function(t) { - s.listen(s.port, function() { - 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!') +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('/merge-headers', function (req, resp) { - assert.equal(req.headers.foo, 'bar') - assert.equal(req.headers.merged, 'yes') - resp.writeHead(200) - resp.end() + s.on('/head', function (req, res) { + res.writeHead(200, {'x-data': JSON.stringify({method: req.method, headers: req.headers})}) + res.end() }) - s.on('/post', function (req, resp) { - assert.equal(req.headers.foo, 'bar') - assert.equal(req.headers['content-type'], null) - assert.equal(req.method, 'POST') - resp.writeHead(200, {'Content-Type': 'application/json'}) - resp.end(JSON.stringify({foo:'bar'})) - }) - - s.on('/patch', function (req, resp) { - assert.equal(req.headers.foo, 'bar') - assert.equal(req.headers['content-type'], null) - assert.equal(req.method, 'PATCH') - resp.writeHead(200, {'Content-Type': 'application/json'}) - resp.end(JSON.stringify({foo:'bar'})) - }) - - s.on('/post-body', 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'})) - }) - - 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'})) - }) - - 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() - }) - - s.on('/get_recursive1', function (req, resp) { - assert.equal(req.headers.foo, 'bar1') - assert.equal(req.method, 'GET') - resp.writeHead(200, {'Content-Type': 'text/plain'}) - resp.end('TESTING!') - }) - - s.on('/get_recursive2', function (req, resp) { - assert.equal(req.headers.foo, 'bar1') - assert.equal(req.headers.baz, 'bar2') - assert.equal(req.method, 'GET') - resp.writeHead(200, {'Content-Type': 'text/plain'}) - resp.end('TESTING!') - }) - - s.on('/get_recursive3', function (req, resp) { - assert.equal(req.headers.foo, 'bar3') - assert.equal(req.headers.baz, 'bar2') - assert.equal(req.method, 'GET') - resp.writeHead(200, {'Content-Type': 'text/plain'}) - resp.end('TESTING!') - }) - - s.on('/get_custom', function(req, resp) { - assert.equal(req.headers.foo, 'bar') - assert.equal(req.headers.x, 'y') - resp.writeHead(200, {'Content-Type': 'text/plain'}) - resp.end() - }) - - s.on('/set-undefined', function (req, resp) { - assert.equal(req.method, 'POST') - assert.equal(req.headers['content-type'], 'application/json') - assert.equal(req.headers['x-foo'], 'baz') + s.on('/set-undefined', function (req, res) { var data = '' - req.on('data', function(d) { + req.on('data', function (d) { data += d }) - req.on('end', function() { - resp.writeHead(200, {'Content-Type': 'application/json'}) - resp.end(data) + 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) + })) }) }) @@ -109,135 +40,240 @@ tape('setup', function(t) { }) }) -tape('get(string, function)', function(t) { +tape('get(string, function)', function (t) { request.defaults({ headers: { foo: 'bar' } - })(s.url + '/get', function (e, r, b) { - t.equal(e, null) - t.equal(b, 'TESTING!') + })(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) { +tape('merge headers', function (t) { request.defaults({ headers: { foo: 'bar', merged: 'no' } - })(s.url + '/merge-headers', { - headers: { merged: 'yes' } + })(s.url + '/', { + headers: { merged: 'yes' }, json: true }, function (e, r, b) { - t.equal(e, null) - t.equal(r.statusCode, 200) + 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('post(string, object, function)', function(t) { +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 + '/post', { json: true }, function (e, r, b) { - t.equal(e, null) - t.equal(b.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) { +tape('patch(string, object, function)', function (t) { request.defaults({ headers: { foo: 'bar' } - }).patch(s.url + '/patch', { json: true }, function (e, r, b) { - t.equal(e, null) - t.equal(b.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) { +tape('post(string, object, function) with body', function (t) { request.defaults({ headers: { foo: 'bar' } - }).post(s.url + '/post-body', { + }).post(s.url + '/', { json: true, body: { bar: 'baz' } }, function (e, r, b) { - t.equal(e, null) - t.equal(b.foo, 'bar') + 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) { +tape('del(string, function)', function (t) { request.defaults({ headers: {foo: 'bar'}, json: true - }).del(s.url + '/del', function (e, r, b) { - t.equal(e, null) - t.equal(b.foo, 'bar') + }).del(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) { +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) { - t.equal(e, null) + 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(6) +tape('recursive defaults', function (t) { + t.plan(11) var defaultsOne = request.defaults({ headers: { foo: 'bar1' } }) - , defaultsTwo = defaultsOne.defaults({ headers: { baz: 'bar2' } }) - , defaultsThree = defaultsTwo.defaults({}, function(options, callback) { - options.headers = { - foo: 'bar3' - } - defaultsTwo(options, callback) - }) + var defaultsTwo = defaultsOne.defaults({ headers: { baz: 'bar2' } }) + var defaultsThree = defaultsTwo.defaults({}, function (options, callback) { + options.headers = { + foo: 'bar3' + } + defaultsTwo(options, callback) + }) - defaultsOne(s.url + '/get_recursive1', function (e, r, b) { - t.equal(e, null) - t.equal(b, 'TESTING!') + defaultsOne(s.url + '/', {json: true}, function (e, r, b) { + t.equal(b.method, 'GET') + t.equal(b.headers.foo, 'bar1') }) - defaultsTwo(s.url + '/get_recursive2', function (e, r, b) { - t.equal(e, null) - t.equal(b, 'TESTING!') + 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 + '/get_recursive3', function (e, r, b) { - t.equal(e, null) - t.equal(b, 'TESTING!') + 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('test custom request handler function', function(t) { - t.plan(2) +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) { + }, function (uri, options, callback) { var params = request.initParams(uri, options, callback) - options = params.options - options.headers.x = 'y' - return request(params.uri, params.options, params.callback) + params.headers.x = 'y' + return request(params.uri, params, params.callback) }) - t.throws(function() { - requestWithCustomHandler.head(s.url + '/get_custom', function(e, r, b) { + 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 + '/get_custom', function(e, r, b) { - t.equal(e, null) + 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) { +tape('test only setting undefined properties', function (t) { request.defaults({ method: 'post', json: true, @@ -247,14 +283,58 @@ tape('test only setting undefined properties', function(t) { json: {foo: 'bar'}, headers: {'x-foo': 'baz'} }, function (e, r, b) { - t.equal(e, null) - t.deepEqual(b, { foo: 'bar' }) + 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() { +tape('cleanup', function (t) { + s.close(function () { t.end() }) }) diff --git a/tests/test-digest-auth.js b/tests/test-digest-auth.js index 5ea141ee5..d5c3c0ee5 100644 --- a/tests/test-digest-auth.js +++ b/tests/test-digest-auth.js @@ -1,20 +1,25 @@ 'use strict' var http = require('http') - , request = require('../index') - , tape = require('tape') +var request = require('../index') +var tape = require('tape') +var crypto = require('crypto') -function makeHeader() { +function makeHeader () { return [].join.call(arguments, ', ') } -function makeHeaderRegex() { +function makeHeaderRegex () { return new RegExp('^' + makeHeader.apply(null, arguments) + '$') } -var digestServer = http.createServer(function(req, res) { - var ok - , testHeader +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) { @@ -47,6 +52,47 @@ var digestServer = http.createServer(function(req, res) { '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 @@ -84,80 +130,103 @@ var digestServer = http.createServer(function(req, res) { } }) -tape('setup', function(t) { - digestServer.listen(6767, function() { +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 sendImmediately = false', function(t) { +tape('with MD5-sess algorithm', function (t) { var numRedirects = 0 request({ method: 'GET', - uri: 'http://localhost:6767/test/', + uri: digestServer.url + '/test/md5-sess', auth: { user: 'test', pass: 'testing', sendImmediately: false } - }, function(error, response, body) { + }, function (error, response, body) { t.equal(error, null) t.equal(response.statusCode, 200) t.equal(numRedirects, 1) t.end() - }).on('redirect', function() { + }).on('redirect', function () { t.equal(this.response.statusCode, 401) numRedirects++ }) }) -tape('without sendImmediately = false', function(t) { +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: 'http://localhost:6767/test/', + uri: digestServer.url + '/test/', auth: { user: 'test', pass: 'testing' } - }, function(error, response, body) { + }, function (error, response, body) { t.equal(error, null) t.equal(response.statusCode, 401) t.equal(numRedirects, 0) t.end() - }).on('redirect', function() { + }).on('redirect', function () { t.equal(this.response.statusCode, 401) numRedirects++ }) }) -tape('with different credentials', function(t) { +tape('with different credentials', function (t) { var numRedirects = 0 request({ method: 'GET', - uri: 'http://localhost:6767/dir/index.html', + uri: digestServer.url + '/dir/index.html', auth: { user: 'Mufasa', pass: 'CircleOfLife', sendImmediately: false } - }, function(error, response, body) { + }, function (error, response, body) { t.equal(error, null) t.equal(response.statusCode, 200) t.equal(numRedirects, 1) t.end() - }).on('redirect', function() { + }).on('redirect', function () { t.equal(this.response.statusCode, 401) numRedirects++ }) }) -tape('cleanup', function(t) { - digestServer.close(function() { +tape('cleanup', function (t) { + digestServer.close(function () { t.end() }) }) diff --git a/tests/test-emptyBody.js b/tests/test-emptyBody.js index 0e4acc55f..684d3d5ae 100644 --- a/tests/test-emptyBody.js +++ b/tests/test-emptyBody.js @@ -1,22 +1,23 @@ 'use strict' var request = require('../index') - , http = require('http') - , tape = require('tape') +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(6767, function() { +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('http://localhost:6767', function(err, res, body) { +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, '') @@ -24,23 +25,23 @@ tape('empty body with encoding', function(t) { }) }) -tape('empty body without encoding', function(t) { +tape('empty body without encoding', function (t) { request({ - url: 'http://localhost:6767', + url: s.url, encoding: null - }, function(err, res, body) { + }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 200) - t.same(body, new Buffer(0)) + t.same(body, Buffer.alloc(0)) t.end() }) }) -tape('empty JSON body', function(t) { +tape('empty JSON body', function (t) { request({ - url: 'http://localhost:6767', + url: s.url, json: {} - }, function(err, res, body) { + }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 200) t.equal(body, undefined) @@ -48,8 +49,8 @@ tape('empty JSON body', function(t) { }) }) -tape('cleanup', function(t) { - s.close(function() { +tape('cleanup', function (t) { + s.close(function () { t.end() }) }) diff --git a/tests/test-errors.js b/tests/test-errors.js index fde97fefe..7060e9fca 100644 --- a/tests/test-errors.js +++ b/tests/test-errors.js @@ -1,20 +1,19 @@ 'use strict' -var server = require('./server') - , request = require('../index') - , tape = require('tape') +var request = require('../index') +var tape = require('tape') -var local = 'http://localhost:8888/asdf' +var local = 'http://localhost:0/asdf' -tape('without uri', function(t) { - t.throws(function() { +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() { +tape('invalid uri 1', function (t) { + t.throws(function () { request({ uri: 'this-is-not-a-valid-uri' }) @@ -22,8 +21,8 @@ tape('invalid uri 1', function(t) { t.end() }) -tape('invalid uri 2', function(t) { - t.throws(function() { +tape('invalid uri 2', function (t) { + t.throws(function () { request({ uri: 'github.com/uri-is-not-valid-without-protocol' }) @@ -31,8 +30,19 @@ tape('invalid uri 2', function(t) { t.end() }) -tape('deprecated unix URL', function(t) { - t.throws(function() { +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' }) @@ -40,8 +50,8 @@ tape('deprecated unix URL', function(t) { t.end() }) -tape('invalid body', function(t) { - t.throws(function() { +tape('invalid body', function (t) { + t.throws(function () { request({ uri: local, body: {} }) @@ -49,8 +59,8 @@ tape('invalid body', function(t) { t.end() }) -tape('invalid multipart', function(t) { - t.throws(function() { +tape('invalid multipart', function (t) { + t.throws(function () { request({ uri: local, multipart: 'foo' @@ -59,8 +69,8 @@ tape('invalid multipart', function(t) { t.end() }) -tape('multipart without body 1', function(t) { - t.throws(function() { +tape('multipart without body 1', function (t) { + t.throws(function () { request({ uri: local, multipart: [ {} ] @@ -69,11 +79,30 @@ tape('multipart without body 1', function(t) { t.end() }) -tape('multipart without body 2', function(t) { - t.throws(function() { +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 index ebaad4c0d..c057a0bb9 100644 --- a/tests/test-event-forwarding.js +++ b/tests/test-event-forwarding.js @@ -1,14 +1,14 @@ 'use strict' var server = require('./server') - , request = require('../index') - , tape = require('tape') +var request = require('../index') +var tape = require('tape') var s = server.createServer() -tape('setup', function(t) { - s.listen(s.port, function() { - s.on('/', function(req, res) { +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() @@ -17,23 +17,23 @@ tape('setup', function(t) { }) }) -tape('should emit socket event', function(t) { +tape('should emit socket event', function (t) { t.plan(4) - var req = request(s.url, function(err, res, body) { + 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) { + req.on('socket', function (socket) { var requestSocket = req.req.socket t.equal(requestSocket, socket) }) }) -tape('cleanup', function(t) { - s.close(function() { +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 index 5e73db258..b40adf84b 100644 --- a/tests/test-follow-all-303.js +++ b/tests/test-follow-all-303.js @@ -1,8 +1,8 @@ 'use strict' var http = require('http') - , request = require('../index') - , tape = require('tape') +var request = require('../index') +var tape = require('tape') var server = http.createServer(function (req, res) { if (req.method === 'POST') { @@ -14,17 +14,18 @@ var server = http.createServer(function (req, res) { } }) -tape('setup', function(t) { - server.listen(6767, function() { +tape('setup', function (t) { + server.listen(0, function () { + server.url = 'http://localhost:' + this.address().port t.end() }) }) -tape('followAllRedirects with 303', function(t) { +tape('followAllRedirects with 303', function (t) { var redirects = 0 request.post({ - url: 'http://localhost:6767/foo', + url: server.url + '/foo', followAllRedirects: true, form: { foo: 'bar' } }, function (err, res, body) { @@ -32,13 +33,13 @@ tape('followAllRedirects with 303', function(t) { t.equal(body, 'ok') t.equal(redirects, 1) t.end() - }).on('redirect', function() { + }).on('redirect', function () { redirects++ }) }) -tape('cleanup', function(t) { - server.close(function() { +tape('cleanup', function (t) { + server.close(function () { t.end() }) }) diff --git a/tests/test-follow-all.js b/tests/test-follow-all.js index d6e00d064..c35d74b3e 100644 --- a/tests/test-follow-all.js +++ b/tests/test-follow-all.js @@ -1,8 +1,8 @@ 'use strict' var http = require('http') - , request = require('../index') - , tape = require('tape') +var request = require('../index') +var tape = require('tape') var server = http.createServer(function (req, res) { // redirect everything 3 times, no matter what. @@ -25,17 +25,18 @@ var server = http.createServer(function (req, res) { res.end('try again') }) -tape('setup', function(t) { - server.listen(6767, function() { +tape('setup', function (t) { + server.listen(0, function () { + server.url = 'http://localhost:' + this.address().port t.end() }) }) -tape('followAllRedirects', function(t) { +tape('followAllRedirects', function (t) { var redirects = 0 request.post({ - url: 'http://localhost:6767/foo', + url: server.url + '/foo', followAllRedirects: true, jar: true, form: { foo: 'bar' } @@ -44,13 +45,13 @@ tape('followAllRedirects', function(t) { t.equal(body, 'ok') t.equal(redirects, 4) t.end() - }).on('redirect', function() { + }).on('redirect', function () { redirects++ }) }) -tape('cleanup', function(t) { - server.close(function() { +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 index 1f3178686..d6ee25d1b 100644 --- a/tests/test-form-data-error.js +++ b/tests/test-form-data-error.js @@ -1,19 +1,19 @@ 'use strict' var request = require('../index') - , server = require('./server') - , tape = require('tape') +var server = require('./server') +var tape = require('tape') var s = server.createServer() -tape('setup', function(t) { - s.listen(s.port, function() { +tape('setup', function (t) { + s.listen(0, function () { t.end() }) }) -tape('re-emit formData errors', function(t) { - s.on('/', function(req, res) { +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.') @@ -21,14 +21,65 @@ tape('re-emit formData errors', function(t) { request.post(s.url, function (err, res, body) { t.equal(err.message, 'form-data: Arrays are not supported.') - setTimeout(function() { + setTimeout(function () { t.end() }, 10) }).form().append('field', ['value1', 'value2']) }) -tape('cleanup', function(t) { - s.close(function() { +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 index 8b09ea17f..990562be5 100644 --- a/tests/test-form-data.js +++ b/tests/test-form-data.js @@ -1,79 +1,92 @@ 'use strict' var http = require('http') - , path = require('path') - , mime = require('mime-types') - , request = require('../index') - , fs = require('fs') - , tape = require('tape') +var path = require('path') +var mime = require('mime-types') +var request = require('../index') +var fs = require('fs') +var tape = require('tape') -function runTest(t, json) { +function runTest (t, options) { var remoteFile = path.join(__dirname, 'googledoodle.jpg') - , localFile = path.join(__dirname, 'unicycle.jpg') - , multipartFormData = {} + var localFile = path.join(__dirname, 'unicycle.jpg') + var multipartFormData = {} - var server = http.createServer(function(req, res) { + var server = http.createServer(function (req, res) { if (req.url === '/file') { - res.writeHead(200, {'content-type': 'image/jpg', 'content-length':7187}) + 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) { + req.on('data', function (d) { data += d }) - req.on('end', function() { + 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 ) + 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 ) + 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 ) + 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 ) + 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 ) + 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 ) + 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 ) + t.ok(data.indexOf('form-data; name="batch"') !== -1) + t.ok(data.match(/form-data; name="batch"/g).length === 2) - // check for http://localhost:6767/file traces - t.ok( data.indexOf('Photoshop ICC') !== -1 ) - t.ok( data.indexOf('Content-Type: ' + mime.lookup(remoteFile) ) !== -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) res.writeHead(200) - res.end(json ? JSON.stringify({status: 'done'}) : 'done') + res.end(options.json ? JSON.stringify({status: 'done'}) : 'done') }) }) - server.listen(6767, function() { - + 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 = new Buffer([1, 2, 3]) + multipartFormData.my_buffer = Buffer.from([1, 2, 3]) multipartFormData.my_file = fs.createReadStream(localFile) - multipartFormData.remote_file = request('http://localhost:6767/file') + multipartFormData.remote_file = request(url + '/file') multipartFormData.secret_file = { value: fs.createReadStream(localFile), options: { @@ -87,28 +100,34 @@ function runTest(t, json) { ] var reqOptions = { - url: 'http://localhost:6767/upload', + url: url + '/upload', formData: multipartFormData } - if (json) { + 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, json ? {status: 'done'} : 'done') - server.close(function() { + t.deepEqual(body, options.json ? {status: 'done'} : 'done') + server.close(function () { t.end() }) }) - }) } -tape('multipart formData', function(t) { - runTest(t, false) +tape('multipart formData', function (t) { + runTest(t, {json: false}) +}) + +tape('multipart formData + JSON', function (t) { + runTest(t, {json: true}) }) -tape('multipart formData + JSON', function(t) { - runTest(t, 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 index ae2f17182..5e46917bb 100644 --- a/tests/test-form-urlencoded.js +++ b/tests/test-form-urlencoded.js @@ -1,25 +1,27 @@ 'use strict' var http = require('http') - , request = require('../index') - , tape = require('tape') +var request = require('../index') +var tape = require('tape') - -function runTest (t, options) { - - var server = http.createServer(function(req, res) { - - t.ok(req.headers['content-type'].match(/application\/x-www-form-urlencoded/)) +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) { + req.on('data', function (d) { data += d }) - req.on('end', function() { + req.on('end', function () { t.equal(data, 'some=url&encoded=data') res.writeHead(200) @@ -27,16 +29,19 @@ function runTest (t, options) { }) }) - server.listen(6767, function() { - - request.post('http://localhost:6767', options, function(err, res, body) { + 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() { + server.close(function () { t.end() }) }) + if (!options.form && !options.body) { + r.form({some: 'url', encoded: 'data'}) + } }) } @@ -45,15 +50,24 @@ 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) + 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 index 0c4ef3959..5f262f204 100644 --- a/tests/test-form.js +++ b/tests/test-form.js @@ -1,65 +1,67 @@ 'use strict' var http = require('http') - , path = require('path') - , mime = require('mime-types') - , request = require('../index') - , fs = require('fs') - , tape = require('tape') - -tape('multipart form append', function(t) { +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') - , localFile = path.join(__dirname, 'unicycle.jpg') - , totalLength = null - , FIELDS = [] + var localFile = path.join(__dirname, 'unicycle.jpg') + var totalLength = null + var FIELDS = [] - var server = http.createServer(function(req, res) { + var server = http.createServer(function (req, res) { if (req.url === '/file') { - res.writeHead(200, {'content-type': 'image/jpg', 'content-length':7187}) + 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) { + req.on('data', function (d) { data += d }) - req.on('end', function() { + 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 ) + 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 ) + 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 ) + 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 ) + 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:6767/file traces - t.ok( data.indexOf('Photoshop ICC') !== -1 ) - t.ok( data.indexOf('Content-Type: ' + mime.lookup(remoteFile) ) !== -1 ) + 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 ) + t.ok(+req.headers['content-length'] === totalLength) res.writeHead(200) res.end('done') @@ -68,30 +70,30 @@ tape('multipart form append', function(t) { }) }) - server.listen(6767, function() { - + server.listen(0, function () { + var url = 'http://localhost:' + this.address().port FIELDS = [ { name: 'my_field', value: 'my_value' }, - { name: 'my_buffer', value: new Buffer([1, 2, 3]) }, + { name: 'my_buffer', value: Buffer.from([1, 2, 3]) }, { name: 'my_file', value: fs.createReadStream(localFile) }, - { name: 'remote_file', value: request('http://localhost:6767/file') } + { name: 'remote_file', value: request(url + '/file') } ] - var req = request.post('http://localhost:6767/upload', function(err, res, body) { + 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() { + server.close(function () { t.end() }) }) var form = req.form() - FIELDS.forEach(function(field) { + FIELDS.forEach(function (field) { form.append(field.name, field.value) }) - form.getLength(function(err, length) { + form.getLength(function (err, length) { t.equal(err, null) totalLength = length }) diff --git a/tests/test-gzip.js b/tests/test-gzip.js index 5c2b3c1ae..933b7bae0 100644 --- a/tests/test-gzip.js +++ b/tests/test-gzip.js @@ -1,47 +1,110 @@ 'use strict' var request = require('../index') - , http = require('http') - , zlib = require('zlib') - , assert = require('assert') - , tape = require('tape') +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' - , testContentGzip +var testContentBig +var testContentBigGzip +var testContentGzip -var server = http.createServer(function(req, res) { +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) { + 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) { - zlib.gzip(testContent, function(err, data) { +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 - server.listen(6767, function() { - t.end() + + 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: 'http://localhost:6767/foo', gzip: true } - request.get(options, function(err, res, body) { +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) @@ -49,30 +112,30 @@ tape('transparently supports gzip decoding to callbacks', function(t) { }) }) -tape('transparently supports gzip decoding to pipes', function(t) { - var options = { url: 'http://localhost:6767/foo', gzip: true } +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) { + .on('data', function (chunk) { chunks.push(chunk) }) - .on('end', function() { + .on('end', function () { t.equal(Buffer.concat(chunks).toString(), testContent) t.end() }) - .on('error', function(err) { + .on('error', function (err) { t.fail(err) }) }) -tape('does not request gzip if user specifies Accepted-Encodings', function(t) { +tape('does not request gzip if user specifies Accepted-Encodings', function (t) { var headers = { 'Accept-Encoding': null } var options = { - url: 'http://localhost:6767/foo', + url: server.url + '/foo', headers: headers, gzip: true } - request.get(options, function(err, res, body) { + request.get(options, function (err, res, body) { t.equal(err, null) t.equal(res.headers['content-encoding'], undefined) t.equal(body, testContent) @@ -80,10 +143,10 @@ tape('does not request gzip if user specifies Accepted-Encodings', function(t) { }) }) -tape('does not decode user-requested encoding by default', function(t) { +tape('does not decode user-requested encoding by default', function (t) { var headers = { 'Accept-Encoding': 'gzip' } - var options = { url: 'http://localhost:6767/foo', headers: headers } - request.get(options, function(err, res, body) { + 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()) @@ -91,32 +154,32 @@ tape('does not decode user-requested encoding by default', function(t) { }) }) -tape('supports character encoding with gzip encoding', function(t) { +tape('supports character encoding with gzip encoding', function (t) { var headers = { 'Accept-Encoding': 'gzip' } var options = { - url: 'http://localhost:6767/foo', + url: server.url + '/foo', headers: headers, gzip: true, encoding: 'utf8' } var strings = [] request.get(options) - .on('data', function(string) { + .on('data', function (string) { t.equal(typeof string, 'string') strings.push(string) }) - .on('end', function() { + .on('end', function () { t.equal(strings.join(''), testContent) t.end() }) - .on('error', function(err) { + .on('error', function (err) { t.fail(err) }) }) -tape('transparently supports gzip error to callbacks', function(t) { - var options = { url: 'http://localhost:6767/error', gzip: true } - request.get(options, function(err, res, body) { +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) @@ -124,10 +187,10 @@ tape('transparently supports gzip error to callbacks', function(t) { }) }) -tape('transparently supports gzip error to pipes', function(t) { - var options = { url: 'http://localhost:6767/error', gzip: true } +tape('transparently supports gzip error to pipes', function (t) { + var options = { url: server.url + '/error', gzip: true } request.get(options) - .on('data', function (/*chunk*/) { + .on('data', function (chunk) { t.fail('Should not receive data event') }) .on('end', function () { @@ -139,8 +202,95 @@ tape('transparently supports gzip error to pipes', function(t) { }) }) -tape('cleanup', function(t) { - server.close(function() { +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 index bd0ac1d2e..3765908cf 100644 --- a/tests/test-hawk.js +++ b/tests/test-hawk.js @@ -1,54 +1,187 @@ 'use strict' var http = require('http') - , request = require('../index') - , hawk = require('hawk') - , tape = require('tape') - , assert = require('assert') - -var server = http.createServer(function(req, res) { - var getCred = function(id, callback) { - assert.equal(id, 'dh37fgj492je') - var credentials = { - key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', - algorithm: 'sha256', - user: 'Steve' - } - return callback(null, credentials) - } +var request = require('../index') +var hawk = require('../lib/hawk') +var tape = require('tape') +var assert = require('assert') - hawk.server.authenticate(req, getCred, {}, function(err, credentials, attributes) { - res.writeHead(err ? 401 : 200, { - 'Content-Type': 'text/plain' - }) - res.end(err ? 'Shoosh!' : 'Hello ' + credentials.user) +var server = http.createServer(function (req, res) { + res.writeHead(200, { + 'Content-Type': 'text/plain' }) + res.end(authenticate(req)) }) -tape('setup', function(t) { - server.listen(6767, function() { +tape('setup', function (t) { + server.listen(0, function () { + server.url = 'http://localhost:' + this.address().port t.end() }) }) -tape('hawk', function(t) { - var creds = { - key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', - algorithm: 'sha256', - id: 'dh37fgj492je' - } - request('http://localhost:6767', { +var creds = { + key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', + algorithm: 'sha256', + id: 'dh37fgj492je' +} + +tape('hawk-get', function (t) { + request(server.url, { hawk: { credentials: creds } - }, function(err, res, body) { + }, 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, 'Hello Steve') + t.equal(body, 'FAIL') t.end() }) }) -tape('cleanup', function(t) { - server.close(function() { +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 773cf863d..68b748691 100644 --- a/tests/test-headers.js +++ b/tests/test-headers.js @@ -1,38 +1,50 @@ 'use strict' var server = require('./server') - , request = require('../index') - , util = require('util') - , tape = require('tape') +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) { +s.on('/redirect/from', function (req, res) { res.writeHead(301, { - location : '/redirect/to' + location: '/redirect/to' }) res.end() }) -s.on('/redirect/to', function(req, res) { +s.on('/redirect/to', function (req, res) { res.end('ok') }) -tape('setup', function(t) { - s.listen(s.port, function() { - t.end() +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) { +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) { + request(requestObj, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 200) t.end() @@ -40,68 +52,91 @@ function runTest(name, path, requestObj, serverAssertFn) { }) } -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') - } -) +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') + }) -runTest( - 'neither headers.cookie nor a cookie jar is specified', - 'no-cookie', - {}, - function(t, req, res) { - t.equal(req.headers.cookie, undefined) + 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) { +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) { - var ondata = socket.ondata - socket.ondata = function(d, start, end) { - rawData += d.slice(start, end).toString() - return ondata.apply(this, arguments) + + 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) { + function checkHostHeader (host) { t.ok( new RegExp('^Host: ' + host + '$', 'm').test(rawData), util.format( @@ -112,9 +147,9 @@ tape('upper-case Host header and redirect', function(t) { var redirects = 0 request({ - url : s.url + '/redirect/from', - headers : { Host : '127.0.0.1' } - }, function(err, res, body) { + 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') @@ -122,15 +157,149 @@ tape('upper-case Host header and redirect', function(t) { // XXX should the host header change like this after a redirect? checkHostHeader('localhost:' + s.port) t.end() - }).on('redirect', function() { + }).on('redirect', function () { redirects++ t.equal(this.uri.href, s.url + '/redirect/to') checkHostHeader('127.0.0.1') }) }) -tape('cleanup', function(t) { - s.close(function() { +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() + }) +}) + +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 + '"]') +} + +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 index 1ad96bafa..5ce015cba 100644 --- a/tests/test-http-signature.js +++ b/tests/test-http-signature.js @@ -1,9 +1,9 @@ 'use strict' var http = require('http') - , request = require('../index') - , httpSignature = require('http-signature') - , tape = require('tape') +var request = require('../index') +var httpSignature = require('http-signature') +var tape = require('tape') var privateKeyPEMs = {} @@ -68,42 +68,43 @@ var server = http.createServer(function (req, res) { res.end() }) -tape('setup', function(t) { - server.listen(6767, function() { +tape('setup', function (t) { + server.listen(0, function () { + server.url = 'http://localhost:' + this.address().port t.end() }) }) -tape('correct key', function(t) { +tape('correct key', function (t) { var options = { httpSignature: { keyId: 'key-1', key: privateKeyPEMs['key-1'] } } - request('http://localhost:6767', options, function(err, res, body) { + request(server.url, options, function (err, res, body) { t.equal(err, null) t.equal(200, res.statusCode) t.end() }) }) -tape('incorrect key', function(t) { +tape('incorrect key', function (t) { var options = { httpSignature: { keyId: 'key-2', key: privateKeyPEMs['key-1'] } } - request('http://localhost:6767', options, function(err, res, body) { + 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() { +tape('cleanup', function (t) { + server.close(function () { t.end() }) }) diff --git a/tests/test-httpModule.js b/tests/test-httpModule.js index 0cb7e606d..4d4e236bf 100644 --- a/tests/test-httpModule.js +++ b/tests/test-httpModule.js @@ -1,26 +1,27 @@ 'use strict' var http = require('http') - , https = require('https') - , server = require('./server') - , request = require('../index') - , tape = require('tape') +var https = require('https') +var destroyable = require('server-destroy') +var server = require('./server') +var request = require('../index') +var tape = require('tape') -var faux_requests_made +var fauxRequestsMade -function clear_faux_requests() { - faux_requests_made = { http: 0, https: 0 } +function clearFauxRequests () { + fauxRequestsMade = { 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) { + Object.keys(module).forEach(function (key) { var value = module[key] if (key === 'request') { - wrapped[key] = function(/*options, callback*/) { - faux_requests_made[name] += 1 + wrapped[key] = function (/* options, callback */) { + fauxRequestsMade[name] += 1 return value.apply(this, arguments) } } else { @@ -31,29 +32,32 @@ function wrap_request(name, module) { return wrapped } -var faux_http = wrap_request('http', http) - , faux_https = wrap_request('https', https) - , plain_server = server.createServer() - , https_server = server.createSSLServer() +var fauxHTTP = wrapRequest('http', http) +var fauxHTTPS = wrapRequest('https', https) +var plainServer = server.createServer() +var httpsServer = server.createSSLServer() -tape('setup', function(t) { - plain_server.listen(plain_server.port, function() { - plain_server.on('/plain', function (req, res) { +destroyable(plainServer) +destroyable(httpsServer) + +tape('setup', function (t) { + plainServer.listen(0, function () { + plainServer.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' }) + plainServer.on('/to_https', function (req, res) { + res.writeHead(301, { 'location': 'https://localhost:' + httpsServer.port + '/https' }) res.end() }) - https_server.listen(https_server.port, function() { - https_server.on('/https', function (req, res) { + httpsServer.listen(0, function () { + httpsServer.on('/https', function (req, res) { res.writeHead(200) res.end('https') }) - https_server.on('/to_plain', function (req, res) { - res.writeHead(302, { 'location': 'http://localhost:' + plain_server.port + '/plain' }) + httpsServer.on('/to_plain', function (req, res) { + res.writeHead(302, { 'location': 'http://localhost:' + plainServer.port + '/plain' }) res.end() }) @@ -62,30 +66,30 @@ tape('setup', function(t) { }) }) -function run_tests(name, httpModules) { - tape(name, function(t) { - var to_https = 'http://localhost:' + plain_server.port + '/to_https' - , to_plain = 'https://localhost:' + https_server.port + '/to_plain' - , options = { httpModules: httpModules, strictSSL: false } - , modulesTest = httpModules || {} +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 || {} - clear_faux_requests() + clearFauxRequests() - request(to_https, options, function (err, res, body) { + request(toHttps, options, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 200) t.equal(body, 'https', 'Received HTTPS server body') - t.equal(faux_requests_made.http, modulesTest['http:' ] ? 1 : 0) - t.equal(faux_requests_made.https, modulesTest['https:'] ? 1 : 0) + t.equal(fauxRequestsMade.http, modulesTest['http:'] ? 1 : 0) + t.equal(fauxRequestsMade.https, modulesTest['https:'] ? 1 : 0) - request(to_plain, options, function (err, res, body) { + request(toPlain, options, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 200) t.equal(body, 'plain', 'Received HTTPS server body') - t.equal(faux_requests_made.http, modulesTest['http:' ] ? 2 : 0) - t.equal(faux_requests_made.https, modulesTest['https:'] ? 2 : 0) + t.equal(fauxRequestsMade.http, modulesTest['http:'] ? 2 : 0) + t.equal(fauxRequestsMade.https, modulesTest['https:'] ? 2 : 0) t.end() }) @@ -93,15 +97,15 @@ function run_tests(name, httpModules) { }) } -run_tests('undefined') -run_tests('empty', {}) -run_tests('http only', { 'http:': faux_http }) -run_tests('https only', { 'https:': faux_https }) -run_tests('http and https', { 'http:': faux_http, 'https:': faux_https }) +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) { - plain_server.close(function() { - https_server.close(function() { +tape('cleanup', function (t) { + plainServer.destroy(function () { + httpsServer.destroy(function () { t.end() }) }) diff --git a/tests/test-https.js b/tests/test-https.js index 04b1aa423..028bc775b 100644 --- a/tests/test-https.js +++ b/tests/test-https.js @@ -4,31 +4,32 @@ // otherwise exactly the same as the ssl test var server = require('./server') - , request = require('../index') - , fs = require('fs') - , path = require('path') - , tape = require('tape') +var request = require('../index') +var fs = require('fs') +var path = require('path') +var tape = require('tape') var s = server.createSSLServer() - , caFile = path.resolve(__dirname, 'ssl/ca/ca.crt') - , ca = fs.readFileSync(caFile) - , opts = { - key: path.resolve(__dirname, 'ssl/ca/server.key'), - cert: path.resolve(__dirname, 'ssl/ca/server.crt') - } - , sStrict = server.createSSLServer(s.port + 1, opts) +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) -function runAllTests(strict, s) { +function runAllTests (strict, s) { var strictMsg = (strict ? 'strict ' : 'relaxed ') - tape(strictMsg + 'setup', function(t) { - s.listen(s.port, function() { + tape(strictMsg + 'setup', function (t) { + s.listen(0, function () { t.end() }) }) - function runTest(name, test) { - tape(strictMsg + name, function(t) { + function runTest (name, test) { + tape(strictMsg + name, function (t) { s.on('/' + name, test.resp) test.uri = s.url + '/' + name if (strict) { @@ -38,7 +39,7 @@ function runAllTests(strict, s) { } else { test.rejectUnauthorized = false } - request(test, function(err, resp, body) { + request(test, function (err, resp, body) { t.equal(err, null) if (test.expectBody) { t.deepEqual(test.expectBody, body) @@ -49,71 +50,67 @@ function runAllTests(strict, s) { } runTest('testGet', { - resp : server.createGetResponse('TESTING!') - , expectBody: 'TESTING!' + resp: server.createGetResponse('TESTING!'), expectBody: 'TESTING!' }) runTest('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: '\uf8ff\u03a9\u2603' + 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} + resp: server.createGetResponse('{"test":true}', 'application/json'), json: true, expectBody: {'test': true} }) runTest('testPutString', { - resp : server.createPostValidator('PUTTINGDATA') - , method : 'PUT' - , body : 'PUTTINGDATA' + resp: server.createPostValidator('PUTTINGDATA'), method: 'PUT', body: 'PUTTINGDATA' }) runTest('testPutBuffer', { - resp : server.createPostValidator('PUTTINGDATA') - , method : 'PUT' - , body : new Buffer('PUTTINGDATA') + resp: server.createPostValidator('PUTTINGDATA'), method: 'PUT', body: Buffer.from('PUTTINGDATA') }) runTest('testPutJSON', { - resp : server.createPostValidator(JSON.stringify({foo: 'bar'})) - , method: 'PUT' - , json: {foo: 'bar'} + 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.'} - ] + 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() { + tape(strictMsg + 'cleanup', function (t) { + s.close(function () { t.end() }) }) } runAllTests(false, s) -runAllTests(true, sStrict) + +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 index c6b930ddc..ae7f3ba11 100644 --- a/tests/test-isUrl.js +++ b/tests/test-isUrl.js @@ -1,92 +1,94 @@ 'use strict' var http = require('http') - , request = require('../index') - , tape = require('tape') +var request = require('../index') +var tape = require('tape') -var s = http.createServer(function(req, res) { +var s = http.createServer(function (req, res) { res.statusCode = 200 res.end('ok') }) -tape('setup', function(t) { - s.listen(6767, function() { +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('http://localhost:6767', function(err, resp, body) { +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('HTTP://localhost:6767', function(err, resp, body) { +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('HtTp://localhost:6767', function(err, resp, body) { +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) { +tape('hostname and port', function (t) { request({ uri: { protocol: 'http:', hostname: 'localhost', - port: 6767 + port: s.port } - }, function(err, res, body) { + }, function (err, res, body) { t.equal(err, null) t.equal(body, 'ok') t.end() }) }) -tape('hostname and port 1', function(t) { +tape('hostname and port 1', function (t) { request({ uri: { protocol: 'http:', hostname: 'localhost', - port: 6767 + port: s.port } - }, function(err, res, body) { + }, function (err, res, body) { t.equal(err, null) t.equal(body, 'ok') t.end() }) }) -tape('hostname and port 2', function(t) { +tape('hostname and port 2', function (t) { request({ protocol: 'http:', hostname: 'localhost', - port: 6767 + port: s.port }, { // need this empty options object, otherwise request thinks no uri was set - }, function(err, res, body) { + }, function (err, res, body) { t.equal(err, null) t.equal(body, 'ok') t.end() }) }) -tape('hostname and port 3', function(t) { +tape('hostname and port 3', function (t) { request({ protocol: 'http:', hostname: 'localhost', - port: 6767 - }, function(err, res, body) { + 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) @@ -94,8 +96,25 @@ tape('hostname and port 3', function(t) { }) }) -tape('cleanup', function(t) { - s.close(function() { +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 index a1d6f32bd..af82f15b5 100644 --- a/tests/test-json-request.js +++ b/tests/test-json-request.js @@ -1,20 +1,19 @@ 'use strict' var server = require('./server') - , stream = require('stream') - , request = require('../index') - , tape = require('tape') +var request = require('../index') +var tape = require('tape') var s = server.createServer() -tape('setup', function(t) { - s.listen(s.port, function() { +tape('setup', function (t) { + s.listen(0, function () { t.end() }) }) -function testJSONValue(testId, value) { - tape('test ' + testId, function(t) { +function testJSONValue (testId, value) { + tape('test ' + testId, function (t) { var testUrl = '/' + testId s.on(testUrl, server.createPostJSONValidator(value, 'application/json')) var opts = { @@ -32,8 +31,8 @@ function testJSONValue(testId, value) { }) } -function testJSONValueReviver(testId, value, reviver, revivedValue) { - tape('test ' + testId, function(t) { +function testJSONValueReviver (testId, value, reviver, revivedValue) { + tape('test ' + testId, function (t) { var testUrl = '/' + testId s.on(testUrl, server.createPostJSONValidator(value, 'application/json')) var opts = { @@ -52,6 +51,26 @@ function testJSONValueReviver(testId, value, reviver, revivedValue) { }) } +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) @@ -73,8 +92,26 @@ testJSONValueReviver('jsonReviver', -48269.592, function (k, v) { }, 48269.592) testJSONValueReviver('jsonReviverInvalid', -48269.592, 'invalid reviver', -48269.592) -tape('cleanup', function(t) { - s.close(function() { +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 index 4445001d5..88a335326 100644 --- a/tests/test-localAddress.js +++ b/tests/test-localAddress.js @@ -1,27 +1,49 @@ 'use strict' - var request = require('../index') - , tape = require('tape') +var tape = require('tape') -tape('bind to invalid address', function(t) { +tape('bind to invalid address', function (t) { request.get({ uri: 'http://www.google.com', localAddress: '1.2.3.4' - }, function(err, res) { + }, function (err, res) { t.notEqual(err, null) - t.equal(err.message, 'bind EADDRNOTAVAIL') + t.equal(true, /bind EADDRNOTAVAIL/.test(err.message)) t.equal(res, undefined) t.end() }) }) -tape('bind to local address', function(t) { +tape('bind to local address', function (t) { request.get({ uri: 'http://www.google.com', localAddress: '127.0.0.1' - }, function(err, res) { + }, 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 index cd7d659aa..8eff422e2 100644 --- a/tests/test-multipart.js +++ b/tests/test-multipart.js @@ -1,67 +1,69 @@ 'use strict' var http = require('http') - , path = require('path') - , request = require('../index') - , fs = require('fs') - , tape = require('tape') +var path = require('path') +var request = require('../index') +var fs = require('fs') +var tape = require('tape') -function runTest(t, a) { +function runTest (t, a) { var remoteFile = path.join(__dirname, 'googledoodle.jpg') - , localFile = path.join(__dirname, 'unicycle.jpg') - , multipartData = [] - , chunked = a.stream || a.chunked || a.encoding + var localFile = path.join(__dirname, 'unicycle.jpg') + var multipartData = [] - var server = http.createServer(function(req, res) { + 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.mixed) { - t.ok(req.headers['content-type'].match(/multipart\/mixed/)) - } else { - t.ok(req.headers['content-type'].match(/multipart\/related/)) - } - - if (chunked) { - t.ok(req.headers['transfer-encoding'] === 'chunked') - t.notOk(req.headers['content-length']) + 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-length']) - t.notOk(req.headers['transfer-encoding']) + t.ok(req.headers['content-type'].match(/^multipart\/related; boundary=[^\s;]+$/)) } // temp workaround var data = '' req.setEncoding('utf8') - req.on('data', function(d) { + req.on('data', function (d) { data += d }) - req.on('end', function() { - // check for the fields' traces + 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) - // 1st field : 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) - // 2nd field : my_buffer - t.ok( data.indexOf('name: my_buffer') !== -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) - if (chunked) { - // 3rd field : 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 ) + // 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) - // 4th field : remote_file - t.ok( data.indexOf('name: remote_file') !== -1 ) - // check for http://localhost:6767/file traces - t.ok( data.indexOf('Photoshop ICC') !== -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) @@ -69,36 +71,25 @@ function runTest(t, a) { }) }) - server.listen(6767, function() { - + 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 = chunked - ? [ - {name: 'my_field', body: 'my_value'}, - {name: 'my_buffer', body: new Buffer([1, 2, 3])}, - {name: 'my_file', body: fs.createReadStream(localFile)}, - {name: 'remote_file', body: request('http://localhost:6767/file')} - ] - : [ - {name: 'my_field', body: 'my_value'}, - {name: 'my_buffer', body: new Buffer([1, 2, 3])} - ] + 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: 'http://localhost:6767/upload', - headers: (function () { - var headers = {} - if (a.mixed) { - headers['content-type'] = 'multipart/mixed' - } - if (a.encoding) { - headers['transfer-encoding'] = 'chunked' - } - return headers - }()), - multipart: a.array - ? multipartData - : {chunked: a.chunked, data: multipartData} + url: url + '/upload', + multipart: multipartData + } + if (a.header) { + reqOptions.headers = { + 'content-type': a.header + } } if (a.json) { reqOptions.json = true @@ -107,64 +98,31 @@ function runTest(t, a) { t.equal(err, null) t.equal(res.statusCode, 200) t.deepEqual(body, a.json ? {status: 'done'} : 'done') - server.close(function() { + server.close(function () { t.end() }) }) - }) } -// array - multipart option is array -// object - multipart option is object -// encoding - headers option have transfer-encoding set to chunked -// mixed - headers option have content-type set to something different than multipart/related -// json - json option -// stream - body contains streams or not -// chunked - chunked is set when multipart is object - -// var methods = ['post', 'get'] -var cases = [ - // based on body type - {name: '+array -stream', args: {array: true, encoding: false, stream: false}}, - {name: '+array +stream', args: {array: true, encoding: false, stream: true}}, - // encoding overrides stream - {name: '+array +encoding', args: {array: true, encoding: true, stream: false}}, - - // based on body type - {name: '+object -stream', args: {object: true, encoding: false, stream: false}}, - {name: '+object +stream', args: {object: true, encoding: false, stream: true}}, - // encoding overrides stream - {name: '+object +encoding', args: {object: true, encoding: true, stream: false}}, - - // based on body type - {name: '+object -chunked -stream', args: {object: true, encoding: false, chunked: false, stream: false}}, - {name: '+object -chunked +stream', args: {object: true, encoding: false, chunked: false, stream: true}}, - // chunked overrides stream - {name: '+object +chunked -stream', args: {object: true, encoding: false, chunked: true, stream: false}}, - // chunked overrides encoding - {name: '+object +encoding -chunked', args: {object: true, encoding: true, chunked: false, stream: false}}, - // stream overrides chunked - {name: '+object +encoding -chunked +stream', args: {object: true, encoding: true, chunked: false, stream: true}} +var testHeaders = [ + null, + 'multipart/mixed', + 'multipart/related; boundary=XXX; type=text/xml; start=""' ] -var suite = ['post', 'get'].forEach(function(method) { - [true, false].forEach(function(json) { - [true, false].forEach(function(mixed) { - cases.forEach(function (test) { - var name = [ - 'multipart related', method, - (json ? '+' : '-') + 'json', - (mixed ? '+' : '-') + 'mixed', - test.name - ].join(' ') - - tape(name, function(t) { - test.args.method = method - test.args.json = json - test.args.mixed = mixed - runTest(t, test.args) - }) +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 index 4d4e5ac0a..bcc6a401d 100644 --- a/tests/test-node-debug.js +++ b/tests/test-node-debug.js @@ -1,51 +1,53 @@ 'use strict' var request = require('../index') - , http = require('http') - , tape = require('tape') +var http = require('http') +var tape = require('tape') -var s = http.createServer(function(req, res) { +var s = http.createServer(function (req, res) { res.statusCode = 200 res.end('') }) var stderr = [] - , prevStderrLen = 0 +var prevStderrLen = 0 -tape('setup', function(t) { +tape('setup', function (t) { process.stderr._oldWrite = process.stderr.write - process.stderr.write = function(string, encoding, fd) { + process.stderr.write = function (string, encoding, fd) { stderr.push(string) } - s.listen(6767, function() { + 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) { +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('http://localhost:6767', function(err, res, body) { + 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: /, - /^REQUEST make request http:\/\/localhost:6767\/\n$/, + 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) { + patterns.forEach(function (pattern) { var found = false - stderr.forEach(function(msg) { + stderr.forEach(function (msg) { if (pattern.test(msg)) { found = true } @@ -57,11 +59,11 @@ tape('a simple request should not fail with debugging enabled', function(t) { }) }) -tape('there should be no further lookups on process.env', function(t) { +tape('there should be no further lookups on process.env', function (t) { process.env.NODE_DEBUG = '' stderr = [] - request('http://localhost:6767', function(err, res, body) { + 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') @@ -69,13 +71,13 @@ tape('there should be no further lookups on process.env', function(t) { }) }) -tape('it should be possible to disable debugging at runtime', function(t) { +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('http://localhost:6767', function(err, res, body) { + 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') @@ -83,11 +85,11 @@ tape('it should be possible to disable debugging at runtime', function(t) { }) }) -tape('cleanup', function(t) { +tape('cleanup', function (t) { process.stderr.write = process.stderr._oldWrite delete process.stderr._oldWrite - s.close(function() { + s.close(function () { t.end() }) }) diff --git a/tests/test-oauth.js b/tests/test-oauth.js index cfb587f04..0358375ed 100644 --- a/tests/test-oauth.js +++ b/tests/test-oauth.js @@ -1,15 +1,16 @@ 'use strict' var oauth = require('oauth-sign') - , qs = require('querystring') - , fs = require('fs') - , path = require('path') - , request = require('../index') - , tape = require('tape') - -function getSignature(r) { +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) { + 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) } @@ -20,254 +21,295 @@ function getSignature(r) { // Tests from Twitter documentation https://dev.twitter.com/docs/auth/oauth var hmacsign = oauth.hmacsign - , rsasign = oauth.rsasign - , rsa_private_pem = fs.readFileSync(path.join(__dirname, 'ssl', 'test.key')) - , reqsign - , reqsign_rsa - , accsign - , accsign_rsa - , upsign - , upsign_rsa - -tape('reqsign', function(t) { +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' + { 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('reqsign_rsa', function(t) { - reqsign_rsa = 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' - }, rsa_private_pem, 'this parameter is not used for RSA signing') +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() +}) - t.equal(reqsign_rsa, 'MXdzEnIrQco3ACPoVWxCwv5pxYrm5MFRXbsP3LfRV+zfcRr+WMp/dOPS/3r+Wcb+17Z2IK3uJ8dMHfzb5LiDNCTUIj7SWBrbxOpy3Y6SA6z3vcrtjSekkTHLek1j+mzxOi3r3fkbYaNwjHx3PyoFSazbEstnkQQotbITeFt5FBE=') +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) { +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' + { 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('accsign_rsa', function(t) { - accsign_rsa = 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' - }, rsa_private_pem) +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(accsign_rsa, 'gZrMPexdgGMVUl9H6RxK0MbR6Db8tzf2kIIj52kOrDFcMgh4BunMBgUZAO1msUwz6oqZIvkVqyfyDAOP2wIrpYem0mBg3vqwPIroSE1AlUWo+TtQxOTuqrU+3kDcXpdvJe7CAX5hUx9Np/iGRqaCcgByqb9DaCcQ9ViQ+0wJiXI=') + t.equal(accsign256, 'y7S9eUhA0tC9/YfRzCPqkg3/bUdYRDpZ93Xi51AvhjQ=') t.end() }) -tape('upsign', function(t) { +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 私のさえずりを設定する' + { 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('upsign_rsa', function(t) { - upsign_rsa = 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 私のさえずりを設定する' - }, rsa_private_pem) +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() +}) - t.equal(upsign_rsa, 'fF4G9BNzDxPu/htctzh9CWzGhtXo9DYYl+ZyRO1/QNOhOZPqnWVUOT+CGUKxmAeJSzLKMAH8y/MFSHI0lzihqwgfZr7nUhTx6kH7lUChcVasr+TZ4qPqvGGEhfJ8Av8D5dF5fytfCSzct62uONU0iHYVqainP+zefk1K7Ptb6hI=') +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) { +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' + { 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() { + process.nextTick(function () { t.equal(reqsign, getSignature(rsign)) rsign.abort() t.end() }) }) -tape('rsign_rsa', function(t) { - var rsign_rsa = 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: rsa_private_pem - , signature_method: 'RSA-SHA1' +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' } }) - process.nextTick(function() { - t.equal(reqsign_rsa, getSignature(rsign_rsa)) - rsign_rsa.abort() + process.nextTick(function () { + t.equal(reqsignRSA, getSignature(rsignRSA)) + rsignRSA.abort() t.end() }) }) -tape('raccsign', function(t) { +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' + { 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() { + process.nextTick(function () { t.equal(accsign, getSignature(raccsign)) raccsign.abort() t.end() }) }) -tape('raccsign_rsa', function(t) { - var raccsign_rsa = 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: rsa_private_pem - , token_secret: 'x6qpRnlEmW9JbQn4PQVVeVG8ZLPEx6A0TOebgwcuA' +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(accsign_rsa, getSignature(raccsign_rsa)) - raccsign_rsa.abort() + process.nextTick(function () { + t.equal(accsignRSA, getSignature(raccsignRSA)) + raccsignRSA.abort() t.end() }) }) -tape('rupsign', function(t) { +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 私のさえずりを設定する'} + { 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() { + process.nextTick(function () { t.equal(upsign, getSignature(rupsign)) rupsign.abort() t.end() }) }) -tape('rupsign_rsa', function(t) { - var rupsign_rsa = 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: rsa_private_pem - , token_secret: 'J6zix3FfA9LofH0awS24M3HcBYXO5nI1iYe8EfBA' - } - , form: {status: 'setting up my twitter 私のさえずりを設定する'} +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(upsign_rsa, getSignature(rupsign_rsa)) - rupsign_rsa.abort() + process.nextTick(function () { + t.equal(upsignRSA, getSignature(rupsignRSA)) + rupsignRSA.abort() t.end() }) }) -tape('rfc5849 example', function(t) { +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: { + { 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() { + process.nextTick(function () { // different signature in rfc5849 because request sets oauth_version by default t.equal('OB33pYjWAnf+xtOHN4Gmbdil168=', getSignature(rfc5849)) rfc5849.abort() @@ -275,255 +317,405 @@ tape('rfc5849 example', function(t) { }) }) -tape('rfc5849 RSA example', function(t) { - var rfc5849_rsa = 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: rsa_private_pem - , token_secret: 'dh893hdasih9' - , realm: 'Example' - } - , form: { +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() { + 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(rfc5849_rsa)) - rfc5849_rsa.abort() + t.equal('ThNYfZhYogcAU6rWgI3ZFlPEhoIXHMZcuMzl+jykJZW/ab+AxyefS03dyd64CclIZ0u8JEW64TQ5SHthoQS8aM8qir4t+t88lRF3LDkD2KtS1krgCZTUQxkDL5BO5pxsqAQ2Zfdcrzaxb6VMGD1Hf+Pno+fsHQo/UUKjq4V3RMo=', getSignature(rfc5849RSA)) + rfc5849RSA.abort() t.end() }) }) -tape('plaintext signature method', function(t) { +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' + { url: 'https://dummy.com', + oauth: { consumer_secret: 'consumer_secret', + token_secret: 'token_secret', + signature_method: 'PLAINTEXT' } }) - process.nextTick(function() { + 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: 'some random string' - } - }) - }, /transport_method invalid/) - +tape('invalid transport_method', function (t) { t.throws( function () { request.post( - { url: 'http://example.com/' - , oauth: - { transport_method: 'headerquery' - } - }) + { url: 'http://example.com/', + oauth: { transport_method: 'headerquery' + } + }) }, /transport_method invalid/) t.end() }) -tape('invalid method while using transport_method \'body\'', function(t) { +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'/) + { 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) { +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'/) + { url: 'http://example.com/', + headers: { 'content-type': 'application/json; charset=UTF-8' }, + oauth: { transport_method: 'body' + } + }) + }, /requires POST/) t.end() }) -tape('query transport_method simple url', function(t) { +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' + { 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(accsign, qs.parse(r.path).oauth_signature) - t.notOk(r.path.match(/\?&/), 'there should be no ampersand at the beginning of the 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 with prexisting url params', function(t) { +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: { + { 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.notOk(r.path.match(/\?&/), 'there should be no ampersand at the beginning of the query') - t.equal('OB33pYjWAnf+xtOHN4Gmbdil168=', qs.parse(r.path).oauth_signature) + 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 with qs parameter and existing query string in url', function(t) { +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: { + { 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.notOk(r.path.match(/\?&/), 'there should be no ampersand at the beginning of the query') - t.equal('OB33pYjWAnf+xtOHN4Gmbdil168=', qs.parse(r.path).oauth_signature) - - var params = qs.parse(r.path.split('?')[1]) - , keys = Object.keys(params) - - var paramNames = [ - 'a2', 'b5', 'a3[0]', 'a3[1]', 'c@', 'c2', - 'realm', 'oauth_nonce', 'oauth_signature_method', 'oauth_timestamp', - 'oauth_token', 'oauth_version', 'oauth_signature' - ] - - for (var i = 0; i < keys.length; i++) { - t.ok(keys[i] === paramNames[i], - 'Non-oauth query params should be first, ' + - 'OAuth query params should be second in query string') - } + } + }) + 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 empty body', function(t) { +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' + { 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.equal(accsign, qs.parse(r.body.toString()).oauth_signature) - t.notOk(r.body.toString().match(/^&/), 'there should be no ampersand at the beginning of the 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 with prexisting body params', function(t) { +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: { + { 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.notOk(r.body.toString().match(/^&/), 'there should be no ampersand at the beginning of the body') - t.equal('OB33pYjWAnf+xtOHN4Gmbdil168=', qs.parse(r.body.toString()).oauth_signature) + 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 index 73a0ae8a3..b2219f246 100644 --- a/tests/test-onelineproxy.js +++ b/tests/test-onelineproxy.js @@ -1,11 +1,11 @@ 'use strict' var http = require('http') - , assert = require('assert') - , request = require('../index') - , tape = require('tape') +var assert = require('assert') +var request = require('../index') +var tape = require('tape') -var server = http.createServer(function(req, resp) { +var server = http.createServer(function (req, resp) { resp.statusCode = 200 if (req.url === '/get') { assert.equal(req.method, 'GET') @@ -16,10 +16,10 @@ var server = http.createServer(function(req, resp) { if (req.url === '/put') { var x = '' assert.equal(req.method, 'PUT') - req.on('data', function(chunk) { + req.on('data', function (chunk) { x += chunk }) - req.on('end', function() { + req.on('end', function () { assert.equal(x, 'content') resp.write('success') resp.end() @@ -28,24 +28,25 @@ var server = http.createServer(function(req, resp) { } if (req.url === '/proxy') { assert.equal(req.method, 'PUT') - req.pipe(request('http://localhost:6767/put')).pipe(resp) + req.pipe(request(server.url + '/put')).pipe(resp) return } if (req.url === '/test') { - request('http://localhost:6767/get').pipe(request.put('http://localhost:6767/proxy')).pipe(resp) + 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(6767, function() { +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('http://localhost:6767/test', function(err, res, body) { +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') @@ -53,8 +54,8 @@ tape('chained one-line proxying', function(t) { }) }) -tape('cleanup', function(t) { - server.close(function() { +tape('cleanup', function (t) { + server.close(function () { t.end() }) }) diff --git a/tests/test-option-reuse.js b/tests/test-option-reuse.js index c2dcf63c6..1c9b09d64 100644 --- a/tests/test-option-reuse.js +++ b/tests/test-option-reuse.js @@ -1,38 +1,39 @@ 'use strict' var request = require('../index') - , http = require('http') - , tape = require('tape') +var http = require('http') +var tape = require('tape') var methodsSeen = { head: 0, get: 0 } -var s = http.createServer(function(req, res) { +var s = http.createServer(function (req, res) { res.statusCode = 200 res.end('ok') methodsSeen[req.method.toLowerCase()]++ }) -tape('setup', function(t) { - s.listen(6767, function() { +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 = 'http://localhost:6767' +tape('options object is not mutated', function (t) { + var url = s.url var options = { url: url } - request.head(options, function(err, resp, body) { + 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) { + request.get(options, function (err, resp, body) { t.equal(err, null) t.equal(body, 'ok') t.equal(Object.keys(options).length, 1) @@ -46,8 +47,8 @@ tape('options object is not mutated', function(t) { }) }) -tape('cleanup', function(t) { - s.close(function() { +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 be19f7be7..4659aa70f 100644 --- a/tests/test-params.js +++ b/tests/test-params.js @@ -1,15 +1,15 @@ 'use strict' var server = require('./server') - , request = require('../index') - , tape = require('tape') +var request = require('../index') +var tape = require('tape') var s = server.createServer() -function runTest(name, test) { - tape(name, function(t) { +function runTest (name, test) { + tape(name, function (t) { s.on('/' + name, test.resp) - request(s.url + '/' + name, test, function(err, resp, body) { + request(s.url + '/' + name, test, function (err, resp, body) { t.equal(err, null) if (test.expectBody) { if (Buffer.isBuffer(test.expectBody)) { @@ -23,80 +23,79 @@ function runTest(name, test) { }) } -tape('setup', function(t) { - s.listen(s.port, function() { +tape('setup', function (t) { + s.listen(0, function () { t.end() }) }) runTest('testGet', { - resp : server.createGetResponse('TESTING!') - , expectBody: 'TESTING!' + resp: server.createGetResponse('TESTING!'), + expectBody: 'TESTING!' }) runTest('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: '\uf8ff\u03a9\u2603' + 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(new Buffer('TESTING!')) - , encoding: null - , expectBody: new Buffer('TESTING!') + 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} + resp: server.createGetResponse('{"test":true}', 'application/json'), + json: true, + expectBody: {'test': true} }) runTest('testPutString', { - resp : server.createPostValidator('PUTTINGDATA') - , method : 'PUT' - , body : 'PUTTINGDATA' + resp: server.createPostValidator('PUTTINGDATA'), + method: 'PUT', + body: 'PUTTINGDATA' }) runTest('testPutBuffer', { - resp : server.createPostValidator('PUTTINGDATA') - , method : 'PUT' - , body : new Buffer('PUTTINGDATA') + resp: server.createPostValidator('PUTTINGDATA'), + method: 'PUT', + body: Buffer.from('PUTTINGDATA') }) runTest('testPutJSON', { - resp : server.createPostValidator(JSON.stringify({foo: 'bar'})) - , method: 'PUT' - , json: {foo: 'bar'} + 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.'} - ] + 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() { +tape('cleanup', function (t) { + s.close(function () { t.end() }) }) diff --git a/tests/test-piped-redirect.js b/tests/test-piped-redirect.js index fecb21d3c..77135d4d1 100644 --- a/tests/test-piped-redirect.js +++ b/tests/test-piped-redirect.js @@ -1,13 +1,13 @@ 'use strict' var http = require('http') - , request = require('../index') - , tape = require('tape') +var request = require('../index') +var tape = require('tape') -var port1 = 6767 - , port2 = 6768 +var port1 +var port2 -var s1 = http.createServer(function(req, resp) { +var s1 = http.createServer(function (req, resp) { if (req.url === '/original') { resp.writeHeader(302, { 'location': '/redirected' @@ -22,31 +22,33 @@ var s1 = http.createServer(function(req, resp) { } }) -var s2 = http.createServer(function(req, resp) { +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(port1, function() { - s2.listen(port2, function() { +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) { +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() { +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 1f42bab75..dab7cb311 100644 --- a/tests/test-pipes.js +++ b/tests/test-pipes.js @@ -1,17 +1,16 @@ 'use strict' var server = require('./server') - , events = require('events') - , stream = require('stream') - , fs = require('fs') - , request = require('../index') - , path = require('path') - , util = require('util') - , tape = require('tape') +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) { +s.on('/cat', function (req, res) { if (req.method === 'GET') { res.writeHead(200, { 'content-type': 'text/plain-test', @@ -20,9 +19,9 @@ s.on('/cat', function(req, res) { res.end('asdf') } else if (req.method === 'PUT') { var body = '' - req.on('data', function(chunk) { + req.on('data', function (chunk) { body += chunk - }).on('end', function() { + }).on('end', function () { res.writeHead(201) res.end() s.emit('catDone', req, res, body) @@ -30,7 +29,7 @@ s.on('/cat', function(req, res) { } }) -s.on('/doodle', function(req, res) { +s.on('/doodle', function (req, res) { if (req.headers['x-oneline-proxy']) { res.setHeader('x-oneline-proxy', 'yup') } @@ -38,13 +37,13 @@ s.on('/doodle', function(req, res) { fs.createReadStream(path.join(__dirname, 'googledoodle.jpg')).pipe(res) }) -function ValidationStream(t, str) { +function ValidationStream (t, str) { this.str = str this.buf = '' - this.on('data', function(data) { + this.on('data', function (data) { this.buf += data }) - this.on('end', function() { + this.on('end', function () { t.equal(this.str, this.buf) }) this.writable = true @@ -52,25 +51,24 @@ function ValidationStream(t, str) { util.inherits(ValidationStream, stream.Stream) -ValidationStream.prototype.write = function(chunk) { +ValidationStream.prototype.write = function (chunk) { this.emit('data', chunk) } -ValidationStream.prototype.end = function(chunk) { +ValidationStream.prototype.end = function (chunk) { if (chunk) { this.emit('data', chunk) } this.emit('end') } - -tape('setup', function(t) { - s.listen(s.port, function() { +tape('setup', function (t) { + s.listen(0, function () { t.end() }) }) -tape('piping to a request object', function(t) { +tape('piping to a request object', function (t) { s.once('/push', server.createPostValidator('mydata')) var mydata = new stream.Stream() @@ -78,10 +76,10 @@ tape('piping to a request object', function(t) { var r1 = request.put({ url: s.url + '/push' - }, function(err, res, body) { + }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 200) - t.equal(body, 'OK') + t.equal(body, 'mydata') t.end() }) mydata.pipe(r1) @@ -90,19 +88,38 @@ tape('piping to a request object', function(t) { mydata.emit('end') }) -tape('piping to a request object with a json body', function(t) { - s.once('/push-json', server.createPostValidator('{"foo":"bar"}', 'application/json')) +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) { + }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 200) - t.equal(body, 'OK') + t.deepEqual(body, obj) t.end() }) mybodydata.pipe(r2) @@ -111,7 +128,7 @@ tape('piping to a request object with a json body', function(t) { mybodydata.emit('end') }) -tape('piping from a request object', function(t) { +tape('piping from a request object', function (t) { s.once('/pull', server.createGetResponse('mypulldata')) var mypulldata = new stream.Stream() @@ -123,32 +140,95 @@ tape('piping from a request object', function(t) { var d = '' - mypulldata.write = function(chunk) { + mypulldata.write = function (chunk) { d += chunk } - mypulldata.end = function() { + mypulldata.end = function () { t.equal(d, 'mypulldata') t.end() } }) -var fileContents = fs.readFileSync(__filename).toString() -function testPipeFromFile(testName, hasContentLength) { - tape(testName, function(t) { - s.once('/pushjs', function(req, res) { +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) + }) + + 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') + }) + + var paused = true + var r = request({ + url: s.url + '/pause-before' + }) + r.pause() + r.on('data', function (data) { + t.notOk(paused, 'Only receive data when not paused') + t.equal(data.toString(), 'Data') + }) + 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.on('data', function(data) { + req.setEncoding('utf8') + req.on('data', function (data) { body += data }) - req.on('end', function() { - t.equal(body, fileContents) + req.on('end', function () { + res.end() + t.equal(body, fileContents.toString()) t.end() }) + } else { res.end() } }) @@ -164,8 +244,8 @@ function testPipeFromFile(testName, hasContentLength) { testPipeFromFile('piping from a file', false) testPipeFromFile('piping from a file with content-length', true) -tape('piping to and from same URL', function(t) { - s.once('catDone', function(req, res, body) { +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') @@ -175,12 +255,12 @@ tape('piping to and from same URL', function(t) { .pipe(request.put(s.url + '/cat')) }) -tape('piping between urls', function(t) { - s.once('/catresp', function(req, res) { +tape('piping between urls', function (t) { + s.once('/catresp', function (req, res) { request.get(s.url + '/cat').pipe(res) }) - request.get(s.url + '/catresp', function(err, res, body) { + 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') @@ -188,12 +268,12 @@ tape('piping between urls', function(t) { }) }) -tape('writing to file', function(t) { +tape('writing to file', function (t) { var doodleWrite = fs.createWriteStream(path.join(__dirname, 'test.jpg')) request.get(s.url + '/doodle').pipe(doodleWrite) - doodleWrite.on('close', function() { + doodleWrite.on('close', function () { t.deepEqual( fs.readFileSync(path.join(__dirname, 'googledoodle.jpg')), fs.readFileSync(path.join(__dirname, 'test.jpg'))) @@ -202,8 +282,8 @@ tape('writing to file', function(t) { }) }) -tape('one-line proxy', function(t) { - s.once('/onelineproxy', function(req, res) { +tape('one-line proxy', function (t) { + s.once('/onelineproxy', function (req, res) { var x = request(s.url + '/doodle') req.pipe(x) x.pipe(res) @@ -212,36 +292,37 @@ tape('one-line proxy', function(t) { request.get({ uri: s.url + '/onelineproxy', headers: { 'x-oneline-proxy': 'nope' } - }, function(err, res, body) { + }, 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() }) }) -tape('piping after response', function(t) { - s.once('/afterresponse', function(req, res) { +tape('piping after response', function (t) { + s.once('/afterresponse', function (req, res) { res.write('d') res.end() }) var rAfterRes = request.post(s.url + '/afterresponse') - rAfterRes.on('response', function() { + rAfterRes.on('response', function () { var v = new ValidationStream(t, 'd') rAfterRes.pipe(v) - v.on('end', function() { + v.on('end', function () { t.end() }) }) }) -tape('piping through a redirect', function(t) { - s.once('/forward1', function(req, res) { - res.writeHead(302, { location: '/forward2' }) +tape('piping through a redirect', function (t) { + s.once('/forward1', function (req, res) { + res.writeHead(302, { location: '/forward2' }) res.end() }) - s.once('/forward2', function(req, res) { + s.once('/forward2', function (req, res) { res.writeHead('200', { 'content-type': 'image/png' }) res.write('d') res.end() @@ -251,27 +332,27 @@ tape('piping through a redirect', function(t) { request.get(s.url + '/forward1').pipe(validateForward) - validateForward.on('end', function() { + validateForward.on('end', function () { t.end() }) }) -tape('pipe options', function(t) { +tape('pipe options', function (t) { s.once('/opts', server.createGetResponse('opts response')) var optsStream = new stream.Stream() - , optsData = '' + var optsData = '' optsStream.writable = true - optsStream.write = function(buf) { + optsStream.write = function (buf) { optsData += buf if (optsData === 'opts response') { - setTimeout(function() { + setTimeout(function () { t.end() }, 10) } } - optsStream.end = function() { + optsStream.end = function () { t.fail('end called') } @@ -280,23 +361,23 @@ tape('pipe options', function(t) { }).pipe(optsStream, { end: false }) }) -tape('request.pipefilter is called correctly', function(t) { - s.once('/pipefilter', function(req, res) { +tape('request.pipefilter is called correctly', function (t) { + s.once('/pipefilter', function (req, res) { res.end('d') }) var validatePipeFilter = new ValidationStream(t, 'd') var r3 = request.get(s.url + '/pipefilter') r3.pipe(validatePipeFilter) - r3.pipefilter = function(res, dest) { + r3.pipefilter = function (res, dest) { t.equal(res, r3.response) t.equal(dest, validatePipeFilter) t.end() } }) -tape('cleanup', function(t) { - s.close(function() { +tape('cleanup', function (t) { + s.close(function () { t.end() }) }) diff --git a/tests/test-pool.js b/tests/test-pool.js index 417870369..f2d96bd1f 100644 --- a/tests/test-pool.js +++ b/tests/test-pool.js @@ -1,25 +1,26 @@ 'use strict' var request = require('../index') - , http = require('http') - , tape = require('tape') +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(6767, function() { +tape('setup', function (t) { + s.listen(0, function () { + s.url = 'http://localhost:' + this.address().port t.end() }) }) -tape('pool', function(t) { +tape('pool', function (t) { request({ - url: 'http://localhost:6767', + url: s.url, pool: false - }, function(err, res, body) { + }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 200) t.equal(body, 'asdf') @@ -30,8 +31,118 @@ tape('pool', function(t) { }) }) -tape('cleanup', function(t) { - s.close(function() { +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 index cc3596874..06800d00e 100644 --- a/tests/test-proxy-connect.js +++ b/tests/test-proxy-connect.js @@ -1,15 +1,13 @@ 'use strict' -var net = require('net') - , request = require('../index') - , tape = require('tape') +var request = require('../index') +var tape = require('tape') -var port = 6768 - , called = false - , proxiedHost = 'google.com' - , data = '' +var called = false +var proxiedHost = 'google.com' +var data = '' -var s = require('net').createServer(function(sock) { +var s = require('net').createServer(function (sock) { called = true sock.once('data', function (c) { data += c @@ -28,31 +26,32 @@ var s = require('net').createServer(function(sock) { }) }) -tape('setup', function(t) { - s.listen(port, function() { +tape('setup', function (t) { + s.listen(0, function () { + s.url = 'http://localhost:' + this.address().port t.end() }) }) -tape('proxy', function(t) { +tape('proxy', function (t) { request({ tunnel: true, url: 'http://' + proxiedHost, - proxy: 'http://localhost:' + port, + 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' + '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) { + }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 200) t.equal(body, 'derp\n') - t.equal(data, [ + var re = new RegExp([ 'CONNECT google.com:80 HTTP/1.1', 'Proxy-Authorization: Basic dXNlcjpwYXNz', 'dont-send-to-dest: ok', @@ -66,18 +65,16 @@ tape('proxy', function(t) { 'dont-send-to-proxy: ok', 'accept: yo', 'user-agent: just another foobar', - 'host: google.com', - 'Connection: keep-alive', - '', - '' + '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() { +tape('cleanup', function (t) { + s.close(function () { t.end() }) }) diff --git a/tests/test-proxy.js b/tests/test-proxy.js index eec0b0c35..77cb7a831 100644 --- a/tests/test-proxy.js +++ b/tests/test-proxy.js @@ -1,14 +1,14 @@ 'use strict' var server = require('./server') - , request = require('../index') - , tape = require('tape') +var request = require('../index') +var tape = require('tape') var s = server.createServer() - , currResponseHandler +var currResponseHandler -['http://google.com/', 'https://google.com/'].forEach(function(url) { - s.on(url, function(req, res) { +['http://google.com/', 'https://google.com/'].forEach(function (url) { + s.on(url, function (req, res) { currResponseHandler(req, res) res.writeHeader(200) res.end('ok') @@ -34,9 +34,9 @@ var proxyEnvVars = [ // `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) { +function runTest (name, options, responseHandler) { + tape(name, function (t) { + proxyEnvVars.forEach(function (v) { delete process.env[v] }) if (options.env) { @@ -47,7 +47,7 @@ function runTest(name, options, responseHandler) { } var called = false - currResponseHandler = function(req, res) { + currResponseHandler = function (req, res) { if (responseHandler) { called = true t.equal(req.headers.host, 'google.com') @@ -60,7 +60,7 @@ function runTest(name, options, responseHandler) { } options.url = options.url || 'http://google.com' - request(options, function(err, res, body) { + request(options, function (err, res, body) { if (responseHandler && !called) { t.fail('proxy response should be called') } @@ -79,228 +79,226 @@ function runTest(name, options, responseHandler) { }) } -tape('setup', function(t) { - s.listen(s.port, function() { - t.end() - }) -}) - - -// 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') - }) +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') - }) + 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:4/' - }, - 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) + // 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 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 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 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 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 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 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 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 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=* 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) + 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 + // 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') - }) + // 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('cleanup', function(t) { - s.close(function() { +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 511ec88a6..f70c685db 100644 --- a/tests/test-qs.js +++ b/tests/test-qs.js @@ -1,47 +1,47 @@ 'use strict' var request = require('../index') - , tape = require('tape') +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) { +function runTest (name, options) { var uri = 'http://www.google.com' + (options.suffix || '') - , requestOptsQs = { - uri : uri - } - , requestOptsQuerystring = { - uri : uri, - useQuerystring : true - } + var opts = { + uri: uri, + qsParseOptions: options.qsParseOptions, + qsStringifyOptions: options.qsStringifyOptions + } if (options.qs) { - requestOptsQs.qs = options.qs - requestOptsQuerystring.qs = options.qs + opts.qs = options.qs } - tape(name + ' using qs', function(t) { - var r = request.get(requestOptsQs) + tape(name + ' - using qs', function (t) { + var r = request.get(opts) if (typeof options.afterRequest === 'function') { options.afterRequest(r) } - process.nextTick(function() { + process.nextTick(function () { t.equal(r.path, options.expected) r.abort() t.end() }) }) - tape(name + ' using querystring', function(t) { - var r = request.get(requestOptsQuerystring) + 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() { + process.nextTick(function () { t.equal(r.path, options.expectedQuerystring || options.expected) r.abort() t.end() @@ -49,55 +49,87 @@ function runTest(name, options) { }) } -function esc(str) { +function esc (str) { return str .replace(/\[/g, '%5B') .replace(/\]/g, '%5D') } runTest('adding a querystring', { - qs : { q : 'search' }, - expected : '/?q=search' + qs: { q: 'search' }, + expected: '/?q=search' }) runTest('replacing a querystring value', { - suffix : '?q=abc', - qs : { q : 'search' }, - expected : '/?q=search' + 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' + suffix: '?x=y', + qs: { q: 'search' }, + expected: '/?x=y&q=search' }) runTest('leaving a querystring alone', { - suffix : '?x=y', - expected : '/?x=y' + suffix: '?x=y', + expected: '/?x=y' }) runTest('giving empty qs property', { - qs : {}, - expected : '/' + qs: {}, + expected: '/' }) runTest('modifying the qs after creating the request', { - qs : {}, - afterRequest : function(r) { - r.qs({ q : 'test' }) + qs: {}, + afterRequest: function (r) { + r.qs({ q: 'test' }) }, - expected : '/?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=' + 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' + 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 index 510604dbc..7aef6edcc 100644 --- a/tests/test-redirect-auth.js +++ b/tests/test-redirect-auth.js @@ -1,37 +1,40 @@ 'use strict' var server = require('./server') - , request = require('../index') - , util = require('util') - , events = require('events') - , tape = require('tape') +var request = require('../index') +var util = require('util') +var tape = require('tape') +var destroyable = require('server-destroy') var s = server.createServer() - , ss = server.createSSLServer() +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' + auth: { + user: 'test', + pass: 'testing' }, - rejectUnauthorized : false + 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) { + from: function (fromProto, fromHost) { return { - to : function(toProto, toHost) { + to: function (toProto, toHost) { var fromPort = (fromProto === 'http' ? s.port : ss.port) - , toPort = (toProto === 'http' ? s.port : ss.port) + var toPort = (toProto === 'http' ? s.port : ss.port) return { - src : util.format( + src: util.format( '%s://%s:%d/to/%s/%s', fromProto, fromHost, fromPort, toProto, toHost), - dst : util.format( + dst: util.format( '%s://%s:%d/from/%s/%s', toProto, toHost, toPort, fromProto, fromHost) } @@ -40,20 +43,20 @@ var redirect = { } } -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) { +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 + location: r.dst }) res.end() }) - srv.on(util.format('/from/%s/%s', proto, host), function(req, res) { + srv.on(util.format('/from/%s/%s', proto, host), function (req, res) { res.end('auth: ' + (req.headers.authorization || '(nothing)')) }) }) @@ -63,33 +66,9 @@ function handleRequests(srv) { handleRequests(s) handleRequests(ss) -tape('setup', function(t) { - s.listen(s.port, function() { - ss.listen(ss.port, function() { - 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() -}) - -function runTest(name, redir, expectAuth) { - tape('redirect to ' + name, function(t) { - request(redir.src, function(err, res, body) { +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) @@ -101,26 +80,52 @@ function runTest(name, redir, expectAuth) { }) } -runTest('same host and protocol', - redirect.from('http', 'localhost').to('http', 'localhost'), - true) +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('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 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) + runTest('different host and protocol', + redirect.from('http', 'localhost').to('https', '127.0.0.1'), + false) +} -tape('cleanup', function(t) { - s.close(function() { - ss.close(function() { +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 index d99b962e1..072b5986c 100644 --- a/tests/test-redirect-complex.js +++ b/tests/test-redirect-complex.js @@ -1,23 +1,29 @@ 'use strict' var server = require('./server') - , request = require('../index') - , events = require('events') - , tape = require('tape') +var request = require('../index') +var events = require('events') +var tape = require('tape') +var destroyable = require('server-destroy') var s = server.createServer() - , ss = server.createSSLServer() - , e = new events.EventEmitter() +var ss = server.createSSLServer() +var e = new events.EventEmitter() -function bouncy(s, serverUrl) { - var redirs = { a: 'b' - , b: 'c' - , c: 'd' - , d: 'e' - , e: 'f' - , f: 'g' - , g: 'h' - , h: 'end' } +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) { @@ -27,7 +33,7 @@ function bouncy(s, serverUrl) { var type = perm ? 301 : 302 perm = !perm s.on('/' + p, function (req, res) { - setTimeout(function() { + setTimeout(function () { res.writeHead(type, { location: serverUrl + '/' + t }) res.end() }, Math.round(Math.random() * 25)) @@ -42,45 +48,45 @@ function bouncy(s, serverUrl) { }) } -tape('setup', function(t) { - s.listen(s.port, function() { - bouncy(s, ss.url) - ss.listen(ss.port, function() { +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) { +tape('lots of redirects', function (t) { var n = 10 t.plan(n * 4) - function doRedirect(i) { + 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) { + }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 200) t.equal(body, key) }) - e.once('hit-' + key, function(v) { + e.once('hit-' + key, function (v) { t.equal(v, key) }) } - for (var i = 0; i < n; i ++) { + for (var i = 0; i < n; i++) { doRedirect(i) } }) -tape('cleanup', function(t) { - s.close(function() { - ss.close(function() { +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 d8301fb60..b7b5ca676 100644 --- a/tests/test-redirect.js +++ b/tests/test-redirect.js @@ -1,29 +1,34 @@ 'use strict' var server = require('./server') - , assert = require('assert') - , request = require('../index') - , tape = require('tape') +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() - , ss = server.createSSLServer() - , hits = {} - , jar = request.jar() +var ss = server.createSSLServer() +var hits = {} +var jar = request.jar() -s.on('/ssl', function(req, res) { +destroyable(s) +destroyable(ss) + +s.on('/ssl', function (req, res) { res.writeHead(302, { - location : ss.url + '/' + location: ss.url + '/' }) res.end() }) -ss.on('/', function(req, res) { +ss.on('/', function (req, res) { res.writeHead(200) res.end('SSL') }) -function createRedirectEndpoint(code, label, landing) { - s.on('/' + label, function(req, res) { +function createRedirectEndpoint (code, label, landing) { + s.on('/' + label, function (req, res) { hits[label] = true res.writeHead(code, { 'location': s.url + '/' + landing, @@ -33,22 +38,22 @@ function createRedirectEndpoint(code, label, landing) { }) } -function createLandingEndpoint(landing) { - s.on('/' + landing, function(req, res) { +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) + res.writeHead(200, {'x-response': req.method.toUpperCase() + ' ' + landing}) res.end(req.method.toUpperCase() + ' ' + landing) }) } -function bouncer(code, label, hops) { - var hop, - landing = label + '_landing', - currentLabel, - currentLanding +function bouncer (code, label, hops) { + var hop + var landing = label + '_landing' + var currentLabel + var currentLanding hops = hops || 1 @@ -66,9 +71,9 @@ function bouncer(code, label, hops) { createLandingEndpoint(landing) } -tape('setup', function(t) { - s.listen(s.port, function() { - ss.listen(ss.port, function() { +tape('setup', function (t) { + s.listen(0, function () { + ss.listen(0, function () { bouncer(301, 'temp') bouncer(301, 'double', 2) bouncer(301, 'treble', 3) @@ -80,14 +85,14 @@ tape('setup', function(t) { }) }) -tape('permanent bounce', function(t) { +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) { + }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 200) t.ok(hits.perm, 'Original request is to /perm') @@ -97,13 +102,32 @@ tape('permanent bounce', function(t) { }) }) -tape('temporary bounce', function(t) { +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) { + }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 200) t.ok(hits.temp, 'Original request is to /temp') @@ -113,14 +137,14 @@ tape('temporary bounce', function(t) { }) }) -tape('prevent bouncing', function(t) { +tape('prevent bouncing', function (t) { hits = {} request({ uri: s.url + '/nope', jar: jar, headers: { cookie: 'foo=bar' }, followRedirect: false - }, function(err, res, body) { + }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 302) t.ok(hits.nope, 'Original request to /nope') @@ -130,12 +154,12 @@ tape('prevent bouncing', function(t) { }) }) -tape('should not follow post redirects by default', function(t) { +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) { + }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 301) t.ok(hits.temp, 'Original request is to /temp') @@ -145,14 +169,14 @@ tape('should not follow post redirects by default', function(t) { }) }) -tape('should follow post redirects when followallredirects true', function(t) { +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) { + }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 200) t.ok(hits.temp, 'Original request is to /temp') @@ -162,14 +186,32 @@ tape('should follow post redirects when followallredirects true', function(t) { }) }) -tape('should not follow post redirects when followallredirects false', function(t) { +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) { + }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 301) t.ok(hits.temp, 'Original request is to /temp') @@ -179,12 +221,12 @@ tape('should not follow post redirects when followallredirects false', function( }) }) -tape('should not follow delete redirects by default', function(t) { +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) { + }, 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') @@ -194,13 +236,13 @@ tape('should not follow delete redirects by default', function(t) { }) }) -tape('should not follow delete redirects even if followredirect is set to true', function(t) { +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) { + }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 301) t.ok(hits.temp, 'Original request is to /temp') @@ -210,13 +252,13 @@ tape('should not follow delete redirects even if followredirect is set to true', }) }) -tape('should follow delete redirects when followallredirects true', function(t) { +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) { + }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 200) t.ok(hits.temp, 'Original request is to /temp') @@ -226,13 +268,13 @@ tape('should follow delete redirects when followallredirects true', function(t) }) }) -tape('should follow 307 delete redirects when followallredirects true', function(t) { +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) { + }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 200) t.ok(hits.fwd, 'Original request is to /fwd') @@ -242,13 +284,13 @@ tape('should follow 307 delete redirects when followallredirects true', function }) }) -tape('double bounce', function(t) { +tape('double bounce', function (t) { hits = {} request({ uri: s.url + '/double', jar: jar, headers: { cookie: 'foo=bar' } - }, function(err, res, body) { + }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 200) t.ok(hits.double, 'Original request is to /double') @@ -259,8 +301,8 @@ tape('double bounce', function(t) { }) }) -tape('double bounce terminated after first redirect', function(t) { - function filterDouble(response) { +tape('double bounce terminated after first redirect', function (t) { + function filterDouble (response) { return (response.headers.location || '').indexOf('double_2') === -1 } @@ -270,7 +312,7 @@ tape('double bounce terminated after first redirect', function(t) { jar: jar, headers: { cookie: 'foo=bar' }, followRedirect: filterDouble - }, function(err, res, body) { + }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 301) t.ok(hits.double, 'Original request is to /double') @@ -279,8 +321,8 @@ tape('double bounce terminated after first redirect', function(t) { }) }) -tape('triple bounce terminated after second redirect', function(t) { - function filterTreble(response) { +tape('triple bounce terminated after second redirect', function (t) { + function filterTreble (response) { return (response.headers.location || '').indexOf('treble_3') === -1 } @@ -290,7 +332,7 @@ tape('triple bounce terminated after second redirect', function(t) { jar: jar, headers: { cookie: 'foo=bar' }, followRedirect: filterTreble - }, function(err, res, body) { + }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 301) t.ok(hits.treble, 'Original request is to /treble') @@ -299,12 +341,12 @@ tape('triple bounce terminated after second redirect', function(t) { }) }) -tape('http to https redirect', function(t) { +tape('http to https redirect', function (t) { hits = {} request.get({ uri: require('url').parse(s.url + '/ssl'), rejectUnauthorized: false - }, function(err, res, body) { + }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 200) t.equal(body, 'SSL', 'Got SSL redirect') @@ -312,9 +354,95 @@ tape('http to https redirect', function(t) { }) }) -tape('cleanup', function(t) { - s.close(function() { - ss.close(function() { +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 index cfd27f11b..a3918628d 100644 --- a/tests/test-rfc3986.js +++ b/tests/test-rfc3986.js @@ -1,22 +1,19 @@ 'use strict' var http = require('http') - , request = require('../index') - , tape = require('tape') - +var request = require('../index') +var tape = require('tape') function runTest (t, options) { - - var server = http.createServer(function(req, res) { - + var server = http.createServer(function (req, res) { var data = '' req.setEncoding('utf8') - req.on('data', function(d) { + req.on('data', function (d) { data += d }) - req.on('end', function() { + req.on('end', function () { if (options.qs) { t.equal(req.url, '/?rfc3986=%21%2A%28%29%27') } @@ -27,11 +24,11 @@ function runTest (t, options) { }) }) - server.listen(6767, function() { - - request.post('http://localhost:6767', options, function(err, res, body) { + 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() { + server.close(function () { t.end() }) }) @@ -39,66 +36,71 @@ function runTest (t, options) { } var bodyEscaped = 'rfc3986=%21%2A%28%29%27' - , bodyJson = '{"rfc3986":"!*()\'"}' +var bodyJson = '{"rfc3986":"!*()\'"}' var cases = [ { _name: 'qs', - qs: {rfc3986: '!*()\''}, + qs: {rfc3986: "!*()'"}, _expectBody: '' }, { _name: 'qs + json', - qs: {rfc3986: '!*()\''}, + qs: {rfc3986: "!*()'"}, json: true, _expectBody: '' }, { _name: 'form', - form: {rfc3986: '!*()\''}, + form: {rfc3986: "!*()'"}, _expectBody: bodyEscaped }, { _name: 'form + json', - form: {rfc3986: '!*()\''}, + form: {rfc3986: "!*()'"}, json: true, _expectBody: bodyEscaped }, { _name: 'qs + form', - qs: {rfc3986: '!*()\''}, - form: {rfc3986: '!*()\''}, + qs: {rfc3986: "!*()'"}, + form: {rfc3986: "!*()'"}, _expectBody: bodyEscaped }, { _name: 'qs + form + json', - qs: {rfc3986: '!*()\''}, - form: {rfc3986: '!*()\''}, + 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=!*()\'', + body: "rfc3986=!*()'", json: true, _expectBody: bodyEscaped }, { _name: 'body + json', - body: {rfc3986: '!*()\''}, + body: {rfc3986: "!*()'"}, json: true, _expectBody: bodyJson }, { _name: 'json object', - json: {rfc3986: '!*()\''}, + json: {rfc3986: "!*()'"}, _expectBody: bodyJson } ] -cases.forEach(function (options) { - tape('rfc3986 ' + options._name, function(t) { - runTest(t, options) +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 a270ef1ed..c87775d3c 100644 --- a/tests/test-timeout.js +++ b/tests/test-timeout.js @@ -1,119 +1,260 @@ 'use strict' -function checkErrCode(t, err) { +function checkErrCode (t, err) { t.notEqual(err, null) t.ok(err.code === 'ETIMEDOUT' || err.code === 'ESOCKETTIMEDOUT', 'Error ETIMEDOUT or ESOCKETTIMEDOUT') } -if (process.env.TRAVIS === 'true') { - console.error('This test is unreliable on Travis; skipping.') - /*eslint no-process-exit:0*/ -} else { - var server = require('./server') - , events = require('events') - , stream = require('stream') - , request = require('../index') - , 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) +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') +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('setup', function(t) { - s.listen(s.port, function() { - t.end() - }) +tape('should timeout', function (t) { + var shouldTimeout = { + url: s.url + '/timeout', + timeout: 100 + } + + request(shouldTimeout, function (err, res, body) { + checkErrCode(t, err) + t.end() }) +}) - tape('should timeout', function(t) { - var shouldTimeout = { - url: s.url + '/timeout', - timeout: 100 - } +tape('should set connect to false', function (t) { + var shouldTimeout = { + url: s.url + '/timeout', + timeout: 100 + } - request(shouldTimeout, function(err, res, body) { - checkErrCode(t, err) - t.end() - }) + 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) +tape('should timeout with events', function (t) { + t.plan(3) - var shouldTimeoutWithEvents = { - url: s.url + '/timeout', - timeout: 100 - } + 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) - }) + var eventsEmitted = 0 + request(shouldTimeoutWithEvents) + .on('error', function (err) { + eventsEmitted++ + t.equal(1, eventsEmitted) + checkErrCode(t, err) + }) +}) + +tape('should not timeout', function (t) { + var shouldntTimeout = { + url: s.url + '/timeout', + timeout: 1200 + } + + 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_ }) +}) - tape('should not timeout', function(t) { - var shouldntTimeout = { - url: s.url + '/timeout', - timeout: 1200 - } +tape('no timeout', function (t) { + var noTimeout = { + url: s.url + '/timeout' + } - request(shouldntTimeout, function(err, res, body) { - t.equal(err, null) - t.equal(body, 'waited') - t.end() - }) + request(noTimeout, function (err, res, body) { + t.equal(err, null) + t.equal(body, 'waited') + t.end() }) +}) - tape('no timeout', function(t) { - var noTimeout = { - url: s.url + '/timeout' +tape('negative timeout', function (t) { // should be treated a zero or the minimum delay + var negativeTimeout = { + url: s.url + '/timeout', + timeout: -1000 + } + + 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() + }) +}) - request(noTimeout, function(err, res, body) { - t.equal(err, null) - t.equal(body, 'waited') - t.end() - }) +tape('float timeout', function (t) { // should be rounded by setTimeout anyway + var floatTimeout = { + url: s.url + '/timeout', + timeout: 100.76 + } + + request(floatTimeout, function (err, res, body) { + checkErrCode(t, err) + t.end() }) +}) - tape('negative timeout', function(t) { // should be treated a zero or the minimum delay - var negativeTimeout = { - url: s.url + '/timeout', - timeout: -1000 +// 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_ + }) +}) - request(negativeTimeout, function(err, res, body) { - checkErrCode(t, err) +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('float timeout', function(t) { // should be rounded by setTimeout anyway - var floatTimeout = { +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.76 + timeout: 100, + agent: agent } - - request(floatTimeout, function(err, res, body) { + 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('cleanup', function(t) { - s.close(function() { - t.end() - }) +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 cc983d75e..43fa79169 100644 --- a/tests/test-toJSON.js +++ b/tests/test-toJSON.js @@ -1,44 +1,45 @@ 'use strict' var request = require('../index') - , http = require('http') - , tape = require('tape') +var http = require('http') +var tape = require('tape') var s = http.createServer(function (req, resp) { resp.statusCode = 200 resp.end('asdf') }) -tape('setup', function(t) { - s.listen(6767, function() { +tape('setup', function (t) { + s.listen(0, function () { + s.url = 'http://localhost:' + this.address().port t.end() }) }) -tape('request().toJSON()', function(t) { +tape('request().toJSON()', function (t) { var r = request({ - url: 'http://localhost:6767', + url: s.url, headers: { foo: 'bar' } - }, function(err, res) { - var json_r = JSON.parse(JSON.stringify(r)) - , json_res = JSON.parse(JSON.stringify(res)) + }, function (err, res) { + var jsonR = JSON.parse(JSON.stringify(r)) + var jsonRes = JSON.parse(JSON.stringify(res)) t.equal(err, null) - t.equal(json_r.uri.href , r.uri.href) - t.equal(json_r.method , r.method) - t.equal(json_r.headers.foo, r.headers.foo) + t.equal(jsonR.uri.href, r.uri.href) + t.equal(jsonR.method, r.method) + t.equal(jsonR.headers.foo, r.headers.foo) - t.equal(json_res.statusCode , res.statusCode) - t.equal(json_res.body , res.body) - t.equal(json_res.headers.date, res.headers.date) + 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() { +tape('cleanup', function (t) { + s.close(function () { t.end() }) }) diff --git a/tests/test-tunnel.js b/tests/test-tunnel.js index cf87731e9..fa2ebce33 100644 --- a/tests/test-tunnel.js +++ b/tests/test-tunnel.js @@ -1,33 +1,34 @@ 'use strict' var server = require('./server') - , tape = require('tape') - , request = require('../index') - , https = require('https') - , net = require('net') - , fs = require('fs') - , path = require('path') - , util = require('util') - , url = require('url') - , destroyable = require('server-destroy') +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') var events = [] - , caFile = path.resolve(__dirname, 'ssl/ca/ca.crt') - , ca = fs.readFileSync(caFile) - , clientCert = fs.readFileSync(path.resolve(__dirname, 'ssl/ca/client.crt')) - , clientKey = fs.readFileSync(path.resolve(__dirname, 'ssl/ca/client-enc.key')) - , clientPassword = 'password' - , sslOpts = { - key : path.resolve(__dirname, 'ssl/ca/localhost.key'), - cert : path.resolve(__dirname, 'ssl/ca/localhost.crt') - } - , mutualSSLOpts = { - key : path.resolve(__dirname, 'ssl/ca/localhost.key'), - cert : path.resolve(__dirname, 'ssl/ca/localhost.crt'), - ca : caFile, - requestCert : true, - rejectUnauthorized : true - } +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') +} + +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 @@ -36,8 +37,8 @@ httpsOpts.ca = httpsOpts.ca || [] httpsOpts.ca.push(ca) var s = server.createServer() - , ss = server.createSSLServer(null, sslOpts) - , ss2 = server.createSSLServer(ss.port + 1, mutualSSLOpts) +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) @@ -45,17 +46,17 @@ destroyable(s) destroyable(ss) destroyable(ss2) -function event() { +function event () { events.push(util.format.apply(null, arguments)) } -function setListeners(server, type) { - server.on('/', function(req, res) { +function setListeners (server, type) { + server.on('/', function (req, res) { event('%s response', type) res.end(type + ' ok') }) - server.on('request', function(req, res) { + server.on('request', function (req, res) { if (/^https?:/.test(req.url)) { // This is a proxy request var dest = req.url.split(':')[0] @@ -65,29 +66,29 @@ function setListeners(server, type) { dest += '->' + match[1] } event('%s proxy to %s', type, dest) - request(req.url, { followRedirect : false }).pipe(res) + request(req.url, { followRedirect: false }).pipe(res) } }) - server.on('/redirect/http', function(req, res) { + server.on('/redirect/http', function (req, res) { event('%s redirect to http', type) res.writeHead(301, { - location : s.url + location: s.url }) res.end() }) - server.on('/redirect/https', function(req, res) { + server.on('/redirect/https', function (req, res) { event('%s redirect to https', type) res.writeHead(301, { - location : ss.url + location: ss.url }) res.end() }) - server.on('connect', function(req, client, head) { + server.on('connect', function (req, client, head) { var u = url.parse(req.url) - var server = net.connect(u.host, u.port, function() { + 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) @@ -101,21 +102,11 @@ setListeners(s, 'http') setListeners(ss, 'https') setListeners(ss2, 'https') -tape('setup', function(t) { - s.listen(s.port, function() { - ss.listen(ss.port, function() { - ss2.listen(ss2.port, 'localhost', function() { - t.end() - }) - }) - }) -}) - // 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) { +https.request = function (options) { if (customCaCount) { options.ca = ca customCaCount-- @@ -123,13 +114,13 @@ https.request = function(options) { return httpsRequestOld.apply(this, arguments) } -function runTest(name, opts, expected) { - tape(name, function(t) { +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) { + request(opts, function (err, res, body) { event(err ? 'err ' + err.message : res.statusCode + ' ' + body) t.deepEqual(events, expected) events = [] @@ -138,334 +129,336 @@ function runTest(name, opts, expected) { }) } +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' + ]) + + // 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' + ]) +} -// 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' -]) - - -// 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 connect to localhost:' + s.port, - '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('cleanup', function(t) { - s.destroy(function() { - ss.destroy(function() { - ss2.destroy(function() { +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() }) }) diff --git a/tests/test-unix.js b/tests/test-unix.js index 16a2cc85d..acf883273 100644 --- a/tests/test-unix.js +++ b/tests/test-unix.js @@ -1,33 +1,63 @@ 'use strict' var request = require('../index') - , http = require('http') - , fs = require('fs') - , rimraf = require('rimraf') - , assert = require('assert') - , tape = require('tape') +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 path = [null, 'test', 'path'].join('/') - , socket = [__dirname, 'tmp-socket'].join('/') - , expectedBody = 'connected' - , statusCode = 200 +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) { - assert.equal(req.url, path, 'requested path is sent to server') +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() { +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', function(t) { - request('http://unix:' + socket + ':' + path, function(err, res, body) { +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') @@ -35,9 +65,9 @@ tape('unix socket connection', function(t) { }) }) -tape('cleanup', function(t) { - s.close(function() { - fs.unlink(socket, function() { +tape('cleanup', function (t) { + s.close(function () { + fs.unlink(socket, function () { t.end() }) })