diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..4312b095 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,5 @@ +/coverage +/scripts +/node_modules +/jsdoc +/locales diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..1792a589 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,168 @@ +{ + "env": { + "browser": true, + "es6": true, + "jquery": true, + "node": true, + "mocha": true, + }, + "globals": {}, + "parserOptions": { "ecmaVersion": 8 }, + "rules": { + "camelcase": [ + 2, + { + "properties": "always" + } + ], + "eqeqeq": 2, + "indent": [ + "error", + 2, + { + "ArrayExpression": 1, + "CallExpression": {"arguments": 1}, + "FunctionDeclaration": {"body": 1, "parameters": 2}, + "MemberExpression": 1, + "ObjectExpression": 1, + "SwitchCase": 1 + } + ], + "no-use-before-define": [ + 2, + { + "functions": false + } + ], + "max-len": [ + 2, + 120 + ], + "max-depth": [ + 2, + 2 + ], + "complexity": [ + 2, + 15 + ], + "new-cap": 2, + "quotes": [ + 2, + "single", + { + "allowTemplateLiterals": true + } + ], + "strict": [ + 2, + "global" + ], + "no-undef": 2, + "no-unused-vars": 2, + "no-eq-null": 2, + "space-before-function-paren": ["error", { + "anonymous": "always", + "named": "never" + }], + "no-empty": [ + 2, + { + "allowEmptyCatch": true + } + ], + "object-curly-spacing": [ + 2, + "always" + ], + "space-in-parens": [ + 2, + "never" + ], + "quote-props": [ + 2, + "as-needed" + ], + "key-spacing": [ + 2, + { + "beforeColon": false, + "afterColon": true + } + ], + "space-unary-ops": [ + 2, + { + "words": false, + "nonwords": false + } + ], + "no-mixed-spaces-and-tabs": 2, + "no-trailing-spaces": 2, + "comma-dangle": 0, + "comma-spacing": [ + 2, + { + "after": true, + "before": false + } + ], + "no-with": 2, + "brace-style": [ + 2, + "1tbs", + { + "allowSingleLine": true + } + ], + "no-multiple-empty-lines": 2, + "no-multi-str": 2, + "one-var": [ + 2, + "never" + ], + "semi-spacing": [ + 2, + { + "before": false, + "after": true + } + ], + "space-before-blocks": [ + 2, + "always" + ], + "wrap-iife": 2, + "comma-style": [ + 2, + "last" + ], + "space-infix-ops": 2, + "eol-last": 2, + "dot-notation": 2, + "curly": [ + 2, + "all" + ], + "keyword-spacing": [ + 2, + {} + ], + "lines-around-comment": [ + 2, + { "afterLineComment": true, "allowBlockEnd": true } + ], + "semi": [ + 2, + "always" + ], + "consistent-this": [ + 2, + "self" + ], + "linebreak-style": [ + 2, + "unix" + ] + } +} diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml new file mode 100644 index 00000000..673bd331 --- /dev/null +++ b/.github/workflows/node.js.yml @@ -0,0 +1,29 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Node.js CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [10.x, 12.x, 14.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci + - run: npm run build --if-present + - run: npm test diff --git a/.jscsrc b/.jscsrc deleted file mode 100644 index b07fcf83..00000000 --- a/.jscsrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "preset": "node-style-guide", - "requireCapitalizedComments": null, - "maximumLineLength": 120, - "requireTrailingComma": null, - "disallowQuotedKeysInObjects":false -} diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 95b5a215..00000000 --- a/.jshintrc +++ /dev/null @@ -1,93 +0,0 @@ -{ - // JSHint Default Configuration File (as on JSHint website) - // See http://jshint.com/docs/ for more details - - "maxerr" : 50, // {int} Maximum error before stopping - - // Enforcing - "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) - "camelcase" : false, // true: Identifiers must be in camelCase - "curly" : true, // true: Require {} for every new block or scope - "eqeqeq" : true, // true: Require triple equals (===) for comparison - "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() - "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. - "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` - "latedef" : false, // true: Require variables/functions to be defined before being used - "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` - "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` - "noempty" : true, // true: Prohibit use of empty blocks - "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. - "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) - "plusplus" : false, // true: Prohibit use of `++` and `--` - "quotmark" : false, // Quotation mark consistency: - // false : do nothing (default) - // true : ensure whatever is used is consistent - // "single" : require single quotes - // "double" : require double quotes - "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) - "unused" : true, // Unused variables: - // true : all variables, last function parameter - // "vars" : all variables only - // "strict" : all variables, all function parameters - "strict" : true, // true: Requires all functions run in ES5 Strict Mode - "maxparams" : false, // {int} Max number of formal params allowed per function - "maxdepth" : false, // {int} Max depth of nested blocks (within functions) - "maxstatements" : false, // {int} Max number statements per function - "maxcomplexity" : false, // {int} Max cyclomatic complexity per function - "maxlen" : false, // {int} Max number of characters per line - "varstmt" : false, // true: Disallow any var statements. Only `let` and `const` are allowed. - - // Relaxing - "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) - "boss" : false, // true: Tolerate assignments where comparisons would be expected - "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. - "eqnull" : false, // true: Tolerate use of `== null` - "esversion" : 5, // {int} Specify the ECMAScript version to which the code must adhere. - "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) - // (ex: `for each`, multiple try/catch, function expression…) - "evil" : false, // true: Tolerate use of `eval` and `new Function()` - "expr" : false, // true: Tolerate `ExpressionStatement` as Programs - "funcscope" : false, // true: Tolerate defining variables inside control statements - "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') - "iterator" : false, // true: Tolerate using the `__iterator__` property - "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block - "laxbreak" : false, // true: Tolerate possibly unsafe line breakings - "laxcomma" : false, // true: Tolerate comma-first style coding - "loopfunc" : false, // true: Tolerate functions being defined in loops - "multistr" : false, // true: Tolerate multi-line strings - "noyield" : false, // true: Tolerate generator functions with no yield statement in them. - "notypeof" : false, // true: Tolerate invalid typeof operator values - "proto" : false, // true: Tolerate using the `__proto__` property - "scripturl" : false, // true: Tolerate script-targeted URLs - "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` - "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation - "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` - "validthis" : false, // true: Tolerate using this in a non-constructor function - - // Environments - "browser" : true, // Web Browser (window, document, etc) - "browserify" : false, // Browserify (node.js code in the browser) - "couch" : false, // CouchDB - "devel" : true, // Development/debugging (alert, confirm, etc) - "dojo" : false, // Dojo Toolkit - "jasmine" : false, // Jasmine - "jquery" : false, // jQuery - "mocha" : true, // Mocha - "mootools" : false, // MooTools - "node" : true, // Node.js - "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) - "phantom" : false, // PhantomJS - "prototypejs" : false, // Prototype and Scriptaculous - "qunit" : false, // QUnit - "rhino" : false, // Rhino - "shelljs" : false, // ShellJS - "typed" : false, // Globals for typed array constructions - "worker" : false, // Web Workers - "wsh" : false, // Windows Scripting Host - "yui" : false, // Yahoo User Interface - - // Custom Globals - "globals" : { // additional predefined global variables - "Promise" : false - } -} diff --git a/.travis.yml b/.travis.yml index af477f99..e2d26a9c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ language: node_js node_js: - - "0.11" - - "4" - - "5" + - "10" + - "12" diff --git a/README.md b/README.md index f2983943..c4f346e7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ -# express-http-proxy [![NPM version](https://badge.fury.io/js/express-http-proxy.svg)](http://badge.fury.io/js/express-http-proxy) [![Build Status](https://travis-ci.org/villadora/express-http-proxy.svg?branch=master)](https://travis-ci.org/villadora/express-http-proxy) [![Dependency Status](https://gemnasium.com/villadora/express-http-proxy.svg)](https://gemnasium.com/villadora/express-http-proxy) +# express-http-proxy [![NPM version](https://badge.fury.io/js/express-http-proxy.svg)](http://badge.fury.io/js/express-http-proxy) [![Build Status](https://travis-ci.org/villadora/express-http-proxy.svg?branch=master)](https://travis-ci.org/villadora/express-http-proxy) -Express proxy middleware to forward request to another host and pass response back + +Express middleware to proxy request to another host and pass response back to original caller. ## Install @@ -13,80 +14,206 @@ $ npm install express-http-proxy --save proxy(host, options); ``` +### Example: To proxy URLS starting with '/proxy' to the host 'www.google.com': ```js var proxy = require('express-http-proxy'); - var app = require('express')(); app.use('/proxy', proxy('www.google.com')); ``` -### Options +### Streaming +Proxy requests and user responses are piped/streamed/chunked by default. +If you define a response modifier (userResDecorator, userResHeaderDecorator), +or need to inspect the response before continuing (maybeSkipToNext), streaming +is disabled, and the request and response are buffered. +This can cause performance issues with large payloads. -#### forwardPath +### Promises + +Many function hooks support Promises. +If any Promise is rejected, ```next(x)``` is called in the hosting application, where ```x``` is whatever you pass to ```Promise.reject```; -The ```forwardPath``` option allows you to modify the path prior to proxying the request. +e.g. ```js -var proxy = require('express-http-proxy'); + app.use(proxy('/reject-promise', { + proxyReqOptDecorator: function() { + return Promise.reject('An arbitrary rejection message.'); + } + })); +``` -var app = require('express')(); +eventually calls -app.use('/proxy', proxy('www.google.com', { - forwardPath: function(req, res) { - return require('url').parse(req.url).path; - } -})); +```js +next('An arbitrary rejection messasage'); ``` -#### forwardPathAsync -The ```forwardPathAsync``` options allows you to modify the path asyncronously prior to proxying the request, using Promises. +### Host + +The first positional argument is for the proxy host; in many cases you will use a static string here, eg. ```js -app.use(proxy('httpbin.org', { - forwardPathAsync: function() { - return new Promise(function(resolve, reject) { - // ... - // eventually - resolve( /* your resolved forwardPath as string */ ) +app.use('/', proxy('http://google.com')) +``` + +However, this argument can also be a function, and that function can be +memoized or computed on each request, based on the setting of +```memoizeHost```. + +```js +function selectProxyHost() { + return (new Date() % 2) ? 'http://google.com' : 'http://altavista.com'; +} + +app.use('/', proxy(selectProxyHost)); +``` +### Middleware mixing + +If you use 'https://www.npmjs.com/package/body-parser' you should declare it AFTER the proxy configuration, otherwise original 'POST' body could be modified and not proxied correctly. + +``` + +app.use('/proxy', 'http://foo.bar.com') + +// Declare use of body-parser AFTER the use of proxy +app.use(bodyParser.foo(bar)) +app.use('/api', ...) +``` + +### Options + +#### proxyReqPathResolver (supports Promises) + +Note: In ```express-http-proxy```, the ```path``` is considered the portion of +the url after the host, and including all query params. E.g. for the URL +```http://smoogle.com/search/path?q=123```; the path is +```/search/path?q=123```. Authors using this resolver must also handle the query parameter portion of the path. + +Provide a proxyReqPathResolver function if you'd like to +operate on the path before issuing the proxy request. Use a Promise for async +operations. + +```js + app.use(proxy('localhost:12345', { + proxyReqPathResolver: function (req) { + var parts = req.url.split('?'); + var queryString = parts[1]; + var updatedPath = parts[0].replace(/test/, 'tent'); + return updatedPath + (queryString ? '?' + queryString : ''); + } + })); +``` +Promise form + +```js +app.use('/proxy', proxy('localhost:12345', { + proxyReqPathResolver: function(req) { + return new Promise(function (resolve, reject) { + setTimeout(function () { // simulate async + var parts = req.url.split('?'); + var queryString = parts[1]; + var updatedPath = parts[0].replace(/test/, 'tent'); + var resolvedPathValue = updatedPath + (queryString ? '?' + queryString : ''); + resolve(resolvedPathValue); + }, 200); }); } })); ``` -#### filter +#### forwardPath + +DEPRECATED. See proxyReqPathResolver + +#### forwardPathAsync + +DEPRECATED. See proxyReqPathResolver + +#### filter (supports Promises) + +The ```filter``` option can be used to limit what requests are proxied. Return +```true``` to continue to execute proxy; return false-y to skip proxy for this +request. -The ```filter``` option can be used to limit what requests are proxied. For example, if you only want to proxy get request +For example, if you only want to proxy get request: ```js app.use('/proxy', proxy('www.google.com', { filter: function(req, res) { return req.method == 'GET'; - }, - forwardPath: function(req, res) { - return require('url').parse(req.url).path; } })); ``` -#### intercept +Promise form: -You can intercept the response before sending it back to the client. +```js + app.use(proxy('localhost:12346', { + filter: function (req, res) { + return new Promise(function (resolve) { + resolve(req.method === 'GET'); + }); + } + })); +``` + +Note that in the previous example, `resolve(false)` will execute the happy path +for filter here (skipping the rest of the proxy, and calling `next()`). +`reject()` will also skip the rest of proxy and call `next()`. + +#### userResDecorator (was: intercept) (supports Promise) + +You can modify the proxy's response before sending it to the client. ```js app.use('/proxy', proxy('www.google.com', { - intercept: function(rsp, data, req, res, callback) { - // rsp - original response from the target - data = JSON.parse(data.toString('utf8')); - callback(null, JSON.stringify(data)); + userResDecorator: function(proxyRes, proxyResData, userReq, userRes) { + data = JSON.parse(proxyResData.toString('utf8')); + data.newProperty = 'exciting data'; + return JSON.stringify(data); + } +})); +``` + +```js +app.use(proxy('httpbin.org', { + userResDecorator: function(proxyRes, proxyResData) { + return new Promise(function(resolve) { + proxyResData.funkyMessage = 'oi io oo ii'; + setTimeout(function() { + resolve(proxyResData); + }, 200); + }); } })); ``` +##### 304 - Not Modified + +When your proxied service returns 304, not modified, this step will be skipped, since there is no body to decorate. + +##### exploiting references +The intent is that this be used to modify the proxy response data only. + +Note: +The other arguments (proxyRes, userReq, userRes) are passed by reference, so +you *can* currently exploit this to modify either response's headers, for +instance, but this is not a reliable interface. I expect to close this +exploit in a future release, while providing an additional hook for mutating +the userRes before sending. + +##### gzip responses + +If your proxy response is gzipped, this program will automatically unzip +it before passing to your function, then zip it back up before piping it to the +user response. There is currently no way to short-circuit this behavior. + #### limit This sets the body size limit (default: `1mb`). If the body size is larger than the specified (or default) limit, @@ -104,7 +231,7 @@ app.use('/proxy', proxy('www.google.com', { Defaults to ```true```. When true, the ```host``` argument will be parsed on first request, and -memoized for all subsequent requests. +memoized for subsequent requests. When ```false```, ```host``` argument will be parsed on each request. @@ -128,29 +255,136 @@ request, and all additional requests would return the value resolved on the first request. +### userResHeaderDecorator + +When a `userResHeaderDecorator` is defined, the return of this method will replace (rather than be merged on to) the headers for `userRes`. + +```js +app.use('/proxy', proxy('www.google.com', { + userResHeaderDecorator(headers, userReq, userRes, proxyReq, proxyRes) { + // recieves an Object of headers, returns an Object of headers. + return headers; + } +})); +``` + + #### decorateRequest -You can change the request options before it is sent to the target. +REMOVED: See ```proxyReqOptDecorator``` and ```proxyReqBodyDecorator```. + + +#### skipToNextHandlerFilter(supports Promise form) +(experimental: this interface may change in upcoming versions) + +Allows you to inspect the proxy response, and decide if you want to continue processing (via express-http-proxy) or call ```next()``` to return control to express. ```js app.use('/proxy', proxy('www.google.com', { - decorateRequest: function(proxyReq, originalReq) { + skipToNextHandlerFilter: function(proxyRes) { + return proxyRes.statusCode === 404; + } +})); +``` + +### proxyErrorHandler + +By default, ```express-http-proxy``` will pass any errors except ECONNRESET to +next, so that your application can handle or react to them, or just drop +through to your default error handling. ECONNRESET errors are immediately +returned to the user for historical reasons. + +If you would like to modify this behavior, you can provide your own ```proxyErrorHandler```. + + +```js +// Example of skipping all error handling. + +app.use(proxy('localhost:12346', { + proxyErrorHandler: function(err, res, next) { + next(err); + } +})); + + +// Example of rolling your own + +app.use(proxy('localhost:12346', { + proxyErrorHandler: function(err, res, next) { + switch (err && err.code) { + case 'ECONNRESET': { return res.status(405).send('504 became 405'); } + case 'ECONNREFUSED': { return res.status(200).send('gotcher back'); } + default: { next(err); } + } +}})); +``` + + + +#### proxyReqOptDecorator (supports Promise form) + +You can override most request options before issuing the proxyRequest. +proxyReqOpt represents the options argument passed to the (http|https).request +module. + +NOTE: req.path cannot be changed via this method; use ```proxyReqPathResolver``` instead. (see https://github.com/villadora/express-http-proxy/issues/243) + +```js +app.use('/proxy', proxy('www.google.com', { + proxyReqOptDecorator: function(proxyReqOpts, srcReq) { // you can update headers - proxyReq.headers['Content-Type'] = 'text/html'; + proxyReqOpts.headers['Content-Type'] = 'text/html'; // you can change the method - proxyReq.method = 'GET'; - // you can munge the bodyContent. - proxyReq.bodyContent = proxyReq.bodyContent.replace(/losing/, 'winning!'); - return proxyReq; + proxyReqOpts.method = 'GET'; + return proxyReqOpts; } })); +``` + +You can use a Promise for async style. +```js +app.use('/proxy', proxy('www.google.com', { + proxyReqOptDecorator: function(proxyReqOpts, srcReq) { + return new Promise(function(resolve, reject) { + proxyReqOpts.headers['Content-Type'] = 'text/html'; + resolve(proxyReqOpts); + }) + } +})); +``` + +#### proxyReqBodyDecorator (supports Promise form) + +You can mutate the body content before sending the proxyRequest. + +```js +app.use('/proxy', proxy('www.google.com', { + proxyReqBodyDecorator: function(bodyContent, srcReq) { + return bodyContent.split('').reverse().join(''); + } +})); +``` + +You can use a Promise for async style. + +```js +app.use('/proxy', proxy('www.google.com', { + proxyReqBodyDecorator: function(proxyReq, srcReq) { + return new Promise(function(resolve, reject) { + http.get('http://dev/null', function (err, res) { + if (err) { reject(err); } + resolve(res); + }); + }) + } +})); ``` #### https -Normally, your proxy request will be made on the same protocol as the original -request. If you'd like to force the proxy request to be https, use this +Normally, your proxy request will be made on the same protocol as the `host` +parameter. If you'd like to force the proxy request to be https, use this option. ```js @@ -169,6 +403,38 @@ app.use('/proxy', proxy('www.google.com', { })); ``` +#### parseReqBody + +The ```parseReqBody``` option allows you to control parsing the request body. +For example, disabling body parsing is useful for large uploads where it would be inefficient +to hold the data in memory. + +##### Note: this setting is required for binary uploads. A future version of this library may handle this for you. + +This defaults to true in order to preserve legacy behavior. + +When false, no action will be taken on the body and accordingly ```req.body``` will no longer be set. + +Note that setting this to false overrides ```reqAsBuffer``` and ```reqBodyEncoding``` below. + +```js +app.use('/proxy', proxy('www.google.com', { + parseReqBody: false +})); +``` +You can use function instead of boolean value for dynamic value generation based on request + +```js +app.use('/proxy', proxy('www.google.com', { + parseReqBody: function (proxyReq) { + if (proxyReq.headers["content-type"] === "application/json") { + return true; + } else { + return false; + } + } +})); +``` #### reqAsBuffer @@ -181,6 +447,8 @@ This defaults to to false in order to preserve legacy behavior. Note that the value of ```reqBodyEnconding``` is used as the encoding when coercing strings (and stringified JSON) to Buffer. +Ignored if ```parseReqBody``` is set to false. + ```js app.use('/proxy', proxy('www.google.com', { reqAsBuffer: true @@ -196,16 +464,19 @@ Accept any values supported by [raw-body](https://www.npmjs.com/package/raw-body The same encoding is used in the intercept method. +Ignored if ```parseReqBody``` is set to false. + ```js app.use('/post', proxy('httpbin.org', { reqBodyEncoding: null })); ``` - #### timeout -By default, node does not express a timeout on connections. Use timeout option to impose a specific timeout. Timed-out requests will respond with 504 status code and a X-Timeout-Reason header. +By default, node does not express a timeout on connections. +Use timeout option to impose a specific timeout. +Timed-out requests will respond with 504 status code and a X-Timeout-Reason header. ```js app.use('/', proxy('httpbin.org', { @@ -213,37 +484,128 @@ app.use('/', proxy('httpbin.org', { })); ``` +## Trace debugging + +The node-debug module is used to provide a trace debugging capability. + +``` +DEBUG=express-http-proxy npm run YOUR_PROGRAM +DEBUG=express-http-proxy npm run YOUR_PROGRAM | grep 'express-http-proxy' # to filter down to just these messages +``` + +Will trace the execution of the express-http-proxy module in order to aide debugging. + -## Questions -### Q: Can it support https proxy? -The library will use https if the provided path has 'https://' or ':443'. You can use decorateRequest to ammend any auth or challenge headers required to succeed https. +## Upgrade to 1.0, transition guide and breaking changes +1. +```decorateRequest``` has been REMOVED, and will generate an error when called. See ```proxyReqOptDecorator``` and ```proxyReqBodyDecorator```. -Here is an older answer about using the https-proxy-agent package. It may be useful if the included functionality in ```http-express-proxy``` does not solve your use case. +Resolution: Most authors will simply need to change the method name for their +decorateRequest method; if author was decorating reqOpts and reqBody in the +same method, this will need to be split up. -A: Yes, you can use the 'https-proxy-agent' package. Something like this: + +2. +```intercept``` has been REMOVED, and will generate an error when called. See ```userResDecorator```. + +Resolution: Most authors will simply need to change the method name from ```intercept``` to ```userResDecorator```, and exit the method by returning the value, rather than passing it to a callback. E.g.: + +Before: ```js -var corporateProxyServer = process.env.HTTP_PROXY || process.env.http_proxy || process.env.HTTPS_PROXY || process.env.https_proxy; +app.use('/proxy', proxy('www.google.com', { + intercept: function(proxyRes, proxyResData, userReq, userRes, cb) { + data = JSON.parse(proxyResData.toString('utf8')); + data.newProperty = 'exciting data'; + cb(null, JSON.stringify(data)); + } +})); +``` -if (corporateProxyServer) { - corporateProxyAgent = new HttpsProxyAgent(corporateProxyServer); -} +Now: + +```js +app.use('/proxy', proxy('www.google.com', { + userResDecorator: function(proxyRes, proxyResData, userReq, userRes) { + data = JSON.parse(proxyResData.toString('utf8')); + data.newProperty = 'exciting data'; + return JSON.stringify(data); + } +})); +``` + +3. +```forwardPath``` and ```forwardPathAsync``` have been DEPRECATED and will generate a warning when called. See ```proxyReqPathResolver```. + +Resolution: Simple update the name of either ```forwardPath``` or ```forwardPathAsync``` to ```proxyReqPathResolver```. + +## When errors occur on your proxy server + +When your proxy server responds with an error, express-http-proxy returns a response with the same status code. See ```test/catchingErrors``` for syntax details. + +When your proxy server times out, express-http-proxy will continue to wait indefinitely for a response, unless you define a ```timeout``` as described above. + + +## Questions + +### Q: Does it support https proxy? + +The library will automatically use https if the provided path has 'https://' or ':443'. You may also set option ```https``` to true to always use https. + +You can use ```proxyReqOptDecorator``` to ammend any auth or challenge headers required to succeed https. + +### Q: How can I support non-standard certificate chains? + +You can use the ability to decorate the proxy request prior to sending. See ```proxyReqOptDecorator``` for more details. + +```js +app.use('/', proxy('internalhost.example.com', { + proxyReqOptDecorator: function(proxyReqOpts, originalReq) { + proxyReqOpts.ca = [caCert, intermediaryCert] + return proxyReqOpts; + } +}) ``` -Then inside the decorateRequest method, add the agent to the request: +### Q: How to ignore self-signed certificates ? + +You can set the `rejectUnauthorized` value in proxy request options prior to sending. See ```proxyReqOptDecorator``` for more details. ```js - req.agent = corporateProxyAgent; +app.use('/', proxy('internalhost.example.com', { + proxyReqOptDecorator: function(proxyReqOpts, originalReq) { + proxyReqOpts.rejectUnauthorized = false + return proxyReqOpts; + } +})) ``` + ## Release Notes | Release | Notes | | --- | --- | -| 0.11.1 | Allow author to prevent host from being memoized between requests. General program cleanup. | +| 1.6.2 | Update node.js versions used by ci. | +| 1.6.1 | Minor bug fixes and documentation. | +| 1.6.0 | Do gzip and gunzip aysyncronously. Test and documentation improvements, dependency updates. | +| 1.5.1 | Fixes bug in stringifying debug messages. | +| 1.5.0 | Fixes bug in `filter` signature. Fix bug in skipToNextHandler, add expressHttpProxy value to user res when skipped. Add tests for host as ip address. | +| 1.4.0 | DEPRECATED. Critical bug in the `filter` api.| +| 1.3.0 | DEPRECATED. Critical bug in the `filter` api. `filter` now supports Promises. Update linter to eslint. | +| 1.2.0 | Auto-stream when no decorations are made to req/res. Improved docs, fixes issues in maybeSkipToNexthandler, allow authors to manage error handling. | +| 1.1.0 | Add step to allow response headers to be modified. +| 1.0.7 | Update dependencies. Improve docs on promise rejection. Fix promise rejection on body limit. Improve debug output. | +| 1.0.6 | Fixes preserveHostHdr not working, skip userResDecorator on 304, add maybeSkipToNext, test improvements and cleanup. | +| 1.0.5 | Minor documentation and test patches | +| 1.0.4 | Minor documentation, test, and package fixes | +| 1.0.3 | Fixes 'limit option is not taken into account | +| 1.0.2 | Minor docs corrections. | +| 1.0.1 | Minor docs adjustments. | +| 1.0.0 | Major revision.
REMOVE decorateRequest, ADD proxyReqOptDecorator and proxyReqBodyDecorator.
REMOVE intercept, ADD userResDecorator
userResDecorator supports a Promise form for async operations.
General cleanup of structure and application of hooks. Documentation improvements. Update all dependencies. Re-organize code as a series of workflow steps, each (potentially) supporting a promise, and creating a reusable pattern for future development. | +| 0.11.0 | Allow author to prevent host from being memoized between requests. General program cleanup. | | 0.10.1| Fixed issue where 'body encoding' was being incorrectly set to the character encoding.
Dropped explicit support for node 0.10.
Intercept can now deal with gziped responses.
Author can now 'force https', even if the original request is over http.
Do not call next after ECONNRESET catch. | | 0.10.0 | Fix regression in forwardPath implementation. | | 0.9.1 | Documentation updates. Set 'Accept-Encoding' header to match bodyEncoding. | diff --git a/app/steps/buildProxyReq.js b/app/steps/buildProxyReq.js new file mode 100644 index 00000000..b8fa06f1 --- /dev/null +++ b/app/steps/buildProxyReq.js @@ -0,0 +1,26 @@ +'use strict'; + +var debug = require('debug')('express-http-proxy'); +var requestOptions = require('../../lib/requestOptions'); + +function buildProxyReq(Container) { + var req = Container.user.req; + var res = Container.user.res; + var options = Container.options; + var host = Container.proxy.host; + + var parseReqBody = (typeof options.parseReqBody === 'function') ? options.parseReqBody(req) : options.parseReqBody; + var parseBody = (!parseReqBody) ? Promise.resolve(null) : requestOptions.bodyContent(req, res, options); + var createReqOptions = requestOptions.create(req, res, options, host); + + return Promise + .all([parseBody, createReqOptions]) + .then(function(responseArray) { + Container.proxy.bodyContent = responseArray[0]; + Container.proxy.reqBuilder = responseArray[1]; + debug('proxy request options:', Container.proxy.reqBuilder); + return Container; + }); +} + +module.exports = buildProxyReq; diff --git a/app/steps/copyProxyResHeadersToUserRes.js b/app/steps/copyProxyResHeadersToUserRes.js new file mode 100644 index 00000000..825f5d80 --- /dev/null +++ b/app/steps/copyProxyResHeadersToUserRes.js @@ -0,0 +1,22 @@ +'use strict'; + +function copyProxyResHeadersToUserRes(container) { + return new Promise(function(resolve) { + var res = container.user.res; + var rsp = container.proxy.res; + + if (!res.headersSent) { + res.status(rsp.statusCode); + Object.keys(rsp.headers) + .filter(function(item) { return item !== 'transfer-encoding'; }) + .forEach(function(item) { + res.set(item, rsp.headers[item]); + }); + } + + resolve(container); + }); +} + +module.exports = copyProxyResHeadersToUserRes; + diff --git a/app/steps/decorateProxyReqBody.js b/app/steps/decorateProxyReqBody.js new file mode 100644 index 00000000..50930f4e --- /dev/null +++ b/app/steps/decorateProxyReqBody.js @@ -0,0 +1,25 @@ +'use strict'; + +var debug = require('debug')('express-http-proxy'); + +function defaultDecorator(proxyReqOptBody/*, userReq */) { + return proxyReqOptBody; +} + +function decorateProxyReqBody(container) { + var userDecorator = container.options.proxyReqBodyDecorator; + var resolverFn = userDecorator || defaultDecorator; + + if (userDecorator) { + debug('using custom proxyReqBodyDecorator'); + } + + return Promise + .resolve(resolverFn(container.proxy.bodyContent, container.user.req)) + .then(function(bodyContent) { + container.proxy.bodyContent = bodyContent; + return Promise.resolve(container); + }); +} + +module.exports = decorateProxyReqBody; diff --git a/app/steps/decorateProxyReqOpts.js b/app/steps/decorateProxyReqOpts.js new file mode 100644 index 00000000..0ee10479 --- /dev/null +++ b/app/steps/decorateProxyReqOpts.js @@ -0,0 +1,22 @@ +'use strict'; + +var debug = require('debug')('express-http-proxy'); + +function defaultDecorator(proxyReqOptBuilder /*, userReq */) { + return proxyReqOptBuilder; +} + +function decorateProxyReqOpt(container) { + var resolverFn = container.options.proxyReqOptDecorator || defaultDecorator; + + return Promise + .resolve(resolverFn(container.proxy.reqBuilder, container.user.req)) + .then(function (processedReqOpts) { + delete processedReqOpts.params; + container.proxy.reqBuilder = processedReqOpts; + debug('Request options (after processing): %o', processedReqOpts); + return Promise.resolve(container); + }); +} + +module.exports = decorateProxyReqOpt; diff --git a/app/steps/decorateUserRes.js b/app/steps/decorateUserRes.js new file mode 100644 index 00000000..5756f868 --- /dev/null +++ b/app/steps/decorateUserRes.js @@ -0,0 +1,86 @@ +'use strict'; + +var as = require('../../lib/as.js'); +var debug = require('debug')('express-http-proxy'); +var zlib = require('zlib'); + +function isResGzipped(res) { + return res.headers['content-encoding'] === 'gzip'; +} + +function zipOrUnzip(method) { + return function(rspData, res) { + return new Promise(function (resolve, reject) { + if (isResGzipped(res) && rspData.length) { + zlib[method](rspData, function(err, buffer) { + if(err) { + reject(err); + } else { + resolve(buffer); + } + }); + } else { + resolve(rspData); + } + }); + }; +} + +var maybeUnzipPromise = zipOrUnzip('gunzip'); +var maybeZipPromise = zipOrUnzip('gzip'); + +function verifyBuffer(rspd, reject) { + if (!Buffer.isBuffer(rspd)) { + return reject(new Error('userResDecorator should return string or buffer as data')); + } +} + +function updateHeaders(res, rspdBefore, rspdAfter, reject) { + if (!res.headersSent) { + res.set('content-length', rspdAfter.length); + } else if (rspdAfter.length !== rspdBefore.length) { + var error = '"Content-Length" is already sent,' + + 'the length of response data can not be changed'; + return reject(new Error(error)); + } +} + +function decorateProxyResBody(container) { + var resolverFn = container.options.userResDecorator; + + if (!resolverFn) { + return Promise.resolve(container); + } + + var proxyResDataPromise = maybeUnzipPromise(container.proxy.resData, container.proxy.res); + var proxyRes = container.proxy.res; + var req = container.user.req; + var res = container.user.res; + var originalResData; + + if (res.statusCode === 304) { + debug('Skipping userResDecorator on response 304'); + return Promise.resolve(container); + } + + return proxyResDataPromise + .then(function(proxyResData){ + originalResData = proxyResData; + return resolverFn(proxyRes, proxyResData, req, res); + }) + .then(function(modifiedResData) { + return new Promise(function(resolve, reject) { + var rspd = as.buffer(modifiedResData, container.options); + verifyBuffer(rspd, reject); + updateHeaders(res, originalResData, rspd, reject); + maybeZipPromise(rspd, container.proxy.res).then(function(buffer) { + container.proxy.resData = buffer; + resolve(container); + }).catch(function(error){ + reject(error); + }); + }); + }); +} + +module.exports = decorateProxyResBody; diff --git a/app/steps/decorateUserResHeaders.js b/app/steps/decorateUserResHeaders.js new file mode 100644 index 00000000..0e9d323e --- /dev/null +++ b/app/steps/decorateUserResHeaders.js @@ -0,0 +1,29 @@ +'use strict'; + + +function decorateUserResHeaders(container) { + var resolverFn = container.options.userResHeaderDecorator; + var headers = container.user.res.getHeaders ? container.user.res.getHeaders() : container.user.res._headers; + + if (!resolverFn) { + return Promise.resolve(container); + } + + const clearAllHeaders = (res) => { + for (const header in res._headers) { + res.removeHeader(header) + } + } + + return Promise + .resolve(resolverFn(headers, container.user.req, container.user.res, container.proxy.req, container.proxy.res)) + .then(function(headers) { + return new Promise(function(resolve) { + clearAllHeaders(container.user.res); + container.user.res.set(headers); + resolve(container); + }); + }); +} + +module.exports = decorateUserResHeaders; diff --git a/app/steps/filterUserRequest.js b/app/steps/filterUserRequest.js new file mode 100644 index 00000000..68f60be8 --- /dev/null +++ b/app/steps/filterUserRequest.js @@ -0,0 +1,24 @@ +'use strict'; + +// No-op version of filter. Allows everything! + +function defaultFilter(proxyReqOptBuilder, userReq) { // eslint-disable-line + return true; +} + +function filterUserRequest(container) { + var resolverFn = container.options.filter || defaultFilter; + + return Promise + .resolve(resolverFn(container.user.req, container.user.res)) + .then(function (shouldIContinue) { + if (shouldIContinue) { + return Promise.resolve(container); + } else { + return Promise.reject(); // reject with no args should simply call next() + } + }); +} + +module.exports = filterUserRequest; + diff --git a/app/steps/handleProxyErrors.js b/app/steps/handleProxyErrors.js new file mode 100644 index 00000000..7c40511c --- /dev/null +++ b/app/steps/handleProxyErrors.js @@ -0,0 +1,21 @@ +'use strict'; + +var debug = require('debug')('express-http-proxy'); + +function connectionResetHandler(err, res) { + if (err && err.code === 'ECONNRESET') { + debug('Error: Connection reset due to timeout.'); + res.setHeader('X-Timeout-Reason', 'express-http-proxy reset the request.'); + res.writeHead(504, {'Content-Type': 'text/plain'}); + res.end(); + } +} + +function handleProxyErrors(err, res, next) { + switch (err && err.code) { + case 'ECONNRESET': { return connectionResetHandler(err, res, next); } + default: { next(err); } + } +} + +module.exports = handleProxyErrors; diff --git a/app/steps/maybeSkipToNextHandler.js b/app/steps/maybeSkipToNextHandler.js new file mode 100644 index 00000000..9d70de14 --- /dev/null +++ b/app/steps/maybeSkipToNextHandler.js @@ -0,0 +1,22 @@ +'use strict'; + +function defaultSkipFilter(/* res */) { + return false; +} + +function maybeSkipToNextHandler(container) { + var resolverFn = container.options.skipToNextHandlerFilter || defaultSkipFilter; + + return Promise + .resolve(resolverFn(container.proxy.res)) + .then(function (shouldSkipToNext) { + if (shouldSkipToNext) { + container.user.res.expressHttpProxy = container.proxy; + return Promise.reject(container.user.next()); + } else { + return Promise.resolve(container); + } + }) +} + +module.exports = maybeSkipToNextHandler; diff --git a/app/steps/prepareProxyReq.js b/app/steps/prepareProxyReq.js new file mode 100644 index 00000000..511f0ded --- /dev/null +++ b/app/steps/prepareProxyReq.js @@ -0,0 +1,40 @@ +'use strict'; + +var as = require('../../lib/as'); + +function getContentLength(body) { + + var result; + if (Buffer.isBuffer(body)) { // Buffer + result = body.length; + } else if (typeof body === 'string') { + result = Buffer.byteLength(body); + } + return result; +} + + +function prepareProxyReq(container) { + return new Promise(function(resolve) { + var bodyContent = container.proxy.bodyContent; + var reqOpt = container.proxy.reqBuilder; + + if (bodyContent) { + bodyContent = container.options.reqAsBuffer ? + as.buffer(bodyContent, container.options) : + as.bufferOrString(bodyContent); + + reqOpt.headers['content-length'] = getContentLength(bodyContent); + + if (container.options.reqBodyEncoding) { + reqOpt.headers['Accept-Charset'] = container.options.reqBodyEncoding; + } + } + + container.proxy.bodyContent = bodyContent; + resolve(container); + }); +} + +module.exports = prepareProxyReq; + diff --git a/app/steps/resolveProxyHost.js b/app/steps/resolveProxyHost.js new file mode 100644 index 00000000..181dda7a --- /dev/null +++ b/app/steps/resolveProxyHost.js @@ -0,0 +1,19 @@ +'use strict'; +var requestOptions = require('../../lib/requestOptions'); + +function resolveProxyHost(container) { + var parsedHost; + + if (container.options.memoizeHost && container.options.memoizedHost) { + parsedHost = container.options.memoizedHost; + } else { + parsedHost = requestOptions.parseHost(container); + } + + container.proxy.reqBuilder.host = parsedHost.host; + container.proxy.reqBuilder.port = container.options.port || parsedHost.port; + container.proxy.requestModule = parsedHost.module; + return Promise.resolve(container); +} + +module.exports = resolveProxyHost; diff --git a/app/steps/resolveProxyReqPath.js b/app/steps/resolveProxyReqPath.js new file mode 100644 index 00000000..fe426b8a --- /dev/null +++ b/app/steps/resolveProxyReqPath.js @@ -0,0 +1,22 @@ +'use strict'; + +var url = require('url'); +var debug = require('debug')('express-http-proxy'); + +function defaultProxyReqPathResolver(req) { + return url.parse(req.url).path; +} + +function resolveProxyReqPath(container) { + var resolverFn = container.options.proxyReqPathResolver || defaultProxyReqPathResolver; + + return Promise + .resolve(resolverFn(container.user.req)) + .then(function(resolvedPath) { + container.proxy.reqBuilder.path = resolvedPath; + debug('resolved proxy path:', resolvedPath); + return Promise.resolve(container); + }); +} + +module.exports = resolveProxyReqPath; diff --git a/app/steps/sendProxyRequest.js b/app/steps/sendProxyRequest.js new file mode 100644 index 00000000..0f7a1b4c --- /dev/null +++ b/app/steps/sendProxyRequest.js @@ -0,0 +1,79 @@ +'use strict'; + +var chunkLength = require('../../lib/chunkLength'); + +function sendProxyRequest(Container) { + var req = Container.user.req; + var bodyContent = Container.proxy.bodyContent; + var reqOpt = Container.proxy.reqBuilder; + var options = Container.options; + + return new Promise(function(resolve, reject) { + var protocol = Container.proxy.requestModule; + var proxyReq = Container.proxy.req = protocol.request(reqOpt, function(rsp) { + if (options.stream) { + Container.proxy.res = rsp; + return resolve(Container); + } + + var chunks = []; + rsp.on('data', function(chunk) { chunks.push(chunk); }); + rsp.on('end', function() { + Container.proxy.res = rsp; + Container.proxy.resData = Buffer.concat(chunks, chunkLength(chunks)); + resolve(Container); + }); + rsp.on('error', reject); + }); + + proxyReq.on('socket', function(socket) { + if (options.timeout) { + socket.setTimeout(options.timeout, function() { + proxyReq.abort(); + }); + } + }); + + proxyReq.on('error', reject); + + var parseReqBody = (typeof options.parseReqBody === 'function') ? options.parseReqBody(req) : options.parseReqBody; + // this guy should go elsewhere, down the chain + if (parseReqBody) { + // We are parsing the body ourselves so we need to write the body content + // and then manually end the request. + + //if (bodyContent instanceof Object) { + //throw new Error + //debugger; + //bodyContent = JSON.stringify(bodyContent); + //} + + if (bodyContent.length) { + var body = bodyContent; + var contentType = proxyReq.getHeader('Content-Type'); + if (contentType === 'x-www-form-urlencoded' || contentType === 'application/x-www-form-urlencoded') { + try { + var params = JSON.parse(body); + body = Object.keys(params).map(function(k) { return k + '=' + params[k]; }).join('&'); + } catch (e) { + // bodyContent is not json-format + } + } + proxyReq.setHeader('Content-Length', Buffer.byteLength(body)); + proxyReq.write(body); + } + proxyReq.end(); + } else { + // Pipe will call end when it has completely read from the request. + req.pipe(proxyReq); + } + + req.on('aborted', function() { + // reject? + proxyReq.abort(); + }); + }); +} + + +module.exports = sendProxyRequest; diff --git a/app/steps/sendUserRes.js b/app/steps/sendUserRes.js new file mode 100644 index 00000000..0f9e9c1c --- /dev/null +++ b/app/steps/sendUserRes.js @@ -0,0 +1,15 @@ +'use strict'; + +function sendUserRes(Container) { + if (!Container.user.res.headersSent) { + if (Container.options.stream) { + Container.proxy.res.pipe(Container.user.res); + } else { + Container.user.res.send(Container.proxy.resData); + } + } + return Promise.resolve(Container); +} + + +module.exports = sendUserRes; diff --git a/index.js b/index.js index a79f2d77..f8129754 100644 --- a/index.js +++ b/index.js @@ -1,344 +1,61 @@ 'use strict'; -var assert = require('assert'); -var url = require('url'); -var http = require('http'); -var https = require('https'); -var getRawBody = require('raw-body'); -var zlib = require('zlib'); - - -function unset(val) { - return (typeof val === 'undefined' || val === '' || val === null); -} +// * Breaks proxying into a series of discrete steps, many of which can be swapped out by authors. +// * Uses Promises to support async. +// * Uses a quasi-Global called Container to tidy up the argument passing between the major work-flow steps. -module.exports = function proxy(host, options) { +var ScopeContainer = require('./lib/scopeContainer'); +var assert = require('assert'); +var debug = require('debug')('express-http-proxy'); + +var buildProxyReq = require('./app/steps/buildProxyReq'); +var copyProxyResHeadersToUserRes = require('./app/steps/copyProxyResHeadersToUserRes'); +var decorateProxyReqBody = require('./app/steps/decorateProxyReqBody'); +var decorateProxyReqOpts = require('./app/steps/decorateProxyReqOpts'); +var decorateUserRes = require('./app/steps/decorateUserRes'); +var decorateUserResHeaders = require('./app/steps/decorateUserResHeaders'); +var filterUserRequest = require('./app/steps/filterUserRequest'); +var handleProxyErrors = require('./app/steps/handleProxyErrors'); +var maybeSkipToNextHandler = require('./app/steps/maybeSkipToNextHandler'); +var prepareProxyReq = require('./app/steps/prepareProxyReq'); +var resolveProxyHost = require('./app/steps/resolveProxyHost'); +var resolveProxyReqPath = require('./app/steps/resolveProxyReqPath'); +var sendProxyRequest = require('./app/steps/sendProxyRequest'); +var sendUserRes = require('./app/steps/sendUserRes'); + +module.exports = function proxy(host, userOptions) { assert(host, 'Host should not be empty'); - options = options || {}; - - var parsedHost; - - /** - * Function :: intercept(targetResponse, data, res, req, function(err, json, sent)); - */ - var intercept = options.intercept; - var decorateRequest = options.decorateRequest; - var forwardPath = options.forwardPath || defaultForwardPath; - var resolveProxyPathAsync = options.forwardPathAsync || defaultForwardPathAsync(forwardPath); - var filter = options.filter || defaultFilter; - var limit = options.limit || '1mb'; - var preserveReqSession = options.preserveReqSession; - var memoizeHost = unset(options.memoizeHost) ? true : options.memoizeHost; - var rejectUnauthorized = !options.skipCertificateValidation; - return function handleProxy(req, res, next) { - if (!filter(req, res)) { return next(); } - var resolvePath = resolveProxyPathAsync(req, res); - var parseBody = maybeParseBody(req, limit); - var prepareRequest = Promise.all([resolvePath, parseBody]); - prepareRequest.then(function(results) { - var path = results[0]; - var bodyContent = results[1]; - sendProxyRequest(req, res, next, path, bodyContent); - }); - }; - - function sendProxyRequest(req, res, next, path, bodyContent) { - parsedHost = (memoizeHost && parsedHost) ? parsedHost : parseHost(host, req, options); - - var reqOpt = { - hostname: parsedHost.host, - port: options.port || parsedHost.port, - headers: reqHeaders(req, options), - method: req.method, - path: path, - bodyContent: bodyContent, - params: req.params, - rejectUnauthorized - }; - - if (preserveReqSession) { - reqOpt.session = req.session; - } + debug('[start proxy] ' + req.path); + var container = new ScopeContainer(req, res, next, host, userOptions); + + filterUserRequest(container) + .then(buildProxyReq) + .then(resolveProxyHost) + .then(decorateProxyReqOpts) + .then(resolveProxyReqPath) + .then(decorateProxyReqBody) + .then(prepareProxyReq) + .then(sendProxyRequest) + .then(maybeSkipToNextHandler) + .then(copyProxyResHeadersToUserRes) + .then(decorateUserResHeaders) + .then(decorateUserRes) + .then(sendUserRes) + .catch(function (err) { + // I sometimes reject without an error to shortcircuit the remaining + // steps and return control to the host application. - if (decorateRequest) { - reqOpt = decorateRequest(reqOpt, req) || reqOpt; - } - - bodyContent = reqOpt.bodyContent; - delete reqOpt.bodyContent; - delete reqOpt.params; - - bodyContent = options.reqAsBuffer ? - asBuffer(bodyContent, options) : - asBufferOrString(bodyContent); - - reqOpt.headers['content-length'] = getContentLength(bodyContent); - - if (bodyEncoding(options)) { - reqOpt.headers['Accept-Charset'] = bodyEncoding(options); - } - - - function postIntercept(res, next, rspData) { - return function(err, rspd, sent) { if (err) { - return next(err); - } - rspd = asBuffer(rspd, options); - rspd = maybeZipResponse(rspd, res); - - if (!Buffer.isBuffer(rspd)) { - next(new Error('intercept should return string or' + - 'buffer as data')); - } - - if (!res.headersSent) { - res.set('content-length', rspd.length); - } else if (rspd.length !== rspData.length) { - var error = '"Content-Length" is already sent,' + - 'the length of response data can not be changed'; - next(new Error(error)); - } - - if (!sent) { - res.send(rspd); - } - }; - } - - var proxyTargetRequest = parsedHost.module.request(reqOpt, function(rsp) { - var chunks = []; - - rsp.on('data', function(chunk) { - chunks.push(chunk); - }); - - rsp.on('end', function() { - - var rspData = Buffer.concat(chunks, chunkLength(chunks)); - - if (intercept) { - rspData = maybeUnzipResponse(rspData, res); - var callback = postIntercept(res, next, rspData); - intercept(rsp, rspData, req, res, callback); + var resolver = (container.options.proxyErrorHandler) ? + container.options.proxyErrorHandler : + handleProxyErrors; + resolver(err, res, next); } else { - // see issue https://github.com/villadora/express-http-proxy/issues/104 - // Not sure how to automate tests on this line, so be careful when changing. - if (!res.headersSent) { - res.send(rspData); - } + next(); } }); - - rsp.on('error', function(err) { - next(err); - }); - - if (!res.headersSent) { - res.status(rsp.statusCode); - Object.keys(rsp.headers) - .filter(function(item) { return item !== 'transfer-encoding'; }) - .forEach(function(item) { - res.set(item, rsp.headers[item]); - }); - } - }); - - proxyTargetRequest.on('socket', function(socket) { - if (options.timeout) { - socket.setTimeout(options.timeout, function() { - proxyTargetRequest.abort(); - }); - } - }); - - proxyTargetRequest.on('error', function(err) { - if (err.code === 'ECONNRESET') { - res.setHeader('X-Timout-Reason', - 'express-http-proxy timed out your request after ' + - options.timeout + 'ms.'); - res.writeHead(504, {'Content-Type': 'text/plain'}); - res.end(); - } else { - next(err); - } - }); - - if (bodyContent.length) { - proxyTargetRequest.write(bodyContent); - } - - proxyTargetRequest.end(); - - req.on('aborted', function() { - proxyTargetRequest.abort(); - }); - } -}; - - - -function extend(obj, source, skips) { - - if (!source) { - return obj; - } - - for (var prop in source) { - if (!skips || skips.indexOf(prop) === -1) { - obj[prop] = source[prop]; - } - } - - return obj; -} - -function parseHost(host, req, options) { - - host = (typeof host === 'function') ? host(req) : host.toString(); - - if (!host) { - return new Error('Empty host parameter'); - } - - if (!/http(s)?:\/\//.test(host)) { - host = 'http://' + host; - } - - var parsed = url.parse(host); - - if (!parsed.hostname) { - return new Error('Unable to parse hostname, possibly missing protocol://?'); - } - - var ishttps = options.https || parsed.protocol === 'https:'; - - return { - host: parsed.hostname, - port: parsed.port || (ishttps ? 443 : 80), - module: ishttps ? https : http, - }; -} - -function reqHeaders(req, options) { - - - var headers = options.headers || {}; - - var skipHdrs = [ 'connection', 'content-length' ]; - if (!options.preserveHostHdr) { - skipHdrs.push('host'); - } - var hds = extend(headers, req.headers, skipHdrs); - hds.connection = 'close'; - - return hds; -} - -function defaultFilter() { - // No-op version of filter. Allows everything! - - return true; -} - -function defaultForwardPath(req) { - - return url.parse(req.url).path; -} - -function bodyEncoding(options) { - - - /* For reqBodyEncoding, these is a meaningful difference between null and - * undefined. null should be passed forward as the value of reqBodyEncoding, - * and undefined should result in utf-8. - */ - - return options.reqBodyEncoding !== undefined ? options.reqBodyEncoding: 'utf-8'; -} - - -function chunkLength(chunks) { - - - return chunks.reduce(function(len, buf) { - return len + buf.length; - }, 0); -} - -function defaultForwardPathAsync(forwardPath) { - - return function(req, res) { - return new Promise(function(resolve) { - resolve(forwardPath(req, res)); - }); }; -} - -function asBuffer(body, options) { - - var ret; - if (Buffer.isBuffer(body)) { - ret = body; - } else if (typeof body === 'object') { - ret = new Buffer(JSON.stringify(body), bodyEncoding(options)); - } else if (typeof body === 'string') { - ret = new Buffer(body, bodyEncoding(options)); - } - return ret; -} - -function asBufferOrString(body) { - - var ret; - if (Buffer.isBuffer(body)) { - ret = body; - } else if (typeof body === 'object') { - ret = JSON.stringify(body); - } else if (typeof body === 'string') { - ret = body; - } - return ret; -} - -function getContentLength(body) { - - var result; - if (Buffer.isBuffer(body)) { // Buffer - result = body.length; - } else if (typeof body === 'string') { - result = Buffer.byteLength(body); - } - return result; -} - -function isResGzipped(res) { - return res._headers['content-encoding'] === 'gzip'; -} - -function zipOrUnzip(method) { - return function(rspData, res) { - return (isResGzipped(res)) ? zlib[method](rspData) : rspData; - }; -} - -function maybeParseBody(req, limit) { - var promise; - if (req._body && req.body) { - promise = new Promise(function(resolve) { - resolve(req.body); - }); - } else { - // Returns a promise if no callback specified and global Promise exists. - promise = getRawBody(req, { - length: req.headers['content-length'], - limit: limit, - }); - } - return promise; -} +}; -var maybeUnzipResponse = zipOrUnzip('gunzipSync'); -var maybeZipResponse = zipOrUnzip('gzipSync'); diff --git a/lib/as.js b/lib/as.js new file mode 100644 index 00000000..1825fb93 --- /dev/null +++ b/lib/as.js @@ -0,0 +1,36 @@ +'use strict'; + +/* + * Trivial convenience methods for parsing Buffers + */ + +function asBuffer(body, options) { + + var ret; + if (Buffer.isBuffer(body)) { + ret = body; + } else if (typeof body === 'object') { + ret = new Buffer(JSON.stringify(body), options.reqBodyEncoding); + } else if (typeof body === 'string') { + ret = new Buffer(body, options.reqBodyEncoding); + } + return ret; +} + +function asBufferOrString(body) { + + var ret; + if (Buffer.isBuffer(body)) { + ret = body; + } else if (typeof body === 'object') { + ret = JSON.stringify(body); + } else if (typeof body === 'string') { + ret = body; + } + return ret; +} + +module.exports = { + buffer: asBuffer, + bufferOrString: asBufferOrString +}; diff --git a/lib/chunkLength.js b/lib/chunkLength.js new file mode 100644 index 00000000..7b361231 --- /dev/null +++ b/lib/chunkLength.js @@ -0,0 +1,9 @@ +'use strict'; + +function chunkLength(chunks) { + return chunks.reduce(function (len, buf) { + return len + buf.length; + }, 0); +} + +module.exports = chunkLength; diff --git a/lib/isUnset.js b/lib/isUnset.js new file mode 100644 index 00000000..9c1162a6 --- /dev/null +++ b/lib/isUnset.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = function (val) { + return (typeof val === 'undefined' || val === '' || val === null); +}; diff --git a/lib/mockHTTP.js b/lib/mockHTTP.js index 8d868903..4b2aed52 100644 --- a/lib/mockHTTP.js +++ b/lib/mockHTTP.js @@ -1,3 +1,5 @@ +'use strict'; + var app = require('express')(); app.use('/status/:status', function (req, res) { diff --git a/lib/requestOptions.js b/lib/requestOptions.js new file mode 100644 index 00000000..24a668f9 --- /dev/null +++ b/lib/requestOptions.js @@ -0,0 +1,114 @@ +'use strict'; +var http = require('http'); +var https = require('https'); +var url = require('url'); +var getRawBody = require('raw-body'); +var isUnset = require('./isUnset'); + +function extend(obj, source, skips) { + + if (!source) { + return obj; + } + + for (var prop in source) { + if (!skips || skips.indexOf(prop) === -1) { + obj[prop] = source[prop]; + } + } + + return obj; +} + +function parseHost(Container) { + var host = Container.params.host; + var req = Container.user.req; + var options = Container.options; + host = (typeof host === 'function') ? host(req) : host.toString(); + + if (!host) { + return new Error('Empty host parameter'); + } + + if (!/http(s)?:\/\//.test(host)) { + host = 'http://' + host; + } + + var parsed = url.parse(host); + + if (!parsed.hostname) { + return new Error('Unable to parse hostname, possibly missing protocol://?'); + } + + var ishttps = options.https || parsed.protocol === 'https:'; + + return { + host: parsed.hostname, + port: parsed.port || (ishttps ? 443 : 80), + module: ishttps ? https : http, + }; +} + +function reqHeaders(req, options) { + + + var headers = options.headers || {}; + + var skipHdrs = [ 'connection', 'content-length' ]; + if (!options.preserveHostHdr) { + skipHdrs.push('host'); + } + var hds = extend(headers, req.headers, skipHdrs); + hds.connection = 'close'; + + return hds; +} + +function createRequestOptions(req, res, options) { + + // prepare proxyRequest + + var reqOpt = { + headers: reqHeaders(req, options), + method: req.method, + path: req.path, + params: req.params, + }; + + if (options.preserveReqSession) { + reqOpt.session = req.session; + } + + return Promise.resolve(reqOpt); +} + +// extract to bodyContent object + +function bodyContent(req, res, options) { + var parseReqBody = isUnset(options.parseReqBody) ? true : options.parseReqBody; + parseReqBody = (typeof parseReqBody === 'function') ? parseReqBody(req) : options.parseReqBody; + + function maybeParseBody(req, limit) { + if (req.body) { + return Promise.resolve(req.body); + } else { + // Returns a promise if no callback specified and global Promise exists. + + return getRawBody(req, { + length: req.headers['content-length'], + limit: limit, + }); + } + } + + if (parseReqBody) { + return maybeParseBody(req, options.limit); + } +} + + +module.exports = { + create: createRequestOptions, + bodyContent: bodyContent, + parseHost: parseHost +}; diff --git a/lib/resolveOptions.js b/lib/resolveOptions.js new file mode 100644 index 00000000..e1cbe8ca --- /dev/null +++ b/lib/resolveOptions.js @@ -0,0 +1,79 @@ +'use strict'; + +var debug = require('debug')('express-http-proxy'); + +var isUnset = require('../lib/isUnset'); + +function resolveBodyEncoding(reqBodyEncoding) { + + /* For reqBodyEncoding, these is a meaningful difference between null and + * undefined. null should be passed forward as the value of reqBodyEncoding, + * and undefined should result in utf-8. + */ + return reqBodyEncoding !== undefined ? reqBodyEncoding : 'utf-8'; +} + +// parse client arguments + +function resolveOptions(options) { + options = options || {}; + var resolved; + + if (options.decorateRequest) { + throw new Error( + 'decorateRequest is REMOVED; use proxyReqOptDecorator and' + + ' proxyReqBodyDecorator instead. see README.md' + ); + } + + if (options.forwardPath || options.forwardPathAsync) { + console.warn( + 'forwardPath and forwardPathAsync are DEPRECATED' + + ' and should be replaced with proxyReqPathResolver' + ); + } + + if (options.intercept) { + console.warn( + 'DEPRECATED: intercept. Use userResDecorator instead.' + + ' Please see README for more information.' + ); + } + + resolved = { + limit: options.limit || '1mb', + proxyReqPathResolver: options.proxyReqPathResolver + || options.forwardPathAsync + || options.forwardPath, + proxyReqOptDecorator: options.proxyReqOptDecorator, + proxyReqBodyDecorator: options.proxyReqBodyDecorator, + userResDecorator: options.userResDecorator || options.intercept, + userResHeaderDecorator: options.userResHeaderDecorator, + proxyErrorHandler: options.proxyErrorHandler, + filter: options.filter, + // For backwards compatability, we default to legacy behavior for newly added settings. + + parseReqBody: isUnset(options.parseReqBody) ? true : options.parseReqBody, + preserveHostHdr: options.preserveHostHdr, + memoizeHost: isUnset(options.memoizeHost) ? true : options.memoizeHost, + reqBodyEncoding: resolveBodyEncoding(options.reqBodyEncoding), + skipToNextHandlerFilter: options.skipToNextHandlerFilter, + headers: options.headers, + preserveReqSession: options.preserveReqSession, + https: options.https, + port: options.port, + reqAsBuffer: options.reqAsBuffer, + timeout: options.timeout + }; + + // automatically opt into stream mode if no response modifiers are specified + + resolved.stream = !resolved.skipToNextHandlerFilter && + !resolved.userResDecorator && + !resolved.userResHeaderDecorator; + + debug(resolved); + return resolved; +} + +module.exports = resolveOptions; diff --git a/lib/scopeContainer.js b/lib/scopeContainer.js new file mode 100644 index 00000000..2bf5b0ad --- /dev/null +++ b/lib/scopeContainer.js @@ -0,0 +1,35 @@ +'use strict'; +var resolveOptions = require('../lib/resolveOptions'); + +// The Container object is passed down the chain of Promises, with each one +// of them mutating and returning Container. The goal is that (eventually) +// author using this library // could hook into/override any of these +// workflow steps with a Promise or simple function. +// Container for scoped arguments in a promise chain. Each promise recieves +// this as its argument, and returns it. +// +// Do not expose the details of this to hooks/user functions. + +function Container(req, res, next, host, userOptions) { + return { + user: { + req: req, + res: res, + next: next, + }, + proxy: { + req: undefined, + res: undefined, + resData: undefined, // from proxy res + bodyContent: undefined, // for proxy req + reqBuilder: {}, // reqOpt, intended as first arg to http(s)?.request + }, + options: resolveOptions(userOptions), + params: { + host: host, + userOptions: userOptions + } + }; +} + +module.exports = Container; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..c542e5a8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2706 @@ +{ + "name": "express-http-proxy", + "version": "1.6.2", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "dev": true, + "requires": { + "mime-types": "~2.1.18", + "negotiator": "0.6.1" + } + }, + "acorn": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", + "dev": true + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "^3.0.4" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "ajv-keywords": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", + "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", + "dev": true + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-escapes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", + "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array.prototype.map": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.2.tgz", + "integrity": "sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.4" + } + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "dev": true + }, + "body-parser": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "dev": true, + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "^0.2.0" + } + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "chai": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", + "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", + "dev": true, + "requires": { + "assertion-error": "^1.0.1", + "check-error": "^1.0.1", + "deep-eql": "^3.0.0", + "get-func-name": "^2.0.0", + "pathval": "^1.0.0", + "type-detect": "^4.0.0" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "chokidar": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz", + "integrity": "sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.3.0" + } + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", + "dev": true + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "dev": true + }, + "cookie-parser": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.3.tgz", + "integrity": "sha1-D+MfoZ0AC5X0qt8fU/3CuKIDuqU=", + "dev": true, + "requires": { + "cookie": "0.3.1", + "cookie-signature": "1.0.6" + } + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "debug": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "requires": { + "globby": "^5.0.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "rimraf": "^2.2.8" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", + "dev": true + }, + "es-get-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz", + "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==", + "dev": true, + "requires": { + "es-abstract": "^1.17.4", + "has-symbols": "^1.0.1", + "is-arguments": "^1.0.4", + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-string": "^1.0.5", + "isarray": "^2.0.5" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es6-promise": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.5.tgz", + "integrity": "sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "4.19.1", + "resolved": "http://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz", + "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==", + "dev": true, + "requires": { + "ajv": "^5.3.0", + "babel-code-frame": "^6.22.0", + "chalk": "^2.1.0", + "concat-stream": "^1.6.0", + "cross-spawn": "^5.1.0", + "debug": "^3.1.0", + "doctrine": "^2.1.0", + "eslint-scope": "^3.7.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^3.5.4", + "esquery": "^1.0.0", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.0.1", + "ignore": "^3.3.3", + "imurmurhash": "^0.1.4", + "inquirer": "^3.0.6", + "is-resolvable": "^1.0.0", + "js-yaml": "^3.9.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.4", + "minimatch": "^3.0.2", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "pluralize": "^7.0.0", + "progress": "^2.0.0", + "regexpp": "^1.0.1", + "require-uncached": "^1.0.3", + "semver": "^5.3.0", + "strip-ansi": "^4.0.0", + "strip-json-comments": "~2.0.1", + "table": "4.0.2", + "text-table": "~0.2.0" + } + }, + "eslint-scope": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz", + "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "dev": true + }, + "espree": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", + "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", + "dev": true, + "requires": { + "acorn": "^5.5.0", + "acorn-jsx": "^3.0.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "express": { + "version": "4.16.3", + "resolved": "http://registry.npmjs.org/express/-/express-4.16.3.tgz", + "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", + "dev": true, + "requires": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.2", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.3", + "qs": "6.5.1", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.1", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "body-parser": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "dev": true, + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.1", + "http-errors": "~1.6.2", + "iconv-lite": "0.4.19", + "on-finished": "~2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "~1.6.15" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "dev": true + }, + "raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "dev": true, + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + }, + "dependencies": { + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", + "dev": true + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "dev": true, + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": ">= 1.3.1 < 2" + } + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", + "dev": true + } + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "dev": true + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "dev": true + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "external-editor": { + "version": "2.2.0", + "resolved": "http://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "dev": true, + "requires": { + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" + } + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "dev": true + } + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "dev": true, + "requires": { + "is-buffer": "~2.0.3" + } + }, + "flat-cache": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", + "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "del": "^2.0.2", + "graceful-fs": "^4.1.2", + "write": "^0.2.1" + } + }, + "form-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.0.tgz", + "integrity": "sha512-WXieX3G/8side6VIqx44ablyULoGruSde5PNTxoUyo5CeyAMX6nVWUd0rgist/EuX655cjhUhTo1Fo3tRYqbcA==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", + "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", + "dev": true + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz", + "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==", + "dev": true + }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "inquirer": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", + "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.0.4", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rx-lite": "^4.0.8", + "rx-lite-aggregates": "^4.0.8", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + } + }, + "ipaddr.js": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", + "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=", + "dev": true + }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz", + "integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "dev": true, + "requires": { + "is-path-inside": "^1.0.0" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", + "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "is-set": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz", + "integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==", + "dev": true + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "iterate-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.1.tgz", + "integrity": "sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw==", + "dev": true + }, + "iterate-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz", + "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==", + "dev": true, + "requires": { + "es-get-iterator": "^1.0.2", + "iterate-iterator": "^1.0.1" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "dev": true + }, + "log-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "lru-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "dev": true + }, + "mime-db": { + "version": "1.36.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", + "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==", + "dev": true + }, + "mime-types": { + "version": "2.1.20", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", + "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", + "dev": true, + "requires": { + "mime-db": "~1.36.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "mocha": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.0.1.tgz", + "integrity": "sha512-vefaXfdYI8+Yo8nPZQQi0QO2o+5q9UIMX1jZ1XMmK3+4+CQjc7+B0hPdUeglXiTlr8IHMVRo63IhO9Mzt6fxOg==", + "dev": true, + "requires": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.3.1", + "debug": "3.2.6", + "diff": "4.0.2", + "escape-string-regexp": "1.0.5", + "find-up": "4.1.0", + "glob": "7.1.6", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "3.0.0", + "minimatch": "3.0.4", + "ms": "2.1.2", + "object.assign": "4.1.0", + "promise.allsettled": "1.0.2", + "serialize-javascript": "3.0.0", + "strip-json-comments": "3.0.1", + "supports-color": "7.1.0", + "which": "2.0.2", + "wide-align": "1.1.3", + "workerpool": "6.0.0", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "1.6.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "strip-json-comments": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", + "dev": true + }, + "nock": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz", + "integrity": "sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==", + "dev": true, + "requires": { + "chai": "^4.1.2", + "debug": "^4.1.0", + "deep-equal": "^1.0.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.5", + "mkdirp": "^0.5.0", + "propagate": "^1.0.0", + "qs": "^6.5.1", + "semver": "^5.5.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pluralize": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", + "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "progress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "dev": true + }, + "promise.allsettled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.2.tgz", + "integrity": "sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg==", + "dev": true, + "requires": { + "array.prototype.map": "^1.0.1", + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "iterate-value": "^1.0.0" + } + }, + "propagate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", + "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", + "dev": true + }, + "proxy-addr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", + "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", + "dev": true, + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.8.0" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", + "dev": true + }, + "raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz", + "integrity": "sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==", + "dev": true, + "requires": { + "picomatch": "^2.0.7" + } + }, + "regexpp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", + "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==", + "dev": true + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "^7.0.5" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, + "rx-lite": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", + "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", + "dev": true + }, + "rx-lite-aggregates": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", + "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", + "dev": true, + "requires": { + "rx-lite": "*" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", + "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==", + "dev": true + }, + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "dev": true + } + } + }, + "serialize-javascript": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.0.0.tgz", + "integrity": "sha512-skZcHYw2vEX4bw90nAr2iTTsz6x2SrHEnfxgKYmZlvJYBEZrvbKtobJWlQ20zczKb3bsHHXXTYt48zBA7ni9cw==", + "dev": true + }, + "serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "slice-ansi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", + "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + } + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + } + }, + "supertest": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-3.4.2.tgz", + "integrity": "sha512-WZWbwceHUo2P36RoEIdXvmqfs47idNNZjCuJOqDz6rvtkk8ym56aU5oglORCpPeXGxT7l9rkJ41+O1lffQXYSA==", + "dev": true, + "requires": { + "methods": "^1.1.2", + "superagent": "^3.8.3" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "table": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", + "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", + "dev": true, + "requires": { + "ajv": "^5.2.3", + "ajv-keywords": "^2.1.0", + "chalk": "^2.1.0", + "lodash": "^4.17.4", + "slice-ansi": "1.0.0", + "string-width": "^2.1.1" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "workerpool": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.0.tgz", + "integrity": "sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA==", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yargs-unparser": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", + "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "dev": true, + "requires": { + "flat": "^4.1.0", + "lodash": "^4.17.15", + "yargs": "^13.3.0" + } + } + } +} diff --git a/package.json b/package.json index 5ddf7a60..6225700c 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,16 @@ { "name": "front-express-http-proxy", - "version": "0.11.0-2", + "version": "1.6.2-1", "description": "http proxy middleware for express", "engines": { - "node": ">=4.0.0" + "node": ">=6.0.0" }, - "engineStrict": true, "main": "index.js", "scripts": { - "test": "npm -s run mocha && npm run -s lint && npm run -s jscs", - "test:debug": "mocha debug -R spec test/*.js", - "mocha": "mocha -R spec test/*.js", - "lint": "jshint index.js test/*.js", - "jscs": "jscs index.js test/*.js" + "test": "npm -s run mocha && npm run -s lint", + "test:debug": "mocha debug -R spec test --recursive --exit", + "mocha": "mocha -R spec test --recursive --exit", + "lint": "eslint index.js **/*js" }, "repository": { "type": "git", @@ -30,16 +28,19 @@ "url": "https://github.com/villadora/express-http-proxy/issues" }, "devDependencies": { - "body-parser": "^1.15.2", - "express": "^4.3.1", - "jscs": "^3.0.7", - "jshint": "^2.5.5", - "mocha": "^2.1.0", - "supertest": "^1.2.0" + "body-parser": "^1.17.2", + "chai": "^4.1.2", + "cookie-parser": "^1.4.3", + "eslint": "^4.19.1", + "express": "^4.15.4", + "mocha": "^8.0.1", + "nock": "^10.0.6", + "supertest": "^3.4.2" }, "dependencies": { - "es6-promise": "^3.2.1", - "raw-body": "^2.1.7" + "debug": "^3.0.1", + "es6-promise": "^4.1.1", + "raw-body": "^2.3.0" }, "contributors": [ { diff --git a/test/bodyEncoding.js b/test/bodyEncoding.js index 1deb5dd9..8cce4d25 100644 --- a/test/bodyEncoding.js +++ b/test/bodyEncoding.js @@ -1,72 +1,204 @@ +'use strict'; var assert = require('assert'); var express = require('express'); var request = require('supertest'); var fs = require('fs'); var os = require('os'); var proxy = require('../'); +var startProxyTarget = require('./support/proxyTarget'); -describe('body encoding', function() { - 'use strict'; - this.timeout(10000); - var app; +describe('body encoding', function () { + var server; + + before(function () { + server = startProxyTarget(8109, 1000); + }); - beforeEach(function() { - app = express(); - app.use(proxy('httpbin.org')); + after(function () { + server.close(); }); + this.timeout(10000); + + var pngHex = '89504e470d0a1a0a0' + + '000000d4948445200' + + '00000100000001080' + + '60000001f15c48900' + + '00000a49444154789' + + 'c6300010000050001' + + '0d0a2db4000000004' + + '9454e44ae426082'; + var pngData = new Buffer(pngHex, 'hex'); - it('allow raw data', function(done) { - var pngHex = '89504e470d0a1a0a0' + - '000000d4948445200' + - '00000100000001080' + - '60000001f15c48900' + - '00000a49444154789' + - 'c6300010000050001' + - '0d0a2db4000000004' + - '9454e44ae426082'; - var pngData = new Buffer(pngHex, 'hex'); + it('allow raw data', function (done) { var filename = os.tmpdir() + '/express-http-proxy-test-' + (new Date()).getTime() + '-png-transparent.png'; var app = express(); - app.use(proxy('httpbin.org', { + + app.use(proxy('localhost:8109', { reqBodyEncoding: null, - decorateRequest: function(reqOpts) { - assert((new Buffer(reqOpts.bodyContent).toString('hex')).indexOf(pngData.toString('hex')) >= 0, + proxyReqBodyDecorator: function (bodyContent) { + assert((new Buffer(bodyContent).toString('hex')).indexOf(pngData.toString('hex')) >= 0, 'body should contain same data'); - return reqOpts; + return bodyContent; } })); - fs.writeFile(filename, pngData, function(err) { + fs.writeFile(filename, pngData, function (err) { if (err) { throw err; } request(app) .post('/post') .attach('image', filename) - .end(function(err, res) { - fs.unlink(filename); - assert.equal(res.body.files.image, 'data:image/png;base64,' + pngData.toString('base64')); + .end(function (err) { + fs.unlinkSync(filename); + // This test is both broken and I think unnecessary. + // Its broken because http.bin no longer supports /post, but this test assertion is based on the old + // httpbin behavior. + // The assertion in the decorateRequest above verifies the test title. + //var response = new Buffer(res.body.attachment.data).toString('base64'); + //assert(response.indexOf(pngData.toString('base64')) >= 0, 'response should include original raw data'); + done(err); }); }); }); + describe('when user sets parseReqBody as bool', function () { + it('should not parse body', function (done) { + var filename = os.tmpdir() + '/express-http-proxy-test-' + (new Date()).getTime() + '-png-transparent.png'; + var app = express(); + app.use(proxy('localhost:8109', { + parseReqBody: false, + proxyReqBodyDecorator: function (bodyContent) { + assert(!bodyContent, 'body content should not be parsed.'); + return bodyContent; + } + })); + + fs.writeFile(filename, pngData, function (err) { + if (err) { throw err; } + request(app) + .post('/post') + .attach('image', filename) + .end(function (err) { + fs.unlinkSync(filename); + // This test is both broken and I think unnecessary. + // Its broken because http.bin no longer supports /post, but this test assertion is based on the old + // httpbin behavior. + // The assertion in the decorateRequest above verifies the test title. + // var response = new Buffer(res.body.attachment.data).toString('base64'); + // assert(response.indexOf(pngData.toString('base64')) >= 0, 'response should include original raw data'); + + done(err); + }); + }); + }); + it('should not fail on large limit', function (done) { + var filename = os.tmpdir() + '/express-http-proxy-test-' + (new Date()).getTime() + '-png-transparent.png'; + var app = express(); + app.use(proxy('localhost:8109', { + parseReqBody: false, + limit: '20gb', + })); + fs.writeFile(filename, pngData, function (err) { + if (err) { throw err; } + request(app) + .post('/post') + .attach('image', filename) + .end(function (err) { + fs.unlinkSync(filename); + assert(err === null); + // This test is both broken and I think unnecessary. + // Its broken because http.bin no longer supports /post, but this test assertion is based on the old + // httpbin behavior. + // The assertion in the decorateRequest above verifies the test title. + //var response = new Buffer(res.body.attachment.data).toString('base64'); + //assert(response.indexOf(pngData.toString('base64')) >= 0, 'response should include original raw data'); - describe('when user sets reqBodyEncoding', function() { - it('should set the accepts-charset header', function(done) { + done(err); + }); + }); + }); + it('should fail with an error when exceeding limit', function (done) { var app = express(); - app.use(proxy('httpbin.org', { + app.use(proxy('localhost:8109', { + limit: 1, + })); + // silence jshint warning about unused vars - express error handler *needs* 4 args + app.use(function (err, req, res, next) { // eslint-disable-line no-unused-vars + res.json(err); + }); + request(app) + .post('/post') + .send({ some: 'json' }) + .end(function (err, response) { + assert(response.body.message === 'request entity too large'); + done(); + }); + }); + }); + + + describe('when user sets parseReqBody as function', function () { + it('should not parse body with form-data content', function (done) { + var filename = os.tmpdir() + '/express-http-proxy-test-' + (new Date()).getTime() + '-png-transparent.png'; + var app = express(); + app.use(proxy('localhost:8109', { + parseReqBody: (proxyReq) => proxyReq.headers['content-type'].includes('application/json'), + proxyReqBodyDecorator: function (bodyContent) { + assert(!bodyContent, 'body content should not be parsed.'); + return bodyContent; + } + })); + + fs.writeFile(filename, pngData, function (err) { + if (err) { throw err; } + request(app) + .post('/post') + .attach('image', filename) + .end(function (err) { + fs.unlinkSync(filename); + + done(err); + }); + }); + }); + it('should parse body with json content', function (done) { + var app = express(); + app.use(proxy('localhost:8109', { + parseReqBody: (proxyReq) => proxyReq.headers['content-type'].includes('application/json'), + proxyReqBodyDecorator: function (bodyContent) { + assert(bodyContent, 'body content should be parsed.'); + return bodyContent; + } + })); + + request(app) + .post('/post') + .send({ some: 'json' }) + .end(function (err) { + done(err); + }); + }); + }); + + + describe('when user sets reqBodyEncoding', function () { + it('should set the accepts-charset header', function (done) { + var app = express(); + app.use(proxy('localhost:8109', { reqBodyEncoding: 'utf-16' })); request(app) .get('/headers') - .end(function(err, res) { + .end(function (err, res) { if (err) { throw err; } - assert.equal(res.body.headers['Accept-Charset'], 'utf-16'); + assert.equal(res.body.headers['accept-charset'], 'utf-16'); done(err); }); }); }); -}); + +}); diff --git a/test/catchingErrors.js b/test/catchingErrors.js new file mode 100644 index 00000000..1bac4069 --- /dev/null +++ b/test/catchingErrors.js @@ -0,0 +1,55 @@ +'use strict'; + +var assert = require('assert'); +var express = require('express'); +var request = require('supertest'); +var proxy = require('../'); + +describe('when server responds with an error', function () { + + this.timeout(10000); + + var app; + var slowTarget; + var serverReference; + + beforeEach(function () { + app = express(); + }); + + afterEach(function () { + serverReference.close(); + }); + + var STATUS_CODES = [ + { code: 403, text: 'Forbidden', toString: 'Error: cannot GET /proxy (403)' }, + { code: 404, text: 'Not Found', toString: 'Error: cannot GET /proxy (404)' }, + { code: 500, text: 'Internal Server Error', toString: 'Error: cannot GET /proxy (500)' } + ]; + + STATUS_CODES.forEach(function (statusCode) { + it('express-http-proxy responds with ' + statusCode.text + + 'when proxy server responds ' + statusCode.code, function (done) { + slowTarget = express(); + slowTarget.use(function (req, res) { res.sendStatus(statusCode.code); }); + serverReference = slowTarget.listen(12345); + + app.use('/proxy', proxy('http://127.0.0.1:12345', { + reqAsBuffer: true, + reqBodyEncoding: null, + parseReqBody: false + })); + + request(app) + .get('/proxy') + .expect(statusCode.code) + .end(function (err, res) { + assert(err === null); + assert(res.error); + assert(res.error.text === statusCode.text); + assert(res.error.toString() === statusCode.toString); + done(); + }); + }); + }); +}); diff --git a/test/cookies.js b/test/cookies.js index 0cf1a4b2..493f1f08 100644 --- a/test/cookies.js +++ b/test/cookies.js @@ -1,25 +1,47 @@ +'use strict'; + var assert = require('assert'); var express = require('express'); var request = require('supertest'); var proxy = require('../'); -describe('proxies cookie', function() { - 'use strict'; +var proxyTarget = require('../test/support/proxyTarget'); +var proxyRouteFn = [{ + method: 'get', + path: '/cookieTest', + fn: function (req, res) { + Object.keys(req.cookies).forEach(function (key) { + res.cookie(key, req.cookies[key]); + }); + res.sendStatus(200); + } +}]; +describe('proxies cookie', function () { this.timeout(10000); var app; + var proxyServer; - beforeEach(function() { + beforeEach(function () { + proxyServer = proxyTarget(12346, 100, proxyRouteFn); app = express(); - app.use(proxy('httpbin.org')); + app.use(proxy('localhost:12346')); + }); + + afterEach(function () { + proxyServer.close(); }); - it('set cookie', function(done) { + it('set cookie', function (done) { request(app) - .get('/cookies/set?mycookie=value') - .end(function(err, res) { - assert(res.headers['set-cookie']); + .get('/cookieTest') + .set('Cookie', 'myApp-token=12345667') + .end(function (err, res) { + var cookiesMatch = res.headers['set-cookie'].filter(function (item) { + return item.match(/myApp-token=12345667/); + }); + assert(cookiesMatch); done(err); }); }); diff --git a/test/decorateRequest.js b/test/decorateRequest.js deleted file mode 100644 index 48aee655..00000000 --- a/test/decorateRequest.js +++ /dev/null @@ -1,54 +0,0 @@ -var assert = require('assert'); -var express = require('express'); -var request = require('supertest'); -var proxy = require('../'); - -describe('decorateRequest', function() { - 'use strict'; - - this.timeout(10000); - - var app; - - beforeEach(function() { - app = express(); - app.use(proxy('httpbin.org')); - }); - - it('decorateRequest', function(done) { - var app = express(); - app.use(proxy('httpbin.org', { - decorateRequest: function(req) { - req.path = '/ip'; - req.bodyContent = 'data'; - } - })); - - request(app) - .get('/user-agent') - .end(function(err, res) { - if (err) { return done(err); } - assert(res.body.origin); - done(); - }); - }); - - it('test decorateRequest has access to calling ip', function(done) { - var app = express(); - app.use(proxy('httpbin.org', { - decorateRequest: function(reqOpts, req) { - assert(req.ip); - return reqOpts; - } - })); - - request(app) - .get('/') - .end(function(err) { - if (err) { return done(err); } - done(); - }); - - }); -}); - diff --git a/test/decorateUserResHeaders.js b/test/decorateUserResHeaders.js new file mode 100644 index 00000000..6fd925ba --- /dev/null +++ b/test/decorateUserResHeaders.js @@ -0,0 +1,77 @@ +'use strict'; + +var assert = require('assert'); +var express = require('express'); +var request = require('supertest'); +var proxy = require('../'); + +describe('when userResHeaderDecorator is defined', function () { + + this.timeout(10000); + + var app; + var serverReference; + + afterEach(function () { + serverReference.close(); + }); + + beforeEach(function () { + app = express(); + var pTarget = express(); + pTarget.use(function (req, res) { + res.header('x-my-not-so-secret-header', 'minnie-mouse'); + res.header('x-my-secret-header', 'mighty-mouse'); + res.json(req.headers); + }); + serverReference = pTarget.listen(12345); + }); + + afterEach(function () { + serverReference.close(); + }); + + it('can delete a header', function (done) { + + app.use('/proxy', proxy('http://127.0.0.1:12345', { + userResHeaderDecorator: function (headers /*, userReq, userRes, proxyReq, proxyRes */) { + delete headers['x-my-secret-header']; + return headers; + } + })); + + app.use(function (req, res) { + res.sendStatus(200); + }); + + request(app) + .get('/proxy') + .expect(function (res) { + assert(Object.keys(res.headers).indexOf('x-my-not-so-secret-header') > -1); + assert(Object.keys(res.headers).indexOf('x-my-secret-header') === -1); + }) + .end(done); + }); + + it('provides an interface for updating headers', function (done) { + + app.use('/proxy', proxy('http://127.0.0.1:12345', { + userResHeaderDecorator: function (headers /*, userReq, userRes, proxyReq, proxyRes */) { + headers.boltedonheader = 'franky'; + return headers; + } + })); + + app.use(function (req, res) { + res.sendStatus(200); + }); + + request(app) + .get('/proxy') + .expect(function (res) { + assert(res.headers.boltedonheader === 'franky'); + }) + .end(done); + }); + +}); diff --git a/test/filter.js b/test/filter.js new file mode 100644 index 00000000..6e73576f --- /dev/null +++ b/test/filter.js @@ -0,0 +1,113 @@ +'use strict'; + +var express = require('express'); +var request = require('supertest'); +var proxy = require('../'); +var proxyTarget = require('../test/support/proxyTarget'); +var proxyRouteFn = [{ + method: 'get', + path: '/', + fn: function (req, res) { + return res.status(209).send('Proxy Server'); + } +}]; + +function nextMethod(req, res, next) { + res.status(201).send('Client Application'); + next(); +} + +describe('filter', function () { + var app = express(); + var proxyServer; + + beforeEach(function () { + proxyServer = proxyTarget(12346, 100, proxyRouteFn); + app = express(); + }); + + afterEach(function () { + proxyServer.close(); + }); + + describe('when filter function returns true', function () { + it('continues route processing', function (done) { + // should capture every possible path + + app.use(proxy('localhost:12346', { + filter: function () { return true; } + })); + + // because prior line should *always* fire and return, we should never get here. + + app.use(nextMethod); + + request(app) + .get('/') + .expect(209) + .end(done); + }); + }); + + describe('when filter function returns false', function () { + + it('hanldes route processing', function (done) { + // should capture every possible path + + app.use(proxy('localhost:12346', { + filter: function () { return false; } + })); + + // because prior line should *always* fire skip, we should get here. + + app.use(nextMethod); + + request(app) + .get('/') + .expect(201) + .end(done); + }); + }); + + describe('promise form', function () { + describe('when filter function returns true', function () { + it('continues route processing', function (done) { + // should capture every possible path + + app.use(proxy('localhost:12346', { + filter: function () { return new Promise(function (resolve) { resolve(true); }); } + })); + + // because prior line should *always* fire and return, we should never get here. + + app.use(nextMethod); + + request(app) + .get('/') + .expect(209) + .end(done); + }); + }); + + describe('when filter function returns false', function () { + + it('hanldes route processing', function (done) { + // should capture every possible path + + app.use(proxy('localhost:12346', { + filter: function () { return new Promise(function (resolve) { resolve(false); }); } + })); + + // because prior line should *always* fire skip, we should get here. + + app.use(nextMethod); + + request(app) + .get('/') + .expect(201) + .end(done); + }); + }); + }); + +}); diff --git a/test/forwardPath.js b/test/forwardPath.js deleted file mode 100644 index ea0dd75b..00000000 --- a/test/forwardPath.js +++ /dev/null @@ -1,145 +0,0 @@ -var assert = require('assert'); -var express = require('express'); -var request = require('supertest'); -var proxy = require('../'); -var promise = require('es6-promise'); - -describe('forwardPath', function() { - 'use strict'; - this.timeout(10000); - - it('test post to unknown path yields 404', function(done) { - var app = express(); - app.use(proxy('httpbin.org')); - - request(app) - .post('/foobar') - .send({ - mypost: 'hello' - }) - .end(function(err, res) { - assert.equal(res.statusCode, 404); - done(err); - }); - }); - - it('test forwardPath to known path yields 200', function(done) { - var app = express(); - app.use(proxy('httpbin.org', { - forwardPath: function() { - return '/post'; - } - })); - - request(app) - .post('/foobar') - .send({ - mypost: 'hello' - }) - .end(function(err, res) { - assert.equal(res.statusCode, 200); - done(err); - }); - }); - - it('test forwardPath to known path with request and response yields 200', function(done) { - var app = express(); - app.use(proxy('httpbin.org', { - forwardPath: function(req, res) { - assert.ok(req); - assert.ok(res); - return '/post'; - } - })); - - request(app) - .post('/foobar') - .send({ - mypost: 'hello' - }) - .end(function(err, res) { - assert.equal(res.statusCode, 200); - done(err); - }); - }); - - it('test forwardPath to undefined yields 404', function(done) { - var app = express(); - app.use(proxy('httpbin.org', { - forwardPath: undefined - })); - - request(app) - .post('/foobar') - .send({ - mypost: 'hello' - }) - .end(function(err, res) { - assert.equal(res.statusCode, 404); - done(err); - }); - }); - - it('test forwardPath as an async function should not work', function(done) { - var app = express(); - app.use(proxy('httpbin.org', { - forwardPath: function() { - setTimeout(function() { - return '/post'; - }, 100); - } - })); - - request(app) - .post('/foobar') - .send({ - mypost: 'hello' - }) - .end(function(err, res) { - assert.equal(res.statusCode, 405); - done(err); - }); - }); - - it('test forwardPathAsync to known path yields 200', function(done) { - var app = express(); - app.use(proxy('httpbin.org', { - forwardPathAsync: function() { - return new promise.Promise(function(resolve) { - setTimeout(function() { - resolve('/post'); - }, 250); - }); - }} - )); - - request(app) - .post('/foobar') - .send({ - mypost: 'hello' - }) - .end(function(err, res) { - assert.equal(res.statusCode, 200); - done(err); - }); - }); - - it('test forwardPathAsync to known path (as function) yields 200', function(done) { - var app = express(); - app.use(proxy('httpbin.org', { - forwardPath: function() { - return ('/post'); - } - })); - - request(app) - .post('/foobar') - .send({ - mypost: 'hello' - }) - .end(function(err, res) { - assert.equal(res.statusCode, 200); - done(err); - }); - }); -}); diff --git a/test/getBody.js b/test/getBody.js new file mode 100644 index 00000000..7ef58bfe --- /dev/null +++ b/test/getBody.js @@ -0,0 +1,158 @@ +'use strict'; + +var assert = require('assert'); +var bodyParser = require('body-parser'); +var express = require('express'); +var nock = require('nock'); +var request = require('supertest'); +var proxy = require('../'); + + +function createLocalApplicationServer() { + var app = express(); + return app; +} + +describe('when proxy request is a GET', function () { + + this.timeout(10000); + + var localServer; + + beforeEach(function () { + localServer = createLocalApplicationServer(); + localServer.use(bodyParser.json()); + }); + + afterEach(function () { + nock.cleanAll(); + }); + + var testCases = [ + { name: 'form encoded', encoding: 'application/x-www-form-urlencoded' }, + { name: 'JSON encoded', encoding: 'application/json' } + ]; + + testCases.forEach(function (test) { + it('should deliver the get query when ' + test.name, function (done) { + var nockedPostWithEncoding = nock('http://127.0.0.1:12345') + .get('/') + .query({ name: 'tobi' }) + .matchHeader('Content-Type', test.encoding) + .reply(200, { + name: 'tobi' + }); + + localServer.use('/proxy', proxy('http://127.0.0.1:12345')); + localServer.use(function (req, res) { res.sendStatus(200); }); + localServer.use(function (err, req, res, next) { throw new Error(err, req, res, next); }); + + request(localServer) + .get('/proxy') + .query({ name: 'tobi' }) + .set('Content-Type', test.encoding) + .expect(function (res) { + assert(res.body.name === 'tobi'); + nockedPostWithEncoding.done(); + }) + .end(done); + }); + + it('should deliver the get body when ' + test.name, function (done) { + var nockedPostWithEncoding = nock('http://127.0.0.1:12345') + .get('/', test.encoding.includes('json') ? { name: 'tobi' } : '') + .matchHeader('Content-Type', test.encoding) + .reply(200, { + name: 'tobi' + }); + + localServer.use('/proxy', proxy('http://127.0.0.1:12345')); + localServer.use(function (req, res) { res.sendStatus(200); }); + localServer.use(function (err, req, res, next) { throw new Error(err, req, res, next); }); + + request(localServer) + .get('/proxy') + .send({ name: 'tobi' }) + .set('Content-Type', test.encoding) + .expect(function (res) { + assert(res.body.name === 'tobi'); + nockedPostWithEncoding.done(); + }) + .end(done); + }); + }); + + it('should deliver empty string get body', function (done) { + var nockedPostWithoutBody = nock('http://127.0.0.1:12345') + .get('/') + .matchHeader('Content-Type', 'application/json') + .reply(200, { + name: 'get with string body' + }); + + localServer.use('/proxy', proxy('http://127.0.0.1:12345')); + localServer.use(function (req, res) { res.sendStatus(200); }); + localServer.use(function (err, req, res, next) { throw new Error(err, req, res, next); }); + + request(localServer) + .get('/proxy') + .send('') + .set('Content-Type', 'application/json') + .expect(function (res) { + assert(res.body.name === 'get with string body'); + nockedPostWithoutBody.done(); + }) + .end(done); + }); + + it('should deliver empty object get body', function (done) { + var nockedPostWithoutBody = nock('http://127.0.0.1:12345') + .get('/', {}) + .matchHeader('Content-Type', 'application/json') + .reply(200, { + name: 'get with object body' + }); + + localServer.use('/proxy', proxy('http://127.0.0.1:12345')); + localServer.use(function (req, res) { res.sendStatus(200); }); + localServer.use(function (err, req, res, next) { throw new Error(err, req, res, next); }); + + request(localServer) + .get('/proxy') + .send({}) + .set('Content-Type', 'application/json') + .expect(function (res) { + assert(res.body.name === 'get with object body'); + nockedPostWithoutBody.done(); + }) + .end(done); + }); + + it('should support parseReqBody', function (done) { + var nockedPostWithBody = nock('http://127.0.0.1:12345') + .get('/', '') + .matchHeader('Content-Type', 'application/json') + .reply(200, { + name: 'get with parseReqBody false' + }); + + localServer.use('/proxy', proxy('http://127.0.0.1:12345', { + parseReqBody: false, + })); + localServer.use(function (req, res) { res.sendStatus(200); }); + localServer.use(function (err, req, res, next) { throw new Error(err, req, res, next); }); + + request(localServer) + .get('/proxy') + .send({ + name: 'tobi' + }) + .set('Content-Type', 'application/json') + .expect(function (res) { + assert(res.body.name === 'get with parseReqBody false'); + nockedPostWithBody.done(); + }) + .end(done); + }); + +}); diff --git a/test/handleProxyError.js b/test/handleProxyError.js new file mode 100644 index 00000000..34a71c0e --- /dev/null +++ b/test/handleProxyError.js @@ -0,0 +1,136 @@ +'use strict'; + +var assert = require('assert'); +var express = require('express'); +var request = require('supertest'); +var proxy = require('../'); +var proxyTarget = require('../test/support/proxyTarget'); +var proxyRouteFn = [{ + method: 'get', + path: '/:errorCode', + fn: function (req, res) { + var errorCode = req.params.errorCode; + if (errorCode === 'timeout') { + return res.status(504).send('mock timeout'); + } + return res.status(parseInt(errorCode)).send('test case error'); + } +}]; + +describe('error handling can be over-ridden by user', function () { + var app = express(); + var proxyServer; + + beforeEach(function () { + proxyServer = proxyTarget(12346, 100, proxyRouteFn); + app = express(); + }); + + afterEach(function () { + proxyServer.close(); + }); + + describe('when user provides a null function', function () { + + describe('when author sets a timeout that fires', function () { + it('passes 504 directly to client', function (done) { + app.use(proxy('localhost:12346', { + timeout: 1, + })); + + request(app) + .get('/200') + .expect(504) + .expect('X-Timeout-Reason', 'express-http-proxy reset the request.') + .end(done); + }); + }); + + it('passes status code (e.g. 504) directly to the client', function (done) { + app.use(proxy('localhost:12346')); + request(app) + .get('/504') + .expect(504) + .expect(function (res) { + assert(res.text === 'test case error'); + return res; + }) + .end(done); + }); + + it('passes status code (e.g. 500) back to the client', function (done) { + app.use(proxy('localhost:12346')); + request(app) + .get('/500') + .expect(500) + .end(function (err, res) { + assert(res.text === 'test case error'); + done(); + }); + }); + }); + + describe('when user provides a handler function', function () { + var intentionallyWeirdStatusCode = 399; + var intentionallyQuirkyStatusMessage = 'monkey skunky'; + + describe('when author sets a timeout that fires', function () { + it('allows author to skip handling and handle in application step', function (done) { + app.use(proxy('localhost:12346', { + timeout: 1, + proxyErrorHandler: function (err, res, next) { + next(err); + } + })); + + app.use(function (err, req, res, next) { // eslint-disable-line no-unused-vars + if (err.code === 'ECONNRESET') { + res.status(intentionallyWeirdStatusCode).send(intentionallyQuirkyStatusMessage); + } + }); + + request(app) + .get('/200') + .expect(function (res) { + assert(res.text === intentionallyQuirkyStatusMessage); + return res; + }) + .expect(intentionallyWeirdStatusCode) + .end(done); + }); + }); + + it('allows authors to sub in their own handling', function (done) { + app.use(proxy('localhost:12346', { + timeout: 1, + proxyErrorHandler: function (err, res, next) { + switch (err && err.code) { + case 'ECONNRESET': { return res.status(405).send('504 became 405'); } + case 'ECONNREFUSED': { return res.status(200).send('gotcher back'); } + default: { next(err); } + } + } })); + + request(app) + .get('/timeout') + .expect(405) + .expect(function (res) { + assert(res.text === '504 became 405'); + return res; + }) + .end(done); + }); + + it('passes status code (e.g. 500) back to the client', function (done) { + app.use(proxy('localhost:12346')); + request(app) + .get('/500') + .expect(500) + .end(function (err, res) { + assert(res.text === 'test case error'); + done(); + }); + }); + }); + +}); diff --git a/test/headers.js b/test/headers.js index e4769556..91349a01 100644 --- a/test/headers.js +++ b/test/headers.js @@ -1,15 +1,16 @@ +'use strict'; + var assert = require('assert'); var express = require('express'); var request = require('supertest'); var proxy = require('../'); -describe('proxies headers', function() { - 'use strict'; +describe('proxies headers', function () { this.timeout(10000); var http; - beforeEach(function() { + beforeEach(function () { http = express(); http.use(proxy('http://httpbin.org', { headers: { @@ -18,23 +19,23 @@ describe('proxies headers', function() { })); }); - it('passed as options', function(done) { + it('passed as options', function (done) { request(http) .get('/headers') .expect(200) - .end(function(err, res) { + .end(function (err, res) { if (err) { return done(err); } assert(res.body.headers['X-Current-President'] === 'taft'); done(); }); }); - it('passed as on request', function(done) { + it('passed as on request', function (done) { request(http) .get('/headers') .set('X-Powerererer', 'XTYORG') .expect(200) - .end(function(err, res) { + .end(function (err, res) { if (err) { return done(err); } assert(res.body.headers['X-Powerererer']); done(); diff --git a/test/host.js b/test/host.js index ed2426d0..a42ac24c 100644 --- a/test/host.js +++ b/test/host.js @@ -1,45 +1,70 @@ +'use strict'; + var express = require('express'); var request = require('supertest'); var proxy = require('../'); -describe('host can be a dynamic function', function() { - 'use strict'; +describe('host can be a dynamic function', function () { this.timeout(10000); var app = express(); - var firstProxyApp = express(); - var secondProxyApp = express(); - var firstPort = Math.floor(Math.random() * 10000); - var secondPort = Math.floor(Math.random() * 10000); - - app.use('/proxy/:port', proxy(function(req) { - return 'localhost:' + req.params.port; - }, { - memoizeHost: false - })); - - firstProxyApp.get('/', function(req, res) { - res.sendStatus(204); - }); - firstProxyApp.listen(firstPort); + describe('and memoization can be disabled', function () { + var firstProxyApp = express(); + var secondProxyApp = express(); + // TODO: This seems like a bug factory. We will have intermittent port conflicts, yeah? - secondProxyApp.get('/', function(req, res) { - res.sendStatus(200); - }); - secondProxyApp.listen(secondPort); + function randomNumberInPortRange() { + return Math.floor(Math.random() * 48000) + 1024; + } + var firstPort = randomNumberInPortRange(); + var secondPort = randomNumberInPortRange(); - it('can proxy with session value', function(done) { - request(app) - .get('/proxy/' + firstPort) - .expect(204) - .end(function(err) { - if (err) { - return done(err); - } - request(app) + var hostFn = function (req) { + return 'localhost:' + req.params.port; + }; + + app.use('/proxy/:port', proxy(hostFn, { memoizeHost: false })); + + firstProxyApp + .get('/', function (req, res) { res.sendStatus(204); }) + .listen(firstPort); + + secondProxyApp + .get('/', function (req, res) { res.sendStatus(200); }) + .listen(secondPort); + + it('when not memoized, host resolves to a second value on the seecond call', function (done) { + request(app) + .get('/proxy/' + firstPort) + .expect(204) + .end(function (err) { + if (err) { + return done(err); + } + request(app) .get('/proxy/' + secondPort) .expect(200, done); - }); + }); + }); }); }); + +describe('host can be an ip address', function () { + it('with a port', function (done) { + var app = express(); + app.use('/proxy/', proxy('127.0.0.1:3020')); + + var targetApp = express(); + targetApp + .get('/', function (req, res) { res.sendStatus(211); }) + .listen(3020); + + + request(app) + .get('/proxy/') + .expect(211) + .end(done); + }); +}); + diff --git a/test/https.js b/test/https.js index 824ffe75..8d380046 100644 --- a/test/https.js +++ b/test/https.js @@ -1,55 +1,56 @@ +'use strict'; + var assert = require('assert'); var express = require('express'); var request = require('supertest'); var proxy = require('../'); -describe('proxies https', function() { - 'use strict'; +describe('proxies https', function () { this.timeout(10000); var app; - beforeEach(function() { + beforeEach(function () { app = express(); }); function assertSecureRequest(app, done) { request(app) .get('/get?show_env=1') - .end(function(err, res) { + .end(function (err, res) { if (err) { return done(err); } - assert(res.body.headers['X-Forwarded-Ssl'] === 'on'); - assert(res.body.headers['X-Forwarded-Protocol'] === 'https'); + assert(res.body.headers['X-Forwarded-Port'] === '443', 'Expects forwarded 443 Port'); + assert(res.body.headers['X-Forwarded-Proto'] === 'https', 'Expects forwarded protocol to be https'); done(); }); } - describe('when host is a String', function() { - describe('and includes "https" as protocol', function() { - it('proxys via https', function(done) { + describe('when host is a String', function () { + describe('and includes "https" as protocol', function () { + it('proxys via https', function (done) { app.use(proxy('https://httpbin.org')); assertSecureRequest(app, done); }); }); - describe('option https is set to "true"', function() { - it('proxys via https', function(done) { - app.use(proxy('http://httpbin.org', {https: true})); + describe('option https is set to "true"', function () { + it('proxys via https', function (done) { + app.use(proxy('http://httpbin.org', { https: true })); assertSecureRequest(app, done); }); }); }); - describe('when host is a Function', function() { - describe('returned value includes "https" as protocol', function() { - it('proxys via https', function(done) { - app.use(proxy(function() { return 'https://httpbin.org'; })); + describe('when host is a Function', function () { + describe('returned value includes "https" as protocol', function () { + it('proxys via https', function (done) { + app.use(proxy(function () { return 'https://httpbin.org'; })); assertSecureRequest(app, done); }); }); - describe('option https is set to "true"', function() { - it('proxys via https', function(done) { - app.use(proxy(function() { return 'http://httpbin.org'; }, {https: true})); + describe('option https is set to "true"', function () { + it('proxys via https', function (done) { + app.use(proxy(function () { return 'http://httpbin.org'; }, { https: true })); assertSecureRequest(app, done); }); }); diff --git a/test/intercept.js b/test/intercept.js deleted file mode 100644 index b6def527..00000000 --- a/test/intercept.js +++ /dev/null @@ -1,156 +0,0 @@ -var assert = require('assert'); -var express = require('express'); -var request = require('supertest'); -var proxy = require('../'); - -describe('intercept', function() { - 'use strict'; - - it('has access to original response', function(done) { - var app = express(); - app.use(proxy('httpbin.org', { - intercept: function(rsp) { - assert(rsp.connection); - assert(rsp.socket); - assert(rsp.headers); - assert(rsp.headers['content-type']); - done(); - } - })); - - request(app).get('/').end(); - }); - - it('can modify the response data', function(done) { - var app = express(); - app.use(proxy('httpbin.org', { - intercept: function(rsp, data, req, res, cb) { - data = JSON.parse(data.toString('utf8')); - data.intercepted = true; - cb(null, JSON.stringify(data)); - } - })); - - request(app) - .get('/ip') - .end(function(err, res) { - if (err) { return done(err); } - assert(res.body.intercepted); - done(); - }); - }); - - - it('can modify the response headers', function(done) { - var app = express(); - app.use(proxy('httpbin.org', { - intercept: function(rsp, data, req, res, cb) { - res.set('x-wombat-alliance', 'mammels'); - res.set('content-type', 'wiki/wiki'); - cb(null, data); - } - })); - - request(app) - .get('/ip') - .end(function(err, res) { - if (err) { return done(err); } - assert(res.headers['content-type'] === 'wiki/wiki'); - assert(res.headers['x-wombat-alliance'] === 'mammels'); - done(); - }); - }); - - it('can mutuate an html response', function(done) { - var app = express(); - app.use(proxy('httpbin.org', { - intercept: function(rsp, data, req, res, cb) { - data = data.toString().replace('Oh', 'Hey'); - assert(data !== ''); - cb(null, data); - } - })); - - request(app) - .get('/html') - .end(function(err, res) { - if (err) { return done(err); } - assert(res.text.indexOf('Hey') > -1); - done(); - }); - }); - - it('can change the location of a redirect', function(done) { - - function redirectingServer(port, origin) { - var app = express(); - app.get('/', function(req, res) { - res.status(302); - res.location(origin + '/proxied/redirect/url'); - res.send(); - }); - return app.listen(port); - } - - var redirectingServerPort = 8012; - var redirectingServerOrigin = ['http://localhost', redirectingServerPort].join(':'); - - var server = redirectingServer(redirectingServerPort, redirectingServerOrigin); - - var proxyApp = express(); - var preferredPort = 3000; - - proxyApp.use(proxy(redirectingServerOrigin, { - intercept: function(rsp, data, req, res, cb) { - var proxyReturnedLocation = res._headers.location; - res.location(proxyReturnedLocation.replace(redirectingServerPort, preferredPort)); - cb(null, data); - } - })); - - request(proxyApp) - .get('/') - .expect(function(res) { - res.headers.location.match(/localhost:3000/); - }) - .end(function() { - server.close(); - done(); - }); - }); -}); - - -describe('test intercept on html response from github',function() { - /* - Github provided a unique situation where the encoding was different than - utf-8 when we didn't explicitly ask for utf-8. This test helped sort out - the issue, and even though its a little too on the nose for a specific - case, it seems worth keeping around to ensure we don't regress on this - issue. - */ - - 'use strict'; - - it('is able to read and manipulate the response', function(done) { - this.timeout(1500); // give it some extra time to get response - var app = express(); - app.use(proxy('https://github.com/villadora/express-http-proxy', { - intercept: function(targetResponse, data, req, res, cb) { - data = data.toString().replace('DOCTYPE','WINNING'); - assert(data !== ''); - cb(null, data); - } - })); - - request(app) - .get('/html') - .end(function(err, res) { - if (err) { return done(err); } - assert(res.text.indexOf('WINNING') > -1); - done(); - }); - - }); -}); - diff --git a/test/maybeSkipToNextHandler.js b/test/maybeSkipToNextHandler.js new file mode 100644 index 00000000..4d2efedc --- /dev/null +++ b/test/maybeSkipToNextHandler.js @@ -0,0 +1,57 @@ +'use strict'; + +var express = require('express'); +var request = require('supertest'); +var proxy = require('../'); +var http = require('http'); +var assert = require('assert'); + +describe('when skipToNextHandlerFilter is defined', function () { + + this.timeout(10000); + + var app; + var slowTarget; + var serverReference; + + beforeEach(function () { + app = express(); + slowTarget = express(); + slowTarget.use(function (req, res) { res.sendStatus(404); }); + serverReference = slowTarget.listen(12345); + }); + + afterEach(function () { + serverReference.close(); + }); + + var OUTCOMES = [ + { shouldSkip: true, expectedStatus: 200 }, + { shouldSkip: false, expectedStatus: 404 } + ]; + + OUTCOMES.forEach(function (outcome) { + describe('and returns ' + outcome.shouldSkip, function () { + it('express-http-proxy' + (outcome.shouldSkip ? ' skips ' : ' doesnt skip ') + 'to next()', function (done) { + + app.use('/proxy', proxy('http://127.0.0.1:12345', { + skipToNextHandlerFilter: function (/*res*/) { + return outcome.shouldSkip; + } + })); + + app.use(function (req, res) { + assert(res.expressHttpProxy instanceof Object); + assert(res.expressHttpProxy.res instanceof http.IncomingMessage); + assert(res.expressHttpProxy.req instanceof Object); + res.sendStatus(200); + }); + + request(app) + .get('/proxy') + .expect(outcome.expectedStatus) + .end(done); + }); + }); + }); +}); diff --git a/test/middlewareCompatibility.js b/test/middlewareCompatibility.js index 923bf0e1..435dd831 100644 --- a/test/middlewareCompatibility.js +++ b/test/middlewareCompatibility.js @@ -1,16 +1,38 @@ +'use strict'; + var assert = require('assert'); var express = require('express'); var request = require('supertest'); var bodyParser = require('body-parser'); var proxy = require('../'); +var proxyTarget = require('../test/support/proxyTarget'); + + +var proxyRouteFn = [{ + method: 'post', + path: '/poster', + fn: function (req, res) { + res.send(req.body); + } +}]; + +describe('middleware compatibility', function () { + var proxyServer; + + beforeEach(function () { + proxyServer = proxyTarget(12346, 100, proxyRouteFn); + }); -describe('middleware compatibility', function() { - 'use strict'; - it('should use req.body if defined', function(done) { + afterEach(function () { + proxyServer.close(); + }); + + it('should use req.body if defined', function (done) { var app = express(); // Simulate another middleware that puts req stream into the body - app.use(function(req, res, next) { + + app.use(function (req, res, next) { var received = []; req.on('data', function onData(chunk) { if (!chunk) { return; } @@ -24,65 +46,63 @@ describe('middleware compatibility', function() { }); }); - app.use(proxy('httpbin.org', { - intercept: function(rsp, data, req, res, cb) { + app.use(proxy('localhost:12346', { + userResDecorator: function (rsp, data, req) { assert(req.body); assert.equal(req.body.foo, 1); assert.equal(req.body.mypost, 'hello'); - cb(null, data); + return data; } })); request(app) - .post('/post') - .send({ - mypost: 'hello' - }) - .expect(function(res) { - assert.equal(res.body.json.foo, 1); - assert.equal(res.body.json.mypost, 'hello'); + .post('/poster') + .send({ mypost: 'hello' }) + .expect(function (res) { + assert.equal(res.body.foo, 1); + assert.equal(res.body.mypost, 'hello'); }) .end(done); }); - it('should stringify req.body when it is a json body so it is written to proxy request', function(done) { + it('should stringify req.body when it is a json body so it is written to proxy request', function (done) { var app = express(); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); - app.use(proxy('httpbin.org')); + app.use(proxy('localhost:12346')); request(app) - .post('/post') + .post('/poster') .send({ mypost: 'hello', doorknob: 'wrect' }) - .expect(function(res) { - assert.equal(res.body.json.doorknob, 'wrect'); - assert.equal(res.body.json.mypost, 'hello'); + .expect(function (res) { + assert.equal(res.body.doorknob, 'wrect'); + assert.equal(res.body.mypost, 'hello'); }) .end(done); }); - it('should convert req.body to a Buffer when reqAsBuffer is set', function(done) { + it('should convert req.body to a Buffer when reqAsBuffer is set', function (done) { var app = express(); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); - app.use(proxy('httpbin.org', { + app.use(proxy('localhost:12346', { reqAsBuffer: true })); request(app) - .post('/post') + .post('/poster') .send({ mypost: 'hello', doorknob: 'wrect' }) - .expect(function(res) { - assert.equal(res.body.json.doorknob, 'wrect'); - assert.equal(res.body.json.mypost, 'hello'); + .expect(function (res) { + assert.equal(res.body.doorknob, 'wrect'); + assert.equal(res.body.mypost, 'hello'); }) .end(done); }); diff --git a/test/params.js b/test/params.js new file mode 100644 index 00000000..94f96aa2 --- /dev/null +++ b/test/params.js @@ -0,0 +1,41 @@ +'use strict'; + +var assert = require('assert'); +var express = require('express'); +var request = require('supertest'); +var proxy = require('../'); + +var proxyTarget = require('../test/support/proxyTarget'); +var proxyRouteFn = [{ + method: 'get', + path: '/test', + fn: function (req, res) { + res.send(req.url); + } +}]; + +describe('proxies query parameters', function () { + this.timeout(10000); + + var app; + var proxyServer; + + beforeEach(function () { + proxyServer = proxyTarget(12346, 100, proxyRouteFn); + app = express(); + app.use(proxy('localhost:12346')); + }); + + afterEach(function () { + proxyServer.close(); + }); + + it('repeats query params to proxy server', function (done) { + request(app) + .get('/test?a=1&b=2&c=3') + .end(function (err, res) { + assert.equal(res.text, '/test?a=1&b=2&c=3'); + done(err); + }); + }); +}); diff --git a/test/path.js b/test/path.js new file mode 100644 index 00000000..f480e3b1 --- /dev/null +++ b/test/path.js @@ -0,0 +1,95 @@ +'use strict'; + +var express = require('express'); +var request = require('supertest'); +var proxy = require('../'); +var startProxyTarget = require('./support/proxyTarget'); +var expect = require('chai').expect; + +describe('uses remote path', function () { + + this.timeout(10000); + + var app = express(); + var proxyRoutes = ['/somePath/', '/somePath/longer/path', '/somePath/long/path/with/many/tokens']; + var proxyKeyPath = '/somePath'; + var server; + + afterEach(function () { + server.close(); + }); + + proxyRoutes.forEach(function (path) { + it('uses path component from inbound request', function (done) { + + var modifiedPath = path.replace(new RegExp(proxyKeyPath), ''); + + var proxyRouteFn = { + method: 'get', + path: modifiedPath, + fn: function (req, res) { + res.json({ path: path, modifiedPath: modifiedPath }); + } + }; + + server = startProxyTarget(8309, 1000, [proxyRouteFn]); + + app.use('/somePath/', proxy('http://localhost:8309')); + + request(app) + .get(path) + .expect(200) + .end(function (err, response) { + if (err) { + return done(err); + } + expect(response.path === path); + expect(response.modifiedPath === path); + done(); + }); + }); + + }); +}); + + +describe('host can be a dynamic function', function () { + + this.timeout(10000); + + var app = express(); + var firstProxyApp = express(); + var secondProxyApp = express(); + var firstPort = 10031; + var secondPort = 10032; + + app.use('/proxy/:port', proxy(function (req) { + return 'localhost:' + req.params.port; + }, { + memoizeHost: false + })); + + firstProxyApp.use('/', function (req, res) { + res.sendStatus(204); + }); + firstProxyApp.listen(firstPort); + + secondProxyApp.use('/', function (req, res) { + res.sendStatus(200); + }); + secondProxyApp.listen(secondPort); + + it('can proxy with session value', function (done) { + request(app) + .get('/proxy/' + firstPort) + .expect(204) + .end(function (err) { + if (err) { + return done(err); + } + request(app) + .get('/proxy/' + secondPort) + .expect(200, done); + }); + }); +}); diff --git a/test/port.js b/test/port.js index 9d498b65..17ae990c 100644 --- a/test/port.js +++ b/test/port.js @@ -1,28 +1,29 @@ +'use strict'; + var assert = require('assert'); var express = require('express'); var request = require('supertest'); var proxy = require('../'); function proxyTarget(port) { - 'use strict'; var other = express(); - other.get('/', function(req, res) { + other.get('/', function (req, res) { res.send('Success'); }); return other.listen(port); } -describe('proxies to requested port', function() { - 'use strict'; +describe('proxies to requested port', function () { + var other; + var http; - var other, http; - beforeEach(function() { + beforeEach(function () { http = express(); - other = proxyTarget(8080); + other = proxyTarget(56001); }); - afterEach(function() { + afterEach(function () { other.close(); }); @@ -31,49 +32,49 @@ describe('proxies to requested port', function() { request(http) .get('/') .expect(200) - .end(function(err, res) { + .end(function (err, res) { if (err) { return done(err); } assert(res.text === 'Success'); done(); }); } - describe('when host is a String', function() { - it('when passed as an option', function(done) { + describe('when host is a String', function () { + it('when passed as an option', function (done) { http.use(proxy('http://localhost', { - port: 8080 + port: 56001 })); assertSuccess(http, done); }); - it('when passed on the host string', function(done) { + it('when passed on the host string', function (done) { - http.use(proxy('http://localhost:8080')); + http.use(proxy('http://localhost:56001')); assertSuccess(http, done); }); }); - describe('when host is a function', function() { + describe('when host is a function', function () { - it('and port is on the String returned', function(done) { + it('and port is on the String returned', function (done) { http.use(proxy( - function() { return 'http://localhost:8080'; } + function () { return 'http://localhost:56001'; } )); assertSuccess(http, done); }); - it('and port passed as an option', function(done) { + it('and port passed as an option', function (done) { http.use(proxy( - function() { return 'http://localhost'; }, - { port: 8080 } + function () { return 'http://localhost'; }, + { port: 56001 } )); assertSuccess(http, done); diff --git a/test/postBody.js b/test/postBody.js new file mode 100644 index 00000000..f853cc05 --- /dev/null +++ b/test/postBody.js @@ -0,0 +1,131 @@ +'use strict'; + +var assert = require('assert'); +var bodyParser = require('body-parser'); +var express = require('express'); +var nock = require('nock'); +var request = require('supertest'); +var proxy = require('../'); + + +function createLocalApplicationServer() { + var app = express(); + return app; +} + +describe('when proxy request is a POST', function () { + + this.timeout(10000); + + var localServer; + + beforeEach(function () { + localServer = createLocalApplicationServer(); + localServer.use(bodyParser.json()); + }); + + afterEach(function () { + nock.cleanAll(); + }); + + var testCases = [ + { name: 'form encoded', encoding: 'application/x-www-form-urlencoded' }, + { name: 'JSON encoded', encoding: 'application/json' } + ]; + + testCases.forEach(function (test) { + it('should deliver the post query when ' + test.name, function (done) { + var nockedPostWithEncoding = nock('http://127.0.0.1:12345') + .post('/') + .query({ name: 'tobi' }) + .matchHeader('Content-Type', test.encoding) + .reply(200, { + name: 'tobi' + }); + + localServer.use('/proxy', proxy('http://127.0.0.1:12345')); + localServer.use(function (req, res) { res.sendStatus(200); }); + localServer.use(function (err, req, res, next) { throw new Error(err, req, res, next); }); + + request(localServer) + .post('/proxy') + .query({ name: 'tobi' }) + .set('Content-Type', test.encoding) + .expect(function (res) { + assert(res.body.name === 'tobi'); + nockedPostWithEncoding.done(); + }) + .end(done); + }); + + it('should deliver the post body when ' + test.name, function (done) { + var nockedPostWithEncoding = nock('http://127.0.0.1:12345') + .post('/', test.encoding.includes('json') ? { name: 'tobi' } : {}) + .matchHeader('Content-Type', test.encoding) + .reply(200, { + name: 'tobi' + }); + + localServer.use('/proxy', proxy('http://127.0.0.1:12345')); + localServer.use(function (req, res) { res.sendStatus(200); }); + localServer.use(function (err, req, res, next) { throw new Error(err, req, res, next); }); + + request(localServer) + .post('/proxy') + .send({ name: 'tobi' }) + .set('Content-Type', test.encoding) + .expect(function (res) { + assert(res.body.name === 'tobi'); + nockedPostWithEncoding.done(); + }) + .end(done); + }); + }); + + it('should deliver empty string post body', function (done) { + var nockedPostWithoutBody = nock('http://127.0.0.1:12345') + .post('/') + .matchHeader('Content-Type', 'application/json') + .reply(200, { + name: 'tobi' + }); + + localServer.use('/proxy', proxy('http://127.0.0.1:12345')); + localServer.use(function (req, res) { res.sendStatus(200); }); + localServer.use(function (err, req, res, next) { throw new Error(err, req, res, next); }); + + request(localServer) + .post('/proxy') + .send('') + .set('Content-Type', 'application/json') + .expect(function (res) { + assert(res.body.name === 'tobi'); + nockedPostWithoutBody.done(); + }) + .end(done); + }); + + it('should deliver empty object post body', function (done) { + var nockedPostWithoutBody = nock('http://127.0.0.1:12345') + .post('/', {}) + .matchHeader('Content-Type', 'application/json') + .reply(200, { + name: 'tobi' + }); + + localServer.use('/proxy', proxy('http://127.0.0.1:12345')); + localServer.use(function (req, res) { res.sendStatus(200); }); + localServer.use(function (err, req, res, next) { throw new Error(err, req, res, next); }); + + request(localServer) + .post('/proxy') + .send({}) + .set('Content-Type', 'application/json') + .expect(function (res) { + assert(res.body.name === 'tobi'); + nockedPostWithoutBody.done(); + }) + .end(done); + }); + +}); diff --git a/test/preserveHostHdr.js b/test/preserveHostHdr.js new file mode 100644 index 00000000..26cf1e13 --- /dev/null +++ b/test/preserveHostHdr.js @@ -0,0 +1,70 @@ +'use strict'; + +var assert = require('assert'); +var express = require('express'); +var request = require('supertest'); +var proxy = require('../'); + +var proxyTarget = require('../test/support/proxyTarget'); +var proxyRouteFn = [{ + method: 'get', + path: '/hostHdrTest', + fn: function (req, res) { + res.send(req.headers.host); + } +}]; + +describe('preserves host header only when requested', function () { + + this.timeout(10000); + + var app; + var proxyServer; + + describe('when preserveHostHdr is true', function () { + before(function () { + proxyServer = proxyTarget(12346, 100, proxyRouteFn); + app = express(); + app.use(proxy('localhost:12346', { + preserveHostHdr: true + })); + }); + + after(function () { + proxyServer.close(); + }); + + it('host is passed forward', function (done) { + request(app) + .get('/hostHdrTest') + .set('host', 'hamburger-helper') + .end(function (err, res) { + assert(res.text === 'hamburger-helper'); + done(); + }); + }); + }); + + describe('when preserveHostHdr is absent or false', function () { + before(function () { + proxyServer = proxyTarget(12346, 100, proxyRouteFn); + app = express(); + app.use(proxy('localhost:12346')); + }); + + after(function () { + proxyServer.close(); + }); + + it('host is not passed forward', function (done) { + request(app) + .get('/hostHdrTest') + .set('host', 'hamburger-helper') + .end(function (err, res) { + assert(res.text !== 'hamburger-helper'); + done(); + }); + }); + }); + +}); diff --git a/test/proxyReqOptDecorator.js b/test/proxyReqOptDecorator.js new file mode 100644 index 00000000..a8b666e2 --- /dev/null +++ b/test/proxyReqOptDecorator.js @@ -0,0 +1,150 @@ +'use strict'; + +var assert = require('assert'); +var express = require('express'); +var http = require('http'); +var request = require('supertest'); +var proxy = require('../'); +var proxyTarget = require('../test/support/proxyTarget'); + +describe('proxyReqOptDecorator', function () { + var server; + + this.timeout(10000); + + before(function () { + var handlers = [{ + method: 'get', + path: '/working', + fn: function (req, res) { + res.send({ headers: req.headers }); + } + }]; + + server = proxyTarget(12345, 100, handlers); + }); + + after(function () { + server.close(); + }); + + this.timeout(10000); + + var app; + + beforeEach(function () { + app = express(); + app.use(proxy('localhost:12345')); + }); + + describe('allows authors to modify a number of request parameters', function () { + it('modify headers', function (done) { + app = express(); + app.use(proxy('localhost:12345', { + proxyReqOptDecorator: function (reqOpt) { + reqOpt.headers['user-agent'] = 'test user agent'; + reqOpt.headers.mmmmmmmmmm = 'misty mountain hop'; + return reqOpt; + } + })); + + request(app) + .get('/working') + .end(function (err, res) { + if (err) { return done(err); } + + assert.equal(res.body.headers['user-agent'], 'test user agent'); + assert.equal(res.body.headers.mmmmmmmmmm, 'misty mountain hop'); + done(); + }); + }); + + describe('when proxyReqOptDecorator is a simple function (non Promise)', function () { + it('can modify headers', function (done) { + var app = express(); + app.use(proxy('localhost:12345', { + proxyReqOptDecorator: function (reqOpt, req) { + reqOpt.headers['user-agent'] = 'test user agent'; + assert(req instanceof http.IncomingMessage); + return reqOpt; + } + })); + + request(app) + .get('/working') + .end(function (err, res) { + if (err) { return done(err); } + assert.equal(res.body.headers['user-agent'], 'test user agent'); + done(); + }); + }); + }); + + describe('when proxyReqOptDecorator is a Promise', function () { + it('should mutate the proxied request', function (done) { + var app = express(); + app.use(proxy('localhost:12345', { + proxyReqOptDecorator: function (reqOpt, req) { + assert(req instanceof http.IncomingMessage); + return new Promise(function (resolve) { + reqOpt.headers['user-agent'] = 'test user agent'; + resolve(reqOpt); + }); + } + })); + + request(app) + .get('/working') + .end(function (err, res) { + if (err) { return done(err); } + assert.equal(res.body.headers['user-agent'], 'test user agent'); + done(); + }); + }); + + describe('when the Promise is rejected', function () { + it('returns err to host application for processing', function (done) { + var app = express(); + + app.use(proxy('/reject-promise', { + proxyReqOptDecorator: function () { + return Promise.reject('An arbitrary rejection message.'); + } + })); + + /* jshint ignore:start */ + app.use(function (err, req, res, next) { // eslint-disable-line no-unused-vars + assert(err === 'An arbitrary rejection message.'); + res.status(221).send(err); + }); + /* jshint ignore:end */ + + request(app) + .get('/reject-promise') + .expect(221, done); // ensures we've entered the handler with assert above + }); + }); + }); + }); + + describe('proxyReqOptDecorator has access to the source request\'s data', function () { + it('should have access to ip', function (done) { + var app = express(); + app.use(proxy('localhost:12345', { + proxyReqOptDecorator: function (reqOpts, req) { + assert(req instanceof http.IncomingMessage); + assert(req.ip); + return reqOpts; + } + })); + + request(app) + .get('/working') + .end(function (err) { + if (err) { return done(err); } + done(); + }); + + }); + }); +}); diff --git a/test/proxyReqPathResolver.js b/test/proxyReqPathResolver.js new file mode 100644 index 00000000..2e0f3bb4 --- /dev/null +++ b/test/proxyReqPathResolver.js @@ -0,0 +1,64 @@ +'use strict'; + +var assert = require('assert'); +var express = require('express'); +var http = require('http'); +var request = require('supertest'); +var proxy = require('../'); +var proxyTarget = require('../test/support/proxyTarget'); + +var aliases = ['forwardPath', 'forwardPathAsync', 'proxyReqPathResolver']; + +describe('resolveProxyReqPath', function () { + var server; + + this.timeout(10000); + + before(function () { + var handlers = [{ + method: 'get', + path: '/working', + fn: function (req, res) { + res.sendStatus(200); + } + }]; + + server = proxyTarget(12345, 100, handlers); + }); + + after(function () { + server.close(); + }); + + aliases.forEach(function (alias) { + describe('when author uses option ' + alias, function () { + it('the proxy request path is the result of the function', function (done) { + var app = express(); + var opts = {}; + opts[alias] = function () { return '/working'; }; + app.use(proxy('localhost:12345', opts)); + + request(app) + .get('/failing') + .expect(200) + .end(done); + }); + + it('the ' + alias + ' method has access to request object', function (done) { + var app = express(); + app.use(proxy('localhost:12345', { + forwardPath: function (req) { + assert.ok(req instanceof http.IncomingMessage); + return '/working'; + } + })); + + request(app) + .get('/foobar') + .expect(200) + .end(done); + }); + + }); + }); +}); diff --git a/test/resolveProxyReqPath.js b/test/resolveProxyReqPath.js new file mode 100644 index 00000000..d02c80c1 --- /dev/null +++ b/test/resolveProxyReqPath.js @@ -0,0 +1,116 @@ +'use strict'; + +var assert = require('assert'); +var ScopeContainer = require('../lib/scopeContainer'); +var resolveProxyReqPath = require('../app/steps/resolveProxyReqPath'); +var expect = require('chai').expect; +var express = require('express'); +var request = require('supertest'); +var proxy = require('../'); + + +describe('resolveProxyReqPath', function () { + var container; + + beforeEach(function () { + container = new ScopeContainer(); + }); + + var tests = [ + { + resolverType: 'undefined', + resolverFn: undefined, + data: [ + { url: 'http://localhost:12345', parsed: '/' }, + { url: 'http://g.com/123?45=67', parsed: '/123?45=67' } + ] + }, + { + resolverType: 'a syncronous function', + resolverFn: function () { return 'the craziest thing'; }, + data: [ + { url: 'http://localhost:12345', parsed: 'the craziest thing' }, + { url: 'http://g.com/123?45=67', parsed: 'the craziest thing' } + ] + }, + { + resolverType: 'a Promise', + resolverFn: function () { + return new Promise(function (resolve) { + resolve('the craziest think'); + }); + }, + data: [ + { url: 'http://localhost:12345', parsed: 'the craziest think' }, + { url: 'http://g.com/123?45=67', parsed: 'the craziest think' } + ] + } + ]; + + describe('when proxyReqPathResolver', function () { + + tests.forEach(function (test) { + describe('is ' + test.resolverType, function () { + describe('it returns a promise which resolves a container with expected url', function () { + test.data.forEach(function (data) { + it(data.url, function (done) { + container.user.req = { url: data.url }; + container.options.proxyReqPathResolver = test.resolverFn; + var r = resolveProxyReqPath(container); + + assert(r instanceof Promise, 'Expect resolver to return a thennable'); + + r.then(function (container) { + var response; + try { + response = container.proxy.reqBuilder.path; + if (!response) { + throw new Error('reqBuilder.url is undefined'); + } + } catch (e) { + done(e); + } + expect(response).to.equal(data.parsed); + done(); + }); + }); + }); + }); + }); + }); + + }); + + describe('testing example code in docs', function () { + it('works as advertised', function (done) { + var proxyTarget = require('../test/support/proxyTarget'); + var proxyRouteFn = [{ + method: 'get', + path: '/tent', + fn: function (req, res) { + res.send(req.url); + } + }]; + + var proxyServer = proxyTarget(12345, 100, proxyRouteFn); + var app = express(); + app.use(proxy('localhost:12345', { + proxyReqPathResolver: function (req) { + var parts = req.url.split('?'); + var queryString = parts[1]; + var updatedPath = parts[0].replace(/test/, 'tent'); + return updatedPath + (queryString ? '?' + queryString : ''); + } + })); + + request(app) + .get('/test?a=1&b=2&c=3') + .end(function (err, res) { + assert.equal(res.text, '/tent?a=1&b=2&c=3'); + proxyServer.close(); + done(err); + }); + }); + }); + +}); diff --git a/test/session.js b/test/session.js index c1d8a4bd..7b43916d 100644 --- a/test/session.js +++ b/test/session.js @@ -1,36 +1,38 @@ +'use strict'; + var assert = require('assert'); var express = require('express'); var request = require('supertest'); var proxy = require('../'); -describe('preserveReqSession', function() { - 'use strict'; +describe('preserveReqSession', function () { this.timeout(10000); var app; - beforeEach(function() { + beforeEach(function () { app = express(); app.use(proxy('httpbin.org')); }); - it('preserveReqSession', function(done) { + it('preserveReqSession', function (done) { var app = express(); - app.use(function(req, res, next) { + app.use(function (req, res, next) { req.session = 'hola'; next(); }); app.use(proxy('httpbin.org', { preserveReqSession: true, - decorateRequest: function(req) { - assert(req.session, 'hola'); + proxyReqOptDecorator: function (reqOpts) { + assert(reqOpts.session, 'hola'); + return reqOpts; } })); request(app) .get('/user-agent') - .end(function(err) { + .end(function (err) { if (err) { return done(err); } done(); }); diff --git a/test/status.js b/test/status.js index 632e613d..872d64cf 100644 --- a/test/status.js +++ b/test/status.js @@ -1,28 +1,28 @@ +'use strict'; + var express = require('express'); var request = require('supertest'); var proxy = require('../'); var mockEndpoint = require('../lib/mockHTTP.js'); -describe('proxies status code', function() { - 'use strict'; - +describe('proxies status code', function () { var proxyServer = express(); - var port = 21231; + var port = 21239; var proxiedEndpoint = 'http://localhost:' + port; var server; proxyServer.use(proxy(proxiedEndpoint)); - beforeEach(function() { - server = mockEndpoint.listen(21231); + beforeEach(function () { + server = mockEndpoint.listen(21239); }); - afterEach(function() { + afterEach(function () { server.close(); }); - [304, 404, 200, 401, 500].forEach(function(status) { - it('on ' + status, function(done) { + [304, 404, 200, 401, 500].forEach(function (status) { + it('on ' + status, function (done) { request(proxyServer) .get('/status/' + status) .expect(status, done); diff --git a/test/streaming.js b/test/streaming.js new file mode 100644 index 00000000..ab5aef53 --- /dev/null +++ b/test/streaming.js @@ -0,0 +1,121 @@ +'use strict'; + +var assert = require('assert'); +var express = require('express'); +var http = require('http'); +var startProxyTarget = require('./support/proxyTarget'); +var proxy = require('../'); + +function chunkingProxyServer() { + var proxyRouteFn = [{ + method: 'get', + path: '/stream', + fn: function (req, res) { + res.write('0'); + setTimeout(function () { res.write('1'); }, 100); + setTimeout(function () { res.write('2'); }, 200); + setTimeout(function () { res.write('3'); }, 300); + setTimeout(function () { res.end(); }, 500); + } + }]; + + return startProxyTarget(8309, 1000, proxyRouteFn); +} + +function simulateUserRequest() { + return new Promise(function (resolve, reject) { + var req = http.request({ hostname: 'localhost', port: 8308, path: '/stream' }, function (res) { + var chunks = []; + res.on('data', function (chunk) { chunks.push(chunk.toString()); }); + res.on('end', function () { resolve(chunks); }); + }); + + req.on('error', function (e) { + reject('problem with request:', e.message); + }); + + req.end(); + }); +} + +function startLocalServer(proxyOptions) { + var app = express(); + app.get('/stream', proxy('http://localhost:8309', proxyOptions)); + return app.listen(8308); +} + +describe('streams / piped requests', function () { + this.timeout(3000); + + var server; + var targetServer; + + beforeEach(function () { + targetServer = chunkingProxyServer(); + }); + + afterEach(function () { + server.close(); + targetServer.close(); + }); + + describe('when streaming options are truthy', function () { + var TEST_CASES = [{ + name: 'vanilla, no options defined', + options: {} + }, { + name: 'proxyReqOptDecorator is defined', + options: { proxyReqOptDecorator: function (reqBuilder) { return reqBuilder; } } + }, { + //// Keep around this case for manually testing that this for sure fails for a few cycles. 2018 NMK + //name: 'proxyReqOptDecorator never returns', + //options: { proxyReqOptDecorator: function () { return new Promise(function () {}); } } + //}, { + + name: 'proxyReqOptDecorator is a Promise', + options: { proxyReqOptDecorator: function (reqBuilder) { return Promise.resolve(reqBuilder); } } + }]; + + TEST_CASES.forEach(function (testCase) { + describe(testCase.name, function () { + it('chunks are received without any buffering, e.g. before request end', function (done) { + server = startLocalServer(testCase.options); + simulateUserRequest() + .then(function (res) { + // Assume that if I'm getting a chunked response, it will be an array of length > 1; + + assert(res instanceof Array, 'res is an Array'); + assert.equal(res.length, 4); + done(); + }) + .catch(done); + }); + }); + }); + }); + + describe('when streaming options are falsey', function () { + var TEST_CASES = [{ + name: 'skipToNextHandler is defined', + options: { skipToNextHandlerFilter: function () { return false; } } + }]; + + TEST_CASES.forEach(function (testCase) { + describe(testCase.name, function () { + it('response arrives in one large chunk', function (done) { + server = startLocalServer(testCase.options); + + simulateUserRequest() + .then(function (res) { + // Assume that if I'm getting a un-chunked response, it will be an array of length = 1; + + assert(res instanceof Array); + assert.equal(res.length, 1); + done(); + }) + .catch(done); + }); + }); + }); + }); +}); diff --git a/test/support/proxyTarget.js b/test/support/proxyTarget.js new file mode 100644 index 00000000..b6ec0810 --- /dev/null +++ b/test/support/proxyTarget.js @@ -0,0 +1,55 @@ +'use strict'; + +var express = require('express'); +var bodyParser = require('body-parser'); +var cookieParser = require('cookie-parser'); + +function proxyTarget(port, timeout, handlers) { + var target = express(); + + timeout = timeout || 100; + + // parse application/x-www-form-urlencoded + target.use(bodyParser.urlencoded({ extended: false })); + + // parse application/json + target.use(bodyParser.json()); + target.use(cookieParser()); + + target.use(function(req, res, next) { + setTimeout(function() { + next(); + },timeout); + }); + + if (handlers) { + handlers.forEach(function(handler) { + target[handler.method](handler.path, handler.fn); + }); + } + + target.get('/get', function (req, res) { + res.send('OK'); + }); + + target.post('/post', function(req, res) { + req.pipe(res); + }); + + target.use('/headers', function(req, res) { + res.json({ headers: req.headers }); + }); + + target.use(function(err, req, res, next) { + res.send(err); + next(); + }); + + target.use(function(req, res) { + res.sendStatus(404); + }); + + return target.listen(port); +} + +module.exports = proxyTarget; diff --git a/test/timeout.js b/test/timeout.js index 2466a601..90ba0a2f 100644 --- a/test/timeout.js +++ b/test/timeout.js @@ -1,29 +1,25 @@ -var assert = require('assert'); +'use strict'; + var express = require('express'); var request = require('supertest'); var proxy = require('../'); +var proxyTarget = require('./support/proxyTarget'); -function proxyTarget(port, timeout) { - 'use strict'; - var other = express(); - other.get('/', function(req, res) { - setTimeout(function() { - res.send('Success'); - },timeout); - }); - return other.listen(port); -} +describe('honors timeout option', function () { -describe('honors timeout option', function() { - 'use strict'; + var other; + var http; - var other, http; - beforeEach(function() { + beforeEach(function () { http = express(); - other = proxyTarget(8080, 1000); + other = proxyTarget(56001, 1000, [{ + method: 'get', + path: '/', + fn: function (req, res) { res.sendStatus(200); } + }]); }); - afterEach(function() { + afterEach(function () { other.close(); }); @@ -31,27 +27,21 @@ describe('honors timeout option', function() { request(http) .get('/') .expect(200) - .end(function(err, res) { - if (err) { return done(err); } - assert(res.text === 'Success'); - done(); - }); + .end(done); } function assertConnectionTimeout(server, done) { request(http) .get('/') - .expect(408) - .expect('X-Timout-Reason', 'express-http-proxy timed out your request after 100 ms.') - .end(function() { - done(); - }); + .expect(504) + .expect('X-Timeout-Reason', 'express-http-proxy reset the request.') + .end(done); } - describe('when timeout option is set lower than server response time', function() { - it('should fail with CONNECTION TIMEOUT', function(done) { + describe('when timeout option is set lower than server response time', function () { + it('should fail with CONNECTION TIMEOUT', function (done) { - http.use(proxy('http://localhost:8080', { + http.use(proxy('http://localhost:56001', { timeout: 100, })); @@ -59,10 +49,10 @@ describe('honors timeout option', function() { }); }); - describe('when timeout option is set higher than server response time', function() { - it('should succeed', function(done) { + describe('when timeout option is set higher than server response time', function () { + it('should succeed', function (done) { - http.use(proxy('http://localhost:8080', { + http.use(proxy('http://localhost:56001', { timeout: 1200, })); diff --git a/test/traceDebugging.js b/test/traceDebugging.js new file mode 100644 index 00000000..92d249a0 --- /dev/null +++ b/test/traceDebugging.js @@ -0,0 +1,55 @@ +'use strict'; + +var debug = require('debug'); +var express = require('express'); +var request = require('supertest'); +var proxy = require('../'); +var proxyTarget = require('../test/support/proxyTarget'); + +/* This test is specifically written because of critical errors thrown while debug logging. */ +describe('trace debugging does not cause the application to fail', function () { + var proxyServer; + + before(function () { + proxyServer = proxyTarget(8109, 1000); + }); + + after(function () { + proxyServer.close(); + }); + + beforeEach(function () { + debug.enable('express-http-proxy'); + }); + + afterEach(function () { + debug.disable('express-http-proxy'); + }); + + it('happy path', function (done) { + debugger; + var app = express(); + app.use(proxy('localhost:8109')); + request(app) + .get('/get') + .expect(200) + .end(done); + }); + + it('when agent is a deeply nested object', function (done) { + var app = express(); + var HttpAgent = require('http').Agent; + var httpAgent = new HttpAgent({ keepAlive: true, keepAliveMsecs: 60e3 }); + app.use(proxy('localhost:8109', { + proxyReqOptDecorator: function (proxyReqOpts) { + proxyReqOpts.agent = httpAgent; + return proxyReqOpts; + } + })); + request(app) + .get('/get') + .expect(200) + .end(done); + }); +}); + diff --git a/test/urlParsing.js b/test/urlParsing.js index fa37424b..339492f1 100644 --- a/test/urlParsing.js +++ b/test/urlParsing.js @@ -1,31 +1,32 @@ +'use strict'; + var assert = require('assert'); var express = require('express'); var request = require('supertest'); var proxy = require('../'); -describe('url parsing', function() { - 'use strict'; +describe('url parsing', function () { this.timeout(10000); - it('can parse a url with a port', function(done) { + it('can parse a url with a port', function (done) { var app = express(); app.use(proxy('http://httpbin.org:80')); request(app) .get('/') - .end(function(err) { + .end(function (err) { if (err) { return done(err); } assert(true); done(); }); }); - it('does not throw `Uncaught RangeError` if you have both a port and a trailing slash', function(done) { + it('does not throw `Uncaught RangeError` if you have both a port and a trailing slash', function (done) { var app = express(); app.use(proxy('http://httpbin.org:80/')); request(app) .get('/') - .end(function(err) { + .end(function (err) { if (err) { return done(err); } assert(true); done(); @@ -34,4 +35,3 @@ describe('url parsing', function() { }); - diff --git a/test/userResDecorator.js b/test/userResDecorator.js new file mode 100644 index 00000000..b500ca5d --- /dev/null +++ b/test/userResDecorator.js @@ -0,0 +1,213 @@ +'use strict'; + +var assert = require('assert'); +var express = require('express'); +var request = require('supertest'); +var proxy = require('../'); + +describe('userResDecorator', function () { + + describe('when handling a 304', function () { + this.timeout(10000); + + var app; + var slowTarget; + var serverReference; + + beforeEach(function () { + app = express(); + slowTarget = express(); + slowTarget.use(function (req, res) { res.sendStatus(304); }); + serverReference = slowTarget.listen(12345); + }); + + afterEach(function () { + serverReference.close(); + }); + + it('skips any handling', function (done) { + app.use('/proxy', proxy('http://127.0.0.1:12345', { + userResDecorator: function (/*res*/) { + throw new Error('expected to never get here because this step should be skipped for 304'); + } + })); + + request(app) + .get('/proxy') + .expect(304) + .end(done); + }); + }); + + it('has access to original response', function (done) { + var app = express(); + app.use(proxy('httpbin.org', { + userResDecorator: function (proxyRes, proxyResData) { + assert(proxyRes.connection); + assert(proxyRes.socket); + assert(proxyRes.headers); + assert(proxyRes.headers['content-type']); + return proxyResData; + } + })); + + request(app).get('/').end(done); + }); + + it('works with promises', function (done) { + var app = express(); + app.use(proxy('httpbin.org', { + userResDecorator: function (proxyRes, proxyResData) { + return new Promise(function (resolve) { + proxyResData.funkyMessage = 'oi io oo ii'; + setTimeout(function () { + resolve(proxyResData); + }, 200); + }); + } + })); + + request(app) + .get('/ip') + .end(function (err, res) { + if (err) { return done(err); } + + assert(res.body.funkyMessage = 'oi io oo ii'); + done(); + }); + + }); + + it('can modify the response data', function (done) { + var app = express(); + app.use(proxy('httpbin.org', { + userResDecorator: function (proxyRes, proxyResData) { + proxyResData = JSON.parse(proxyResData.toString('utf8')); + proxyResData.intercepted = true; + return JSON.stringify(proxyResData); + } + })); + + request(app) + .get('/ip') + .end(function (err, res) { + if (err) { return done(err); } + + assert(res.body.intercepted); + done(); + }); + }); + + + it('can modify the response headers, [deviant case, supported by pass-by-reference atm]', function (done) { + var app = express(); + app.use(proxy('httpbin.org', { + userResDecorator: function (rsp, data, req, res) { + res.set('x-wombat-alliance', 'mammels'); + res.set('content-type', 'wiki/wiki'); + return data; + } + })); + + request(app) + .get('/ip') + .end(function (err, res) { + if (err) { return done(err); } + assert(res.headers['content-type'] === 'wiki/wiki'); + assert(res.headers['x-wombat-alliance'] === 'mammels'); + done(); + }); + }); + + it('can mutuate an html response', function (done) { + var app = express(); + app.use(proxy('httpbin.org', { + userResDecorator: function (rsp, data) { + data = data.toString().replace('Oh', 'Hey'); + assert(data !== ''); + return data; + } + })); + + request(app) + .get('/html') + .end(function (err, res) { + if (err) { return done(err); } + assert(res.text.indexOf('Hey') > -1); + done(); + }); + }); + + it('can change the location of a redirect', function (done) { + + function redirectingServer(port, origin) { + var app = express(); + app.get('/', function (req, res) { + res.status(302); + res.location(origin + '/proxied/redirect/url'); + res.send(); + }); + return app.listen(port); + } + + var redirectingServerPort = 8012; + var redirectingServerOrigin = ['http://localhost', redirectingServerPort].join(':'); + + var server = redirectingServer(redirectingServerPort, redirectingServerOrigin); + + var proxyApp = express(); + var preferredPort = 3000; + + proxyApp.use(proxy(redirectingServerOrigin, { + userResDecorator: function (rsp, data, req, res) { + var proxyReturnedLocation = res.getHeaders ? res.getHeaders().location : res._headers.location; + res.location(proxyReturnedLocation.replace(redirectingServerPort, preferredPort)); + return data; + } + })); + + request(proxyApp) + .get('/') + .expect(function (res) { + res.headers.location.match(/localhost:3000/); + }) + .end(function () { + server.close(); + done(); + }); + }); +}); + + +describe('test userResDecorator on html response from github', function () { + + /* + Github provided a unique situation where the encoding was different than + utf-8 when we didn't explicitly ask for utf-8. This test helped sort out + the issue, and even though its a little too on the nose for a specific + case, it seems worth keeping around to ensure we don't regress on this + issue. + */ + + it('is able to read and manipulate the response', function (done) { + this.timeout(15000); // give it some extra time to get response + var app = express(); + app.use(proxy('https://github.com/villadora/express-http-proxy', { + userResDecorator: function (targetResponse, data) { + data = data.toString().replace('DOCTYPE', 'WINNING'); + assert(data !== ''); + return data; + } + })); + + request(app) + .get('/html') + .end(function (err, res) { + if (err) { return done(err); } + assert(res.text.indexOf('WINNING') > -1); + done(); + }); + + }); +}); + diff --git a/test/verbs.js b/test/verbs.js index ca4b9538..84ab13b4 100644 --- a/test/verbs.js +++ b/test/verbs.js @@ -1,23 +1,27 @@ +'use strict'; + var assert = require('assert'); var express = require('express'); +var bodyParser = require('body-parser'); var request = require('supertest'); var proxy = require('../'); -describe('http verbs', function() { - 'use strict'; +describe('http verbs', function () { this.timeout(10000); var app; - beforeEach(function() { + beforeEach(function () { app = express(); + app.use(bodyParser.json()); + app.use(bodyParser.urlencoded({ extended: false })); app.use(proxy('httpbin.org')); }); - it('test proxy get', function(done) { + it('test proxy get', function (done) { request(app) .get('/get') - .end(function(err, res) { + .end(function (err, res) { if (err) { return done(err); } assert(/node-superagent/.test(res.body.headers['User-Agent'])); assert.equal(res.body.url, 'http://httpbin.org/get'); @@ -25,49 +29,60 @@ describe('http verbs', function() { }); }); - it('test proxy post', function(done) { + it('test proxy post', function (done) { request(app) .post('/post') .send({ mypost: 'hello' }) - .end(function(err, res) { + .end(function (err, res) { assert.equal(res.body.data, '{"mypost":"hello"}'); done(err); }); }); - it('test proxy put', function(done) { + it('test proxy post by x-www-form-urlencoded', function (done) { + request(app) + .post('/post') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('mypost=hello') + .end(function (err, res) { + assert.equal(JSON.stringify(res.body.form), '{"mypost":"hello"}'); + done(err); + }); + }); + + it('test proxy put', function (done) { request(app) .put('/put') .send({ mypost: 'hello' }) - .end(function(err, res) { + .end(function (err, res) { assert.equal(res.body.data, '{"mypost":"hello"}'); done(err); }); }); - it('test proxy patch', function(done) { + it('test proxy patch', function (done) { request(app) .patch('/patch') .send({ mypost: 'hello' }) - .end(function(err, res) { + .end(function (err, res) { assert.equal(res.body.data, '{"mypost":"hello"}'); done(err); }); }); - it('test proxy delete', function(done) { + it('test proxy delete', function (done) { request(app) .del('/delete') .send({ mypost: 'hello' }) - .end(function(err, res) { + .end(function (err, res) { assert.equal(res.body.data, '{"mypost":"hello"}'); done(err); });