├── .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 | --------------------------------------------------------------------------------