├── .github
└── workflows
│ └── nodejs.yml
├── .gitignore
├── README.md
├── bin
└── needle
├── examples
├── deflated-stream.js
├── digest-auth.js
├── download-to-file.js
├── multipart-stream.js
├── parsed-stream.js
├── parsed-stream2.js
├── stream-events.js
├── stream-multiple
│ ├── app.js
│ ├── env.js
│ ├── package.json
│ └── stream-multiple.js
├── stream-to-file.js
└── upload-image.js
├── lib
├── auth.js
├── cookies.js
├── decoder.js
├── multipart.js
├── needle.js
├── parsers.js
├── querystring.js
└── utils.js
├── license.txt
├── package-lock.json
├── package.json
└── test
├── auth_digest_spec.js
├── basic_auth_spec.js
├── compression_spec.js
├── cookies_spec.js
├── decoder_spec.js
├── errors_spec.js
├── files
├── Appalachia.html
└── tomcat_charset.html
├── headers_spec.js
├── helpers.js
├── long_string_spec.js
├── mimetype.js
├── output_spec.js
├── parsing_spec.js
├── post_data_spec.js
├── proxy_spec.js
├── querystring_spec.js
├── redirect_spec.js
├── redirect_with_timeout.js
├── request_stream_spec.js
├── response_stream_spec.js
├── socket_cleanup_spec.js
├── socket_pool_spec.js
├── stream_events_spec.js
├── tls_options_spec.js
├── uri_modifier_spec.js
├── url_spec.js
├── utils
├── formidable.js
├── proxy.js
└── test.js
└── utils_spec.js
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | name: Node CI
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | build:
6 | runs-on: ubuntu-latest
7 |
8 | strategy:
9 | matrix:
10 | node-version: [4.x, 6.x, 8.x, 10.x, 12.x, 14.x, 16.x, 17.x, 18.x]
11 | fail-fast: false
12 |
13 | steps:
14 | - uses: actions/checkout@v1
15 | - name: Use Node.js ${{ matrix.node-version }}
16 | uses: actions/setup-node@v1
17 | with:
18 | node-version: ${{ matrix.node-version }}
19 | - name: npm install, build, and test
20 | run: |
21 | npm install
22 | mkdir -p test/keys
23 | openssl genrsa -out test/keys/ssl.key 2048
24 | openssl req -new -key test/keys/ssl.key -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -x509 -days 999 -out test/keys/ssl.cert
25 | npm run build --if-present
26 | npm test
27 | env:
28 | CI: true
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | node_modules
3 | temp
4 | sandbox
5 | .idea
6 | node
7 | test/keys
8 |
--------------------------------------------------------------------------------
/bin/needle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | var needle = require('./../lib/needle');
3 |
4 | function exit(code, str) {
5 | console.log(str) || process.exit(code);
6 | }
7 |
8 | function usage() {
9 | var out = ['Usage: needle [get|head|post|put|delete] url [query]'];
10 | out.push('Examples: \n needle get google.com\n needle post server.com/api foo=bar');
11 | exit(1, out.join('\n'))
12 | }
13 |
14 | if (process.argv[2] == '-v' || process.argv[2] == '--version')
15 | exit(0, needle.version);
16 | else if (process.argv[2] == null)
17 | usage();
18 |
19 | var method = process.argv[2],
20 | url = process.argv[3],
21 | options = { compressed: true, parse_response: true, follow_max: 5, timeout: 10000 };
22 |
23 | if (!needle[method]) {
24 | url = method;
25 | method = 'get';
26 | }
27 |
28 | var callback = function(err, resp) {
29 | if (err) return exit(1, "Error: " + err.message);
30 |
31 | if (process.argv.indexOf('-i') != -1)
32 | console.log(resp.headers) || console.log('');
33 |
34 | console.log(resp.body.toString());
35 | };
36 |
37 | if (method == 'post' || method == 'put')
38 | needle[method](url, process.argv[4], options, callback);
39 | else
40 | needle[method](url, options, callback);
41 |
--------------------------------------------------------------------------------
/examples/deflated-stream.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs'),
2 | stream = require('stream'),
3 | needle = require('./../');
4 |
5 | var url = 'http://ibl.gamechaser.net/f/tagqfxtteucbuldhezkz/bt_level1.gz';
6 |
7 | var resp = needle.get(url, { compressed: true, follow_max: 10 });
8 | console.log('Downloading...');
9 |
10 | resp.on('readable', function() {
11 |
12 | while (data = this.read()) {
13 | var lines = data.toString().split('\n');
14 | console.log('Got ' + lines.length + ' items.');
15 | // console.log(lines);
16 | }
17 |
18 | })
19 |
20 | resp.on('done', function(data) {
21 | console.log('Done');
22 | })
23 |
--------------------------------------------------------------------------------
/examples/digest-auth.js:
--------------------------------------------------------------------------------
1 | var needle = require('./..');
2 |
3 | var opts = {
4 | username: 'user3',
5 | password: 'user3',
6 | auth: 'digest'
7 | }
8 |
9 | needle.get('http://test.webdav.org/auth-digest/', opts, function(err, resp, body) {
10 | console.log(resp.headers);
11 |
12 | if (resp.statusCode == 401)
13 | console.log('\nIt failed.')
14 | else
15 | console.log('\nIt worked!')
16 | });
17 |
--------------------------------------------------------------------------------
/examples/download-to-file.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs'),
2 | needle = require('./..'),
3 | path = require('path');
4 |
5 | var url = process.argv[2] || 'https://upload.wikimedia.org/wikipedia/commons/a/af/Tux.png';
6 | var file = path.basename(url);
7 |
8 | console.log('Downloading ' + file);
9 |
10 | needle.get(url, { output: file, follow: 3 }, function(err, resp, data){
11 | console.log('File saved: ' + process.cwd() + '/' + file);
12 |
13 | var size = fs.statSync(file).size;
14 | if (size == resp.bytes)
15 | console.log(resp.bytes + ' bytes written to file.');
16 | else
17 | throw new Error('File size mismatch: ' + size + ' != ' + resp.bytes);
18 | });
19 |
--------------------------------------------------------------------------------
/examples/multipart-stream.js:
--------------------------------------------------------------------------------
1 | var needle = require('./../');
2 |
3 | var url = 'http://posttestserver.com/post.php?dir=needle';
4 |
5 | var black_pixel = Buffer.from("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=", 'base64');
6 |
7 | var data = {
8 | foo: 'bar',
9 | nested: {
10 | test: 123
11 | },
12 | image: { buffer: black_pixel, content_type: 'image/gif' }
13 | }
14 |
15 | var resp = needle.post(url, data, { multipart: true });
16 |
17 | resp.on('readable', function() {
18 | while (data = this.read()) {
19 | console.log(data.toString());
20 | }
21 | })
22 |
23 | resp.on('done', function(data) {
24 | console.log('Done.');
25 | })
26 |
--------------------------------------------------------------------------------
/examples/parsed-stream.js:
--------------------------------------------------------------------------------
1 | //////////////////////////////////////////
2 | // This example demonstrates what happends
3 | // when you use the built-in JSON parser.
4 | //////////////////////////////////////////
5 |
6 | var fs = require('fs'),
7 | stream = require('stream'),
8 | needle = require('./../');
9 |
10 | var url = 'http://ip.jsontest.com/',
11 | resp = needle.get(url, { parse: true });
12 |
13 | resp.on('readable', function(obj) {
14 | var chunk;
15 |
16 | while (chunk = this.read()) {
17 | console.log('root = ', chunk);
18 | }
19 | });
20 |
21 | resp.on('done', function() {
22 | console.log('Done.');
23 | });
24 |
--------------------------------------------------------------------------------
/examples/parsed-stream2.js:
--------------------------------------------------------------------------------
1 | //////////////////////////////////////////
2 | // This example illustrates a more complex
3 | // example of parsing a JSON stream.
4 | //////////////////////////////////////////
5 |
6 | var needle = require('./../'),
7 | JSONStream = require('JSONStream');
8 |
9 | var url = 'http://jsonplaceholder.typicode.com/db';
10 |
11 | // Initialize our GET request with our default (JSON)
12 | // parsers disabled.
13 |
14 | var json = new needle.get(url, {parse: false})
15 | // And now interpret the stream as JSON, returning only the
16 | // title of all the posts.
17 | .pipe(new JSONStream.parse('posts.*.title'));
18 |
19 | json.on('data', function (obj) {
20 | console.log('got title: \'' + obj + '\'');
21 | })
22 |
--------------------------------------------------------------------------------
/examples/stream-events.js:
--------------------------------------------------------------------------------
1 | var needle = require('./..');
2 |
3 | var resp = needle.get('google.com', { follow_max: 10, timeout: 5000 });
4 |
5 | resp.on('readable', function() {
6 | var chunk;
7 | while (chunk = this.read()) {
8 | console.log('Got ' + chunk.length + ' bytes');
9 | }
10 | })
11 |
12 | resp.on('headers', function(headers) {
13 | console.log('Got headers', headers);
14 | })
15 |
16 | resp.on('redirect', function(url) {
17 | console.log('Redirected to url ' + url);
18 | })
19 |
20 | resp.on('done', function(err) {
21 | console.log('Finished. No more data to receive.');
22 | if (err) console.log('With error', err)
23 | })
24 |
--------------------------------------------------------------------------------
/examples/stream-multiple/app.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const bodyParser = require("body-parser");
3 | const app = express();
4 | const { stream_multiple } = require('./stream-multiple')
5 | const env = require('./env')
6 |
7 | app.use(
8 | bodyParser.urlencoded({ extended: false }),
9 | bodyParser.json(),
10 | express.static(__dirname + '/public'),
11 | );
12 |
13 | app.get('/', (req, res) => res.send(`
14 |
Thanks Tomás Pollak
15 | Stream Multiple Files
16 | `));
17 |
18 | app.get('/stream_multiple_files', (req, res) => stream_multiple(req, res, env._urls, env.stream_dir));
19 |
20 | let PORT = process.env.PORT || 3000;
21 | app.listen(PORT, console.log(`Main Server: ${PORT}`));
--------------------------------------------------------------------------------
/examples/stream-multiple/env.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | _urls: [
3 | "https://images.unsplash.com/photo-1619410283995-43d9134e7656?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
4 | "https://images.unsplash.com/photo-1555949963-aa79dcee981c?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
5 | "https://images.unsplash.com/photo-1511376777868-611b54f68947?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80"
6 | ],
7 | _url: "https://images.unsplash.com/photo-1511376777868-611b54f68947?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
8 | stream_dir: `public/`
9 | }
--------------------------------------------------------------------------------
/examples/stream-multiple/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "express-needle",
3 | "version": "1.0.0",
4 | "description": "Express & Needle are friends <3",
5 | "main": "app.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "nodemon app.js"
9 | },
10 | "keywords": [],
11 | "author": "mohamed-bahaa21",
12 | "license": "ISC",
13 | "dependencies": {
14 | "body-parser": "^1.20.1",
15 | "express": "^4.18.2",
16 | "fs-extra": "^10.1.0",
17 | "needle": "^3.1.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/examples/stream-multiple/stream-multiple.js:
--------------------------------------------------------------------------------
1 | var needle = require('needle');
2 | const fs = require('fs-extra')
3 |
4 | function stream_multiple(req, res, _urls, stream_dir, index = 0) {
5 | if (index == 0) {
6 | // initial state
7 | }
8 |
9 | let writeStream;
10 | const uri = _urls[index];
11 |
12 | if (index == undefined) {
13 | index = 0;
14 | stream_multiple(req, res, _urls, stream_dir, index);
15 | } else {
16 |
17 | writeStream = fs.createWriteStream(`${stream_dir}` + `${index}.jpeg`);
18 |
19 | writeStream.on("ready", () => console.log({ msg: `STREAM::WRITE::READY::${index}` }));
20 | writeStream.on("open", () => console.log({ msg: `STREAM::WRITE::OPEN::${index}` }));
21 | writeStream.on("finish", () => console.log({ msg: `STREAM::WRITE::DONE::${index}` }));
22 |
23 | writeStream.on('close', () => {
24 | if (index >= _urls.length - 1) {
25 | res.redirect('/');
26 | } else {
27 | stream_multiple(req, res, _urls, stream_dir, index + 1);
28 | }
29 | })
30 |
31 | needle
32 | .get(uri, function (error, response) {
33 | if (response.bytes >= 1) {
34 | // you want to kill our servers
35 | }
36 |
37 | if (!error && response.statusCode == 200) {
38 | // good
39 | } else {
40 | // then we can retry later
41 | }
42 | })
43 | .pipe(writeStream)
44 | .on('done', function () {
45 | // needle
46 | });
47 | }
48 | }
49 |
50 | module.exports = { stream_multiple }
--------------------------------------------------------------------------------
/examples/stream-to-file.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs'),
2 | needle = require('./..'),
3 | path = require('path');
4 |
5 | var url = process.argv[2] || 'http://www.google.com/images/errors/robot.png';
6 | var file = path.basename(url);
7 |
8 | console.log('Downloading ' + file + '...');
9 | needle
10 | .get(url)
11 | .pipe(fs.createWriteStream(file))
12 | .on('done', function() {
13 | console.log('Done!')
14 | })
15 |
--------------------------------------------------------------------------------
/examples/upload-image.js:
--------------------------------------------------------------------------------
1 | var needle = require('../'),
2 | path = require('path');
3 |
4 | var image = 'https://upload.wikimedia.org/wikipedia/commons/a/af/Tux.png';
5 |
6 | function upload(obj, cb) {
7 | console.log('Uploading image...');
8 |
9 | var url = 'http://deviantsart.com';
10 |
11 | var opts = {
12 | timeout: 10000,
13 | follow: 3,
14 | multipart: true
15 | };
16 |
17 | var params = {
18 | file: obj
19 | }
20 |
21 | needle.post(url, params, opts, function(err, resp) {
22 | if (err || !resp.body.match('url'))
23 | return cb(err || new Error('No image URL found.'))
24 |
25 | cb(null, JSON.parse(resp.body).url)
26 | })
27 | }
28 |
29 | function download(url, cb) {
30 | console.log('Getting ' + url);
31 | needle.get(url, function(err, resp) {
32 | if (err) throw err;
33 |
34 | cb(null, resp.body);
35 | })
36 | }
37 |
38 | ////////////////////////////////////////
39 | // ok, now go.
40 |
41 | download(image, function(err, buffer) {
42 | if (err) throw err;
43 |
44 | var obj = { buffer: buffer, content_type: 'image/png' };
45 |
46 | upload(obj, function(err, url) {
47 | if (err) throw err;
48 |
49 | console.log('Image uploaded to ' + url);
50 | })
51 | })
52 |
--------------------------------------------------------------------------------
/lib/auth.js:
--------------------------------------------------------------------------------
1 | var createHash = require('crypto').createHash;
2 |
3 | function get_header(header, credentials, opts) {
4 | var type = header.split(' ')[0],
5 | user = credentials[0],
6 | pass = credentials[1];
7 |
8 | if (type == 'Digest') {
9 | return digest.generate(header, user, pass, opts.method, opts.path);
10 | } else if (type == 'Basic') {
11 | return basic(user, pass);
12 | }
13 | }
14 |
15 | ////////////////////
16 | // basic
17 |
18 | function md5(string) {
19 | return createHash('md5').update(string).digest('hex');
20 | }
21 |
22 | function basic(user, pass) {
23 | var str = typeof pass == 'undefined' ? user : [user, pass].join(':');
24 | return 'Basic ' + Buffer.from(str).toString('base64');
25 | }
26 |
27 | ////////////////////
28 | // digest
29 | // logic inspired from https://github.com/simme/node-http-digest-client
30 |
31 | var digest = {};
32 |
33 | digest.parse_header = function(header) {
34 | var challenge = {},
35 | matches = header.match(/([a-z0-9_-]+)="?([a-z0-9_=\/\.@\s-\+:)()]+)"?/gi);
36 |
37 | for (var i = 0, l = matches.length; i < l; i++) {
38 | var parts = matches[i].split('='),
39 | key = parts.shift(),
40 | val = parts.join('=').replace(/^"/, '').replace(/"$/, '');
41 |
42 | challenge[key] = val;
43 | }
44 |
45 | return challenge;
46 | }
47 |
48 | digest.update_nc = function(nc) {
49 | var max = 99999999;
50 | nc++;
51 |
52 | if (nc > max)
53 | nc = 1;
54 |
55 | var padding = new Array(8).join('0') + '';
56 | nc = nc + '';
57 | return padding.substr(0, 8 - nc.length) + nc;
58 | }
59 |
60 | digest.generate = function(header, user, pass, method, path) {
61 |
62 | var nc = 1,
63 | cnonce = null,
64 | challenge = digest.parse_header(header);
65 |
66 | var ha1 = md5(user + ':' + challenge.realm + ':' + pass),
67 | ha2 = md5(method.toUpperCase() + ':' + path),
68 | resp = [ha1, challenge.nonce];
69 |
70 | if (typeof challenge.qop === 'string') {
71 | cnonce = md5(Math.random().toString(36)).substr(0, 8);
72 | nc = digest.update_nc(nc);
73 | resp = resp.concat(nc, cnonce);
74 | resp = resp.concat(challenge.qop, ha2);
75 | } else {
76 | resp = resp.concat(ha2);
77 | }
78 |
79 | var params = {
80 | uri : path,
81 | realm : challenge.realm,
82 | nonce : challenge.nonce,
83 | username : user,
84 | response : md5(resp.join(':'))
85 | }
86 |
87 | if (challenge.qop) {
88 | params.qop = challenge.qop;
89 | }
90 |
91 | if (challenge.opaque) {
92 | params.opaque = challenge.opaque;
93 | }
94 |
95 | if (cnonce) {
96 | params.nc = nc;
97 | params.cnonce = cnonce;
98 | }
99 |
100 | header = []
101 | for (var k in params)
102 | header.push(k + '="' + params[k] + '"')
103 |
104 | return 'Digest ' + header.join(', ');
105 | }
106 |
107 | module.exports = {
108 | header : get_header,
109 | basic : basic,
110 | digest : digest.generate
111 | }
112 |
--------------------------------------------------------------------------------
/lib/cookies.js:
--------------------------------------------------------------------------------
1 |
2 | // Simple cookie handling implementation based on the standard RFC 6265.
3 | //
4 | // This module just has two functionalities:
5 | // - Parse a set-cookie-header as a key value object
6 | // - Write a cookie-string from a key value object
7 | //
8 | // All cookie attributes are ignored.
9 |
10 | var unescape = require('querystring').unescape;
11 |
12 | var COOKIE_PAIR = /^([^=\s]+)\s*=\s*("?)\s*(.*)\s*\2\s*$/;
13 | var EXCLUDED_CHARS = /[\x00-\x1F\x7F\x3B\x3B\s\"\,\\"%]/g;
14 | var TRAILING_SEMICOLON = /\x3B+$/;
15 | var SEP_SEMICOLON = /\s*\x3B\s*/;
16 |
17 | // i know these should be 'const', but I'd like to keep
18 | // supporting earlier node.js versions as long as I can. :)
19 |
20 | var KEY_INDEX = 1; // index of key from COOKIE_PAIR match
21 | var VALUE_INDEX = 3; // index of value from COOKIE_PAIR match
22 |
23 | // Returns a copy str trimmed and without trainling semicolon.
24 | function cleanCookieString(str) {
25 | return str.trim().replace(/\x3B+$/, '');
26 | }
27 |
28 | function getFirstPair(str) {
29 | var index = str.indexOf('\x3B');
30 | return index === -1 ? str : str.substr(0, index);
31 | }
32 |
33 | // Returns a encoded copy of str based on RFC6265 S4.1.1.
34 | function encodeCookieComponent(str) {
35 | return str.toString().replace(EXCLUDED_CHARS, encodeURIComponent);
36 | }
37 |
38 | // Parses a set-cookie-string based on the standard defined in RFC6265 S4.1.1.
39 | function parseSetCookieString(str) {
40 | str = cleanCookieString(str);
41 | str = getFirstPair(str);
42 |
43 | var res = COOKIE_PAIR.exec(str);
44 | if (!res || !res[VALUE_INDEX]) return null;
45 |
46 | return {
47 | name : unescape(res[KEY_INDEX]),
48 | value : unescape(res[VALUE_INDEX])
49 | };
50 | }
51 |
52 | // Parses a set-cookie-header and returns a key/value object.
53 | // Each key represents the name of a cookie.
54 | function parseSetCookieHeader(header) {
55 | if (!header) return {};
56 | header = Array.isArray(header) ? header : [header];
57 |
58 | return header.reduce(function(res, str) {
59 | var cookie = parseSetCookieString(str);
60 | if (cookie) res[cookie.name] = cookie.value;
61 | return res;
62 | }, {});
63 | }
64 |
65 | // Writes a set-cookie-string based on the standard definded in RFC6265 S4.1.1.
66 | function writeCookieString(obj) {
67 | return Object.keys(obj).reduce(function(str, name) {
68 | var encodedName = encodeCookieComponent(name);
69 | var encodedValue = encodeCookieComponent(obj[name]);
70 | str += (str ? '; ' : '') + encodedName + '=' + encodedValue;
71 | return str;
72 | }, '');
73 | }
74 |
75 | // returns a key/val object from an array of cookie strings
76 | exports.read = parseSetCookieHeader;
77 |
78 | // writes a cookie string header
79 | exports.write = writeCookieString;
80 |
--------------------------------------------------------------------------------
/lib/decoder.js:
--------------------------------------------------------------------------------
1 | var iconv,
2 | inherits = require('util').inherits,
3 | stream = require('stream');
4 |
5 | var regex = /(?:charset|encoding)\s*=\s*['"]? *([\w\-]+)/i;
6 |
7 | inherits(StreamDecoder, stream.Transform);
8 |
9 | function StreamDecoder(charset) {
10 | if (!(this instanceof StreamDecoder))
11 | return new StreamDecoder(charset);
12 |
13 | stream.Transform.call(this, charset);
14 | this.charset = charset;
15 | this.parsed_chunk = false;
16 | }
17 |
18 | StreamDecoder.prototype._transform = function(chunk, encoding, done) {
19 | // try to get charset from chunk, but just once
20 | if (!this.parsed_chunk && (this.charset == 'utf-8' || this.charset == 'utf8')) {
21 | this.parsed_chunk = true;
22 |
23 | var matches = regex.exec(chunk.toString());
24 |
25 | if (matches) {
26 | var found = matches[1].toLowerCase().replace('utf8', 'utf-8'); // canonicalize;
27 | // set charset, but only if iconv can handle it
28 | if (iconv.encodingExists(found)) this.charset = found;
29 | }
30 | }
31 |
32 | // if charset is already utf-8 or given encoding isn't supported, just pass through
33 | if (this.charset == 'utf-8' || !iconv.encodingExists(this.charset)) {
34 | this.push(chunk);
35 | return done();
36 | }
37 |
38 | // initialize stream decoder if not present
39 | var self = this;
40 | if (!this.decoder) {
41 | this.decoder = iconv.decodeStream(this.charset);
42 | this.decoder.on('data', function(decoded_chunk) {
43 | self.push(decoded_chunk);
44 | });
45 | };
46 |
47 | this.decoder.write(chunk);
48 | done();
49 | }
50 |
51 | module.exports = function(charset) {
52 | try {
53 | if (!iconv) iconv = require('iconv-lite');
54 | } catch(e) {
55 | /* iconv not found */
56 | }
57 |
58 | if (iconv)
59 | return new StreamDecoder(charset);
60 | else
61 | return new stream.PassThrough;
62 | }
63 |
--------------------------------------------------------------------------------
/lib/multipart.js:
--------------------------------------------------------------------------------
1 | var readFile = require('fs').readFile,
2 | basename = require('path').basename;
3 |
4 | exports.build = function(data, boundary, callback) {
5 |
6 | if (typeof data != 'object' || typeof data.pipe == 'function')
7 | return callback(new Error('Multipart builder expects data as key/val object.'));
8 |
9 | var body = '',
10 | object = flatten(data),
11 | count = Object.keys(object).length;
12 |
13 | if (count === 0)
14 | return callback(new Error('Empty multipart body. Invalid data.'))
15 |
16 | function done(err, section) {
17 | if (err) return callback(err);
18 | if (section) body += section;
19 | --count || callback(null, body + '--' + boundary + '--');
20 | };
21 |
22 | for (var key in object) {
23 | var value = object[key];
24 | if (value === null || typeof value == 'undefined') {
25 | done();
26 | } else if (Buffer.isBuffer(value)) {
27 | var part = { buffer: value, content_type: 'application/octet-stream' };
28 | generate_part(key, part, boundary, done);
29 | } else {
30 | var part = (value.buffer || value.file || value.content_type) ? value : { value: value };
31 | generate_part(key, part, boundary, done);
32 | }
33 | }
34 |
35 | }
36 |
37 | function generate_part(name, part, boundary, callback) {
38 |
39 | var return_part = '--' + boundary + '\r\n';
40 | return_part += 'Content-Disposition: form-data; name="' + name + '"';
41 |
42 | function append(data, filename) {
43 |
44 | if (data) {
45 | var binary = part.content_type.indexOf('text') == -1;
46 | return_part += '; filename="' + encodeURIComponent(filename) + '"\r\n';
47 | if (binary) return_part += 'Content-Transfer-Encoding: binary\r\n';
48 | return_part += 'Content-Type: ' + part.content_type + '\r\n\r\n';
49 | return_part += binary ? data.toString('binary') : data.toString('utf8');
50 | }
51 |
52 | callback(null, return_part + '\r\n');
53 | };
54 |
55 | if ((part.file || part.buffer) && part.content_type) {
56 |
57 | var filename = part.filename ? part.filename : part.file ? basename(part.file) : name;
58 | if (part.buffer) return append(part.buffer, filename);
59 |
60 | readFile(part.file, function(err, data) {
61 | if (err) return callback(err);
62 | append(data, filename);
63 | });
64 |
65 | } else {
66 |
67 | if (typeof part.value == 'object')
68 | return callback(new Error('Object received for ' + name + ', expected string.'))
69 |
70 | if (part.content_type) {
71 | return_part += '\r\n';
72 | return_part += 'Content-Type: ' + part.content_type;
73 | }
74 |
75 | return_part += '\r\n\r\n';
76 | return_part += Buffer.from(String(part.value), 'utf8').toString('binary');
77 | append();
78 |
79 | }
80 |
81 | }
82 |
83 | // flattens nested objects for multipart body
84 | function flatten(object, into, prefix) {
85 | into = into || {};
86 |
87 | for(var key in object) {
88 | var prefix_key = prefix ? prefix + '[' + key + ']' : key;
89 | var prop = object[key];
90 |
91 | if (prop && typeof prop === 'object' && !(prop.buffer || prop.file || prop.content_type))
92 | flatten(prop, into, prefix_key)
93 | else
94 | into[prefix_key] = prop;
95 | }
96 |
97 | return into;
98 | }
99 |
--------------------------------------------------------------------------------
/lib/parsers.js:
--------------------------------------------------------------------------------
1 | //////////////////////////////////////////
2 | // Defines mappings between content-type
3 | // and the appropriate parsers.
4 | //////////////////////////////////////////
5 |
6 | var Transform = require('stream').Transform;
7 | var sax = require('sax');
8 |
9 | function parseXML(str, cb) {
10 | var obj, current, parser = sax.parser(true, { trim: true, lowercase: true })
11 | parser.onerror = parser.onend = done;
12 |
13 | function done(err) {
14 | parser.onerror = parser.onend = function() { }
15 | cb(err, obj)
16 | }
17 |
18 | function newElement(name, attributes) {
19 | return {
20 | name: name || '',
21 | value: '',
22 | attributes: attributes || {},
23 | children: []
24 | }
25 | }
26 |
27 | parser.oncdata = parser.ontext = function(t) {
28 | if (current) current.value += t
29 | }
30 |
31 | parser.onopentag = function(node) {
32 | var element = newElement(node.name, node.attributes)
33 | if (current) {
34 | element.parent = current
35 | current.children.push(element)
36 | } else { // root object
37 | obj = element
38 | }
39 |
40 | current = element
41 | };
42 |
43 | parser.onclosetag = function() {
44 | if (typeof current.parent !== 'undefined') {
45 | var just_closed = current
46 | current = current.parent
47 | delete just_closed.parent
48 | }
49 | }
50 |
51 | parser.write(str).close()
52 | }
53 |
54 | function parserFactory(name, fn) {
55 |
56 | function parser() {
57 | var chunks = [],
58 | stream = new Transform({ objectMode: true });
59 |
60 | // Buffer all our data
61 | stream._transform = function(chunk, encoding, done) {
62 | chunks.push(chunk);
63 | done();
64 | }
65 |
66 | // And call the parser when all is there.
67 | stream._flush = function(done) {
68 | var self = this,
69 | data = Buffer.concat(chunks);
70 |
71 | try {
72 | fn(data, function(err, result) {
73 | if (err) throw err;
74 | self.push(result);
75 | });
76 | } catch (err) {
77 | self.push(data); // just pass the original data
78 | } finally {
79 | done();
80 | }
81 | }
82 |
83 | return stream;
84 | }
85 |
86 | return { fn: parser, name: name };
87 | }
88 |
89 | var parsers = {}
90 |
91 | function buildParser(name, types, fn) {
92 | var parser = parserFactory(name, fn);
93 | types.forEach(function(type) {
94 | parsers[type] = parser;
95 | })
96 | }
97 |
98 | buildParser('json', [
99 | 'application/json',
100 | 'application/hal+json',
101 | 'text/javascript',
102 | 'application/vnd.api+json'
103 | ], function(buffer, cb) {
104 | var err, data;
105 | try { data = JSON.parse(buffer); } catch (e) { err = e; }
106 | cb(err, data);
107 | });
108 |
109 | buildParser('xml', [
110 | 'text/xml',
111 | 'application/xml',
112 | 'application/rdf+xml',
113 | 'application/rss+xml',
114 | 'application/atom+xml'
115 | ], function(buffer, cb) {
116 | parseXML(buffer.toString(), function(err, obj) {
117 | cb(err, obj)
118 | })
119 | });
120 |
121 | module.exports = parsers;
122 | module.exports.use = buildParser;
123 |
--------------------------------------------------------------------------------
/lib/querystring.js:
--------------------------------------------------------------------------------
1 | // based on the qs module, but handles null objects as expected
2 | // fixes by Tomas Pollak.
3 |
4 | var toString = Object.prototype.toString;
5 |
6 | function stringify(obj, prefix) {
7 | if (prefix && (obj === null || typeof obj == 'undefined')) {
8 | return prefix + '=';
9 | } else if (toString.call(obj) == '[object Array]') {
10 | return stringifyArray(obj, prefix);
11 | } else if (toString.call(obj) == '[object Object]') {
12 | return stringifyObject(obj, prefix);
13 | } else if (toString.call(obj) == '[object Date]') {
14 | return obj.toISOString();
15 | } else if (prefix) { // string inside array or hash
16 | return prefix + '=' + encodeURIComponent(String(obj));
17 | } else if (String(obj).indexOf('=') !== -1) { // string with equal sign
18 | return String(obj);
19 | } else {
20 | throw new TypeError('Cannot build a querystring out of: ' + obj);
21 | }
22 | };
23 |
24 | function stringifyArray(arr, prefix) {
25 | var ret = [];
26 |
27 | for (var i = 0, len = arr.length; i < len; i++) {
28 | if (prefix)
29 | ret.push(stringify(arr[i], prefix + '[]'));
30 | else
31 | ret.push(stringify(arr[i]));
32 | }
33 |
34 | return ret.join('&');
35 | }
36 |
37 | function stringifyObject(obj, prefix) {
38 | var ret = [];
39 |
40 | Object.keys(obj).forEach(function(key) {
41 | ret.push(stringify(obj[key], prefix
42 | ? prefix + '[' + encodeURIComponent(key) + ']'
43 | : encodeURIComponent(key)));
44 | })
45 |
46 | return ret.join('&');
47 | }
48 |
49 | exports.build = stringify;
50 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs'),
2 | url = require('url'),
3 | stream = require('stream');
4 |
5 | function resolve_url(href, base) {
6 | if (url.URL)
7 | return new url.URL(href, base);
8 |
9 | // older Node version (< v6.13)
10 | return base ? url.resolve(base, href) : href;
11 | }
12 |
13 | function host_and_ports_match(url1, url2) {
14 | if (url1.indexOf('http') < 0) url1 = 'http://' + url1;
15 | if (url2.indexOf('http') < 0) url2 = 'http://' + url2;
16 | var a = url.parse(url1), b = url.parse(url2);
17 |
18 | return a.host == b.host
19 | && String(a.port || (a.protocol == 'https:' ? 443 : 80))
20 | == String(b.port || (b.protocol == 'https:' ? 443 : 80));
21 | }
22 |
23 | // returns false if a no_proxy host or pattern matches given url
24 | function should_proxy_to(uri) {
25 | var no_proxy = get_env_var(['NO_PROXY'], true);
26 | if (!no_proxy) return true;
27 |
28 | // previous (naive, simple) strategy
29 | // var host, hosts = no_proxy.split(',');
30 | // for (var i in hosts) {
31 | // host = hosts[i];
32 | // if (host_and_ports_match(host, uri)) {
33 | // return false;
34 | // }
35 | // }
36 |
37 | var pattern, pattern_list = no_proxy.split(/[\s,]+/);
38 | for (var i in pattern_list) {
39 | pattern = pattern_list[i];
40 | if (pattern.trim().length == 0) continue;
41 |
42 | // replace leading dot by asterisk, escape dots and finally replace asterisk by .*
43 | var regex = new RegExp(pattern.replace(/^\./, "*").replace(/[.]/g, '\\$&').replace(/\*/g, '.*'))
44 | if (uri.match(regex)) return false;
45 | }
46 |
47 | return true;
48 | }
49 |
50 | function get_env_var(keys, try_lower) {
51 | var val, i = -1, env = process.env;
52 | while (!val && i < keys.length-1) {
53 | val = env[keys[++i]];
54 | if (!val && try_lower) {
55 | val = env[keys[i].toLowerCase()];
56 | }
57 | }
58 | return val;
59 | }
60 |
61 | function parse_content_type(header) {
62 | if (!header || header === '') return {};
63 |
64 | var found, charset = 'utf8', arr = header.split(';');
65 |
66 | if (arr.length > 1 && (found = arr[1].match(/charset=(.+)/)))
67 | charset = found[1];
68 |
69 | return { type: arr[0], charset: charset };
70 | }
71 |
72 | function is_stream(obj) {
73 | return typeof obj.pipe === 'function';
74 | }
75 |
76 | function get_stream_length(stream, given_length, cb) {
77 | if (given_length > 0)
78 | return cb(given_length);
79 |
80 | if (stream.end !== void 0 && stream.end !== Infinity && stream.start !== void 0)
81 | return cb((stream.end + 1) - (stream.start || 0));
82 |
83 | fs.stat(stream.path, function(err, stat) {
84 | cb(stat ? stat.size - (stream.start || 0) : null);
85 | });
86 | }
87 |
88 | function pump_streams(streams, cb) {
89 | if (stream.pipeline)
90 | return stream.pipeline.apply(null, streams.concat(cb));
91 |
92 | var tmp = streams.shift();
93 | while (streams.length) {
94 | tmp = tmp.pipe(streams.shift());
95 | tmp.once('error', function(e) {
96 | cb && cb(e);
97 | cb = null;
98 | })
99 | }
100 | }
101 |
102 | module.exports = {
103 | resolve_url: resolve_url,
104 | get_env_var: get_env_var,
105 | host_and_ports_match: host_and_ports_match,
106 | should_proxy_to: should_proxy_to,
107 | parse_content_type: parse_content_type,
108 | is_stream: is_stream,
109 | get_stream_length: get_stream_length,
110 | pump_streams: pump_streams
111 | }
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) Fork, Ltd.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "needle",
3 | "version": "3.3.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "JSONStream": {
8 | "version": "1.3.5",
9 | "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz",
10 | "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==",
11 | "dev": true,
12 | "requires": {
13 | "jsonparse": "^1.2.0",
14 | "through": ">=2.2.7 <3"
15 | }
16 | },
17 | "balanced-match": {
18 | "version": "1.0.2",
19 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
20 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
21 | "dev": true
22 | },
23 | "brace-expansion": {
24 | "version": "1.1.11",
25 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
26 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
27 | "dev": true,
28 | "requires": {
29 | "balanced-match": "^1.0.0",
30 | "concat-map": "0.0.1"
31 | }
32 | },
33 | "browser-stdout": {
34 | "version": "1.3.1",
35 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
36 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
37 | "dev": true
38 | },
39 | "commander": {
40 | "version": "2.15.1",
41 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
42 | "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
43 | "dev": true
44 | },
45 | "concat-map": {
46 | "version": "0.0.1",
47 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
48 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
49 | "dev": true
50 | },
51 | "diff": {
52 | "version": "3.5.0",
53 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
54 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
55 | "dev": true
56 | },
57 | "end-of-stream": {
58 | "version": "1.4.4",
59 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
60 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
61 | "dev": true,
62 | "requires": {
63 | "once": "^1.4.0"
64 | }
65 | },
66 | "escape-string-regexp": {
67 | "version": "1.0.5",
68 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
69 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
70 | "dev": true
71 | },
72 | "formatio": {
73 | "version": "1.2.0",
74 | "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz",
75 | "integrity": "sha1-87IWfZBoxGmKjVH092CjmlTYGOs=",
76 | "dev": true,
77 | "requires": {
78 | "samsam": "1.x"
79 | }
80 | },
81 | "fs.realpath": {
82 | "version": "1.0.0",
83 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
84 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
85 | "dev": true
86 | },
87 | "glob": {
88 | "version": "7.1.2",
89 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
90 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
91 | "dev": true,
92 | "requires": {
93 | "fs.realpath": "^1.0.0",
94 | "inflight": "^1.0.4",
95 | "inherits": "2",
96 | "minimatch": "^3.0.4",
97 | "once": "^1.3.0",
98 | "path-is-absolute": "^1.0.0"
99 | }
100 | },
101 | "growl": {
102 | "version": "1.10.5",
103 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
104 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
105 | "dev": true
106 | },
107 | "has-flag": {
108 | "version": "3.0.0",
109 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
110 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
111 | "dev": true
112 | },
113 | "he": {
114 | "version": "1.1.1",
115 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
116 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
117 | "dev": true
118 | },
119 | "iconv-lite": {
120 | "version": "0.6.3",
121 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
122 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
123 | "requires": {
124 | "safer-buffer": ">= 2.1.2 < 3.0.0"
125 | }
126 | },
127 | "inflight": {
128 | "version": "1.0.6",
129 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
130 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
131 | "dev": true,
132 | "requires": {
133 | "once": "^1.3.0",
134 | "wrappy": "1"
135 | }
136 | },
137 | "inherits": {
138 | "version": "2.0.4",
139 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
140 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
141 | "dev": true
142 | },
143 | "isarray": {
144 | "version": "0.0.1",
145 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
146 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
147 | "dev": true
148 | },
149 | "jschardet": {
150 | "version": "1.6.0",
151 | "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-1.6.0.tgz",
152 | "integrity": "sha512-xYuhvQ7I9PDJIGBWev9xm0+SMSed3ZDBAmvVjbFR1ZRLAF+vlXcQu6cRI9uAlj81rzikElRVteehwV7DuX2ZmQ==",
153 | "dev": true
154 | },
155 | "jsonparse": {
156 | "version": "1.3.1",
157 | "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
158 | "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=",
159 | "dev": true
160 | },
161 | "lolex": {
162 | "version": "1.6.0",
163 | "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz",
164 | "integrity": "sha1-OpoCg0UqR9dDnnJzG54H1zhuSfY=",
165 | "dev": true
166 | },
167 | "minimatch": {
168 | "version": "3.0.4",
169 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
170 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
171 | "dev": true,
172 | "requires": {
173 | "brace-expansion": "^1.1.7"
174 | }
175 | },
176 | "minimist": {
177 | "version": "0.0.8",
178 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
179 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
180 | "dev": true
181 | },
182 | "mkdirp": {
183 | "version": "0.5.1",
184 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
185 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
186 | "dev": true,
187 | "requires": {
188 | "minimist": "0.0.8"
189 | }
190 | },
191 | "mocha": {
192 | "version": "5.2.0",
193 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz",
194 | "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==",
195 | "dev": true,
196 | "requires": {
197 | "browser-stdout": "1.3.1",
198 | "commander": "2.15.1",
199 | "debug": "3.1.0",
200 | "diff": "3.5.0",
201 | "escape-string-regexp": "1.0.5",
202 | "glob": "7.1.2",
203 | "growl": "1.10.5",
204 | "he": "1.1.1",
205 | "minimatch": "3.0.4",
206 | "mkdirp": "0.5.1",
207 | "supports-color": "5.4.0"
208 | },
209 | "dependencies": {
210 | "debug": {
211 | "version": "3.1.0",
212 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
213 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
214 | "dev": true,
215 | "requires": {
216 | "ms": "2.0.0"
217 | }
218 | },
219 | "ms": {
220 | "version": "2.0.0",
221 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
222 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
223 | "dev": true
224 | }
225 | }
226 | },
227 | "ms": {
228 | "version": "2.1.1",
229 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
230 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
231 | },
232 | "native-promise-only": {
233 | "version": "0.8.1",
234 | "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz",
235 | "integrity": "sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE=",
236 | "dev": true
237 | },
238 | "once": {
239 | "version": "1.4.0",
240 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
241 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
242 | "dev": true,
243 | "requires": {
244 | "wrappy": "1"
245 | }
246 | },
247 | "path-is-absolute": {
248 | "version": "1.0.1",
249 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
250 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
251 | "dev": true
252 | },
253 | "path-to-regexp": {
254 | "version": "1.8.0",
255 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
256 | "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
257 | "dev": true,
258 | "requires": {
259 | "isarray": "0.0.1"
260 | }
261 | },
262 | "pump": {
263 | "version": "3.0.0",
264 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
265 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
266 | "dev": true,
267 | "requires": {
268 | "end-of-stream": "^1.1.0",
269 | "once": "^1.3.1"
270 | }
271 | },
272 | "q": {
273 | "version": "1.5.1",
274 | "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
275 | "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=",
276 | "dev": true
277 | },
278 | "safer-buffer": {
279 | "version": "2.1.2",
280 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
281 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
282 | },
283 | "samsam": {
284 | "version": "1.3.0",
285 | "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz",
286 | "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==",
287 | "dev": true
288 | },
289 | "sax": {
290 | "version": "1.2.4",
291 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
292 | "integrity": "sha1-KBYjTiN4vdxOU1T6tcqold9xANk="
293 | },
294 | "should": {
295 | "version": "13.2.3",
296 | "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz",
297 | "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==",
298 | "dev": true,
299 | "requires": {
300 | "should-equal": "^2.0.0",
301 | "should-format": "^3.0.3",
302 | "should-type": "^1.4.0",
303 | "should-type-adaptors": "^1.0.1",
304 | "should-util": "^1.0.0"
305 | }
306 | },
307 | "should-equal": {
308 | "version": "2.0.0",
309 | "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz",
310 | "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==",
311 | "dev": true,
312 | "requires": {
313 | "should-type": "^1.4.0"
314 | }
315 | },
316 | "should-format": {
317 | "version": "3.0.3",
318 | "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz",
319 | "integrity": "sha1-m/yPdPo5IFxT04w01xcwPidxJPE=",
320 | "dev": true,
321 | "requires": {
322 | "should-type": "^1.3.0",
323 | "should-type-adaptors": "^1.0.1"
324 | }
325 | },
326 | "should-type": {
327 | "version": "1.4.0",
328 | "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz",
329 | "integrity": "sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM=",
330 | "dev": true
331 | },
332 | "should-type-adaptors": {
333 | "version": "1.1.0",
334 | "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz",
335 | "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==",
336 | "dev": true,
337 | "requires": {
338 | "should-type": "^1.3.0",
339 | "should-util": "^1.0.0"
340 | }
341 | },
342 | "should-util": {
343 | "version": "1.0.0",
344 | "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.0.tgz",
345 | "integrity": "sha1-yYzaN0qmsZDfi6h8mInCtNtiAGM=",
346 | "dev": true
347 | },
348 | "sinon": {
349 | "version": "2.3.0",
350 | "resolved": "https://registry.npmjs.org/sinon/-/sinon-2.3.0.tgz",
351 | "integrity": "sha1-s0M77qlRSpBHY7/r9IrNIdOeioc=",
352 | "dev": true,
353 | "requires": {
354 | "diff": "^3.1.0",
355 | "formatio": "1.2.0",
356 | "lolex": "^1.6.0",
357 | "native-promise-only": "^0.8.1",
358 | "path-to-regexp": "^1.7.0",
359 | "samsam": "^1.1.3",
360 | "text-encoding": "0.6.4",
361 | "type-detect": "^4.0.0"
362 | }
363 | },
364 | "supports-color": {
365 | "version": "5.4.0",
366 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
367 | "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
368 | "dev": true,
369 | "requires": {
370 | "has-flag": "^3.0.0"
371 | }
372 | },
373 | "text-encoding": {
374 | "version": "0.6.4",
375 | "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz",
376 | "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=",
377 | "dev": true
378 | },
379 | "through": {
380 | "version": "2.3.8",
381 | "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz",
382 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
383 | "dev": true
384 | },
385 | "type-detect": {
386 | "version": "4.0.8",
387 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
388 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
389 | "dev": true
390 | },
391 | "wrappy": {
392 | "version": "1.0.2",
393 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
394 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
395 | "dev": true
396 | },
397 | "xml2js": {
398 | "version": "0.4.19",
399 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
400 | "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
401 | "dev": true,
402 | "requires": {
403 | "sax": ">=0.6.0",
404 | "xmlbuilder": "~9.0.1"
405 | }
406 | },
407 | "xmlbuilder": {
408 | "version": "9.0.7",
409 | "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
410 | "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=",
411 | "dev": true
412 | }
413 | }
414 | }
415 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "needle",
3 | "version": "3.3.0",
4 | "description": "The leanest and most handsome HTTP client in the Nodelands.",
5 | "keywords": [
6 | "http",
7 | "https",
8 | "simple",
9 | "request",
10 | "client",
11 | "multipart",
12 | "upload",
13 | "proxy",
14 | "deflate",
15 | "timeout",
16 | "charset",
17 | "iconv",
18 | "cookie",
19 | "redirect"
20 | ],
21 | "tags": [
22 | "http",
23 | "https",
24 | "simple",
25 | "request",
26 | "client",
27 | "multipart",
28 | "upload",
29 | "proxy",
30 | "deflate",
31 | "timeout",
32 | "charset",
33 | "iconv",
34 | "cookie",
35 | "redirect"
36 | ],
37 | "author": "Tomás Pollak ",
38 | "repository": {
39 | "type": "git",
40 | "url": "https://github.com/tomas/needle.git"
41 | },
42 | "dependencies": {
43 | "iconv-lite": "^0.6.3",
44 | "sax": "^1.2.4"
45 | },
46 | "devDependencies": {
47 | "JSONStream": "^1.3.5",
48 | "jschardet": "^1.6.0",
49 | "mocha": "^5.2.0",
50 | "pump": "^3.0.0",
51 | "q": "^1.5.1",
52 | "should": "^13.2.3",
53 | "sinon": "^2.3.0",
54 | "xml2js": "^0.4.19"
55 | },
56 | "scripts": {
57 | "test": "mocha test"
58 | },
59 | "directories": {
60 | "lib": "./lib"
61 | },
62 | "main": "./lib/needle",
63 | "bin": {
64 | "needle": "./bin/needle"
65 | },
66 | "license": "MIT",
67 | "engines": {
68 | "node": ">= 4.4.x"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/test/auth_digest_spec.js:
--------------------------------------------------------------------------------
1 | var needle = require('../'),
2 | auth = require('../lib/auth'),
3 | sinon = require('sinon'),
4 | should = require('should'),
5 | http = require('http'),
6 | helpers = require('./helpers');
7 |
8 | var createHash = require('crypto').createHash;
9 |
10 | function md5(string) {
11 | return createHash('md5').update(string).digest('hex');
12 | }
13 |
14 | function parse_header(header) {
15 | var challenge = {},
16 | matches = header.match(/([a-z0-9_-]+)="?([a-z0-9=\/\.@\s-\+]+)"?/gi);
17 |
18 | for (var i = 0, l = matches.length; i < l; i++) {
19 | var parts = matches[i].split('='),
20 | key = parts.shift(),
21 | val = parts.join('=').replace(/^"/, '').replace(/"$/, '');
22 |
23 | challenge[key] = val;
24 | }
25 |
26 | return challenge;
27 | }
28 |
29 | describe('auth_digest', function() {
30 | describe('With qop (RFC 2617)', function() {
31 | it('should generate a proper header', function() {
32 | // from https://tools.ietf.org/html/rfc2617
33 | var performDigest = function() {
34 | var header = 'Digest realm="testrealm@host.com", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"';
35 | var user = 'Mufasa';
36 | var pass = 'Circle Of Life';
37 | var method = 'get';
38 | var path = '/dir/index.html';
39 |
40 | var updatedHeader = auth.digest(header, user, pass, method, path);
41 | var parsedUpdatedHeader = parse_header(updatedHeader);
42 |
43 | var ha1 = md5(user + ':' + parsedUpdatedHeader.realm + ':' + pass);
44 | var ha2 = md5(method.toUpperCase() + ':' + path);
45 | var expectedResponse = md5([
46 | ha1,
47 | parsedUpdatedHeader.nonce,
48 | parsedUpdatedHeader.nc,
49 | parsedUpdatedHeader.cnonce,
50 | parsedUpdatedHeader.qop,
51 | ha2
52 | ].join(':'));
53 |
54 | return {
55 | header: updatedHeader,
56 | parsed: parsedUpdatedHeader,
57 | expectedResponse: expectedResponse,
58 | }
59 | }
60 |
61 | const result = performDigest();
62 |
63 | (result.header).should
64 | .match(/qop="auth"/)
65 | .match(/uri="\/dir\/index.html"/)
66 | .match(/opaque="5ccc069c403ebaf9f0171e9517f40e41"/)
67 | .match(/realm="testrealm@host\.com"/)
68 | .match(/response=/)
69 | .match(/nc=/)
70 | .match(/nonce=/)
71 | .match(/cnonce=/);
72 |
73 | (result.parsed.response).should.be.eql(result.expectedResponse);
74 | });
75 | });
76 |
77 | describe('With plus character in nonce header', function() {
78 | it('should generate a proper header', function() {
79 | // from https://tools.ietf.org/html/rfc2617
80 | var performDigest = function() {
81 | var header = 'Digest realm="testrealm@host.com", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f6+00bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"';
82 | var user = 'Mufasa';
83 | var pass = 'Circle Of Life';
84 | var method = 'get';
85 | var path = '/dir/index.html';
86 |
87 | var updatedHeader = auth.digest(header, user, pass, method, path);
88 | var parsedUpdatedHeader = parse_header(updatedHeader);
89 |
90 | var ha1 = md5(user + ':' + parsedUpdatedHeader.realm + ':' + pass);
91 | var ha2 = md5(method.toUpperCase() + ':' + path);
92 | var expectedResponse = md5([
93 | ha1,
94 | parsedUpdatedHeader.nonce,
95 | parsedUpdatedHeader.nc,
96 | parsedUpdatedHeader.cnonce,
97 | parsedUpdatedHeader.qop,
98 | ha2
99 | ].join(':'));
100 |
101 | return {
102 | header: updatedHeader,
103 | parsed: parsedUpdatedHeader,
104 | expectedResponse: expectedResponse,
105 | }
106 | }
107 |
108 | const result = performDigest();
109 |
110 | (result.header).should
111 | .match(/nonce="dcd98b7102dd2f0e8b11d0f6\+00bfb0c093"/)
112 | });
113 | });
114 |
115 | describe('With colon character in nonce header', function() {
116 | it('should generate a proper header', function() {
117 | // from https://tools.ietf.org/html/rfc2617
118 | var performDigest = function() {
119 | var header = 'Digest realm="IP Camera", charset="UTF-8", algorithm="MD5", nonce="636144c2:2970b5fdd41b5ac6b669848f43d2d22b", qop="auth"';
120 | var user = 'Mufasa';
121 | var pass = 'Circle Of Life';
122 | var method = 'get';
123 | var path = '/dir/index.html';
124 |
125 | var updatedHeader = auth.digest(header, user, pass, method, path);
126 | var parsedUpdatedHeader = parse_header(updatedHeader);
127 |
128 | var ha1 = md5(user + ':' + parsedUpdatedHeader.realm + ':' + pass);
129 | var ha2 = md5(method.toUpperCase() + ':' + path);
130 | var expectedResponse = md5([
131 | ha1,
132 | parsedUpdatedHeader.nonce,
133 | parsedUpdatedHeader.nc,
134 | parsedUpdatedHeader.cnonce,
135 | parsedUpdatedHeader.qop,
136 | ha2
137 | ].join(':'));
138 |
139 | return {
140 | header: updatedHeader,
141 | parsed: parsedUpdatedHeader,
142 | expectedResponse: expectedResponse,
143 | }
144 | }
145 |
146 | const result = performDigest();
147 |
148 | (result.header).should
149 | .match(/nonce="636144c2:2970b5fdd41b5ac6b669848f43d2d22b"/)
150 | });
151 | });
152 |
153 |
154 | describe('With brackets in realm header', function() {
155 | it('should generate a proper header', function() {
156 | // from https://tools.ietf.org/html/rfc2617
157 | var performDigest = function() {
158 | var header = 'Digest qop="auth", realm="IP Camera(76475)", nonce="4e4449794d575269597a706b5a575935595441324d673d3d", stale="FALSE", Basic realm="IP Camera(76475)"';
159 | var user = 'Mufasa';
160 | var pass = 'Circle Of Life';
161 | var method = 'get';
162 | var path = '/dir/index.html';
163 |
164 | var updatedHeader = auth.digest(header, user, pass, method, path);
165 | var parsedUpdatedHeader = parse_header(updatedHeader);
166 |
167 | var ha1 = md5(user + ':' + parsedUpdatedHeader.realm + ':' + pass);
168 | var ha2 = md5(method.toUpperCase() + ':' + path);
169 | var expectedResponse = md5([
170 | ha1,
171 | parsedUpdatedHeader.nonce,
172 | parsedUpdatedHeader.nc,
173 | parsedUpdatedHeader.cnonce,
174 | parsedUpdatedHeader.qop,
175 | ha2
176 | ].join(':'));
177 |
178 | return {
179 | header: updatedHeader,
180 | parsed: parsedUpdatedHeader,
181 | expectedResponse: expectedResponse,
182 | }
183 | }
184 |
185 | const result = performDigest();
186 |
187 | (result.header).should
188 | .match(/realm="IP Camera\(76475\)"/)
189 | });
190 | });
191 |
192 | describe('Without qop (RFC 2617)', function() {
193 | it('should generate a proper header', function() {
194 | // from https://tools.ietf.org/html/rfc2069
195 | var performDigest = function() {
196 | var header = 'Digest realm="testrealm@host.com", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"';
197 | var user = 'Mufasa';
198 | var pass = 'Circle Of Life';
199 | var method = 'get';
200 | var path = '/dir/index.html';
201 |
202 | var updatedHeader = auth.digest(header, user, pass, method, path);
203 | var parsedUpdatedHeader = parse_header(updatedHeader);
204 |
205 | var ha1 = md5(user + ':' + parsedUpdatedHeader.realm + ':' + pass);
206 | var ha2 = md5(method.toUpperCase() + ':' + path);
207 | var expectedResponse = md5([ha1, parsedUpdatedHeader.nonce, ha2].join(':'));
208 |
209 | return {
210 | header: updatedHeader,
211 | parsed: parsedUpdatedHeader,
212 | expectedResponse: expectedResponse,
213 | }
214 | }
215 |
216 | const result = performDigest();
217 |
218 | (result.header).should
219 | .not.match(/qop=/)
220 | .match(/uri="\/dir\/index.html"/)
221 | .match(/opaque="5ccc069c403ebaf9f0171e9517f40e41"/)
222 | .match(/realm="testrealm@host\.com"/)
223 | .match(/response=/)
224 | .not.match(/nc=/)
225 | .match(/nonce=/)
226 | .not.match(/cnonce=/);
227 |
228 | (result.parsed.response).should.be.eql(result.expectedResponse);
229 | });
230 | });
231 | })
--------------------------------------------------------------------------------
/test/basic_auth_spec.js:
--------------------------------------------------------------------------------
1 | var helpers = require('./helpers'),
2 | should = require('should'),
3 | needle = require('./../'),
4 | server;
5 |
6 | var port = 7707;
7 |
8 | describe('Basic Auth', function() {
9 |
10 | before(function(done) {
11 | server = helpers.server({ port: port }, done);
12 | })
13 |
14 | after(function(done) {
15 | server.close(done);
16 | })
17 |
18 | ///////////////// helpers
19 |
20 | var get_auth = function(header) {
21 | var token = header.split(/\s+/).pop();
22 | return token && Buffer.from(token, 'base64').toString().split(':');
23 | }
24 |
25 | describe('when neither username or password are passed', function() {
26 |
27 | it('doesnt send any Authorization headers', function(done) {
28 | needle.get('localhost:' + port, { parse: true }, function(err, resp) {
29 | var sent_headers = resp.body.headers;
30 | Object.keys(sent_headers).should.not.containEql('authorization');
31 | done();
32 | })
33 | })
34 |
35 | })
36 |
37 | describe('when username is an empty string, and password is a valid string', function() {
38 |
39 | var opts = { username: '', password: 'foobar', parse: true };
40 |
41 | it('doesnt send any Authorization headers', function(done) {
42 | needle.get('localhost:' + port, { parse: true }, function(err, resp) {
43 | var sent_headers = resp.body.headers;
44 | Object.keys(sent_headers).should.not.containEql('authorization');
45 | done();
46 | })
47 | })
48 |
49 | });
50 |
51 | describe('when username is a valid string, but no username is passed', function() {
52 |
53 | var opts = { username: 'foobar', parse: true };
54 |
55 | it('sends Authorization header', function(done) {
56 | needle.get('localhost:' + port, opts, function(err, resp) {
57 | var sent_headers = resp.body.headers;
58 | Object.keys(sent_headers).should.containEql('authorization');
59 | done();
60 | })
61 | })
62 |
63 | it('Basic Auth only includes username, without colon', function(done) {
64 | needle.get('localhost:' + port, opts, function(err, resp) {
65 | var sent_headers = resp.body.headers;
66 | var auth = get_auth(sent_headers['authorization']);
67 | auth[0].should.equal('foobar');
68 | auth.should.have.lengthOf(1);
69 | done();
70 | })
71 | })
72 |
73 | })
74 |
75 | describe('when username is a valid string, and password is null', function() {
76 |
77 | var opts = { username: 'foobar', password: null, parse: true };
78 |
79 | it('sends Authorization header', function(done) {
80 | needle.get('localhost:' + port, opts, function(err, resp) {
81 | var sent_headers = resp.body.headers;
82 | Object.keys(sent_headers).should.containEql('authorization');
83 | done();
84 | })
85 | })
86 |
87 | it('Basic Auth only includes both username and password', function(done) {
88 | needle.get('localhost:' + port, opts, function(err, resp) {
89 | var sent_headers = resp.body.headers;
90 | var auth = get_auth(sent_headers['authorization']);
91 | auth[0].should.equal('foobar');
92 | auth[1].should.equal('');
93 | done();
94 | })
95 | })
96 |
97 | })
98 |
99 | describe('when username is a valid string, and password is an empty string', function() {
100 |
101 | var opts = { username: 'foobar', password: '', parse: true };
102 |
103 | it('sends Authorization header', function(done) {
104 | needle.get('localhost:' + port, opts, function(err, resp) {
105 | var sent_headers = resp.body.headers;
106 | Object.keys(sent_headers).should.containEql('authorization');
107 | done();
108 | })
109 | })
110 |
111 | it('Basic Auth only includes both username and password', function(done) {
112 | needle.get('localhost:' + port, opts, function(err, resp) {
113 | var sent_headers = resp.body.headers;
114 | var auth = get_auth(sent_headers['authorization']);
115 | auth[0].should.equal('foobar');
116 | auth[1].should.equal('');
117 | auth.should.have.lengthOf(2);
118 | done();
119 | })
120 | })
121 |
122 | })
123 |
124 | describe('when username AND password are non empty strings', function() {
125 |
126 | var opts = { username: 'foobar', password: 'jakub', parse: true };
127 |
128 | it('sends Authorization header', function(done) {
129 | needle.get('localhost:' + port, opts, function(err, resp) {
130 | var sent_headers = resp.body.headers;
131 | Object.keys(sent_headers).should.containEql('authorization');
132 | done();
133 | })
134 | })
135 |
136 | it('Basic Auth only includes both user and password', function(done) {
137 | needle.get('localhost:' + port, opts, function(err, resp) {
138 | var sent_headers = resp.body.headers;
139 | var auth = get_auth(sent_headers['authorization']);
140 | auth[0].should.equal('foobar');
141 | auth[1].should.equal('jakub');
142 | auth.should.have.lengthOf(2);
143 | done();
144 | })
145 | })
146 |
147 | })
148 |
149 | describe('URL with @ but not username/pass', function() {
150 | it('doesnt send Authorization header', function(done) {
151 | var url = 'localhost:' + port + '/abc/@def/xyz.zip';
152 |
153 | needle.get(url, {}, function(err, resp) {
154 | var sent_headers = resp.body.headers;
155 | Object.keys(sent_headers).should.not.containEql('authorization');
156 | done();
157 | })
158 | })
159 |
160 | it('sends user:pass headers if passed via options', function(done) {
161 | var url = 'localhost:' + port + '/abc/@def/xyz.zip';
162 |
163 | needle.get(url, { username: 'foo' }, function(err, resp) {
164 | var sent_headers = resp.body.headers;
165 | Object.keys(sent_headers).should.containEql('authorization');
166 | sent_headers['authorization'].should.eql('Basic Zm9v')
167 | done();
168 | })
169 | })
170 | })
171 |
172 | describe('when username/password are included in URL', function() {
173 | var opts = { parse: true };
174 |
175 | it('sends Authorization header', function(done) {
176 | needle.get('foobar:jakub@localhost:' + port, opts, function(err, resp) {
177 | var sent_headers = resp.body.headers;
178 | Object.keys(sent_headers).should.containEql('authorization');
179 | done();
180 | })
181 | })
182 |
183 | it('Basic Auth only includes both user and password', function(done) {
184 | needle.get('foobar:jakub@localhost:' + port, opts, function(err, resp) {
185 | var sent_headers = resp.body.headers;
186 | var auth = get_auth(sent_headers['authorization']);
187 | auth[0].should.equal('foobar');
188 | auth[1].should.equal('jakub');
189 | auth.should.have.lengthOf(2);
190 | done();
191 | })
192 | })
193 |
194 | })
195 |
196 | })
197 |
--------------------------------------------------------------------------------
/test/compression_spec.js:
--------------------------------------------------------------------------------
1 | var should = require('should'),
2 | needle = require('./../'),
3 | http = require('http'),
4 | zlib = require('zlib'),
5 | stream = require('stream'),
6 | port = 11123,
7 | server;
8 |
9 | describe('compression', function(){
10 |
11 | require.bind(null, 'zlib').should.not.throw()
12 |
13 | var jsonData = '{"foo":"bar"}';
14 |
15 | describe('when server supports compression', function(){
16 |
17 | before(function(){
18 | server = http.createServer(function(req, res) {
19 | var raw = new stream.PassThrough();
20 |
21 | var acceptEncoding = req.headers['accept-encoding'];
22 | if (!acceptEncoding) {
23 | acceptEncoding = '';
24 | }
25 |
26 | if (acceptEncoding.match(/\bdeflate\b/)) {
27 | res.setHeader('Content-Encoding', 'deflate');
28 | raw.pipe(zlib.createDeflate()).pipe(res);
29 | } else if (acceptEncoding.match(/\bgzip\b/)) {
30 | res.setHeader('Content-Encoding', 'gzip');
31 | raw.pipe(zlib.createGzip()).pipe(res);
32 | } else if (acceptEncoding.match(/\bbr\b/)) {
33 | res.setHeader('Content-Encoding', 'br');
34 | raw.pipe(zlib.createBrotliCompress()).pipe(res);
35 | } else {
36 | raw.pipe(res);
37 | }
38 |
39 | res.setHeader('Content-Type', 'application/json')
40 | if (req.headers['with-bad']) {
41 | res.end('foo'); // end, no deflate data
42 | } else {
43 | raw.end(jsonData)
44 | }
45 |
46 | })
47 |
48 | server.listen(port);
49 | });
50 |
51 | after(function(done){
52 | server.close(done);
53 | })
54 |
55 | describe('and client requests no compression', function() {
56 | it('should have the body decompressed', function(done){
57 | needle.get('localhost:' + port, function(err, response, body){
58 | should.ifError(err);
59 | body.should.have.property('foo', 'bar');
60 | response.bytes.should.equal(jsonData.length);
61 | done();
62 | })
63 | })
64 | })
65 |
66 | describe('and client requests gzip compression', function() {
67 | it('should have the body decompressed', function(done){
68 | needle.get('localhost:' + port, {headers: {'Accept-Encoding': 'gzip'}}, function(err, response, body){
69 | should.ifError(err);
70 | body.should.have.property('foo', 'bar');
71 | response.bytes.should.not.equal(jsonData.length);
72 | done();
73 | })
74 | })
75 | })
76 |
77 | describe('and client requests deflate compression', function() {
78 | it('should have the body decompressed', function(done){
79 | needle.get('localhost:' + port, {headers: {'Accept-Encoding': 'deflate'}}, function(err, response, body){
80 | should.ifError(err);
81 | body.should.have.property('foo', 'bar');
82 | response.bytes.should.not.equal(jsonData.length);
83 | done();
84 | })
85 | })
86 |
87 | it('should rethrow errors from decompressors', function(done){
88 | needle.get('localhost:' + port, {headers: {'Accept-Encoding': 'deflate', 'With-Bad': 'true'}}, function(err, response, body) {
89 | should.exist(err);
90 | err.message.should.equal("incorrect header check");
91 | err.code.should.equal("Z_DATA_ERROR")
92 | done();
93 | })
94 | })
95 | })
96 |
97 | describe('and client requests brotli compression', function() {
98 | it('should have the body decompressed', function(done){
99 | // Skip this test if Brotli is not supported
100 | if (typeof zlib.BrotliDecompress !== 'function') {
101 | return done();
102 | }
103 | needle.get('localhost:' + port, {headers: {'Accept-Encoding': 'br'}}, function(err, response, body){
104 | should.ifError(err);
105 | body.should.have.property('foo', 'bar');
106 | response.bytes.should.not.equal(jsonData.length);
107 | done();
108 | })
109 | })
110 | })
111 | })
112 | })
113 |
--------------------------------------------------------------------------------
/test/cookies_spec.js:
--------------------------------------------------------------------------------
1 | var needle = require('../'),
2 | cookies = require('../lib/cookies'),
3 | sinon = require('sinon'),
4 | http = require('http'),
5 | should = require('should');
6 |
7 | var WEIRD_COOKIE_NAME = 'wc',
8 | BASE64_COOKIE_NAME = 'bc',
9 | FORBIDDEN_COOKIE_NAME = 'fc',
10 | NUMBER_COOKIE_NAME = 'nc';
11 |
12 | var WEIRD_COOKIE_VALUE = '!\'*+#()&-./0123456789:<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~',
13 | BASE64_COOKIE_VALUE = 'Y29va2llCg==',
14 | FORBIDDEN_COOKIE_VALUE = ' ;"\\,',
15 | NUMBER_COOKIE_VALUE = 12354342;
16 |
17 | var NO_COOKIES_TEST_PORT = 11112,
18 | ALL_COOKIES_TEST_PORT = 11113;
19 |
20 | describe('cookies', function() {
21 |
22 | var setCookieHeader, headers, server, opts;
23 |
24 | function decode(str) {
25 | return decodeURIComponent(str);
26 | }
27 |
28 | function encode(str) {
29 | str = str.toString().replace(/[\x00-\x1F\x7F]/g, encodeURIComponent);
30 | return str.replace(/[\s\"\,;\\%]/g, encodeURIComponent);
31 | }
32 |
33 | before(function() {
34 | setCookieHeader = [
35 | WEIRD_COOKIE_NAME + '=' + encode(WEIRD_COOKIE_VALUE) + ';',
36 | BASE64_COOKIE_NAME + '=' + encode(BASE64_COOKIE_VALUE) + ';',
37 | FORBIDDEN_COOKIE_NAME + '=' + encode(FORBIDDEN_COOKIE_VALUE) + ';',
38 | NUMBER_COOKIE_NAME + '=' + encode(NUMBER_COOKIE_VALUE) + ';'
39 | ];
40 | });
41 |
42 | before(function(done) {
43 | serverAllCookies = http.createServer(function(req, res) {
44 | res.setHeader('Content-Type', 'text/html');
45 | res.setHeader('Set-Cookie', setCookieHeader);
46 | res.end('200');
47 | }).listen(ALL_COOKIES_TEST_PORT, done);
48 | });
49 |
50 | after(function(done) {
51 | serverAllCookies.close(done);
52 | });
53 |
54 | describe('with default options', function() {
55 | it('no cookie header is set on request', function(done) {
56 | needle.get(
57 | 'localhost:' + ALL_COOKIES_TEST_PORT, function(err, response) {
58 | should.not.exist(response.req._headers.cookie);
59 | done();
60 | });
61 | });
62 | });
63 |
64 | describe('if response does not contain cookies', function() {
65 | before(function(done) {
66 | serverNoCookies = http.createServer(function(req, res) {
67 | res.setHeader('Content-Type', 'text/html');
68 | res.end('200');
69 | }).listen(NO_COOKIES_TEST_PORT, done);
70 | });
71 |
72 | it('response.cookies is undefined', function(done) {
73 | needle.get(
74 | 'localhost:' + NO_COOKIES_TEST_PORT, function(error, response) {
75 | should.not.exist(response.cookies);
76 | done();
77 | });
78 | });
79 |
80 | after(function(done) {
81 | serverNoCookies.close(done);
82 | });
83 | });
84 |
85 | describe('if response contains cookies', function() {
86 |
87 | it('puts them on resp.cookies', function(done) {
88 | needle.get(
89 | 'localhost:' + ALL_COOKIES_TEST_PORT, function(error, response) {
90 | response.should.have.property('cookies');
91 | done();
92 | });
93 | });
94 |
95 | it('parses them as a object', function(done) {
96 | needle.get(
97 | 'localhost:' + ALL_COOKIES_TEST_PORT, function(error, response) {
98 | response.cookies.should.be.an.instanceOf(Object)
99 | .and.have.property(WEIRD_COOKIE_NAME);
100 | response.cookies.should.have.property(BASE64_COOKIE_NAME);
101 | response.cookies.should.have.property(FORBIDDEN_COOKIE_NAME);
102 | response.cookies.should.have.property(NUMBER_COOKIE_NAME);
103 | done();
104 | });
105 | });
106 |
107 | it('must decode it', function(done) {
108 | needle.get(
109 | 'localhost:' + ALL_COOKIES_TEST_PORT, function(error, response) {
110 | response.cookies.wc.should.be.eql(WEIRD_COOKIE_VALUE);
111 | response.cookies.bc.should.be.eql(BASE64_COOKIE_VALUE);
112 | response.cookies.fc.should.be.eql(FORBIDDEN_COOKIE_VALUE);
113 | response.cookies.nc.should.be.eql(NUMBER_COOKIE_VALUE.toString());
114 | done();
115 | });
116 | });
117 |
118 | describe('when a cookie value is invalid', function() {
119 |
120 | before(function() {
121 | setCookieHeader = [
122 | 'geo_city=%D1%E0%ED%EA%F2-%CF%E5%F2%E5%F0%E1%F3%F0%E3'
123 | ];
124 | })
125 |
126 | it('doesnt blow up', function(done) {
127 | needle.get('localhost:' + ALL_COOKIES_TEST_PORT, function(error, response) {
128 | should.not.exist(error)
129 | var whatever = 'efbfbdefbfbdefbfbdefbfbdefbfbd2defbfbdefbfbdefbfbdefbfbdefbfbdefbfbdefbfbdefbfbdefbfbd';
130 | Buffer.from(response.cookies.geo_city).toString('hex').should.eql(whatever)
131 | done();
132 | });
133 | })
134 |
135 | })
136 |
137 | describe('and response is a redirect', function() {
138 |
139 | var redirectServer, testPort = 22222;
140 | var requestCookies = [];
141 |
142 | var responseCookies = [
143 | [ // first req
144 | WEIRD_COOKIE_NAME + '=' + encode(WEIRD_COOKIE_VALUE) + ';',
145 | BASE64_COOKIE_NAME + '=' + encode(BASE64_COOKIE_VALUE) + ';',
146 | 'FOO=123;'
147 | ], [ // second req
148 | FORBIDDEN_COOKIE_NAME + '=' + encode(FORBIDDEN_COOKIE_VALUE) + ';',
149 | NUMBER_COOKIE_NAME + '=' + encode(NUMBER_COOKIE_VALUE) + ';'
150 | ], [ // third red
151 | 'FOO=BAR;'
152 | ]
153 | ]
154 |
155 | before(function(done) {
156 | redirectServer = http.createServer(function(req, res) {
157 | var number = parseInt(req.url.replace('/', ''));
158 | var nextUrl = 'http://' + 'localhost:' + testPort + '/' + (number + 1);
159 |
160 | if (number == 0) requestCookies = []; // reset
161 | requestCookies.push(req.headers['cookie']);
162 |
163 | if (responseCookies[number]) { // we should send cookies for this request
164 | res.statusCode = 302;
165 | res.setHeader('Set-Cookie', responseCookies[number]);
166 | res.setHeader('Location', nextUrl);
167 | } else if (number == 3) {
168 | res.statusCode = 302; // redirect but without cookies
169 | res.setHeader('Location', nextUrl);
170 | }
171 |
172 | res.end('OK');
173 | }).listen(22222, done);
174 | });
175 |
176 | after(function(done) {
177 | redirectServer.close(done);
178 | })
179 |
180 | describe('and follow_set_cookies is false', function() {
181 |
182 | describe('with original request cookie', function() {
183 |
184 | var opts = {
185 | follow_set_cookies: false,
186 | follow_max: 4,
187 | cookies: { 'xxx': 123 }
188 | };
189 |
190 | it('request cookie is not passed to redirects', function(done) {
191 | needle.get('localhost:' + testPort + '/0', opts, function(err, resp) {
192 | requestCookies.should.eql(["xxx=123", undefined, undefined, undefined, undefined])
193 | done();
194 | });
195 | });
196 |
197 | it('response cookies are not passed either', function(done) {
198 | needle.get('localhost:' + testPort + '/0', opts, function(err, resp) {
199 | should.not.exist(resp.cookies);
200 | done();
201 | });
202 | });
203 |
204 | })
205 |
206 | describe('without original request cookie', function() {
207 |
208 | var opts = {
209 | follow_set_cookies: false,
210 | follow_max: 4,
211 | };
212 |
213 | it('no request cookies are sent', function(done) {
214 | needle.get('localhost:' + testPort + '/0', opts, function(err, resp) {
215 | requestCookies.should.eql([undefined, undefined, undefined, undefined, undefined])
216 | done();
217 | });
218 | });
219 |
220 | it('response cookies are not passed either', function(done) {
221 | needle.get('localhost:' + testPort + '/0', opts, function(err, resp) {
222 | should.not.exist(resp.cookies);
223 | done();
224 | });
225 | });
226 |
227 | })
228 |
229 | });
230 |
231 | describe('and follow_set_cookies is true', function() {
232 |
233 | describe('with original request cookie', function() {
234 |
235 | var opts = {
236 | follow_set_cookies: true,
237 | follow_max: 4,
238 | cookies: { 'xxx': 123 }
239 | };
240 |
241 | it('request cookie is passed passed to redirects, and response cookies are added too', function(done) {
242 | needle.get('localhost:' + testPort + '/0', opts, function(err, resp) {
243 | requestCookies.should.eql([
244 | "xxx=123",
245 | "xxx=123; wc=!'*+#()&-./0123456789:<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~; bc=Y29va2llCg==; FOO=123",
246 | "xxx=123; wc=!\'*+#()&-./0123456789:<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~; bc=Y29va2llCg==; FOO=123; fc=%20%3B%22%5C%2C; nc=12354342",
247 | "xxx=123; wc=!\'*+#()&-./0123456789:<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~; bc=Y29va2llCg==; FOO=BAR; fc=%20%3B%22%5C%2C; nc=12354342",
248 | "xxx=123; wc=!\'*+#()&-./0123456789:<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~; bc=Y29va2llCg==; FOO=BAR; fc=%20%3B%22%5C%2C; nc=12354342"
249 | ])
250 | done();
251 | });
252 | });
253 |
254 | it('response cookies are passed as well', function(done) {
255 | needle.get('localhost:' + testPort + '/0', opts, function(err, resp) {
256 | resp.cookies.should.have.property(WEIRD_COOKIE_NAME);
257 | resp.cookies.should.have.property(BASE64_COOKIE_NAME);
258 | resp.cookies.should.have.property(FORBIDDEN_COOKIE_NAME);
259 | resp.cookies.should.have.property(NUMBER_COOKIE_NAME);
260 | resp.cookies.should.have.property('FOO');
261 | resp.cookies.FOO.should.eql('BAR'); // should overwrite previous one
262 | done();
263 | });
264 | });
265 |
266 | })
267 |
268 | describe('without original request cookie', function() {
269 |
270 | var opts = {
271 | follow_set_cookies: true,
272 | follow_max: 4,
273 | };
274 |
275 | it('response cookies are passed to redirects', function(done) {
276 | needle.get('localhost:' + testPort + '/0', opts, function(err, resp) {
277 | requestCookies.should.eql([
278 | undefined,
279 | "wc=!'*+#()&-./0123456789:<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~; bc=Y29va2llCg==; FOO=123",
280 | "wc=!\'*+#()&-./0123456789:<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~; bc=Y29va2llCg==; FOO=123; fc=%20%3B%22%5C%2C; nc=12354342",
281 | "wc=!\'*+#()&-./0123456789:<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~; bc=Y29va2llCg==; FOO=BAR; fc=%20%3B%22%5C%2C; nc=12354342",
282 | "wc=!\'*+#()&-./0123456789:<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~; bc=Y29va2llCg==; FOO=BAR; fc=%20%3B%22%5C%2C; nc=12354342"
283 | ])
284 | done();
285 | });
286 | });
287 |
288 | it('response cookies are passed as well', function(done) {
289 | needle.get('localhost:' + testPort + '/0', opts, function(err, resp) {
290 | // resp.cookies.should.have.property(WEIRD_COOKIE_NAME);
291 | // resp.cookies.should.have.property(BASE64_COOKIE_NAME);
292 | // resp.cookies.should.have.property(FORBIDDEN_COOKIE_NAME);
293 | // resp.cookies.should.have.property(NUMBER_COOKIE_NAME);
294 | // resp.cookies.should.have.property('FOO');
295 | // resp.cookies.FOO.should.eql('BAR'); // should overwrite previous one
296 | done();
297 | });
298 | });
299 |
300 | })
301 |
302 | });
303 | });
304 |
305 | describe('with parse_cookies = false', function() {
306 | it('does not parse them', function(done) {
307 | needle.get(
308 | 'localhost:' + ALL_COOKIES_TEST_PORT, { parse_cookies: false }, function(error, response) {
309 | should.not.exist(response.cookies);
310 | done();
311 | });
312 | });
313 | });
314 | });
315 |
316 | describe('if request contains cookie header', function() {
317 | var opts = {
318 | cookies: {}
319 | };
320 |
321 | before(function() {
322 | opts.cookies[WEIRD_COOKIE_NAME] = WEIRD_COOKIE_VALUE;
323 | opts.cookies[BASE64_COOKIE_NAME] = BASE64_COOKIE_VALUE;
324 | opts.cookies[FORBIDDEN_COOKIE_NAME] = FORBIDDEN_COOKIE_VALUE;
325 | opts.cookies[NUMBER_COOKIE_NAME] = NUMBER_COOKIE_VALUE;
326 | });
327 |
328 | it('must be a valid cookie string', function(done) {
329 | var COOKIE_PAIR = /^([^=\s]+)\s*=\s*("?)\s*(.*)\s*\2\s*$/;
330 |
331 | var full_header = [
332 | WEIRD_COOKIE_NAME + '=' + WEIRD_COOKIE_VALUE,
333 | BASE64_COOKIE_NAME + '=' + BASE64_COOKIE_VALUE,
334 | FORBIDDEN_COOKIE_NAME + '=' + encode(FORBIDDEN_COOKIE_VALUE),
335 | NUMBER_COOKIE_NAME + '=' + NUMBER_COOKIE_VALUE
336 | ].join('; ')
337 |
338 | needle.get('localhost:' + ALL_COOKIES_TEST_PORT, opts, function(error, response) {
339 | var cookieString = response.req._headers.cookie;
340 | cookieString.should.be.type('string');
341 |
342 | cookieString.split(/\s*;\s*/).forEach(function(pair) {
343 | COOKIE_PAIR.test(pair).should.be.exactly(true);
344 | });
345 |
346 | cookieString.should.be.exactly(full_header);
347 | done();
348 | });
349 | });
350 |
351 | it('dont have to encode allowed characters', function(done) {
352 | var COOKIE_PAIR = /^([^=\s]+)\s*=\s*("?)\s*(.*)\s*\2\s*$/,
353 | KEY_INDEX = 1,
354 | VALUE_INEX = 3;
355 |
356 | needle.get('localhost:' + ALL_COOKIES_TEST_PORT, opts, function(error, response) {
357 | var cookieObj = {},
358 | cookieString = response.req._headers.cookie;
359 |
360 | cookieString.split(/\s*;\s*/).forEach(function(str) {
361 | var pair = COOKIE_PAIR.exec(str);
362 | cookieObj[pair[KEY_INDEX]] = pair[VALUE_INEX];
363 | });
364 |
365 | cookieObj[WEIRD_COOKIE_NAME].should.be.exactly(WEIRD_COOKIE_VALUE);
366 | cookieObj[BASE64_COOKIE_NAME].should.be.exactly(BASE64_COOKIE_VALUE);
367 | done();
368 | });
369 | });
370 |
371 | it('must encode forbidden characters', function(done) {
372 | var COOKIE_PAIR = /^([^=\s]+)\s*=\s*("?)\s*(.*)\s*\2\s*$/,
373 | KEY_INDEX = 1,
374 | VALUE_INEX = 3;
375 |
376 | needle.get('localhost:' + ALL_COOKIES_TEST_PORT, opts, function(error, response) {
377 | var cookieObj = {},
378 | cookieString = response.req._headers.cookie;
379 |
380 | cookieString.split(/\s*;\s*/).forEach(function(str) {
381 | var pair = COOKIE_PAIR.exec(str);
382 | cookieObj[pair[KEY_INDEX]] = pair[VALUE_INEX];
383 | });
384 |
385 | cookieObj[FORBIDDEN_COOKIE_NAME].should.not.be.eql(
386 | FORBIDDEN_COOKIE_VALUE);
387 | cookieObj[FORBIDDEN_COOKIE_NAME].should.be.exactly(
388 | encode(FORBIDDEN_COOKIE_VALUE));
389 | cookieObj[FORBIDDEN_COOKIE_NAME].should.be.exactly(
390 | encodeURIComponent(FORBIDDEN_COOKIE_VALUE));
391 | done();
392 | });
393 | });
394 | });
395 |
396 | });
397 |
--------------------------------------------------------------------------------
/test/decoder_spec.js:
--------------------------------------------------------------------------------
1 | var should = require('should'),
2 | needle = require('./../'),
3 | decoder = require('./../lib/decoder'),
4 | Q = require('q'),
5 | chardet = require('jschardet'),
6 | fs = require('fs'),
7 | http = require('http'),
8 | helpers = require('./helpers');
9 |
10 | describe('character encoding', function() {
11 |
12 | this.timeout(5000);
13 |
14 | function staticServerFor(file, content_type) {
15 | return http.createServer(function(req, res) {
16 | req.on('data', function(chunk) {})
17 | req.on('end', function() {
18 | // We used to pull from a particular site that is no longer up.
19 | // This is a local mirror pulled from archive.org
20 | // https://web.archive.org/web/20181003202907/http://www.nina.jp/server/slackware/webapp/tomcat_charset.html
21 | fs.readFile(file, function(err, data) {
22 | if (err) {
23 | res.writeHead(404);
24 | res.end(JSON.stringify(err));
25 | return;
26 | }
27 | res.writeHeader(200, { 'Content-Type': content_type })
28 | res.end(data);
29 | });
30 | })
31 | })
32 | }
33 |
34 | describe('Given content-type: "text/html; charset=EUC-JP"', function() {
35 | var server, port = 2233;
36 |
37 | before(function(done) {
38 | server = staticServerFor('test/files/tomcat_charset.html', 'text/html; charset=EUC-JP')
39 | server.listen(port, done)
40 | url = 'http://localhost:' + port;
41 | })
42 |
43 | after(function(done) {
44 | server.close(done)
45 | })
46 |
47 | describe('with decode = false', function() {
48 | it('does not decode', function(done) {
49 | needle.get(url, { decode: false }, function(err, resp) {
50 | resp.body.should.be.a.String;
51 | chardet.detect(resp.body).encoding.should.eql('windows-1252');
52 | resp.body.indexOf('EUCを使う').should.eql(-1);
53 | done();
54 | })
55 | })
56 | })
57 |
58 | describe('with decode = true', function() {
59 | it('decodes', function(done) {
60 | needle.get(url, { decode: true }, function(err, resp) {
61 | resp.body.should.be.a.String;
62 | chardet.detect(resp.body).encoding.should.eql('ascii');
63 | resp.body.indexOf('EUCを使う').should.not.eql(-1);
64 | done();
65 | })
66 | })
67 | })
68 | })
69 |
70 | describe('Given content-type: "text/html but file is charset: gb2312', function() {
71 |
72 | it('encodes to UTF-8', function(done) {
73 |
74 | // Our Needle wrapper that requests a chinese website.
75 | var task = Q.nbind(needle.get, needle, 'http://www.chinesetop100.com/');
76 |
77 | // Different instantiations of this task
78 | var tasks = [Q.fcall(task, {decode: true}),
79 | Q.fcall(task, {decode: false})];
80 |
81 | var results = tasks.map(function(task) {
82 | return task.then(function(obj) {
83 | return obj[0].body;
84 | });
85 | });
86 |
87 | // Execute all requests concurrently
88 | Q.all(results).done(function(bodies) {
89 |
90 | var charsets = [
91 | chardet.detect(bodies[0]).encoding,
92 | chardet.detect(bodies[1]).encoding,
93 | ]
94 |
95 | // We wanted to decode our first stream as specified by options
96 | charsets[0].should.equal('ascii');
97 | bodies[0].indexOf('全球中文网站前二十强').should.not.equal(-1);
98 |
99 | // But not our second stream
100 | charsets[1].should.equal('windows-1252');
101 | bodies[1].indexOf('全球中文网站前二十强').should.equal(-1);
102 |
103 | done();
104 | });
105 | })
106 | })
107 |
108 | describe('Given content-type: text/html; charset=maccentraleurope', function() {
109 | var server, port = 2233;
110 |
111 | // from 'https://wayback.archive-it.org/3259/20160921140616/https://www.arc.gov/research/MapsofAppalachia.asp?MAP_ID=11';
112 | before(function(done) {
113 | server = staticServerFor('test/files/Appalachia.html', 'text/html; charset=maccentraleurope')
114 | server.listen(port, done)
115 | url = 'http://localhost:' + port;
116 | })
117 |
118 | after(function(done) {
119 | server.close(done)
120 | })
121 |
122 | describe('with decode = false', function() {
123 | it('does not decode', function(done) {
124 | needle.get(url, { decode: false }, function(err, resp) {
125 | resp.body.should.be.a.String;
126 | chardet.detect(resp.body).encoding.should.eql('ascii');
127 | done();
128 | })
129 | })
130 | })
131 |
132 | describe('with decode = true', function() {
133 | it('does not explode', function(done) {
134 | (function() {
135 | needle.get(url, { decode: true }, function(err, resp) {
136 | resp.body.should.be.a.String;
137 | chardet.detect(resp.body).encoding.should.eql('ascii');
138 | done();
139 | })
140 | }).should.not.throw();
141 | })
142 | })
143 | })
144 |
145 | describe('Given content-type: "text/html"', function () {
146 |
147 | var server,
148 | port = 54321,
149 | text = 'Magyarországi Fióktelepe'
150 |
151 | before(function(done) {
152 | server = helpers.server({
153 | port: port,
154 | response: text,
155 | headers: { 'Content-Type': 'text/html' }
156 | }, done);
157 | })
158 |
159 | after(function(done) {
160 | server.close(done)
161 | })
162 |
163 | describe('with decode = false', function () {
164 | it('decodes by default to utf-8', function (done) {
165 |
166 | needle.get('http://localhost:' + port, { decode: false }, function (err, resp) {
167 | resp.body.should.be.a.String;
168 | chardet.detect(resp.body).encoding.should.eql('ISO-8859-2');
169 | resp.body.should.eql('Magyarországi Fióktelepe')
170 | done();
171 | })
172 |
173 | })
174 |
175 | })
176 | })
177 |
178 | describe('multibyte characters split across chunks', function () {
179 |
180 | describe('with encoding = utf-8', function() {
181 |
182 | var d,
183 | result = Buffer.allocUnsafe(0);
184 |
185 | before(function(done) {
186 | d = decoder('utf-8');
187 | done();
188 | });
189 |
190 | it('reassembles split multibyte characters', function (done) {
191 |
192 | d.on("data", function(chunk){
193 | result = Buffer.concat([ result, chunk ]);
194 | });
195 |
196 | d.on("end", function(){
197 | result.toString("utf-8").should.eql('慶');
198 | done();
199 | });
200 |
201 | // write '慶' in utf-8 split across chunks
202 | d.write(Buffer.from([0xE6]));
203 | d.write(Buffer.from([0x85]));
204 | d.write(Buffer.from([0xB6]));
205 | d.end();
206 |
207 | })
208 | })
209 |
210 | describe('with encoding = euc-jp', function() {
211 |
212 | var d,
213 | result = Buffer.allocUnsafe(0);
214 |
215 | before(function(done) {
216 | d = decoder('euc-jp');
217 | done();
218 | });
219 |
220 | it('reassembles split multibyte characters', function (done) {
221 |
222 | d.on("data", function(chunk){
223 | result = Buffer.concat([ result, chunk ]);
224 | });
225 |
226 | d.on("end", function(){
227 | result.toString("utf-8").should.eql('慶');
228 | done();
229 | });
230 |
231 | // write '慶' in euc-jp split across chunks
232 | d.write(Buffer.from([0xB7]));
233 | d.write(Buffer.from([0xC4]));
234 | d.end();
235 |
236 | })
237 | })
238 |
239 | describe('with encoding = gb18030', function() {
240 |
241 | var d,
242 | result = Buffer.allocUnsafe(0);
243 |
244 | before(function(done) {
245 | d = decoder('gb18030');
246 | done();
247 | });
248 |
249 | it('reassembles split multibyte characters', function (done) {
250 |
251 | d.on("data", function(chunk){
252 | result = Buffer.concat([ result, chunk ]);
253 | });
254 |
255 | d.on("end", function(){
256 | result.toString("utf-8").should.eql('慶');
257 | done();
258 | });
259 |
260 | // write '慶' in gb18030 split across chunks
261 | d.write(Buffer.from([0x91]));
262 | d.write(Buffer.from([0x63]));
263 | d.end();
264 |
265 | })
266 | })
267 |
268 | })
269 |
270 | })
271 |
--------------------------------------------------------------------------------
/test/errors_spec.js:
--------------------------------------------------------------------------------
1 | var needle = require('../'),
2 | sinon = require('sinon'),
3 | should = require('should'),
4 | http = require('http'),
5 | Emitter = require('events').EventEmitter,
6 | helpers = require('./helpers');
7 |
8 | var get_catch = function(url, opts) {
9 | var err;
10 | try {
11 | needle.get(url, opts);
12 | } catch(e) {
13 | err = e;
14 | }
15 | return err;
16 | }
17 |
18 | describe('errors', function() {
19 |
20 | after(function(done) {
21 | setTimeout(done, 100)
22 | })
23 |
24 | describe('when host does not exist', function() {
25 |
26 | var url = 'http://unexistinghost/foo';
27 |
28 | describe('with callback', function() {
29 |
30 | it('does not throw', function() {
31 | var ex = get_catch(url);
32 | should.not.exist(ex);
33 | })
34 |
35 | it('callbacks an error', function(done) {
36 | needle.get(url, function(err) {
37 | err.should.be.a.Error;
38 | done();
39 | })
40 | })
41 |
42 | it('error should be ENOTFOUND or EADDRINFO or EAI_AGAIN', function(done) {
43 | needle.get(url, function(err) {
44 | err.code.should.match(/ENOTFOUND|EADDRINFO|EAI_AGAIN/)
45 | done();
46 | })
47 | })
48 |
49 | it('does not callback a response', function(done) {
50 | needle.get(url, function(err, resp) {
51 | should.not.exist(resp);
52 | done();
53 | })
54 | })
55 |
56 | it('does not emit an error event', function(done) {
57 | var emitted = false;
58 | var req = needle.get(url, function(err, resp) { })
59 |
60 | req.on('error', function() {
61 | emitted = true;
62 | })
63 |
64 | setTimeout(function() {
65 | emitted.should.eql(false);
66 | done();
67 | }, 100);
68 | })
69 |
70 | })
71 |
72 | describe('without callback', function() {
73 |
74 | it('does not throw', function() {
75 | var ex = get_catch(url);
76 | should.not.exist(ex);
77 | })
78 |
79 | it('emits end event once, with error', function(done) {
80 | var callcount = 0,
81 | stream = needle.get(url);
82 |
83 | stream.on('done', function(err) {
84 | err.code.should.match(/ENOTFOUND|EADDRINFO|EAI_AGAIN/)
85 | callcount++;
86 | })
87 |
88 | setTimeout(function() {
89 | callcount.should.equal(1);
90 | done();
91 | }, 200)
92 | })
93 |
94 | it('does not emit a readable event', function(done) {
95 | var called = false,
96 | stream = needle.get(url);
97 |
98 | stream.on('readable', function() {
99 | called = true;
100 | })
101 |
102 | stream.on('done', function(err) {
103 | called.should.be.false;
104 | done();
105 | })
106 | })
107 |
108 | it('does not emit an error event', function(done) {
109 | var emitted = false,
110 | stream = needle.get(url);
111 |
112 | stream.on('error', function() {
113 | emitted = true;
114 | })
115 |
116 | stream.on('done', function(err) {
117 | emitted.should.eql(false);
118 | done();
119 | })
120 | })
121 |
122 | })
123 |
124 | })
125 |
126 | describe('when request times out waiting for response', function() {
127 |
128 | var server,
129 | url = 'http://localhost:3333/foo';
130 |
131 | var send_request = function(cb) {
132 | return needle.get(url, { response_timeout: 200 }, cb);
133 | }
134 |
135 | before(function() {
136 | server = helpers.server({ port: 3333, wait: 1000 });
137 | })
138 |
139 | after(function() {
140 | server.close();
141 | })
142 |
143 | describe('with callback', function() {
144 |
145 | it('aborts the request', function(done) {
146 |
147 | var time = new Date();
148 |
149 | send_request(function(err) {
150 | var timediff = (new Date() - time);
151 | timediff.should.be.within(200, 300);
152 | done();
153 | })
154 |
155 | })
156 |
157 | it('callbacks an error', function(done) {
158 | send_request(function(err) {
159 | err.should.be.a.Error;
160 | done();
161 | })
162 | })
163 |
164 | it('error should be ECONNRESET', function(done) {
165 | send_request(function(err) {
166 | err.code.should.equal('ECONNRESET')
167 | done();
168 | })
169 | })
170 |
171 | it('does not callback a response', function(done) {
172 | send_request(function(err, resp) {
173 | should.not.exist(resp);
174 | done();
175 | })
176 | })
177 |
178 | it('does not emit an error event', function(done) {
179 | var emitted = false;
180 |
181 | var req = send_request(function(err, resp) {
182 | should.not.exist(resp);
183 | })
184 |
185 | req.on('error', function() {
186 | emitted = true;
187 | })
188 |
189 | setTimeout(function() {
190 | emitted.should.eql(false);
191 | done();
192 | }, 350);
193 | })
194 |
195 | })
196 |
197 | describe('without callback', function() {
198 |
199 | it('emits done event once, with error', function(done) {
200 | var error,
201 | called = 0,
202 | stream = send_request();
203 |
204 | stream.on('done', function(err) {
205 | err.code.should.equal('ECONNRESET');
206 | called++;
207 | })
208 |
209 | setTimeout(function() {
210 | called.should.equal(1);
211 | done();
212 | }, 250)
213 | })
214 |
215 | it('aborts the request', function(done) {
216 |
217 | var time = new Date();
218 | var stream = send_request();
219 |
220 | stream.on('done', function(err) {
221 | var timediff = (new Date() - time);
222 | timediff.should.be.within(200, 300);
223 | done();
224 | })
225 |
226 | })
227 |
228 | it('error should be ECONNRESET', function(done) {
229 | var error,
230 | stream = send_request();
231 |
232 | stream.on('done', function(err) {
233 | err.code.should.equal('ECONNRESET')
234 | done();
235 | })
236 | })
237 |
238 | it('does not emit a readable event', function(done) {
239 | var called = false,
240 | stream = send_request();
241 |
242 | stream.on('readable', function() {
243 | called = true;
244 | })
245 |
246 | stream.on('done', function(err) {
247 | called.should.be.false;
248 | done();
249 | })
250 | })
251 |
252 | it('does not emit an error event', function(done) {
253 | var emitted = false;
254 | var stream = send_request();
255 |
256 | stream.on('error', function() {
257 | emitted = true;
258 | })
259 |
260 | stream.on('done', function(err) {
261 | err.should.be.a.Error;
262 | err.code.should.equal('ECONNRESET')
263 | emitted.should.eql(false);
264 | done();
265 | })
266 | })
267 |
268 | })
269 |
270 | })
271 |
272 | var node_major_ver = process.version.split('.')[0].replace('v', '');
273 | if (node_major_ver >= 16) {
274 | describe('when request is aborted by signal', function() {
275 |
276 | var server,
277 | url = 'http://localhost:3333/foo';
278 |
279 | before(function() {
280 | server = helpers.server({ port: 3333, wait: 600 });
281 | })
282 |
283 | after(function() {
284 | server.close();
285 | })
286 |
287 | afterEach(function() {
288 | // reset signal to default
289 | needle.defaults({signal: null});
290 | })
291 |
292 | it('works if passing an already aborted signal aborts the request', function(done) {
293 | var abortedSignal = AbortSignal.abort();
294 | var start = new Date();
295 |
296 | abortedSignal.aborted.should.equal(true);
297 |
298 | needle.get(url, { signal: abortedSignal, response_timeout: 10000 }, function(err, res) {
299 | var timediff = (new Date() - start);
300 |
301 | should.not.exist(res);
302 | err.code.should.equal('ABORT_ERR');
303 | timediff.should.be.within(0, 50);
304 |
305 | done();
306 | });
307 | })
308 |
309 | it('works if request aborts before timing out', function(done) {
310 | var cancel = new AbortController();
311 | var start = new Date();
312 |
313 | needle.get(url, { signal: cancel.signal, response_timeout: 500, open_timeout: 500, read_timeout: 500 }, function(err, res) {
314 | var timediff = (new Date() - start);
315 |
316 | should.not.exist(res);
317 | if (node_major_ver <= 16)
318 | err.code.should.equal('ECONNRESET');
319 | if (node_major_ver > 16)
320 | err.code.should.equal('ABORT_ERR');
321 | cancel.signal.aborted.should.equal(true);
322 | timediff.should.be.within(200, 250);
323 |
324 | done();
325 | });
326 |
327 | function abort() {
328 | cancel.abort();
329 | }
330 | setTimeout(abort, 200);
331 | })
332 |
333 | it('works if request times out before being aborted', function(done) {
334 | var cancel = new AbortController();
335 | var start = new Date();
336 |
337 | needle.get(url, { signal: cancel.signal, response_timeout: 200, open_timeout: 200, read_timeout: 200 }, function(err, res) {
338 | var timediff = (new Date() - start);
339 |
340 | should.not.exist(res);
341 | err.code.should.equal('ECONNRESET');
342 | timediff.should.be.within(200, 250);
343 | });
344 |
345 | function abort() {
346 | cancel.signal.aborted.should.equal(false);
347 | done();
348 | }
349 | setTimeout(abort, 500);
350 | })
351 |
352 | it('works if setting default signal aborts all requests', function(done) {
353 | var cancel = new AbortController();
354 |
355 | needle.defaults({signal: cancel.signal});
356 |
357 | var start = new Date();
358 | var count = 0;
359 | function cb(err, res) {
360 | var timediff = (new Date() - start);
361 |
362 | should.not.exist(res);
363 | if (node_major_ver <= 16)
364 | err.code.should.equal('ECONNRESET');
365 | if (node_major_ver > 16)
366 | err.code.should.equal('ABORT_ERR');
367 | cancel.signal.aborted.should.equal(true);
368 | timediff.should.be.within(200, 250);
369 |
370 | if ( count++ === 2 ) done();
371 | }
372 |
373 | needle.get(url, { timeout: 300 }, cb);
374 | needle.get(url, { timeout: 350 }, cb);
375 | needle.get(url, { timeout: 400 }, cb);
376 |
377 | function abort() {
378 | cancel.abort();
379 | }
380 | setTimeout(abort, 200);
381 | })
382 |
383 | it('does not work if invalid signal passed', function(done) {
384 | try {
385 | needle.get(url, { signal: 'invalid signal' }, function(err, res) {
386 | done(new Error('A bad option error expected to be thrown'));
387 | });
388 | } catch(e) {
389 | e.should.be.a.TypeError;
390 | done();
391 | }
392 | })
393 |
394 | it('does not work if invalid signal set by default', function(done) {
395 | try {
396 | needle.defaults({signal: new Error(), timeout: 1200});
397 | done(new Error('A bad option error expected to be thrown'));
398 | } catch(e) {
399 | e.should.be.a.TypeError;
400 | done();
401 | }
402 | })
403 |
404 | })
405 | }
406 | })
407 |
--------------------------------------------------------------------------------
/test/files/Appalachia.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomas/needle/9454d7bdc94b5a9a9c8f60eafefb6408f83a9a37/test/files/Appalachia.html
--------------------------------------------------------------------------------
/test/files/tomcat_charset.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomas/needle/9454d7bdc94b5a9a9c8f60eafefb6408f83a9a37/test/files/tomcat_charset.html
--------------------------------------------------------------------------------
/test/headers_spec.js:
--------------------------------------------------------------------------------
1 | var http = require('http'),
2 | helpers = require('./helpers'),
3 | should = require('should');
4 |
5 | var port = 54321;
6 |
7 |
8 | describe('request headers', function() {
9 |
10 | var needle,
11 | server,
12 | existing_sockets,
13 | original_defaultMaxSockets;
14 |
15 | before(function(done) {
16 | setTimeout(function() {
17 | existing_sockets = get_active_sockets().length;
18 | server = helpers.server({ port: port }, done);
19 | }, 100);
20 | })
21 |
22 | after(function(done) {
23 | server.close(done);
24 | })
25 |
26 | function send_request(opts, cb) {
27 | needle.get('http://localhost:' + port, opts, cb);
28 | }
29 |
30 | function get_active_sockets() {
31 | var handles = process._getActiveHandles();
32 |
33 | return handles.filter(function(el) {
34 | if (el.constructor.name.toString() == 'Socket') {
35 | return el.destroyed !== true;
36 | }
37 | })
38 | }
39 |
40 | describe('old node versions (<0.11.4) with persistent keep-alive connections', function() {
41 |
42 | // emulate old node behaviour
43 | before(function() {
44 | delete require.cache[require.resolve('..')] // in case it was already loaded
45 | original_defaultMaxSockets = http.Agent.defaultMaxSockets;
46 | http.Agent.defaultMaxSockets = 5;
47 | needle = require('..');
48 | })
49 |
50 | after(function() {
51 | http.Agent.defaultMaxSockets = original_defaultMaxSockets;
52 | delete require.cache[require.resolve('..')]
53 | })
54 |
55 | describe('default options', function() {
56 |
57 | it('sends a Connection: close header', function(done) {
58 | send_request({}, function(err, resp) {
59 | resp.body.headers['connection'].should.eql('close');
60 | done();
61 | })
62 | })
63 |
64 | it('no open sockets remain after request', function(done) {
65 | send_request({}, function(err, resp) {
66 | setTimeout(function() {
67 | get_active_sockets().length.should.eql(existing_sockets);
68 | done();
69 | }, 10)
70 | });
71 | })
72 |
73 | })
74 |
75 | describe('passing connection: close', function() {
76 |
77 | it('sends a Connection: close header', function(done) {
78 | send_request({ connection: 'close' }, function(err, resp) {
79 | resp.body.headers['connection'].should.eql('close');
80 | done();
81 | })
82 | })
83 |
84 | it('no open sockets remain after request', function(done) {
85 | send_request({ connection: 'close' }, function(err, resp) {
86 | setTimeout(function() {
87 | get_active_sockets().length.should.eql(existing_sockets);
88 | done();
89 | }, 10)
90 | });
91 | })
92 |
93 | })
94 |
95 | describe('passing connection: keep-alive', function() {
96 |
97 | it('sends a Connection: keep-alive header (using options.headers.connection)', function(done) {
98 | send_request({ headers: { connection: 'keep-alive' }}, function(err, resp) {
99 | resp.body.headers['connection'].should.eql('keep-alive');
100 | done();
101 | })
102 | })
103 |
104 | it('sends a Connection: keep-alive header (using options.connection)', function(done) {
105 | send_request({ connection: 'keep-alive' }, function(err, resp) {
106 | resp.body.headers['connection'].should.eql('keep-alive');
107 | done();
108 | })
109 | })
110 |
111 | it('one open socket remain after request', function(done) {
112 | send_request({ connection: 'keep-alive' }, function(err, resp) {
113 | get_active_sockets().length.should.eql(existing_sockets + 1);
114 | done();
115 | });
116 | })
117 |
118 | })
119 |
120 | })
121 |
122 | describe('new node versions with smarter connection disposing', function() {
123 |
124 | before(function() {
125 | delete require.cache[require.resolve('..')]
126 | original_defaultMaxSockets = http.Agent.defaultMaxSockets;
127 | http.Agent.defaultMaxSockets = Infinity;
128 | needle = require('..');
129 | })
130 |
131 | after(function() {
132 | http.Agent.defaultMaxSockets = original_defaultMaxSockets;
133 | delete require.cache[require.resolve('..')]
134 | })
135 |
136 | describe('default options', function() {
137 |
138 | var node_major_ver = process.version.split('.')[0].replace('v', '');
139 |
140 | if (parseInt(node_major_ver) >= 4) {
141 |
142 | it('sets Connection header to close (> v4)', function(done) {
143 | send_request({}, function(err, resp) {
144 | resp.body.headers['connection'].should.eql('close');
145 | done()
146 | })
147 | })
148 |
149 | } else {
150 |
151 | it('sets Connection header to keep-alive (< v4)', function(done) {
152 | send_request({}, function(err, resp) {
153 | resp.body.headers['connection'].should.eql('keep-alive');
154 | done();
155 | })
156 | })
157 |
158 | }
159 |
160 | if (parseInt(node_major_ver) >= 14) {
161 |
162 | // TODO: figure out why this happens
163 | it('two open sockets remains after request (>= v14)', function(done) {
164 | send_request({}, function(err, resp) {
165 | get_active_sockets().length.should.eql(existing_sockets + 2);
166 | done();
167 | });
168 | })
169 |
170 | } else if (parseInt(node_major_ver) >= 8 || parseInt(node_major_ver) == 0) {
171 |
172 | it('one open socket remains after request (> v8 && v0.10)', function(done) {
173 | send_request({}, function(err, resp) {
174 | get_active_sockets().length.should.eql(existing_sockets + 1);
175 | done();
176 | });
177 | })
178 |
179 | } else {
180 |
181 | it('no open sockets remain after request (> v0.10 && < v8)', function(done) {
182 | send_request({}, function(err, resp) {
183 | get_active_sockets().length.should.eql(existing_sockets);
184 | done();
185 | });
186 | })
187 |
188 | }
189 |
190 | })
191 |
192 | describe('passing connection: close', function() {
193 |
194 | it('sends a Connection: close header', function(done) {
195 | send_request({ connection: 'close' }, function(err, resp) {
196 | resp.body.headers['connection'].should.eql('close');
197 | done();
198 | })
199 | })
200 |
201 | it('no open sockets remain after request', function(done) {
202 | send_request({ connection: 'close' }, function(err, resp) {
203 | setTimeout(function() {
204 | get_active_sockets().length.should.eql(existing_sockets);
205 | done();
206 | }, 10);
207 | });
208 | })
209 |
210 | })
211 |
212 | describe('passing connection: keep-alive', function() {
213 |
214 | it('sends a Connection: keep-alive header (using options.headers.connection)', function(done) {
215 | send_request({ headers: { connection: 'keep-alive' }}, function(err, resp) {
216 | resp.body.headers['connection'].should.eql('keep-alive');
217 | done();
218 | })
219 | })
220 |
221 | it('sends a Connection: keep-alive header (using options.connection)', function(done) {
222 | send_request({ connection: 'keep-alive' }, function(err, resp) {
223 | resp.body.headers['connection'].should.eql('keep-alive');
224 | done();
225 | })
226 | })
227 |
228 | it('one open socket remain after request', function(done) {
229 | send_request({ connection: 'keep-alive' }, function(err, resp) {
230 | get_active_sockets().length.should.eql(existing_sockets + 1);
231 | done();
232 | });
233 | })
234 |
235 | })
236 |
237 | })
238 |
239 | describe('using shared keep-alive agent', function() {
240 |
241 | before(function() {
242 | needle.defaults({ agent: http.Agent({ keepAlive: true }) })
243 | })
244 |
245 | after(function() {
246 | needle.defaults().agent.destroy(); // close existing connections
247 | needle.defaults({ agent: null }); // and reset default value
248 | })
249 |
250 | describe('default options', function() {
251 |
252 | it('sends a Connection: keep-alive header', function(done) {
253 | send_request({}, function(err, resp) {
254 | resp.body.headers['connection'].should.eql('keep-alive');
255 | done();
256 | })
257 | })
258 |
259 | it('one open socket remain after request', function(done) {
260 | send_request({ connection: 'keep-alive' }, function(err, resp) {
261 | setTimeout(function() {
262 | get_active_sockets().length.should.eql(existing_sockets + 1);
263 | done();
264 | }, 10);
265 | });
266 | })
267 |
268 | })
269 |
270 | describe('passing connection: close', function() {
271 |
272 | it('sends a Connection: close header', function(done) {
273 | send_request({ connection: 'close' }, function(err, resp) {
274 | resp.body.headers['connection'].should.eql('close');
275 | done();
276 | })
277 | })
278 |
279 | it('no open sockets remain after request', function(done) {
280 | send_request({ connection: 'close' }, function(err, resp) {
281 | setTimeout(function() {
282 | get_active_sockets().length.should.eql(existing_sockets);
283 | done();
284 | }, 10)
285 | });
286 | })
287 |
288 | })
289 |
290 | describe('passing connection: keep-alive', function() {
291 |
292 | it('sends a Connection: keep-alive header (using options.headers.connection)', function(done) {
293 | send_request({ headers: { connection: 'keep-alive' }}, function(err, resp) {
294 | resp.body.headers['connection'].should.eql('keep-alive');
295 | done();
296 | })
297 | })
298 |
299 | it('sends a Connection: keep-alive header (using options.connection)', function(done) {
300 | send_request({ connection: 'keep-alive' }, function(err, resp) {
301 | resp.body.headers['connection'].should.eql('keep-alive');
302 | done();
303 | })
304 | })
305 |
306 | it('one open socket remain after request', function(done) {
307 | send_request({ connection: 'keep-alive' }, function(err, resp) {
308 | setTimeout(function() {
309 | get_active_sockets().length.should.eql(existing_sockets + 1);
310 | done();
311 | }, 10);
312 | })
313 | })
314 |
315 | })
316 |
317 |
318 | })
319 |
320 | })
321 |
--------------------------------------------------------------------------------
/test/helpers.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 |
3 | var protocols = {
4 | http : require('http'),
5 | https : require('https')
6 | }
7 |
8 | var keys = {
9 | cert : fs.readFileSync(__dirname + '/keys/ssl.cert'),
10 | key : fs.readFileSync(__dirname + '/keys/ssl.key')
11 | }
12 |
13 | var helpers = {};
14 |
15 | helpers.server = function(opts, cb) {
16 |
17 | var defaults = {
18 | code : 200,
19 | headers : {'Content-Type': 'application/json'}
20 | }
21 |
22 | var mirror_response = function(req) {
23 | return JSON.stringify({
24 | headers: req.headers,
25 | body: req.body
26 | })
27 | }
28 |
29 | var get = function(what) {
30 | if (!opts[what])
31 | return defaults[what];
32 |
33 | if (typeof opts[what] == 'function')
34 | return opts[what](); // set them at runtime
35 | else
36 | return opts[what];
37 | }
38 |
39 | var finish = function(req, res) {
40 | if (opts.handler) return opts.handler(req, res);
41 |
42 | res.writeHead(get('code'), get('headers'));
43 | res.end(opts.response || mirror_response(req));
44 | }
45 |
46 | var handler = function(req, res) {
47 |
48 | req.setEncoding('utf8'); // get as string
49 | req.body = '';
50 | req.on('data', function(str) { req.body += str })
51 | req.socket.on('error', function(e) {
52 | // res.writeHead(500, {'Content-Type': 'text/plain'});
53 | // res.end('Error: ' + e.message);
54 | })
55 |
56 | setTimeout(function(){
57 | finish(req, res);
58 | }, opts.wait || 0);
59 |
60 | };
61 |
62 | var protocol = opts.protocol || 'http';
63 | var server;
64 |
65 | if (protocol == 'https')
66 | server = protocols[protocol].createServer(keys, handler);
67 | else
68 | server = protocols[protocol].createServer(handler);
69 |
70 | server.listen(opts.port, cb);
71 | return server;
72 | }
73 |
74 | module.exports = helpers;
--------------------------------------------------------------------------------
/test/long_string_spec.js:
--------------------------------------------------------------------------------
1 | var needle = require('../'),
2 | should = require('should');
3 |
4 | describe('when posting a very long string', function() {
5 |
6 | this.timeout(20000);
7 |
8 | function get_string(length) {
9 | var str = '';
10 | for (var i = 0; i < length; i++) {
11 | str += 'x';
12 | }
13 | return str;
14 | }
15 |
16 | var major_version = process.version.split('.')[0];
17 |
18 | it("shouldn't throw an EPIPE error out of nowhere", function(done) {
19 |
20 | // for some reason this test fails in Github Actions with Node v8.x
21 | // although in my Linux box passes without issues
22 | if (process.env.CI && (major_version == 'v8' || major_version == 'v6')) {
23 | return done();
24 | }
25 |
26 | var error;
27 |
28 | function finished() {
29 | setTimeout(function() {
30 | should.not.exist(error);
31 | done();
32 | }, 300);
33 | }
34 |
35 | try {
36 | needle.post('https://google.com', { data: get_string(Math.pow(2, 20)) }, finished)
37 | } catch(e) {
38 | error = e;
39 | }
40 |
41 | })
42 |
43 | })
44 |
--------------------------------------------------------------------------------
/test/mimetype.js:
--------------------------------------------------------------------------------
1 | var should = require('should'),
2 | needle = require('./../'),
3 | helpers = require('./helpers');
4 |
5 | describe('receiving json and xml content as string', function() {
6 |
7 | this.timeout(5000);
8 |
9 | ["text/plain", "application/json", "application/ld+json", "application/xml", "image/svg+xml"].forEach(function(mimetype, offset){
10 |
11 | describe('Given content-type: "'+mimetype+'"', function () {
12 |
13 | var server, port = 54330+offset;
14 |
15 | before(function(done) {
16 | server = helpers.server({
17 | port: port,
18 | response: 'content',
19 | headers: { 'Content-Type': mimetype }
20 | }, done);
21 | })
22 |
23 | after(function(done) {
24 | server.close(done)
25 | })
26 |
27 | describe('with parse = false', function () {
28 | it('delivers by default as string', function (done) {
29 |
30 | needle.get('http://localhost:' + port, { parse: false }, function (err, resp) {
31 |
32 | resp.body.should.be.a.String;
33 | (typeof resp.body).should.eql('string')
34 | done();
35 | })
36 |
37 | })
38 |
39 | })
40 |
41 | })
42 |
43 | });
44 |
45 | ["application/octet-stream", "image/png"].forEach(function(mimetype, offset){
46 |
47 | describe('Given content-type: "'+mimetype+'"', function () {
48 |
49 | var server, port = 54340+offset;
50 |
51 | before(function(done) {
52 | server = helpers.server({
53 | port: port,
54 | response: 'content',
55 | headers: { 'Content-Type': mimetype }
56 | }, done);
57 | })
58 |
59 | after(function(done) {
60 | server.close(done)
61 | })
62 |
63 | describe('with parse = false', function () {
64 | it('delivers by default as Buffer', function (done) {
65 |
66 | needle.get('http://localhost:' + port, { parse: false }, function (err, resp) {
67 |
68 | resp.body.should.be.a.Buffer;
69 | (resp.body instanceof Buffer).should.eql(true)
70 | done();
71 | })
72 |
73 | })
74 |
75 | })
76 |
77 | })
78 |
79 | })
80 |
81 | })
82 |
--------------------------------------------------------------------------------
/test/output_spec.js:
--------------------------------------------------------------------------------
1 | // this lets us run tests in ancient node versions (v0.10.x)
2 | if (process.version.split('.')[0] == 'v0' && !Buffer.from) {
3 | Buffer.from = function(args) {
4 | return new Buffer(args);
5 | }
6 | }
7 |
8 | var should = require('should'),
9 | needle = require('./../'),
10 | http = require('http'),
11 | sinon = require('sinon'),
12 | stream = require('stream'),
13 | fs = require('fs'),
14 | port = 11111,
15 | server;
16 |
17 | describe('with output option', function() {
18 |
19 | var server, handler, file = '/tmp/foobar.out';
20 |
21 | function send_request_cb(where, cb) {
22 | var url = 'http://localhost:' + port + '/whatever.file';
23 | return needle.get(url, { output: where }, cb);
24 | }
25 |
26 | function send_request_stream(where, cb) {
27 | var url = 'http://localhost:' + port + '/whatever.file';
28 | var stream = needle.get(url, { output: where });
29 | stream.on('end', cb);
30 | }
31 |
32 | // this will only work in UNICES
33 | function get_open_file_descriptors() {
34 | var list = fs.readdirSync('/proc/self/fd');
35 | return list.length;
36 | }
37 |
38 | var send_request = send_request_cb;
39 |
40 | before(function(){
41 | server = http.createServer(function(req, res) {
42 | handler(req, res);
43 | }).listen(port);
44 | });
45 |
46 | after(function() {
47 | server.close();
48 | })
49 |
50 | beforeEach(function() {
51 | try { fs.unlinkSync(file) } catch(e) { };
52 | })
53 |
54 | describe('and a 404 response', function() {
55 |
56 | before(function() {
57 | handler = function(req, res) {
58 | res.writeHead(404, {'Content-Type': 'text/plain' });
59 | res.end();
60 | }
61 | })
62 |
63 | it('doesnt attempt to write a file', function(done) {
64 | var spy = sinon.spy(fs, 'createWriteStream');
65 | send_request(file, function(err, resp) {
66 | resp.statusCode.should.eql(404);
67 | spy.called.should.eql(false);
68 | spy.restore();
69 | done();
70 | })
71 | })
72 |
73 | it('doesnt actually write a file', function(done) {
74 | send_request(file, function(err, resp) {
75 | resp.statusCode.should.eql(404);
76 | fs.existsSync(file).should.eql(false);
77 | done();
78 | })
79 | })
80 |
81 | })
82 |
83 | describe('and a 200 response', function() {
84 |
85 | describe('for an empty response', function() {
86 |
87 | before(function() {
88 | handler = function(req, res) {
89 | res.writeHead(200, { 'Content-Type': 'text/plain' });
90 | res.end();
91 | }
92 | })
93 |
94 | it('uses a writableStream', function(done) {
95 | var spy = sinon.spy(fs, 'createWriteStream');
96 | send_request(file, function(err, resp) {
97 | resp.statusCode.should.eql(200);
98 | spy.called.should.eql(true);
99 | spy.restore();
100 | done();
101 | })
102 | })
103 |
104 | it('writes a file', function(done) {
105 | fs.existsSync(file).should.eql(false);
106 | send_request(file, function(err, resp) {
107 | fs.existsSync(file).should.eql(true);
108 | done();
109 | })
110 | })
111 |
112 | it('file is zero bytes in length', function(done) {
113 | send_request(file, function(err, resp) {
114 | fs.statSync(file).size.should.equal(0);
115 | done();
116 | })
117 | })
118 |
119 | if (process.platform == 'linux') {
120 | it('closes the file descriptor', function(done) {
121 | var open_descriptors = get_open_file_descriptors();
122 | send_request(file + Math.random(), function(err, resp) {
123 | var current_descriptors = get_open_file_descriptors();
124 | open_descriptors.should.eql(current_descriptors);
125 | done()
126 | })
127 | })
128 | }
129 |
130 | })
131 |
132 | describe('for a JSON response', function() {
133 |
134 | before(function() {
135 | handler = function(req, res) {
136 | res.writeHead(200, { 'Content-Type': 'application/javascript' });
137 | res.end(JSON.stringify({foo: 'bar'}));
138 | }
139 | })
140 |
141 | it('uses a writableStream', function(done) {
142 | var spy = sinon.spy(fs, 'createWriteStream');
143 | send_request(file, function(err, resp) {
144 | resp.statusCode.should.eql(200);
145 | spy.called.should.eql(true);
146 | spy.restore();
147 | done();
148 | })
149 | })
150 |
151 | it('writes a file', function(done) {
152 | fs.existsSync(file).should.eql(false);
153 | send_request(file, function(err, resp) {
154 | fs.existsSync(file).should.eql(true);
155 | done();
156 | })
157 | })
158 |
159 | it('file size equals response length', function(done) {
160 | send_request(file, function(err, resp) {
161 | fs.statSync(file).size.should.equal(resp.bytes);
162 | done();
163 | })
164 | })
165 |
166 | it('response pipeline is honoured (JSON is decoded by default)', function(done) {
167 | send_request_stream(file, function(err, resp) {
168 | // we need to wait a bit since writing to config.output
169 | // happens independently of needle's callback logic.
170 | setTimeout(function() {
171 | fs.readFileSync(file).toString().should.eql('{\"foo\":\"bar\"}');
172 | done();
173 | }, 20);
174 | })
175 | })
176 |
177 | if (process.platform == 'linux') {
178 | it('closes the file descriptor', function(done) {
179 | var open_descriptors = get_open_file_descriptors();
180 | send_request(file + Math.random(), function(err, resp) {
181 | var current_descriptors = get_open_file_descriptors();
182 | open_descriptors.should.eql(current_descriptors);
183 | done()
184 | })
185 | })
186 | }
187 |
188 | })
189 |
190 | describe('for a binary file', function() {
191 |
192 | var pixel = Buffer.from("base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs", "base64");
193 |
194 | before(function() {
195 | handler = function(req, res) {
196 | res.writeHead(200, { 'Content-Type': 'application/octet-stream', 'Transfer-Encoding': 'chunked' });
197 | res.write(pixel.slice(0, 10));
198 | res.write(pixel.slice(10, 20));
199 | res.write(pixel.slice(20, 30));
200 | res.write(pixel.slice(30));
201 | res.end();
202 | }
203 | })
204 |
205 | it('uses a writableStream', function(done) {
206 | var spy = sinon.spy(fs, 'createWriteStream');
207 | send_request(file, function(err, resp) {
208 | resp.statusCode.should.eql(200);
209 | spy.called.should.eql(true);
210 | spy.restore();
211 | done();
212 | })
213 | })
214 |
215 | it('writes a file', function(done) {
216 | fs.existsSync(file).should.eql(false);
217 | send_request(file, function(err, resp) {
218 | fs.existsSync(file).should.eql(true);
219 | done();
220 | })
221 | })
222 |
223 | it('file size equals response length', function(done) {
224 | send_request(file, function(err, resp) {
225 | fs.statSync(file).size.should.equal(resp.bytes);
226 | done();
227 | })
228 | })
229 |
230 | it('file is equal to original buffer', function(done) {
231 | send_request(file, function(err, resp) {
232 | // we need to wait a bit since writing to config.output
233 | // happens independently of needle's callback logic.
234 | setTimeout(function() {
235 | fs.readFileSync(file).should.eql(pixel);
236 | done();
237 | }, 20);
238 | })
239 | })
240 |
241 | it('returns the data in resp.body too', function(done) {
242 | send_request(file, function(err, resp) {
243 | resp.body.should.eql(pixel);
244 | done();
245 | })
246 | })
247 |
248 | if (process.platform == 'linux') {
249 | it('closes the file descriptor', function(done) {
250 | var open_descriptors = get_open_file_descriptors();
251 | send_request(file + Math.random(), function(err, resp) {
252 | var current_descriptors = get_open_file_descriptors();
253 | open_descriptors.should.eql(current_descriptors);
254 | done()
255 | })
256 | })
257 | }
258 |
259 | })
260 |
261 | })
262 |
263 | })
264 |
--------------------------------------------------------------------------------
/test/parsing_spec.js:
--------------------------------------------------------------------------------
1 | var should = require('should'),
2 | needle = require('./../'),
3 | http = require('http'),
4 | port = 11111,
5 | server;
6 |
7 | describe('parsing', function(){
8 |
9 | describe('when response is an JSON string', function(){
10 |
11 | var json_string = '{"foo":"bar"}';
12 |
13 | before(function(done){
14 | server = http.createServer(function(req, res) {
15 | res.setHeader('Content-Type', 'application/json');
16 | res.end(json_string);
17 | }).listen(port, done);
18 | });
19 |
20 | after(function(done){
21 | server.close(done);
22 | })
23 |
24 | describe('and parse option is not passed', function() {
25 |
26 | describe('with default parse_response', function() {
27 |
28 | before(function() {
29 | needle.defaults().parse_response.should.eql('all')
30 | })
31 |
32 | it('should return object', function(done){
33 | needle.get('localhost:' + port, function(err, response, body){
34 | should.ifError(err);
35 | body.should.have.property('foo', 'bar');
36 | done();
37 | })
38 | })
39 |
40 | })
41 |
42 | describe('and default parse_response is set to false', function() {
43 |
44 | it('does NOT return object when disabled using .defaults', function(done){
45 | needle.defaults({ parse_response: false })
46 |
47 | needle.get('localhost:' + port, function(err, response, body) {
48 | should.not.exist(err);
49 | body.should.be.an.instanceof(String)
50 | body.toString().should.eql('{"foo":"bar"}');
51 |
52 | needle.defaults({ parse_response: 'all' });
53 | done();
54 | })
55 | })
56 |
57 |
58 | })
59 |
60 | })
61 |
62 | describe('and parse option is true', function() {
63 |
64 | describe('and JSON is valid', function() {
65 |
66 | it('should return object', function(done) {
67 | needle.get('localhost:' + port, { parse: true }, function(err, response, body){
68 | should.not.exist(err);
69 | body.should.have.property('foo', 'bar')
70 | done();
71 | })
72 | })
73 |
74 | it('should have a .parser = json property', function(done) {
75 | needle.get('localhost:' + port, { parse: true }, function(err, resp) {
76 | should.not.exist(err);
77 | resp.parser.should.eql('json');
78 | done();
79 | })
80 | })
81 |
82 | });
83 |
84 | describe('and response is empty', function() {
85 |
86 | var old_json_string;
87 |
88 | before(function() {
89 | old_json_string = json_string;
90 | json_string = "";
91 | });
92 |
93 | after(function() {
94 | json_string = old_json_string;
95 | });
96 |
97 | it('should return an empty string', function(done) {
98 | needle.get('localhost:' + port, { parse: true }, function(err, resp) {
99 | should.not.exist(err);
100 | resp.body.should.equal('');
101 | done();
102 | })
103 | })
104 |
105 | })
106 |
107 | describe('and JSON is invalid', function() {
108 |
109 | var old_json_string;
110 |
111 | before(function() {
112 | old_json_string = json_string;
113 | json_string = "this is not going to work";
114 | });
115 |
116 | after(function() {
117 | json_string = old_json_string;
118 | });
119 |
120 | it('does not throw', function(done) {
121 | (function(){
122 | needle.get('localhost:' + port, { parse: true }, done);
123 | }).should.not.throw();
124 | });
125 |
126 | it('does NOT return object', function(done) {
127 | needle.get('localhost:' + port, { parse: true }, function(err, response, body) {
128 | should.not.exist(err);
129 | body.should.be.a.String;
130 | body.toString().should.eql('this is not going to work');
131 | done();
132 | })
133 | })
134 |
135 | });
136 |
137 | })
138 |
139 | describe('and parse option is false', function() {
140 |
141 | it('does NOT return object', function(done){
142 | needle.get('localhost:' + port, { parse: false }, function(err, response, body) {
143 | should.not.exist(err);
144 | body.should.be.an.instanceof(String)
145 | body.toString().should.eql('{"foo":"bar"}');
146 | done();
147 | })
148 | })
149 |
150 | it('should NOT have a .parser = json property', function(done) {
151 | needle.get('localhost:' + port, { parse: false }, function(err, resp) {
152 | should.not.exist(err);
153 | should.not.exist(resp.parser);
154 | done();
155 | })
156 | })
157 |
158 | })
159 |
160 | describe('and parse option is "xml"', function() {
161 |
162 | it('does NOT return object', function(done){
163 | needle.get('localhost:' + port, { parse: 'xml' }, function(err, response, body) {
164 | should.not.exist(err);
165 | body.should.be.an.instanceof(String)
166 | body.toString().should.eql('{"foo":"bar"}');
167 | done();
168 | })
169 | })
170 |
171 | it('should NOT have a .parser = json property', function(done) {
172 | needle.get('localhost:' + port, { parse: 'xml' }, function(err, resp) {
173 | should.not.exist(err);
174 | should.not.exist(resp.parser);
175 | done();
176 | })
177 | })
178 |
179 | })
180 |
181 | });
182 |
183 | describe('when response is JSON \'false\'', function(){
184 |
185 | var json_string = 'false';
186 |
187 | before(function(done){
188 | server = http.createServer(function(req, res) {
189 | res.setHeader('Content-Type', 'application/json');
190 | res.end(json_string);
191 | }).listen(port, done);
192 | });
193 |
194 | after(function(done){
195 | server.close(done);
196 | })
197 |
198 | describe('and parse option is not passed', function() {
199 |
200 | it('should return object', function(done){
201 | needle.get('localhost:' + port, function(err, response, body){
202 | should.ifError(err);
203 | body.should.equal(false);
204 | done();
205 | })
206 | })
207 |
208 | })
209 |
210 | describe('and parse option is true', function() {
211 |
212 | describe('and JSON is valid', function() {
213 |
214 | it('should return object', function(done){
215 | needle.get('localhost:' + port, { parse: true }, function(err, response, body){
216 | should.not.exist(err);
217 | body.should.equal(false)
218 | done();
219 | })
220 | })
221 |
222 | });
223 |
224 | describe('and response is empty', function() {
225 |
226 | var old_json_string;
227 |
228 | before(function() {
229 | old_json_string = json_string;
230 | json_string = "";
231 | });
232 |
233 | after(function() {
234 | json_string = old_json_string;
235 | });
236 |
237 | it('should return an empty string', function(done) {
238 | needle.get('localhost:' + port, { parse: true }, function(err, resp) {
239 | should.not.exist(err);
240 | resp.body.should.equal('');
241 | done();
242 | })
243 | })
244 |
245 | })
246 |
247 | describe('and JSON is invalid', function() {
248 |
249 | var old_json_string;
250 |
251 | before(function() {
252 | old_json_string = json_string;
253 | json_string = "this is not going to work";
254 | });
255 |
256 | after(function() {
257 | json_string = old_json_string;
258 | });
259 |
260 | it('does not throw', function(done) {
261 | (function(){
262 | needle.get('localhost:' + port, { parse: true }, done);
263 | }).should.not.throw();
264 | });
265 |
266 | it('does NOT return object', function(done) {
267 | needle.get('localhost:' + port, { parse: true }, function(err, response, body) {
268 | should.not.exist(err);
269 | body.should.be.a.String;
270 | body.toString().should.eql('this is not going to work');
271 | done();
272 | })
273 | })
274 |
275 | });
276 |
277 | })
278 |
279 | describe('and parse option is false', function() {
280 |
281 | it('does NOT return object', function(done){
282 | needle.get('localhost:' + port, { parse: false }, function(err, response, body) {
283 | should.not.exist(err);
284 | body.should.be.an.instanceof(String)
285 | body.toString().should.eql('false');
286 | done();
287 | })
288 | })
289 |
290 | })
291 |
292 | describe('and parse option is "xml"', function() {
293 |
294 | it('does NOT return object', function(done){
295 | needle.get('localhost:' + port, { parse: 'xml' }, function(err, response, body) {
296 | should.not.exist(err);
297 | body.should.be.an.instanceof(String)
298 | body.toString().should.eql('false');
299 | done();
300 | })
301 | })
302 |
303 | })
304 |
305 |
306 | });
307 |
308 | describe('when response is an invalid XML string', function(){
309 |
310 | before(function(done){
311 | server = http.createServer(function(req, res) {
312 | res.writeHeader(200, {'Content-Type': 'application/xml'})
313 | res.end("")
314 | }).listen(port, done);
315 | });
316 |
317 | after(function(done){
318 | server.close(done);
319 | })
320 |
321 | describe('and parse_response is true', function(){
322 |
323 | it('should return original string', function(done) {
324 | needle.get('localhost:' + port, { parse_response: true }, function(err, response, body) {
325 | should.not.exist(err);
326 | body.should.eql('')
327 | should.not.exist(body.name);
328 | done();
329 | })
330 | })
331 |
332 | it('should not have a .parser = xml property', function(done) {
333 | needle.get('localhost:' + port, { parse_response: true }, function(err, resp) {
334 | should.not.exist(err);
335 | should.not.exist(resp.parser);
336 | done();
337 | })
338 | })
339 |
340 | })
341 |
342 | describe('and parse response is false', function(){
343 |
344 | it('should return valid object', function(done) {
345 | needle.get('localhost:' + port, { parse_response: false }, function(err, response, body){
346 | should.not.exist(err);
347 | body.toString().should.eql('')
348 | done();
349 | })
350 | })
351 |
352 | it('should not have a .parser property', function(done) {
353 | needle.get('localhost:' + port, { parse_response: false }, function(err, resp) {
354 | should.not.exist(err);
355 | should.not.exist(resp.parser)
356 | done();
357 | })
358 | })
359 |
360 | })
361 |
362 | })
363 |
364 | describe('when response is a valid XML string', function(){
365 |
366 | before(function(done) {
367 | server = http.createServer(function(req, res) {
368 | res.writeHeader(200, {'Content-Type': 'application/xml'})
369 | res.end("hello
")
370 | }).listen(port, done);
371 | });
372 |
373 | after(function(done) {
374 | server.close(done);
375 | })
376 |
377 | describe('and parse_response is true', function(){
378 |
379 | it('should return valid object', function(done) {
380 | needle.get('localhost:' + port, { parse_response: true }, function(err, response, body) {
381 | should.not.exist(err);
382 | body.name.should.eql('post')
383 | body.children[0].name.should.eql('p')
384 | body.children[0].value.should.eql('hello')
385 |
386 | body.children[1].name.should.eql('p')
387 | body.children[1].value.should.eql('world')
388 | done();
389 | })
390 | })
391 |
392 | it('should have a .parser = xml property', function(done) {
393 | needle.get('localhost:' + port, { parse_response: true }, function(err, resp) {
394 | should.not.exist(err);
395 | resp.parser.should.eql('xml');
396 | done();
397 | })
398 | })
399 |
400 | })
401 |
402 | describe('and parse response is false', function(){
403 |
404 | it('should return valid object', function(done) {
405 | needle.get('localhost:' + port, { parse_response: false }, function(err, response, body){
406 | should.not.exist(err);
407 | body.toString().should.eql('hello
')
408 | done();
409 | })
410 | })
411 |
412 | it('should not have a .parser property', function(done) {
413 | needle.get('localhost:' + port, { parse_response: false }, function(err, resp) {
414 | should.not.exist(err);
415 | should.not.exist(resp.parser)
416 | done();
417 | })
418 | })
419 |
420 | })
421 |
422 | })
423 |
424 | describe('valid XML, using xml2js', function() {
425 |
426 | var parsers, origParser;
427 |
428 | before(function(done) {
429 | var xml2js = require('xml2js')
430 | parsers = require('../lib/parsers');
431 | origParser = parsers['application/xml'];
432 |
433 | var customParser = require('xml2js').parseString;
434 | parsers.use('xml2js', ['application/xml'], function(buff, cb) {
435 | var opts = { explicitRoot: true, explicitArray: false };
436 | customParser(buff, opts, cb);
437 | })
438 |
439 | server = http.createServer(function(req, res) {
440 | res.writeHeader(200, {'Content-Type': 'application/xml'})
441 | res.end("hello
world
")
442 | }).listen(port, done);
443 | });
444 |
445 | after(function(done) {
446 | parsers['application/xml'] = origParser;
447 | server.close(done);
448 | })
449 |
450 | describe('and parse_response is true', function(){
451 |
452 | it('should return valid object', function(done) {
453 | needle.get('localhost:' + port, { parse_response: true }, function(err, response, body) {
454 | should.not.exist(err);
455 | body.should.eql({ post: { p: ['hello', 'world' ]}})
456 | done();
457 | })
458 | })
459 |
460 | it('should have a .parser = xml property', function(done) {
461 | needle.get('localhost:' + port, { parse_response: true }, function(err, resp) {
462 | should.not.exist(err);
463 | resp.parser.should.eql('xml2js');
464 | done();
465 | })
466 | })
467 |
468 | })
469 |
470 | describe('and parse response is false', function(){
471 |
472 | it('should return valid object', function(done) {
473 | needle.get('localhost:' + port, { parse_response: false }, function(err, response, body){
474 | should.not.exist(err);
475 | body.toString().should.eql('hello
world
')
476 | done();
477 | })
478 | })
479 |
480 | it('should not have a .parser property', function(done) {
481 | needle.get('localhost:' + port, { parse_response: false }, function(err, resp) {
482 | should.not.exist(err);
483 | should.not.exist(resp.parser)
484 | done();
485 | })
486 | })
487 |
488 | })
489 |
490 | })
491 |
492 | describe('when response is a JSON API flavored JSON string', function () {
493 |
494 | var json_string = '{"data":[{"type":"articles","id":"1","attributes":{"title":"Needle","body":"The leanest and most handsome HTTP client in the Nodelands."}}],"included":[{"type":"people","id":"42","attributes":{"name":"Tomás"}}]}';
495 |
496 | before(function(done){
497 | server = http.createServer(function(req, res) {
498 | res.setHeader('Content-Type', 'application/vnd.api+json');
499 | res.end(json_string);
500 | }).listen(port, done);
501 | });
502 |
503 | after(function(done){
504 | server.close(done);
505 | });
506 |
507 | describe('and parse option is not passed', function() {
508 |
509 | describe('with default parse_response', function() {
510 |
511 | before(function() {
512 | needle.defaults().parse_response.should.eql('all')
513 | })
514 |
515 | it('should return object', function(done){
516 | needle.get('localhost:' + port, function(err, response, body){
517 | should.ifError(err);
518 | body.should.deepEqual({
519 | "data": [{
520 | "type": "articles",
521 | "id": "1",
522 | "attributes": {
523 | "title": "Needle",
524 | "body": "The leanest and most handsome HTTP client in the Nodelands."
525 | }
526 | }],
527 | "included": [
528 | {
529 | "type": "people",
530 | "id": "42",
531 | "attributes": {
532 | "name": "Tomás"
533 | }
534 | }
535 | ]
536 | });
537 | done();
538 | });
539 | });
540 |
541 | });
542 |
543 | })
544 |
545 | });
546 |
547 | describe('when response is a HAL JSON content-type', function () {
548 |
549 | var json_string = '{"name": "Tomás", "_links": {"href": "https://github.com/tomas/needle.git"}}';
550 |
551 | before(function(done){
552 | server = http.createServer(function(req, res) {
553 | res.setHeader('Content-Type', 'application/hal+json');
554 | res.end(json_string);
555 | }).listen(port, done);
556 | });
557 |
558 | after(function(done){
559 | server.close(done);
560 | });
561 |
562 | describe('and parse option is not passed', function() {
563 |
564 | describe('with default parse_response', function() {
565 |
566 | before(function() {
567 | needle.defaults().parse_response.should.eql('all')
568 | })
569 |
570 | it('should return object', function(done){
571 | needle.get('localhost:' + port, function(err, response, body){
572 | should.ifError(err);
573 | body.should.deepEqual({
574 | 'name': 'Tomás',
575 | '_links': {
576 | 'href': 'https://github.com/tomas/needle.git'
577 | }});
578 | done();
579 | });
580 | });
581 |
582 | });
583 |
584 | })
585 |
586 | });
587 |
588 | })
589 |
--------------------------------------------------------------------------------
/test/proxy_spec.js:
--------------------------------------------------------------------------------
1 | var helpers = require('./helpers'),
2 | should = require('should'),
3 | sinon = require('sinon'),
4 | http = require('http'),
5 | needle = require('./../');
6 |
7 | var port = 7707;
8 | var url = 'localhost:' + port;
9 | var nonexisting_host = 'awepfokawepofawe.com';
10 |
11 | describe('proxy option', function() {
12 |
13 | var spy, opts;
14 |
15 | function send_request(opts, done) {
16 | if (spy) spy.restore();
17 | spy = sinon.spy(http, 'request');
18 | needle.get(url, opts, done);
19 | }
20 |
21 | //////////////////////
22 | // proxy opts helpers
23 |
24 | function not_proxied(done) {
25 | return function(err, resp) {
26 | var path = spy.args[0][0].path;
27 | path.should.eql('/'); // not the full original URI
28 | spy.restore();
29 | done();
30 | }
31 | }
32 |
33 | function proxied(host, port, done) {
34 | return function(err, resp) {
35 | var path = spy.args[0][0].path;
36 | path.should.eql('http://' + url); // the full original URI
37 |
38 | var http_host = spy.args[0][0].host;
39 | if (http_host) http_host.should.eql(host);
40 |
41 | var http_port = spy.args[0][0].port;
42 | if (http_port) http_port.should.eql(port);
43 |
44 | spy.restore();
45 | done();
46 | }
47 | }
48 |
49 | //////////////////////
50 | // auth helpers
51 |
52 | function get_auth(header) {
53 | var token = header.split(/\s+/).pop();
54 | return token && Buffer.from(token, 'base64').toString().split(':');
55 | }
56 |
57 | function no_proxy_auth(done) {
58 | return function(err, resp) {
59 | var headers = spy.args[0][0].headers;
60 | Object.keys(headers).should.not.containEql('proxy-authorization');
61 | done();
62 | }
63 | }
64 |
65 | function header_set(name, user, pass, done) {
66 | return function(err, resp) {
67 | var headers = spy.args[0][0].headers;
68 | var auth = get_auth(headers[name]);
69 | auth[0].should.eql(user);
70 | auth[1].should.eql(pass);
71 | done();
72 | }
73 | }
74 |
75 | function proxy_auth_set(user, pass, done) {
76 | return header_set('proxy-authorization', user, pass, done);
77 | }
78 |
79 | function basic_auth_set(user, pass, done) {
80 | return header_set('authorization', user, pass, done);
81 | }
82 |
83 | after(function() {
84 | spy.restore();
85 | })
86 |
87 | describe('when null proxy is passed', function() {
88 |
89 | it('does not proxy', function(done) {
90 | send_request({ proxy: null }, not_proxied(done))
91 | })
92 |
93 | describe('but defaults has been set', function() {
94 |
95 | before(function() {
96 | needle.defaults({ proxy: 'foobar' });
97 | })
98 |
99 | after(function() {
100 | needle.defaults({ proxy: null });
101 | })
102 |
103 | it('tries to proxy anyway', function(done) {
104 | send_request({}, proxied('foobar', 80, done))
105 | })
106 |
107 | })
108 |
109 | })
110 |
111 | describe('when weird string is passed', function() {
112 |
113 | it('tries to proxy anyway', function(done) {
114 | send_request({ proxy: 'alfalfa' }, proxied('alfalfa', 80, done))
115 | })
116 | })
117 |
118 | describe('when valid url is passed', function() {
119 |
120 | describe('without NO_PROXY env var set', function() {
121 | it('proxies request', function(done) {
122 | send_request({ proxy: nonexisting_host + ':123/done' }, proxied(nonexisting_host, '123', done))
123 | })
124 |
125 | it('does not set a Proxy-Authorization header', function(done) {
126 | send_request({ proxy: nonexisting_host + ':123/done' }, no_proxy_auth(done));
127 | })
128 | })
129 |
130 | describe('with NO_PROXY env var set', function() {
131 |
132 | it('proxies request if matching host not found in list', function(done) {
133 | process.env.NO_PROXY = 'foo';
134 | send_request({ proxy: nonexisting_host + ':123/done' }, proxied(nonexisting_host, '123', function() {
135 | delete process.env.NO_PROXY;
136 | done();
137 | }))
138 | })
139 |
140 | it('does not proxy request if matching host in list and just has a different port', function(done) {
141 | process.env.NO_PROXY = 'localhost';
142 | send_request({ proxy: nonexisting_host + ':123/done' }, not_proxied(function() {
143 | delete process.env.NO_PROXY;
144 | done();
145 | }))
146 | })
147 |
148 | it('does not proxy if matching host found in list', function(done) {
149 | process.env.NO_PROXY = 'foo,' + url;
150 | send_request({ proxy: nonexisting_host + ':123/done' }, not_proxied(function() {
151 | delete process.env.NO_PROXY;
152 | done();
153 | }))
154 | })
155 | })
156 |
157 | describe('and proxy url contains user:pass', function() {
158 |
159 | before(function() {
160 | opts = {
161 | proxy: 'http://mj:x@' + nonexisting_host + ':123/done'
162 | }
163 | })
164 |
165 | it('proxies request', function(done) {
166 | send_request(opts, proxied(nonexisting_host, '123', done))
167 | })
168 |
169 | it('sets Proxy-Authorization header', function(done) {
170 | send_request(opts, proxy_auth_set('mj', 'x', done));
171 | })
172 |
173 | })
174 |
175 | describe('and a proxy_user is passed', function() {
176 |
177 | before(function() {
178 | opts = {
179 | proxy: nonexisting_host + ':123',
180 | proxy_user: 'someone',
181 | proxy_pass: 'else'
182 | }
183 | })
184 |
185 | it('proxies request', function(done) {
186 | send_request(opts, proxied(nonexisting_host, '123', done))
187 | })
188 |
189 | it('sets Proxy-Authorization header', function(done) {
190 | send_request(opts, proxy_auth_set('someone', 'else', done));
191 | })
192 |
193 | describe('and url also contains user:pass', function() {
194 |
195 | it('url user:pass wins', function(done) {
196 | var opts = {
197 | proxy: 'http://xxx:yyy@' + nonexisting_host + ':123',
198 | proxy_user: 'someone',
199 | proxy_pass: 'else'
200 | }
201 |
202 | send_request(opts, proxy_auth_set('xxx', 'yyy', done));
203 | })
204 |
205 | })
206 |
207 | describe('and options.username is also present', function() {
208 |
209 | before(function() {
210 | opts = { proxy_user: 'foobar', username: 'someone' };
211 | })
212 |
213 | it('a separate Authorization header is set', function(done) {
214 | var opts = {
215 | proxy: nonexisting_host + ':123',
216 | proxy_user: 'someone',
217 | proxy_pass: 'else',
218 | username: 'test',
219 | password: 'X'
220 | }
221 |
222 | send_request(opts, basic_auth_set('test', 'X', done));
223 | })
224 |
225 | })
226 |
227 | })
228 |
229 | })
230 |
231 | describe('when environment variable is set', function() {
232 |
233 | describe('and default is unchanged', function() {
234 |
235 | before(function() {
236 | process.env.HTTP_PROXY = 'foobar';
237 | })
238 |
239 | after(function() {
240 | delete process.env.HTTP_PROXY;
241 | })
242 |
243 | it('tries to proxy', function(done) {
244 | send_request({}, proxied('foobar', 80, done))
245 | })
246 |
247 | })
248 |
249 | describe('and functionality is disabled', function() {
250 |
251 | before(function() {
252 | process.env.HTTP_PROXY = 'foobar';
253 | })
254 |
255 | after(function() {
256 | delete process.env.HTTP_PROXY;
257 | })
258 |
259 | it('ignores proxy', function(done) {
260 | send_request({
261 | use_proxy_from_env_var: false
262 | }, not_proxied(done))
263 | })
264 |
265 | })
266 | })
267 |
268 | })
269 |
--------------------------------------------------------------------------------
/test/querystring_spec.js:
--------------------------------------------------------------------------------
1 | var should = require('should'),
2 | stringify = require('../lib/querystring').build;
3 |
4 | describe('stringify', function() {
5 |
6 | describe('with null', function() {
7 |
8 | it('throws', function() {
9 | (function() {
10 | var res = stringify(null);
11 | }).should.throw();
12 | })
13 |
14 | })
15 |
16 | describe('with a number', function() {
17 |
18 | it('throws', function() {
19 | (function() {
20 | var res = stringify(100);
21 | }).should.throw();
22 | })
23 |
24 | })
25 |
26 | describe('with a string', function() {
27 |
28 | describe('that is empty', function() {
29 |
30 | it('throws', function() {
31 | (function() {
32 | var res = stringify('');
33 | }).should.throw();
34 | })
35 |
36 | })
37 |
38 | describe('that doesnt contain an equal sign', function() {
39 |
40 | it('throws', function() {
41 | (function() {
42 | var res = stringify('boomshagalaga');
43 | }).should.throw();
44 | })
45 |
46 | })
47 |
48 | describe('that contains an equal sign', function() {
49 |
50 | it('works', function() {
51 | var res = stringify('hello=123');
52 | res.should.eql('hello=123');
53 | })
54 |
55 | })
56 |
57 | })
58 |
59 | describe('with an array', function() {
60 |
61 | describe('with key val objects', function() {
62 |
63 | it('works', function() {
64 | var res = stringify([ {foo: 'bar'} ]);
65 | res.should.eql('foo=bar');
66 | })
67 |
68 | })
69 |
70 | describe('where all elements are strings with an equal sign', function() {
71 |
72 | it('works', function() {
73 | var res = stringify([ 'bar=123', 'quux=' ]);
74 | res.should.eql('bar=123&quux=');
75 | })
76 |
77 | })
78 |
79 | describe('with random words', function() {
80 |
81 | it('throws', function() {
82 | (function() {
83 | var res = stringify(['hello', 'there']);
84 | }).should.throw();
85 | })
86 |
87 | })
88 |
89 | describe('with integers', function() {
90 |
91 | it('throws', function() {
92 | (function() {
93 | var res = stringify([123, 432]);
94 | }).should.throw();
95 | })
96 |
97 | })
98 |
99 | })
100 |
101 | describe('with an object', function() {
102 |
103 | it('works', function() {
104 | var res = stringify({ test: 100 });
105 | res.should.eql('test=100');
106 | })
107 |
108 | describe('with object where val is an array', function() {
109 |
110 | it('works', function() {
111 | var res = stringify({ foo: ['bar', 'baz'] });
112 | res.should.eql('foo[]=bar&foo[]=baz');
113 | })
114 |
115 | })
116 |
117 | describe('with object where val is an array of key val objects', function() {
118 |
119 | it('works', function() {
120 | var res = stringify({ foo: [{'1': 'bar'}, {'2': 'baz'}] });
121 | res.should.eql('foo[][1]=bar&foo[][2]=baz');
122 | })
123 |
124 | })
125 |
126 | })
127 |
128 | })
129 |
--------------------------------------------------------------------------------
/test/redirect_spec.js:
--------------------------------------------------------------------------------
1 | var helpers = require('./helpers'),
2 | should = require('should'),
3 | sinon = require('sinon'),
4 | needle = require('./../');
5 |
6 | var ports = {
7 | http : 8888,
8 | https : 9999
9 | }
10 |
11 | var protocols = {
12 | http : require('http'),
13 | https : require('https')
14 | }
15 |
16 | var code = 301;
17 | var location; // var to set the response location
18 |
19 | function response_code() {
20 | return code;
21 | }
22 |
23 | function response_headers() {
24 | return { 'Content-Type': 'text/plain', 'Location': location }
25 | }
26 |
27 | describe('redirects', function() {
28 |
29 | var spies = {},
30 | servers = {};
31 |
32 | var current_protocol;
33 | var hostname = require('os').hostname();
34 |
35 | // open two servers, one that responds to a redirect
36 | before(function(done) {
37 |
38 | var conf = {
39 | port : ports.http,
40 | code : response_code,
41 | headers : response_headers
42 | }
43 |
44 | servers.http = helpers.server(conf, function() {
45 | conf.port = ports.https;
46 | conf.protocol = 'https';
47 | servers.https = helpers.server(conf, done);
48 | });
49 | })
50 |
51 | after(function(done) {
52 | servers.http.close(function() {
53 | servers.https.close(done);
54 | });
55 | })
56 |
57 | var prots = {'http': 'https'};
58 | Object.keys(prots).forEach(function(protocol) {
59 |
60 | current_protocol = protocol;
61 | var other_protocol = protocol == 'http' ? 'https' : 'http';
62 |
63 | var opts, // each test will modify this
64 | host = '127.0.0.1',
65 | url = protocol + '://' + host + ':' + ports[protocol] + '/hello';
66 |
67 | function send_request(opts, cb) {
68 | if (protocol == 'https') opts.rejectUnauthorized = false;
69 | // console.log(' -- sending request ' + url + ' -- redirect to ' + location);
70 | needle.post(url, { foo: 'bar' }, opts, cb);
71 | }
72 |
73 | function not_followed(done) {
74 | send_request(opts, function(err, resp) {
75 | resp.statusCode.should.eql(301);
76 | if (current_protocol == 'http') {
77 | spies.http.callCount.should.eql(1); // only original request
78 | spies.https.callCount.should.eql(0);
79 | } else {
80 | spies.http.callCount.should.eql(0);
81 | spies.https.callCount.should.eql(1); // only original request
82 | }
83 | done();
84 | })
85 | }
86 |
87 | function followed_same_protocol(done) {
88 | send_request(opts, function(err, resp) {
89 | // the original request plus the redirect one
90 | spies[current_protocol].callCount.should.eql(2);
91 | done();
92 | })
93 | }
94 |
95 | function followed_other_protocol(done) {
96 | send_request(opts, function(err, resp) {
97 | // on new-ish node versions, https.request calls http.request internally,
98 | // so we need to amount for that additional call.
99 | // update: this doesn't happen on node > 10.x
100 |
101 | var node_major_ver = process.version.split('.')[0].replace('v', '');
102 | var http_calls = protocols.http.Agent.defaultMaxSockets == Infinity && parseInt(node_major_ver) < 10 ? 2 : 1;
103 |
104 | spies.http.callCount.should.eql(http_calls); // the one(s) from http.request
105 | spies.https.callCount.should.eql(1); // the one from https.request (redirect)
106 | done();
107 | })
108 | }
109 |
110 | // set a spy on [protocol].request
111 | // so we can see how many times a request was made
112 | before(function() {
113 | spies.http = sinon.spy(protocols.http, 'request');
114 | spies.https = sinon.spy(protocols.https, 'request');
115 | })
116 |
117 | // and make sure it is restored after each test
118 | afterEach(function() {
119 | spies.http.reset();
120 | spies.https.reset();
121 | })
122 |
123 | after(function() {
124 | spies.http.restore();
125 | spies.https.restore();
126 | })
127 |
128 | describe('when overriding defaults', function() {
129 |
130 | before(function() {
131 | needle.defaults({ follow_max: 10 });
132 | opts = {};
133 | })
134 |
135 | after(function() {
136 | // reset values to previous
137 | needle.defaults({ follow_max: 0 });
138 | })
139 |
140 | describe('and redirected to the same path on same host and protocol', function() {
141 | before(function() {
142 | location = url;
143 | })
144 | it('does not follow redirect', not_followed);
145 | })
146 |
147 | describe('and redirected to the same path on same host and different protocol', function() {
148 | before(function() {
149 | location = url.replace(protocol, other_protocol).replace(ports[protocol], ports[other_protocol]);
150 | })
151 |
152 | it('follows redirect', followed_other_protocol);
153 | })
154 |
155 | describe('and redirected to a different path on same host, same protocol', function() {
156 | before(function() {
157 | location = url.replace('/hello', '/goodbye');
158 | })
159 | it('follows redirect', followed_same_protocol);
160 | })
161 |
162 | describe('and redirected to a different path on same host, different protocol', function() {
163 | before(function() {
164 | location = url.replace('/hello', '/goodbye').replace(protocol, other_protocol).replace(ports[protocol], ports[other_protocol]);
165 | })
166 | it('follows redirect', followed_other_protocol);
167 | })
168 |
169 | describe('and redirected to same path on another host, same protocol', function() {
170 | before(function() {
171 | location = url.replace(host, hostname);
172 | })
173 | it('follows redirect', followed_same_protocol);
174 | })
175 |
176 | describe('and redirected to same path on another host, different protocol', function() {
177 | before(function() {
178 | location = url.replace(host, hostname).replace(protocol, other_protocol).replace(ports[protocol], ports[other_protocol]);
179 | })
180 | it('follows redirect', followed_other_protocol);
181 | })
182 |
183 | })
184 |
185 | // false and null have the same result
186 | var values = [false, null];
187 | values.forEach(function(value) {
188 |
189 | describe('when follow is ' + value, function() {
190 |
191 | before(function() {
192 | opts = { follow: value };
193 | })
194 |
195 | describe('and redirected to the same path on same host and protocol', function() {
196 | before(function() {
197 | location = url;
198 | })
199 |
200 | it('throws an error', function() {
201 | (function() {
202 | send_request(opts, function() { });
203 | }).should.throw;
204 | })
205 |
206 | })
207 |
208 | })
209 |
210 | })
211 |
212 | describe('when follow is true', function() {
213 |
214 | before(function() {
215 | opts = { follow: true };
216 | })
217 |
218 | describe('and redirected to the same path on same host and protocol', function() {
219 | before(function() { location = url })
220 |
221 | it('throws an error', function() {
222 | (function() {
223 | send_request(opts, function() { });
224 | }).should.throw;
225 | })
226 |
227 | })
228 |
229 | })
230 |
231 | describe('when follow is > 0', function() {
232 |
233 | before(function() {
234 | needle.defaults({ follow: 10 });
235 | })
236 |
237 | after(function() {
238 | needle.defaults({ follow: 0 });
239 | })
240 |
241 | describe('when keep_method is false', function() {
242 |
243 | before(function() {
244 | opts = { follow_keep_method: false };
245 | })
246 |
247 | // defaults to follow host and protocol
248 | describe('and redirected to the same path on same host and different protocol', function() {
249 |
250 | before(function() {
251 | location = url.replace(protocol, other_protocol);
252 | })
253 |
254 | it('follows redirect', followed_other_protocol);
255 |
256 | it('sends a GET request with no data', function(done) {
257 | send_request(opts, function(err, resp) {
258 | // spy.args[0][3].should.eql(null);
259 | spies.http.args[0][0].method.should.eql('GET');
260 | done();
261 | })
262 | })
263 |
264 | it('does not resend cookies if follow_set_cookies is false', function(done) {
265 | opts.cookies = {foo: 'bar'};
266 | opts.follow_set_cookies = false;
267 | send_request(opts, function(err, resp) {
268 | should.not.exist(spies.http.args[0][0].headers['cookie']);
269 | done();
270 | })
271 | })
272 |
273 | it('resends cookies if follow_set_cookies is true', function(done) {
274 | opts.cookies = {foo: 'bar'};
275 | opts.follow_set_cookies = true;
276 | send_request(opts, function(err, resp) {
277 | spies.http.args[0][0].headers['cookie'].should.eql('foo=bar')
278 | done();
279 | })
280 | })
281 |
282 | })
283 |
284 | })
285 |
286 | describe('and set_referer is true', function() {
287 |
288 | before(function() {
289 | opts = { follow_set_referer: true };
290 | })
291 |
292 | // defaults to follow host and protocol
293 | describe('and redirected to the same path on same host and different protocol', function() {
294 |
295 | before(function() {
296 | location = url.replace(protocol, other_protocol);
297 | })
298 |
299 | it('follows redirect', followed_other_protocol);
300 |
301 | it('sets Referer header when following redirect', function(done) {
302 | send_request(opts, function(err, resp) {
303 | // spies.http.args[0][3].should.eql({ foo: 'bar'});
304 | spies.http.args[0][0].headers['referer'].should.eql("http://" + host + ":8888/hello");
305 | done();
306 | })
307 | })
308 |
309 | it('does not resend cookies if follow_set_cookies is false', function(done) {
310 | opts.cookies = {foo: 'bar'};
311 | opts.follow_set_cookies = false;
312 | send_request(opts, function(err, resp) {
313 | should.not.exist(spies.http.args[0][0].headers['cookie']);
314 | done();
315 | })
316 | })
317 |
318 | it('resends cookies if follow_set_cookies is true', function(done) {
319 | opts.cookies = {foo: 'bar'};
320 | opts.follow_set_cookies = true;
321 | send_request(opts, function(err, resp) {
322 | spies.http.args[0][0].headers['cookie'].should.eql('foo=bar')
323 | done();
324 | })
325 | })
326 |
327 | })
328 |
329 | })
330 |
331 | describe('and keep_method is true', function() {
332 |
333 | before(function() {
334 | opts = { follow_keep_method: true };
335 | })
336 |
337 | // defaults to follow host and protocol
338 | describe('and redirected to the same path on same host and different protocol', function() {
339 |
340 | before(function() {
341 | location = url.replace(protocol, other_protocol);
342 | })
343 |
344 | it('follows redirect', followed_other_protocol);
345 |
346 | it('sends a POST request with the original data', function(done) {
347 | send_request(opts, function(err, resp) {
348 | spies.http.args[0][0].method.should.eql('post');
349 | // spies.http.args[0][3].should.eql({ foo: 'bar'});
350 | done();
351 | })
352 | })
353 |
354 | it('does not resend cookies if follow_set_cookies is false', function(done) {
355 | opts.cookies = {foo: 'bar'};
356 | opts.follow_set_cookies = false;
357 | send_request(opts, function(err, resp) {
358 | should.not.exist(spies.http.args[0][0].headers['cookie']);
359 | done();
360 | })
361 | })
362 |
363 | it('resends cookies if follow_set_cookies is true', function(done) {
364 | opts.cookies = {foo: 'bar'};
365 | opts.follow_set_cookies = true;
366 | send_request(opts, function(err, resp) {
367 | spies.http.args[0][0].headers['cookie'].should.eql('foo=bar')
368 | done();
369 | })
370 | })
371 |
372 | })
373 |
374 | })
375 |
376 | describe('and if_same_host is false', function() {
377 |
378 | before(function() {
379 | opts = { follow_if_same_host: false };
380 | })
381 |
382 | // by default it will follow other protocols
383 | describe('and redirected to same path on another domain, same protocol', function() {
384 | before(function() {
385 | location = url.replace(host, hostname);
386 | })
387 |
388 | it('follows redirect', followed_same_protocol);
389 |
390 | it('does not resend cookies even if follow_set_cookies is true', function(done) {
391 | opts.cookies = {foo: 'bar'};
392 | opts.follow_set_cookies = true;
393 | send_request(opts, function(err, resp) {
394 | should.not.exist(spies.http.args[0][0].headers['cookie']);
395 | done();
396 | })
397 | })
398 | })
399 |
400 | })
401 |
402 | describe('and if_same_host is true', function() {
403 |
404 | before(function() {
405 | opts = { follow_if_same_host: true };
406 | })
407 |
408 | // by default it will follow other protocols
409 | describe('and redirected to same path on another domain, same protocol', function() {
410 | before(function() {
411 | location = url.replace(host, hostname);
412 | })
413 |
414 | it('does not follow redirect', not_followed);
415 | })
416 |
417 | })
418 |
419 | describe('and if_same_protocol is false', function() {
420 |
421 | before(function() {
422 | opts = { follow_if_same_protocol: false };
423 | })
424 |
425 | // by default it will follow other hosts
426 | describe('and redirected to same path on another domain, different protocol', function() {
427 | before(function() {
428 | location = url.replace(host, hostname).replace(protocol, other_protocol).replace(ports[protocol], ports[other_protocol]);
429 | })
430 |
431 | it('follows redirect', followed_other_protocol);
432 |
433 | it('does not resend cookies even if follow_set_cookies is true', function(done) {
434 | opts.cookies = {foo: 'bar'};
435 | opts.follow_set_cookies = true;
436 | send_request(opts, function(err, resp) {
437 | should.not.exist(spies.http.args[0][0].headers['cookie']);
438 | done();
439 | })
440 | })
441 | })
442 |
443 | })
444 |
445 | describe('and if_same_protocol is true', function() {
446 |
447 | before(function() {
448 | opts = { follow_if_same_protocol: true };
449 | })
450 |
451 | // by default it will follow other hosts
452 | describe('and redirected to same path on another domain, different protocol', function() {
453 | before(function() {
454 | location = url.replace(host, hostname).replace(protocol, other_protocol).replace(ports[protocol], ports[other_protocol]);
455 | })
456 | it('does not follow redirect', not_followed);
457 | })
458 |
459 | })
460 |
461 | })
462 |
463 | })
464 |
465 | });
466 |
--------------------------------------------------------------------------------
/test/redirect_with_timeout.js:
--------------------------------------------------------------------------------
1 | var should = require('should')
2 | var needle = require('./../')
3 |
4 | describe('follow redirects when read_timeout is set', function () {
5 |
6 | it('clear timeout before following redirect', function (done) {
7 | var opts = {
8 | open_timeout: 1000,
9 | read_timeout: 3000,
10 | follow: 5,
11 | user_agent: 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36'
12 | }
13 |
14 | var timedOut = 0
15 | var redirects = 0
16 |
17 | var timer = setTimeout(function () {
18 | var hasRedirects = redirects > 0
19 | hasRedirects.should.equal(true)
20 | done()
21 | }, opts.read_timeout || 3000)
22 |
23 | var resp = needle.get('http://google.com/', opts, function (err, resp, body) {
24 | var noErr = err === null
25 | var hasBody = body.length > 0
26 | noErr.should.equal(true);
27 | hasBody.should.equal(true);
28 | });
29 |
30 | resp.on('redirect', function (location) {
31 | redirects++
32 | // console.info(' Redirected to ', location)
33 | })
34 |
35 | resp.on('timeout', function (type) {
36 | timedOut++
37 | timedOut.should.equal(0)
38 | // console.error(' ', type, 'timeout')
39 | clearTimeout(timer)
40 | done()
41 | })
42 |
43 | }).timeout(30000)
44 |
45 | })
--------------------------------------------------------------------------------
/test/request_stream_spec.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs'),
2 | needle = require('..'),
3 | stream = require('stream'),
4 | http = require('http'),
5 | should = require('should'),
6 | sinon = require('sinon');
7 |
8 | var port = 2233;
9 |
10 | var node_major_ver = parseInt(process.version.split('.')[0].replace('v', ''));
11 | var node_minor_ver = parseInt(process.version.split('.')[1]);
12 |
13 | describe('request stream length', function() {
14 |
15 | var server, writable;
16 |
17 | function createServer() {
18 | return http.createServer(function(req, res) {
19 |
20 | req.on('data', function(chunk) {
21 | // console.log(chunk.length);
22 | })
23 |
24 | req.on('end', function() {
25 | res.writeHeader(200, { 'Content-Type': 'application/json'})
26 | res.end(JSON.stringify({ headers: req.headers }))
27 | })
28 |
29 | })
30 | }
31 |
32 | before(function(done) {
33 | server = createServer();
34 | server.listen(port, done)
35 | })
36 |
37 | beforeEach(function() {
38 | writable = new stream.Readable();
39 | writable._read = function() {
40 | this.push('hello world');
41 | this.push(null);
42 | }
43 | })
44 |
45 | after(function(done) {
46 | server.close(done)
47 | })
48 |
49 | function send_request(opts, cb) {
50 | needle.post('http://localhost:' + port, writable, opts, cb)
51 | }
52 |
53 | describe('no stream_length set', function() {
54 |
55 | it('doesnt set Content-Length header', function(done) {
56 | send_request({}, function(err, resp) {
57 | should.not.exist(resp.body.headers['content-length']);
58 | done()
59 | })
60 | })
61 |
62 | it('works if Transfer-Encoding is not set', function(done) {
63 | send_request({}, function(err, resp) {
64 | should.not.exist(err);
65 | resp.statusCode.should.eql(200);
66 | done()
67 | })
68 | })
69 |
70 | })
71 |
72 | describe('stream_length is set to valid value', function() {
73 |
74 | it('sets Content-Length header to that value', function(done) {
75 | send_request({ stream_length: 11 }, function(err, resp) {
76 | resp.body.headers['content-length'].should.eql('11');
77 | done()
78 | })
79 | })
80 |
81 | it('works if Transfer-Encoding is set to a blank string', function(done) {
82 | send_request({ stream_length: 11, headers: { 'Transfer-Encoding': '' }}, function(err, resp) {
83 | should.not.exist(err);
84 | var code = node_major_ver == 10 && node_minor_ver > 15 ? 400 : 200;
85 | resp.statusCode.should.eql(code);
86 | done()
87 | })
88 | })
89 |
90 | it('works if Transfer-Encoding is not set', function(done) {
91 | send_request({ stream_length: 11 }, function(err, resp) {
92 | should.not.exist(err);
93 | resp.statusCode.should.eql(200);
94 | done()
95 | })
96 | })
97 |
98 | })
99 |
100 |
101 | describe('stream_length set to 0', function() {
102 |
103 | describe('stream with path', function() {
104 |
105 | var stub;
106 |
107 | beforeEach(function() {
108 | writable.path = '/foo/bar';
109 | stub = sinon.stub(fs, 'stat').callsFake(function(path, cb) {
110 | cb(null, { size: 11 })
111 | })
112 | })
113 |
114 | afterEach(function() {
115 | stub.restore();
116 | })
117 |
118 | it('sets Content-Length header to streams length', function(done) {
119 | send_request({ stream_length: 0 }, function(err, resp) {
120 | resp.body.headers['content-length'].should.eql('11');
121 | done()
122 | })
123 | })
124 |
125 | it('works if Transfer-Encoding is set to a blank string', function(done) {
126 | send_request({ stream_length: 0, headers: { 'Transfer-Encoding': '' }}, function(err, resp) {
127 | should.not.exist(err);
128 | var code = node_major_ver == 10 && node_minor_ver > 15 ? 400 : 200;
129 | resp.statusCode.should.eql(code);
130 | done()
131 | })
132 | })
133 |
134 | it('works if Transfer-Encoding is not set', function(done) {
135 | send_request({ stream_length: 0 }, function(err, resp) {
136 | should.not.exist(err);
137 | resp.statusCode.should.eql(200);
138 | done()
139 | })
140 | })
141 |
142 | })
143 |
144 | describe('stream without path', function() {
145 |
146 | it('does not set Content-Length header', function(done) {
147 | send_request({ stream_length: 0 }, function(err, resp) {
148 | should.not.exist(resp.body.headers['content-length']);
149 | done()
150 | })
151 | })
152 |
153 | it('works if Transfer-Encoding is not set', function(done) {
154 | send_request({ stream_length: 0 }, function(err, resp) {
155 | should.not.exist(err);
156 | resp.statusCode.should.eql(200);
157 | done()
158 | })
159 | })
160 | })
161 |
162 | })
163 |
164 | })
165 |
--------------------------------------------------------------------------------
/test/response_stream_spec.js:
--------------------------------------------------------------------------------
1 | var should = require('should'),
2 | needle = require('./../'),
3 | http = require('http'),
4 | stream = require('stream'),
5 | fs = require('fs'),
6 | port = 11111,
7 | server;
8 |
9 | describe('response streams', function() {
10 |
11 | describe('when the server sends back json', function(){
12 |
13 | before(function(done) {
14 | server = http.createServer(function(req, res) {
15 | res.setHeader('Content-Type', 'application/json')
16 | res.end('{"foo":"bar"}')
17 | }).listen(port, done);
18 | });
19 |
20 | after(function(done) {
21 | server.close(done);
22 | })
23 |
24 | describe('and the client uses streams', function(){
25 |
26 | it('creates a proper streams2 stream', function(done) {
27 | var stream = needle.get('localhost:' + port)
28 |
29 | // newer node versions set this to null instead of false
30 | var bool = !!stream._readableState.flowing;
31 | should.equal(false, bool);
32 |
33 | var readableCalled = false;
34 | stream.on('readable', function() {
35 | readableCalled = true;
36 | })
37 |
38 | stream.on('finish', function() {
39 | readableCalled.should.be.true;
40 | done();
41 | });
42 |
43 | stream.resume();
44 | })
45 |
46 | it('emits a single data item which is our JSON object', function(done) {
47 | var stream = needle.get('localhost:' + port)
48 |
49 | var chunks = [];
50 | stream.on('readable', function () {
51 | while (chunk = this.read()) {
52 | chunk.should.be.an.Object;
53 | chunks.push(chunk);
54 | }
55 | })
56 |
57 | stream.on('done', function () {
58 | chunks.should.have.length(1)
59 | chunks[0].should.have.property('foo', 'bar');
60 | done();
61 | });
62 | })
63 |
64 | it('emits a raw buffer if we do not want to parse JSON', function(done) {
65 | var stream = needle.get('localhost:' + port, { parse: false })
66 |
67 | var chunks = [];
68 | stream.on('readable', function () {
69 | while (chunk = this.read()) {
70 | Buffer.isBuffer(chunk).should.be.true;
71 | chunks.push(chunk);
72 | }
73 | })
74 |
75 | stream.on('done', function() {
76 | var body = Buffer.concat(chunks).toString();
77 | body.should.equal('{"foo":"bar"}')
78 | done();
79 | });
80 | })
81 |
82 | })
83 | })
84 |
85 | describe('when the server sends back what was posted to it', function () {
86 | var file = 'asdf.txt';
87 |
88 | before(function(done){
89 | server = http.createServer(function(req, res) {
90 | res.setHeader('Content-Type', 'application/octet')
91 | req.pipe(res);
92 | }).listen(port);
93 |
94 | fs.writeFile(file, 'contents of stream', done);
95 | });
96 |
97 | after(function(done){
98 | server.close();
99 | fs.unlink(file, done);
100 | })
101 |
102 | it('can PUT a stream', function (done) {
103 | var stream = needle.put('localhost:' + port, fs.createReadStream(file), { stream: true });
104 |
105 | var chunks = [];
106 | stream.on('readable', function () {
107 | while (chunk = this.read()) {
108 | Buffer.isBuffer(chunk).should.be.true;
109 | chunks.push(chunk);
110 | }
111 | })
112 |
113 | stream.on('end', function () {
114 | var body = Buffer.concat(chunks).toString();
115 | body.should.equal('contents of stream')
116 | done();
117 | });
118 | });
119 |
120 | it('can PATCH a stream', function (done) {
121 | var stream = needle.patch('localhost:' + port, fs.createReadStream(file), { stream: true });
122 |
123 | var chunks = [];
124 | stream.on('readable', function () {
125 | while (chunk = this.read()) {
126 | Buffer.isBuffer(chunk).should.be.true;
127 | chunks.push(chunk);
128 | }
129 | })
130 |
131 | stream.on('end', function () {
132 | var body = Buffer.concat(chunks).toString();
133 | body.should.equal('contents of stream')
134 | done();
135 | });
136 | });
137 | })
138 | })
139 |
--------------------------------------------------------------------------------
/test/socket_cleanup_spec.js:
--------------------------------------------------------------------------------
1 | var should = require('should'),
2 | needle = require('./../'),
3 | fs = require('fs'),
4 | https = require('https'),
5 | stream = require('stream');
6 |
7 | describe('socket cleanup', function(){
8 |
9 | var outFile = 'test/tmp';
10 | var httpAgent, readStream, writeStream
11 |
12 | var file = 'ubuntu-21.04-desktop-amd64.iso',
13 | url = 'https://releases.ubuntu.com/21.04/' + file;
14 |
15 | function getActiveSockets() {
16 | return Object.keys(httpAgent.sockets).length
17 | }
18 |
19 | before(function() {
20 | httpAgent = new https.Agent({
21 | keepAlive : true,
22 | maxSockets : 1
23 | });
24 | })
25 |
26 | after(function() {
27 | httpAgent.destroy()
28 | fs.unlinkSync(outFile);
29 | })
30 |
31 | it('should cleanup sockets on ERR_STREAM_PREMATURE_CLOSE (using .pipe)', function(done) {
32 | getActiveSockets().should.eql(0);
33 |
34 | var resp = needle.get(url, { agent: httpAgent });
35 | var writable = fs.createWriteStream(outFile);
36 | resp.pipe(writable);
37 |
38 | writable.on('close', function(e) {
39 | if (!resp.done) resp.abort();
40 | })
41 |
42 | setTimeout(function() {
43 | getActiveSockets().should.eql(1);
44 | writable.destroy();
45 | }, 50);
46 |
47 | setTimeout(function() {
48 | getActiveSockets().should.eql(0);
49 | done();
50 | }, 500); // takes a bit
51 | })
52 |
53 | it('should cleanup sockets on ERR_STREAM_PREMATURE_CLOSE (using stream.pipeline)', function(done) {
54 | if (!stream.pipeline)
55 | return done()
56 |
57 | getActiveSockets().should.eql(0);
58 |
59 | var resp = needle.get(url, { agent: httpAgent });
60 | var writable = fs.createWriteStream(outFile);
61 |
62 | stream.pipeline(resp, writable, function(err) {
63 | err.code.should.eql('ERR_STREAM_PREMATURE_CLOSE')
64 | if (err) resp.request.destroy();
65 | });
66 |
67 | setTimeout(function() {
68 | getActiveSockets().should.eql(1);
69 | writable.destroy();
70 | }, 50);
71 |
72 | setTimeout(function() {
73 | getActiveSockets().should.eql(0);
74 | done();
75 | }, 1000); // takes a bit
76 |
77 | })
78 |
79 | })
80 |
--------------------------------------------------------------------------------
/test/socket_pool_spec.js:
--------------------------------------------------------------------------------
1 | var needle = require('../'),
2 | should = require('should'),
3 | http = require('http');
4 |
5 | var server, port = 11112;
6 |
7 | describe('socket reuse', function() {
8 |
9 | var httpAgent = new http.Agent({
10 | keepAlive : true,
11 | maxSockets : 1
12 | });
13 |
14 | before(function(done) {
15 | server = http.createServer(function(req, res) {
16 | res.setHeader('Content-Type', 'application/json');
17 | setTimeout(function() {
18 | res.end('{"foo":"bar"}');
19 | }, 50);
20 | }).listen(port, done);
21 | });
22 |
23 | after(function(done) {
24 | httpAgent.destroy();
25 | server.close(done);
26 | });
27 |
28 | describe('when sockets are reused', function() {
29 |
30 | it('does not duplicate listeners on .end', function(done) {
31 |
32 | var last_error;
33 | var count = 10;
34 |
35 | function completed(err) {
36 | --count || done(last_error);
37 | }
38 |
39 | function send() {
40 | needle.get('localhost:' + port, { agent: httpAgent }, function(err, resp) {
41 | if (err)
42 | throw new Error("Unexpected error: " + err);
43 |
44 | // lets go through all sockets and inspect all socket objects
45 | for (hostTarget in httpAgent.sockets) {
46 | httpAgent.sockets[hostTarget].forEach(function(socket) {
47 | // normally, there are 2 internal listeners and 1 needle sets up,
48 | // but to be sure the test does not fail even if newer node versions
49 | // introduce additional listeners, we use a higher limit.
50 | try {
51 | socket.listeners('end').length.should.be.below(5, "too many listeners on the socket object's end event");
52 | } catch (e) {
53 | last_error = e;
54 | }
55 | });
56 | }
57 |
58 | completed();
59 | });
60 | }
61 |
62 | for (var i = 0; i < count; i++) {
63 | send();
64 | }
65 | });
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/test/stream_events_spec.js:
--------------------------------------------------------------------------------
1 | var needle = require('../'),
2 | fs = require('fs'),
3 | should = require('should'),
4 | helpers = require('./helpers');
5 |
6 | describe('stream events', function() {
7 |
8 | var server,
9 | port = 3456,
10 | responseData,
11 | serverOpts = {},
12 | requestHandler = function(req, res) { res.end('OK') }
13 |
14 | before(function() {
15 | var opts = {
16 | port: port,
17 | handler: function(req, res) { requestHandler(req, res) }
18 | }
19 | server = helpers.server(opts);
20 | })
21 |
22 | after(function() {
23 | server.close();
24 | })
25 |
26 | beforeEach(function() {
27 | responseData = '';
28 | })
29 |
30 | describe('when consuming data directly', function() {
31 |
32 | function send_request(opts, cb) {
33 | return needle
34 | .get('http://localhost:' + port, opts)
35 | .on('data', function(data) { responseData += data })
36 | }
37 |
38 | describe('and request stream fails', function() {
39 |
40 | it('emits done event with error', function(done) {
41 | requestHandler = function(req, res) { req.socket.destroy() }
42 |
43 | send_request({}).on('done', function(err) {
44 | err.code.should.eql('ECONNRESET');
45 | responseData.should.eql('');
46 | done()
47 | })
48 | })
49 |
50 | })
51 |
52 | describe('and request succeeds but decoding fails', function() {
53 |
54 | it('emits done event without error', function(done) {
55 | requestHandler = function(req, res) {
56 | res.setHeader('Content-Type', 'application/json')
57 | res.end('invalid:json')
58 | }
59 |
60 | send_request({ json: true }).on('done', function(err) {
61 | should.not.exist(err);
62 | responseData.should.eql('invalid:json');
63 | done()
64 | })
65 | })
66 |
67 | })
68 |
69 | describe('and request succeeds and pipeline works ok', function() {
70 |
71 | it('emits done event without error', function(done) {
72 | requestHandler = function(req, res) { res.end('{"ok":1}') }
73 |
74 | send_request({ json: true }).on('done', function(err) {
75 | should.not.exist(err);
76 | responseData.should.eql('{"ok":1}');
77 | done()
78 | })
79 | })
80 |
81 | })
82 |
83 | })
84 |
85 | describe('when piping to a fs writableStream', function() {
86 |
87 | var outFile = 'test/tmp.dat';
88 |
89 | function send_request(opts, cb) {
90 | return needle
91 | .get('http://localhost:' + port, opts)
92 | .pipe(fs.createWriteStream(outFile))
93 | .on('data', function(data) { responseData += data })
94 | }
95 |
96 | after(function(done) {
97 | fs.unlink(outFile, done)
98 | })
99 |
100 | describe('and request stream fails', function() {
101 |
102 | it('final stream emits done event with error', function(done) {
103 | requestHandler = function(req, res) { req.socket.destroy() }
104 |
105 | send_request({}).on('done', function(err) {
106 | err.code.should.eql('ECONNRESET');
107 | done()
108 | })
109 | })
110 |
111 | })
112 |
113 | describe('and request succeeds but decoding fails', function() {
114 |
115 | it('final stream emits done event without error', function(done) {
116 | requestHandler = function(req, res) {
117 | res.setHeader('Content-Type', 'application/json')
118 | res.end('invalid:json')
119 | }
120 |
121 | send_request({ json: true }).on('done', function(err) {
122 | should.not.exist(err);
123 | done()
124 | })
125 | })
126 |
127 | })
128 |
129 | describe('and request succeeds and pipeline works ok', function() {
130 |
131 | it('final stream emits done event without error', function(done) {
132 | requestHandler = function(req, res) { res.end('{"ok":1}') }
133 |
134 | send_request({ json: true }).on('done', function(err) {
135 | should.not.exist(err);
136 | done()
137 | })
138 | })
139 |
140 | })
141 |
142 | })
143 |
144 | })
--------------------------------------------------------------------------------
/test/tls_options_spec.js:
--------------------------------------------------------------------------------
1 | var needle = require('..'),
2 | https = require('https'),
3 | helpers = require('./helpers'),
4 | should = require('should');
5 |
6 | describe('tls options', function() {
7 |
8 | describe('rejectUnauthorized: false', function() {
9 |
10 | var url = 'https://expired-rsa-dv.ssl.com/';
11 |
12 | it('is an expired cert', function(done) {
13 | needle.get(url, function(err, resp) {
14 | err.code.should.eql('CERT_HAS_EXPIRED')
15 | should.not.exist(resp)
16 | done()
17 | })
18 | })
19 |
20 | it('allows fetching pages under expired certificates', function(done) {
21 | needle.get(url, { rejectUnauthorized: false }, function(err, resp) {
22 | should.not.exist(err);
23 | resp.statusCode.should.eql(200);
24 | done()
25 | })
26 | })
27 |
28 | it('also works when using custom agent', function(done) {
29 | var agent = new https.Agent({ rejectUnauthorized: true })
30 |
31 | // should overwrite value from custom agent
32 | needle.get(url, { rejectUnauthorized: false }, function(err, resp) {
33 | should.not.exist(err);
34 | resp.statusCode.should.eql(200);
35 | done()
36 | })
37 |
38 | })
39 |
40 | it('also works with shared/default agent', function(done) {
41 | var agent = new https.Agent({ rejectUnauthorized: true })
42 | needle.defaults({ agent: agent })
43 |
44 | // should overwrite value from custom agent
45 | needle.get(url, { rejectUnauthorized: false }, function(err, resp) {
46 | should.not.exist(err);
47 | resp.statusCode.should.eql(200);
48 |
49 | needle.defaults({ agent: null })
50 | done()
51 | })
52 |
53 | })
54 |
55 | })
56 |
57 | })
--------------------------------------------------------------------------------
/test/uri_modifier_spec.js:
--------------------------------------------------------------------------------
1 | var needle = require('../'),
2 | sinon = require('sinon'),
3 | should = require('should'),
4 | http = require('http'),
5 | helpers = require('./helpers');
6 |
7 | var port = 3456;
8 |
9 | describe('uri_modifier config parameter function', function() {
10 |
11 | var server, uri;
12 |
13 | function send_request(mw, cb) {
14 | needle.get(uri, { uri_modifier: mw }, cb);
15 | }
16 |
17 | before(function(done){
18 | server = helpers.server({ port: port }, done);
19 | })
20 |
21 | after(function(done) {
22 | server.close(done);
23 | })
24 |
25 | describe('modifies uri', function() {
26 |
27 | var path = '/foo/replace';
28 |
29 | before(function() {
30 | uri = 'localhost:' + port + path
31 | });
32 |
33 | it('should modify path', function(done) {
34 | send_request(function(uri) {
35 | return uri.replace('/replace', '');
36 | }, function(err, res) {
37 | should.not.exist(err);
38 | should(res.req.path).be.exactly('/foo');
39 | done();
40 | });
41 |
42 | });
43 |
44 | })
45 |
46 | })
47 |
--------------------------------------------------------------------------------
/test/url_spec.js:
--------------------------------------------------------------------------------
1 | var needle = require('../'),
2 | sinon = require('sinon'),
3 | should = require('should'),
4 | http = require('http'),
5 | helpers = require('./helpers');
6 |
7 | var port = 3456;
8 |
9 | describe('urls', function() {
10 |
11 | var server, url;
12 |
13 | function send_request(cb) {
14 | return needle.get(url, cb);
15 | }
16 |
17 | before(function(done){
18 | server = helpers.server({ port: port }, done);
19 | })
20 |
21 | after(function(done) {
22 | server.close(done);
23 | })
24 |
25 | describe('null URL', function(){
26 |
27 | it('throws', function(){
28 | (function() {
29 | send_request()
30 | }).should.throw();
31 | })
32 |
33 | })
34 |
35 | describe('invalid protocol', function(){
36 |
37 | before(function() {
38 | url = 'foo://google.com/what'
39 | })
40 |
41 | it('does not throw', function(done) {
42 | (function() {
43 | send_request(function(err) {
44 | done();
45 | })
46 | }).should.not.throw()
47 | })
48 |
49 | it('returns an error', function(done) {
50 | send_request(function(err) {
51 | err.should.be.an.Error;
52 | err.code.should.match(/ENOTFOUND|EADDRINFO|EAI_AGAIN/)
53 | done();
54 | })
55 | })
56 |
57 | })
58 |
59 | describe('invalid host', function(){
60 |
61 | before(function() {
62 | url = 'http://s1\\\u0002.com/'
63 | })
64 |
65 | it('fails', function(done) {
66 | (function() {
67 | send_request(function(){ })
68 | }.should.throw(TypeError))
69 | done()
70 | })
71 |
72 | })
73 |
74 | /*
75 | describe('invalid path', function(){
76 |
77 | before(function() {
78 | url = 'http://www.google.com\\\/x\\\ %^&*() /x2.com/'
79 | })
80 |
81 | it('fails', function(done) {
82 | send_request(function(err) {
83 | err.should.be.an.Error;
84 | done();
85 | })
86 | })
87 |
88 | })
89 | */
90 |
91 | describe('valid protocol and path', function() {
92 |
93 | before(function() {
94 | url = 'http://localhost:' + port + '/foo';
95 | })
96 |
97 | it('works', function(done) {
98 | send_request(function(err){
99 | should.not.exist(err);
100 | done();
101 | })
102 | })
103 |
104 | })
105 |
106 | describe('no protocol but with slashes and valid path', function() {
107 |
108 | before(function() {
109 | url = '//localhost:' + port + '/foo';
110 | })
111 |
112 | it('works', function(done) {
113 | send_request(function(err){
114 | should.not.exist(err);
115 | done();
116 | })
117 | })
118 |
119 | })
120 |
121 | describe('no protocol nor slashes and valid path', function() {
122 |
123 | before(function() {
124 | url = 'localhost:' + port + '/foo';
125 | })
126 |
127 | it('works', function(done) {
128 | send_request(function(err){
129 | should.not.exist(err);
130 | done();
131 | })
132 | })
133 |
134 | })
135 |
136 | describe('double encoding', function() {
137 |
138 | var path = '/foo?email=' + encodeURIComponent('what-ever@Example.Com');
139 |
140 | before(function() {
141 | url = 'localhost:' + port + path
142 | });
143 |
144 | it('should not occur', function(done) {
145 | send_request(function(err, res) {
146 | should.not.exist(err);
147 | should(res.req.path).be.exactly(path);
148 | done();
149 | });
150 |
151 | });
152 |
153 | })
154 |
155 | })
156 |
--------------------------------------------------------------------------------
/test/utils/formidable.js:
--------------------------------------------------------------------------------
1 | var formidable = require('formidable'),
2 | http = require('http'),
3 | util = require('util');
4 |
5 | var port = process.argv[2] || 8888;
6 |
7 | http.createServer(function(req, res) {
8 | var form = new formidable.IncomingForm();
9 | form.parse(req, function(err, fields, files) {
10 | res.writeHead(200, {'content-type': 'text/plain'});
11 | res.write('received upload:\n\n');
12 | console.log(util.inspect({fields: fields, files: files}))
13 | res.end(util.inspect({fields: fields, files: files}));
14 | });
15 | }).listen(port);
16 |
17 | console.log('HTTP server listening on port ' + port);
--------------------------------------------------------------------------------
/test/utils/proxy.js:
--------------------------------------------------------------------------------
1 | var http = require('http'),
2 | https = require('https'),
3 | url = require('url');
4 |
5 | var port = 1234,
6 | log = true,
7 | request_auth = false;
8 |
9 | http.createServer(function(request, response) {
10 |
11 | console.log(request.headers);
12 | console.log("Got request: " + request.url);
13 | console.log("Forwarding request to " + request.headers['host']);
14 |
15 | if (request_auth) {
16 | if (!request.headers['proxy-authorization']) {
17 | response.writeHead(407, {'Proxy-Authenticate': 'Basic realm="proxy.com"'})
18 | return response.end('Hello.');
19 | }
20 | }
21 |
22 | var remote = url.parse(request.url);
23 | var protocol = remote.protocol == 'https:' ? https : http;
24 |
25 | var opts = {
26 | host: request.headers['host'],
27 | port: remote.port || (remote.protocol == 'https:' ? 443 : 80),
28 | method: request.method,
29 | path: remote.pathname,
30 | headers: request.headers
31 | }
32 |
33 | var proxy_request = protocol.request(opts, function(proxy_response){
34 |
35 | proxy_response.on('data', function(chunk) {
36 | if (log) console.log(chunk.toString());
37 | response.write(chunk, 'binary');
38 | });
39 | proxy_response.on('end', function() {
40 | response.end();
41 | });
42 |
43 | response.writeHead(proxy_response.statusCode, proxy_response.headers);
44 | });
45 |
46 | request.on('data', function(chunk) {
47 | if (log) console.log(chunk.toString());
48 | proxy_request.write(chunk, 'binary');
49 | });
50 |
51 | request.on('end', function() {
52 | proxy_request.end();
53 | });
54 |
55 | }).listen(port);
56 |
57 | process.on('uncaughtException', function(err){
58 | console.log('Uncaught exception!');
59 | console.log(err);
60 | });
61 |
62 | console.log("Proxy server listening on port " + port);
63 |
--------------------------------------------------------------------------------
/test/utils/test.js:
--------------------------------------------------------------------------------
1 | // TODO: write specs. :)
2 |
3 | var fs = require('fs'),
4 | client = require('./../../');
5 |
6 | process.env.DEBUG = true;
7 |
8 | var response_callback = function(err, resp, body){
9 | console.log(err);
10 | if(resp) console.log("Got status code " + resp.statusCode)
11 | console.log(body);
12 | }
13 |
14 | function simple_head(){
15 | client.head('http://www.amazon.com', response_callback);
16 | }
17 |
18 | function simple_get(){
19 | client.get('http://www.nodejs.org', response_callback);
20 | }
21 |
22 | function proxy_get(){
23 | client.get('https://www.google.com/search?q=nodejs', {proxy: 'http://localhost:1234'}, response_callback);
24 | }
25 |
26 | function auth_get(){
27 | client.get('https://www.twitter.com', {username: 'asd', password: '123'}, response_callback);
28 | }
29 |
30 | function simple_post(url){
31 |
32 | var data = {
33 | foo: 'bar',
34 | baz: {
35 | nested: 'attribute'
36 | }
37 | }
38 |
39 | client.post(url, data, response_callback);
40 |
41 | }
42 |
43 | function multipart_post(url){
44 |
45 | var filename = 'test_file.txt';
46 | var data = 'Plain text data.\nLorem ipsum dolor sit amet.\nBla bla bla.\n';
47 | fs.writeFileSync(filename, data);
48 |
49 | var black_pixel = Buffer.from("data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=".replace(/^data:image\/\w+;base64,/, ""), "base64");
50 |
51 | var data = {
52 | foo: 'bar',
53 | bar: 'baz',
54 | nested: {
55 | my_document: { file: filename, content_type: 'text/plain' },
56 | even: {
57 | more: 'nesting'
58 | }
59 | },
60 | pixel: { filename: 'black_pixel.gif', buffer: black_pixel, content_type: 'image/gif' },
61 | field2: {value: JSON.stringify({"json":[ {"one":1}, {"two":2} ]}), content_type: 'application/json' }
62 | }
63 |
64 | client.post(url, data, {multipart: true}, function(err, resp, body){
65 |
66 | console.log(err);
67 | console.log("Got status code " + resp.statusCode)
68 | console.log(body);
69 | fs.unlink(filename);
70 |
71 | });
72 |
73 | }
74 |
75 | switch(process.argv[2]){
76 | case 'head':
77 | simple_head();
78 | break;
79 | case 'get':
80 | simple_get();
81 | break;
82 | case 'auth':
83 | auth_get();
84 | break;
85 | case 'proxy':
86 | proxy_get();
87 | break;
88 | case 'post':
89 | simple_post(process.argv[3] || 'http://posttestserver.com/post.php');
90 | break;
91 | case 'multipart':
92 | multipart_post(process.argv[3] || 'http://posttestserver.com/post.php?dir=example');
93 | break;
94 | case 'all':
95 | simple_head();
96 | simple_get();
97 | auth_get();
98 | proxy_get();
99 | simple_post(process.argv[3] || 'http://posttestserver.com/post.php');
100 | multipart_post(process.argv[3] || 'http://posttestserver.com/post.php?dir=example');
101 | break;
102 | default:
103 | console.log("Usage: ./test.js [head|get|auth|proxy|multipart]")
104 | }
105 |
--------------------------------------------------------------------------------
/test/utils_spec.js:
--------------------------------------------------------------------------------
1 | var helpers = require('./helpers'),
2 | should = require('should'),
3 | sinon = require('sinon'),
4 | utils = require('./../lib/utils');
5 |
6 | describe('utils.should_proxy_to()', function() {
7 |
8 | var should_proxy_to = utils.should_proxy_to;
9 |
10 | var noProxy = ".ic1.mycorp,localhost,127.0.0.1,*.mycorp.org";
11 | var noProxyWithPorts = " ,.mycorp.org:1234,.ic1.mycorp,localhost,127.0.0.1";
12 |
13 | var uris = {
14 | hostname: "http://registry.random.opr.mycorp.org",
15 | with_port: "http://registry.random.opr.mycorp.org:9874",
16 | with_another_port: "http://registry.random.opr.mycorp.org:1234",
17 | localhost: "http://localhost",
18 | ip: "http://127.0.0.1"
19 | }
20 |
21 | it("returns true if NO_PROXY is undefined", function(done) {
22 | process.env.NO_PROXY = undefined;
23 | should_proxy_to(uris.hostname).should.true()
24 | delete process.env.NO_PROXY;
25 | done();
26 | });
27 |
28 | it("returns true if NO_PROXY is empty", function(done) {
29 | process.env.NO_PROXY = "";
30 | should_proxy_to(uris.hostname).should.true()
31 | delete process.env.NO_PROXY;
32 | done();
33 | });
34 |
35 | it("returns false if NO_PROXY is a wildcard", function(done) {
36 | process.env.NO_PROXY = "*";
37 | should_proxy_to(uris.hostname).should.false()
38 | delete process.env.NO_PROXY;
39 | done();
40 | });
41 |
42 | it("returns true if the host matches and the ports don't (URI doesn't have port specified)", function(done) {
43 | process.env.NO_PROXY = noProxyWithPorts;
44 | should_proxy_to(uris.hostname).should.true()
45 | delete process.env.NO_PROXY;
46 | done();
47 | });
48 |
49 | it("returns true if the host matches and the ports don't (both have a port specified but just different values)", function(done) {
50 | process.env.NO_PROXY = noProxyWithPorts;
51 | should_proxy_to(uris.with_port).should.true()
52 | delete process.env.NO_PROXY;
53 | done();
54 | });
55 |
56 | it("returns false if the host matches and the ports don't (no_proxy pattern doesn't have a port)", function(done) {
57 | process.env.NO_PROXY = noProxy;
58 | should_proxy_to(uris.with_port).should.false()
59 | delete process.env.NO_PROXY;
60 | done();
61 | });
62 |
63 | it("returns false if host matches", function(done) {
64 | process.env.NO_PROXY = noProxy;
65 | should_proxy_to(uris.hostname).should.false()
66 | delete process.env.NO_PROXY;
67 | done();
68 | });
69 |
70 | it("returns false if the host and port matches", function(done) {
71 | process.env.NO_PROXY = noProxyWithPorts;
72 | should_proxy_to(uris.with_another_port).should.false()
73 | delete process.env.NO_PROXY;
74 | done();
75 | });
76 |
77 | it("returns false if the host matches (localhost)", function(done) {
78 | process.env.NO_PROXY = noProxy;
79 | should_proxy_to(uris.localhost).should.false()
80 | delete process.env.NO_PROXY;
81 | done();
82 | });
83 |
84 | it("returns false if the host matches (ip)", function(done) {
85 | process.env.NO_PROXY = noProxy;
86 | should_proxy_to(uris.ip).should.false()
87 | delete process.env.NO_PROXY;
88 | done();
89 | });
90 |
91 | it("returns false if the host matches (ip)", function(done) {
92 | process.env.NO_PROXY = noProxy.replace(/,g/, " ");
93 | should_proxy_to(uris.ip).should.false()
94 | delete process.env.NO_PROXY;
95 | done();
96 | });
97 |
98 | it("returns false if the host matches (ip)", function(done) {
99 | process.env.NO_PROXY = noProxy.replace(/,g/, " ");
100 | should_proxy_to(uris.ip).should.false()
101 | delete process.env.NO_PROXY;
102 | done();
103 | });
104 |
105 | })
106 |
--------------------------------------------------------------------------------