├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .travis.yml ├── CODEOWNERS ├── LICENSE ├── README.md ├── config ├── default.example.json ├── production.example.json └── testing.json ├── examples ├── how-it-works.png ├── limits-config.sample.json ├── mock-server.js ├── screencast.png └── settings.sample.json ├── index.js ├── lib ├── log4js-configuration.json ├── rate-limiter.js └── reverse-proxy-rate-limiter │ ├── bucket.js │ ├── conditions │ ├── conditions.js │ ├── predicates.js │ └── subject-types.js │ ├── counter.js │ ├── index.js │ ├── ipextractor.js │ ├── ipresolver.js │ ├── limits-config-schema.js │ ├── limits-config.js │ ├── limits-evaluator.js │ └── settings.js ├── package-lock.json ├── package.json ├── start-rate-limiter.js └── test ├── bucket-test.js ├── condition-test.js ├── config-update-test.js ├── config-validation-test.js ├── counterstore-test.js ├── fixtures └── example_configuration.json ├── helpers.js ├── integration ├── complex-limit-tests.js ├── counter-test.js ├── error-tests.js ├── eventhook-test.js ├── healthcheck-test.js ├── integration-tester.js ├── integration-utils.js ├── simple-limit-tests.js └── simple-tests.js ├── ipextractor-test.js ├── ipresolver-test.js └── rate-limiter-test.js /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Reason of the change 2 | *JIRA ticket URL or free-text reason why these changes are needed.* 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # IntelliJ 31 | *.iml 32 | .idea 33 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | config/ 2 | test/ 3 | start-rate-limiter.js 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "11" 4 | env: NODE_ENV=testing 5 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @prezi/dx @prezi/sre -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | 3 | Version 2.0, January 2004 4 | 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 16 | 17 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 18 | 19 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 20 | 21 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 22 | 23 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 24 | 25 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 26 | 27 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 28 | 29 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 30 | 31 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 32 | 33 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 34 | 35 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 36 | 37 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 38 | 39 | You must cause any modified files to carry prominent notices stating that You changed the files; and 40 | 41 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 42 | 43 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 44 | 45 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 46 | 47 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 48 | 49 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 50 | 51 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 52 | 53 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 54 | 55 | END OF TERMS AND CONDITIONS 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reverse-proxy-rate-limiter 2 | 3 | [![Build Status](https://travis-ci.org/prezi/reverse-proxy-rate-limiter.svg)](https://travis-ci.org/prezi/reverse-proxy-rate-limiter) 4 | 5 | `reverse-proxy-rate-limiter` is a reverse proxy written in Node.js that protects the service behind it from being overloaded. It limits incoming requests based on their origin and the number of active concurrent requests while ensuring that the service’s capacity is fully utilized. 6 | 7 | ## Usecase 8 | Web services are often used by very different types of clients. If we imagine a service storing presentations, there could be users loading presentations in their browser and perhaps a search service that would like to index the content of the presentations. In this scenario, the requests from the user wanting to present is much more important than the one from the search service - the latter can wait and come back if it couldn’t retrieve presentations, but the user cannot. 9 | 10 | The `reverse-proxy-rate-limiter` helps with managing such a situation. Standing between the clients and the service providing presentations, it ensures that the search service won’t consume capacity that is needed for serving requests from the users. The specific capacity that is required for serving user traffic is computed dynamically. If users would stop requesting presentations from this service, the search service would automatically be enabled to consume all of the service’s capacity. 11 | 12 | ## Rate-limiting concept 13 | 14 | ![How it works](https://github.com/prezi/reverse-proxy-rate-limiter/blob/master/examples/how-it-works.png?raw=true) 15 | 16 | ### Buckets 17 | The `reverse-proxy-rate-limiter` prioritizes requests from different clients by assigning them to different buckets (shown as red and blue slots within the rate-limiter in the figure above) based on HTTP headers. A bucket is basically a set of limitation rules that we want to apply on traffic that we mapped to a bucket. Based on those rules and the active requests both in the bucket and overall service, a request will be forwarded to the service or rejected (indicated with the `429` status code in the figure). Buckets expand beyond their designated capacity if other buckets are not fully consuming their capacity. For example, if the blue client above would stop sending requests, the red client would eventually be able to fill most of the slots so that none of its requests would be rejected. 18 | 19 | ### Concurrent Active Requests 20 | Many rate-limiting solutions reject requests based on the number of incoming requests. This is not an effective measure if the service is handling requests that take different amounts of time to be processed. 100 requests/second might be fine if the requests are processed within 10ms, but not so much if they take 1000ms each. 21 | Instead of this approach, the `reverse-proxy-rate-limiter` limits incoming traffic based on the number of requests that are already handled concurrently by the backend service. 22 | 23 | ## Getting started 24 | `reverse-proxy-rate-limiter` can be installed in a few seconds, let's check out our screencast about it: 25 | 26 | [![Installation screencast](https://github.com/prezi/reverse-proxy-rate-limiter/blob/master/examples/screencast.png?raw=true)](https://asciinema.org/a/17616) 27 | 28 | At first clone the rate-limiter github repository: 29 | ```shell 30 | $ git clone git@github.com:prezi/reverse-proxy-rate-limiter.git 31 | ``` 32 | 33 | Then install the needed npm packages: 34 | ```shell 35 | $ cd reverse-proxy-rate-limiter 36 | $ npm install 37 | ``` 38 | 39 | You can start the rate-limiter with a sample settings file that can be found in the `examples` directory: 40 | ```shell 41 | $ cat examples/settings.sample.json 42 | { 43 | "serviceName": "authservice", 44 | "listenPort": 7000, 45 | "forwardPort": 7001, 46 | "forwardHost": "localhost", 47 | "configRefreshInterval": 0, 48 | "configEndpoint": "file:./examples/limits-config.sample.json" 49 | } 50 | ``` 51 | 52 | The interesting information is that the rate-limiter will be listening on port ``7000``, and will forward the http requests to ``localhost:7001``. 53 | 54 | The configuration of the limitation will be read from a file in this case: ``examples/limits-config.sample.json``: 55 | ```shell 56 | $ cat examples/limits-config.sample.json 57 | { 58 | "version": 1, 59 | "max_requests": 2, 60 | "buffer_ratio": 0, 61 | "buckets": [ 62 | { 63 | "name": "default" 64 | } 65 | ] 66 | } 67 | ``` 68 | 69 | There is no special rule added, but the rate-limiter won't allow more than 2 requests to be served simultaneously. 70 | 71 | Let's start the rate-limiter: 72 | ```shell 73 | $ node start-rate-limiter.js -c examples/settings.sample.json 74 | ``` 75 | 76 | We can start a sample service in the background, listening on the port 7001, which will reply with a JSON that contains all the headers it got from the rate-limiter: 77 | ```shell 78 | $ node examples/mock-server.js 79 | ``` 80 | 81 | We can send a http request to the rate-limiter now: 82 | ```shell 83 | $ curl localhost:7000/test/ 84 | Hello ratelimiter! 85 | /test/ 86 | { 87 | "accept": "*/*", 88 | "host": "localhost:7000", 89 | "user-agent": "curl/7.30.0", 90 | "connection": "close", 91 | "x-ratelimiter-bucket": "default" 92 | } 93 | ``` 94 | 95 | The ``x-ratelimiter-bucket`` is a special header the rate-limiter sets to the forwarded request to give some information to the service in the background about the traffic. 96 | 97 | Let's try to overload the rate-limiter to start rejecting requests. It's not hard, if you send the request to the ``/sleep5secs/`` url, the mock-server won't answer for 5 seconds. With this we can easily send more than 2 requests: 98 | ```shell 99 | $ curl localhost:7000/sleep5secs/ & 100 | [1] 25948 101 | 102 | $ curl localhost:7000/sleep5secs/ & 103 | [2] 25954 104 | 105 | $ curl localhost:7000/sleep5secs/ & 106 | [3] 25960 107 | Request has been rejected by the rate limiter[3] + 25960 done 108 | 109 | $ curl localhost:7000/sleep5secs/ & 110 | [3] 25966 111 | Request has been rejected by the rate limiter[3] + 25966 done 112 | 113 | $ curl localhost:7000/sleep5secs/ & 114 | [3] 25972 115 | Request has been rejected by the rate limiter[3] + 25972 done 116 | ``` 117 | 118 | You can see, that the rate-limiter didn't allow the 3rd request to go to the service. This is basically the gist of how the rate-limiter will protect your service. 119 | 120 | ## Configuration 121 | There are two types of configuration in the context of the `reverse-proxy-rate-limiter`. One configures the reverse proxy itself, the other one configures the buckets and their limits. To avoid confusion, we refer to the former as “settings” and the latter as “limits configuration”. 122 | 123 | ### Settings 124 | There are 5 levels of sources for the settings (all but the first one optional). From lowest to highest priority: 125 | 126 | * Default settings values hard-coded in `lib/reverse-proxy-rate-limiter/settings.js` 127 | * `$PWD/config/default.json` if it exists 128 | * `$PWD/config/$NODE_ENV.json` if it exists 129 | * The second parameter to `lib/reverse-proxy-rate-limiter/settings.js#load` is an optional function which gets called with 130 | a `ConfigBuilder` instance as its only argument. It can make `add{Obj,File,Dir}` calls on it to add 131 | any custom config sources. 132 | * The first parameter to `lib/reverse-proxy-rate-limiter/settings.js#load` is an optional string which is the path to 133 | a settings file. If called from `lib/reverse-proxy-rate-limiter/settings.js#init` (used by `start-rate-limiter.js`), the 134 | command-line argument `--config` (or just `-c`) is passed in here. 135 | 136 | ### Limits Configuration 137 | The limits configuration is periodically loaded by the `reverse-proxy-rate-limiter` from a file or the backend service behind the rate-limiter. The exact path or URL is determined in the settings (it defaults to `:/rate-limiter`). An example limits configuration can be found [here](https://github.com/prezi/reverse-proxy-rate-limiter/blob/master/test/fixtures/example_configuration.json). 138 | 139 | ## Events 140 | The rate-limiter can be run directly (`node start-rate-limiter.js -c settings.sample.json`) or within another project that depends on it. In the latter case, you can listen to events - to implement monitoring, for example - like this: 141 | 142 | ```javascript 143 | const rateLimiter = require("reverse-proxy-rate-limiter"); 144 | const rl = rateLimiter.createRateLimiter(settings); 145 | rl.proxyEvent.on('rejected', function(req, errorCode, reason) { 146 | console.log('Rejected: ', reason); 147 | }); 148 | ``` 149 | 150 | The following events are emitted: 151 | - `forwarded`, params: req - when an incoming request was forwarded to the backend service 152 | - `served`, params: req, res - when a forwarded request was successfully served to the client 153 | - `failed`, params: err, req, res - when the request failed between the rate-limiter and the backend service 154 | - `rejected`, params: req, errorCode, reason - when an incoming request was rejected 155 | - `rejectRequest`, params: req, res, errorCode, reason - allows custom handling of a rejected request. It falls back to a default handler that returns a `429` response if the event is not handled. 156 | 157 | ## Naught integration 158 | The `reverse-proxy-rate-limiter` can be wrapped with [naught](https://github.com/andrewrk/naught) so it supports zero downtime deployment and automatic restarts if the nodejs process dies. 159 | 160 | ## Contribution 161 | Pull requests are very welcome. For discussions, please head over to the [mailing list](https://groups.google.com/forum/#!forum/reverse-proxy-rate-limiter-dev). 162 | 163 | We have a [JSHint](https://packagecontrol.io/packages/JSHint) configuration in place that can help with polishing your code. 164 | 165 | ## License 166 | `reverse-proxy-rate-limiter` is available under the [Apache License, Version 2.0](https://github.com/prezi/reverse-proxy-rate-limiter/blob/master/LICENSE). 167 | -------------------------------------------------------------------------------- /config/default.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "log4js": { 3 | "appenders": { 4 | "console" : { 5 | "type": "console", 6 | "layout": { 7 | "type": "pattern", 8 | "pattern": "%d{ISO8601} %h %c %p %m%n" 9 | } 10 | } 11 | }, 12 | "categories": { 13 | "default": { 14 | "appenders": ["console"], 15 | "level": "debug" 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /config/production.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "log4js": { 3 | "appenders": { 4 | "console" : { 5 | "type": "console", 6 | "layout": { 7 | "type": "pattern", 8 | "pattern": "%d{ISO8601} %h %c %p %m%n" 9 | } 10 | } 11 | }, 12 | "categories": { 13 | "default": { 14 | "appenders": ["console"], 15 | "level": "info" 16 | } 17 | } 18 | }, 19 | "forwarded_headers": { 20 | "X-EXAMPLE-FORWARDED-FOR": { 21 | "ignored_ip_ranges": [ 22 | "127.0.0.0/8", 23 | "10.0.0.0/8", 24 | "172.16.0.0/12", 25 | "192.0.2.0/24", 26 | "192.168.0.0/16", 27 | "193.45.0.0/16" 28 | ] 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /config/testing.json: -------------------------------------------------------------------------------- 1 | { 2 | "log4js": { 3 | "appenders": { 4 | "console" : { 5 | "type": "console", 6 | "layout": { 7 | "type": "pattern", 8 | "pattern": "%d{ISO8601} %h %c %p %m%n" 9 | } 10 | } 11 | }, 12 | "categories": { 13 | "default": { 14 | "appenders": ["console"], 15 | "level": "off" 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/how-it-works.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prezi/reverse-proxy-rate-limiter/ec2c5ad5b11774df11f7bec59efb51804cb7b62c/examples/how-it-works.png -------------------------------------------------------------------------------- /examples/limits-config.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "max_requests": 2, 4 | "buffer_ratio": 0, 5 | "buckets": [ 6 | { 7 | "name": "default" 8 | } 9 | ] 10 | } 11 | 12 | -------------------------------------------------------------------------------- /examples/mock-server.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | const http = require("http"); 5 | 6 | function handler(req, res) { 7 | const status_code = 200; 8 | 9 | let body = ""; 10 | body += "Hello ratelimiter!\n"; 11 | body += req.url + "\n"; 12 | body += JSON.stringify(req.headers, true, 2); 13 | 14 | res.writeHead(status_code, {'Content-Type': 'text/plain'}); 15 | res.write(body); 16 | res.end(); 17 | 18 | 19 | console.log(["request", req.url, status_code].join(" ")) 20 | } 21 | 22 | console.log("Starting mock server on localhost:7001"); 23 | 24 | http.createServer(function (req, res) { 25 | if (req.url == "/sleep5secs/") { 26 | setTimeout(handler, 5000, req, res); 27 | } else { 28 | handler(req, res); 29 | } 30 | }).listen(7001); 31 | })(); 32 | -------------------------------------------------------------------------------- /examples/screencast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prezi/reverse-proxy-rate-limiter/ec2c5ad5b11774df11f7bec59efb51804cb7b62c/examples/screencast.png -------------------------------------------------------------------------------- /examples/settings.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "serviceName": "authservice", 3 | "listenPort": 7000, 4 | "forwardPort": 7001, 5 | "forwardHost": "localhost", 6 | "configRefreshInterval": 0, 7 | "configEndpoint": "file:./examples/limits-config.sample.json" 8 | } 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/rate-limiter'); 2 | 3 | -------------------------------------------------------------------------------- /lib/log4js-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "appenders": [ 3 | { 4 | "type": "console", 5 | "layout": { 6 | "type": "pattern", 7 | "pattern": "%d{ISO8601} %h %c %p %m%n" 8 | } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /lib/rate-limiter.js: -------------------------------------------------------------------------------- 1 | const rateLimiter = require('./reverse-proxy-rate-limiter/'); 2 | 3 | module.exports.createRateLimiter = function createRateLimiter(settings) { 4 | return new rateLimiter.RateLimiter(settings); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/reverse-proxy-rate-limiter/bucket.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Condition = require("./conditions/conditions").Condition; 4 | 5 | function Bucket(bucketConfig) { 6 | this.name = bucketConfig.name; 7 | 8 | this.capacityUnit = 0; 9 | this.maxRequestsPerIp = 0; 10 | this.maxRequests = 0; 11 | 12 | if ("limits" in bucketConfig) { 13 | if ("capacity_unit" in bucketConfig.limits) { 14 | this.capacityUnit = bucketConfig.limits.capacity_unit; 15 | } 16 | if ("max_requests_per_ip" in bucketConfig.limits) { 17 | this.maxRequestsPerIp = bucketConfig.limits.max_requests_per_ip; 18 | } 19 | } 20 | 21 | if (Array.isArray(bucketConfig.conditions)) { 22 | this.conditions = bucketConfig.conditions.map(function (c) { 23 | return new Condition(c); 24 | }); 25 | } else { 26 | this.conditions = []; 27 | } 28 | } 29 | exports.Bucket = Bucket; 30 | 31 | Bucket.prototype = { 32 | isDefault: function () { 33 | return this.conditions.length === 0; 34 | }, 35 | 36 | matches: function (request) { 37 | for (let i = 0; i < this.conditions.length; i++) { 38 | if (!this.conditions[i].evaluate(request)) { 39 | return false; 40 | } 41 | } 42 | 43 | return true; 44 | }, 45 | 46 | getMaxRequests: function () { 47 | return this.maxRequests; 48 | }, 49 | 50 | getMaxRequestsPerIp: function () { 51 | return this.maxRequestsPerIp; 52 | } 53 | }; -------------------------------------------------------------------------------- /lib/reverse-proxy-rate-limiter/conditions/conditions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const _ = require("lodash")._; 4 | const Predicates = require("./predicates").Predicates; 5 | const SubjectTypes = require("./subject-types").SubjectTypes; 6 | 7 | exports.Condition = Condition; 8 | 9 | // ["header", "X-Prezi-Client", "equals","reuse-e5759ce4bb1c298b063f2d8aa1a334"] 10 | // ["client_ip", "equals", "12.23.45.56"] 11 | function Condition(conditionArray) { 12 | if (!Array.isArray(conditionArray)) { 13 | throw new Error("conditionArray parameter must be an array"); 14 | } 15 | this.subject = findSubjectType(conditionArray[0]); 16 | 17 | const expectedArraySize = this.subject.parameterCount + 3; // subject + subject parameters + predicate + value 18 | if (conditionArray.length !== expectedArraySize) { 19 | throw new Error("Expected conditionArray size is " + expectedArraySize + " but was " + conditionArray.length); 20 | } 21 | 22 | conditionArray = _.drop(conditionArray); 23 | if (this.subject.parameterCount === 0) { 24 | this.parameters = []; 25 | } else { 26 | this.parameters = _.slice(conditionArray, 0, this.subject.parameterCount); 27 | conditionArray = _.slice(conditionArray, this.subject.parameterCount); 28 | } 29 | 30 | this.predicateName = conditionArray[0]; 31 | this.predicate = findPredicate(this.predicateName); 32 | if (!_.includes(this.subject.predicates, this.predicate)) { 33 | throw new Error("Predicate " + this.predicateName + " not usable for subject " + this.subject.name); 34 | } 35 | 36 | this.expectedValue = conditionArray[1]; 37 | } 38 | 39 | Condition.prototype.toString = function () { 40 | if (typeof this.stringValue === 'undefined') { 41 | let s = "Condition[" + this.subject.name; 42 | if (this.parameters.length > 0) { 43 | s += "[" + this.parameters + "]"; 44 | } 45 | s += " " + this.predicateName + " '" + this.expectedValue + "']"; 46 | this.stringValue = s; 47 | } 48 | return this.stringValue; 49 | }; 50 | 51 | Condition.prototype.evaluate = function (request) { 52 | const actualValue = this.subject.extractValue(request, this.parameters); 53 | if (actualValue === undefined) { 54 | return false; 55 | } 56 | return this.predicate(actualValue, this.expectedValue); 57 | }; 58 | 59 | function findSubjectType(subject) { 60 | const ret = SubjectTypes[subject]; 61 | if (ret === undefined) { 62 | throw new Error("Invalid subject: " + subject); 63 | } 64 | return ret; 65 | } 66 | 67 | function findPredicate(predicate) { 68 | const ret = Predicates[predicate]; 69 | if (ret === undefined) { 70 | throw new Error("Invalid predicate: " + predicate); 71 | } 72 | return ret; 73 | } 74 | -------------------------------------------------------------------------------- /lib/reverse-proxy-rate-limiter/conditions/predicates.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.Predicates = { 4 | eq: function (actual, expected) { 5 | return actual === expected; 6 | }, 7 | 8 | ne: function (actual, expected) { 9 | return actual !== expected; 10 | }, 11 | 12 | gt: function (actual, expected) { 13 | return actual > expected; 14 | }, 15 | 16 | matches: function (actual, expected) { 17 | return new RegExp(expected).test(actual); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /lib/reverse-proxy-rate-limiter/conditions/subject-types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Predicates = require("./predicates").Predicates, 4 | url = require("url"); 5 | 6 | const SubjectTypes = { 7 | header: { 8 | parameterCount: 1, 9 | predicates: [Predicates.eq, Predicates.ne, Predicates.matches], 10 | extractValue: extractHeader 11 | }, 12 | client_ip: { 13 | parameterCount: 0, 14 | predicates: [Predicates.eq, Predicates.ne], 15 | extractValue: extractClientIp 16 | }, 17 | path: { 18 | parameterCount: 0, 19 | predicates: [Predicates.eq, Predicates.ne, Predicates.matches], 20 | extractValue: extractPath 21 | }, 22 | "true": { // to have a let-everything-through option with ['true', 'eq', 'true'] 23 | parameterCount: 0, 24 | predicates: [Predicates.eq], 25 | extractValue: function () { 26 | return "true"; 27 | } 28 | } 29 | }; 30 | 31 | function extractHeader(request, parameters) { 32 | if (typeof request === "undefined" || typeof request.headers === "undefined") { 33 | return undefined; 34 | } 35 | return request.headers[parameters[0].toLowerCase()]; 36 | } 37 | 38 | function extractClientIp() { 39 | return "1.2.3.4"; 40 | } 41 | 42 | function extractPath(request) { 43 | const urlObject = url.parse(request.url); 44 | return urlObject.pathname; 45 | } 46 | 47 | for (let st in SubjectTypes) { 48 | SubjectTypes[st].name = st; 49 | } 50 | exports.SubjectTypes = SubjectTypes; 51 | 52 | 53 | -------------------------------------------------------------------------------- /lib/reverse-proxy-rate-limiter/counter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.CounterStore = CounterStore; 4 | 5 | const BUCKET_PREFIX = "bucket:"; 6 | const IP_PREFIX = "ip:"; 7 | const GLOBAL_KEY = "global"; 8 | 9 | function CounterStore() { 10 | // counters : { 11 | // "global": 100, 12 | // "bucket:default": 10, 13 | // "ip:default:192.168.1.1": 1, 14 | // "ip:default:192.168.1.2": 1, 15 | // "bucket:reuse": 15 16 | // } 17 | 18 | this.counters = {}; 19 | } 20 | 21 | CounterStore.prototype = { 22 | 23 | getGlobalRequestCount: function () { 24 | const key = GLOBAL_KEY; 25 | return this.counters[key] || 0; 26 | }, 27 | 28 | getRequestCountForBucket: function (bucket) { 29 | const key = BUCKET_PREFIX + bucket.name; 30 | return this.counters[key] || 0; 31 | }, 32 | 33 | getRequestCountForBucketAndIP: function (bucket, ip) { 34 | const key = IP_PREFIX + bucket.name + ":" + ip; 35 | return this.counters[key] || 0; 36 | }, 37 | 38 | increment: function (bucket, ip) { 39 | this.changeValue(bucket, ip, 1); 40 | }, 41 | 42 | decrement: function (bucket, ip) { 43 | this.changeValue(bucket, ip, -1); 44 | }, 45 | 46 | changeValue: function (bucket, ip, incrementBy) { 47 | if (typeof bucket === "undefined") { 48 | return; 49 | } 50 | 51 | const keys = getKeys(bucket, ip); 52 | for (let i = 0; i < keys.length; i++) { 53 | this.changeValueForKey(keys[i], incrementBy); 54 | } 55 | }, 56 | 57 | changeValueForKey: function (key, incrementBy) { 58 | let val; 59 | if (key in this.counters) { 60 | val = this.counters[key]; 61 | } else { 62 | val = 0; 63 | } 64 | val += incrementBy; 65 | if (val > 0) { 66 | this.counters[key] = val; 67 | } else { 68 | delete this.counters[key]; 69 | } 70 | } 71 | }; 72 | 73 | function getKeys(bucket, ip) { 74 | return [GLOBAL_KEY, BUCKET_PREFIX + bucket.name, IP_PREFIX + bucket.name + ":" + ip]; 75 | } 76 | -------------------------------------------------------------------------------- /lib/reverse-proxy-rate-limiter/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const _ = require("lodash"); 4 | const LimitsEvaluator = require("./limits-evaluator"); 5 | const httpProxy = require('http-proxy'); 6 | const http = require("http"); 7 | const log4js = require('log4js'); 8 | const EventEmitter = require('events').EventEmitter; 9 | 10 | module.exports.RateLimiter = RateLimiter; 11 | const logger = log4js.getLogger(); 12 | 13 | // http://nodejs.org/docs/v0.10.35/api/http.html#http_agent_maxsockets 14 | // in v0.12 maxSockets default was changed to Infinity 15 | http.globalAgent.maxSockets = Infinity; 16 | 17 | function RateLimiter(settings) { 18 | this.settings = settings; 19 | log4js.configure(settings.log4js); 20 | 21 | this.proxyEvent = new EventEmitter(); 22 | this.evaluator = new LimitsEvaluator(settings, this.proxyEvent); 23 | 24 | this.initProxy(); 25 | } 26 | 27 | RateLimiter.prototype = { 28 | 29 | initProxy: function () { 30 | logger.info("New worker is being spawned."); 31 | const _this = this; 32 | this.proxy = httpProxy.createProxyServer({}); 33 | 34 | this.proxy.on('proxyReq', function (proxyReq, req, res, options) { 35 | _this.requestForwarded(proxyReq, req); 36 | }); 37 | this.proxy.on('proxyRes', function (proxyRes, req, res) { 38 | _this.requestServed(proxyRes, req, res); 39 | }); 40 | this.proxy.on('error', function (err, req, res, options) { 41 | _this.requestFailed(err, req, res, options); 42 | }); 43 | this.proxyEvent.on('forwarded', this.onForward.bind(this)); 44 | 45 | const server = http.createServer(function (req, res) { 46 | const resultMethod = _this.evaluator.evaluate(req, 47 | _this.makeForward.bind(_this), 48 | _this.makeReject.bind(_this), 49 | _this.makeHealthcheck.bind(_this)); 50 | resultMethod(req, res); 51 | }); 52 | server.listen(_this.settings.listenPort, function () { 53 | logger.info("New worker successfully spawned."); 54 | if (process.send) { 55 | logger.info("New worker sends 'online' message."); 56 | process.send('online'); 57 | } 58 | }); 59 | this.server = server; 60 | 61 | this.processEventListeners = {}; 62 | 63 | this.processEventListeners['SIGTERM'] = function () { 64 | _this.terminate(0); 65 | }; 66 | 67 | this.processEventListeners['message'] = function (message) { 68 | if (message === 'shutdown') { 69 | _this.terminate(0); 70 | } 71 | }; 72 | 73 | this.processEventListeners['uncaughtException'] = function(err) { 74 | logger.error('uncaughtException handler received: ' + err); 75 | _this.proxyEvent.emit('uncaughtException', err); 76 | if (err !== 'TestError') { 77 | _this.terminate(1); 78 | } 79 | }; 80 | 81 | for (let listener in this.processEventListeners) { 82 | if (this.processEventListeners.hasOwnProperty(listener)) { 83 | process.on(listener, this.processEventListeners[listener]); 84 | } 85 | } 86 | }, 87 | 88 | // event handlers 89 | 90 | onForward: function (req) { 91 | logger.debug(req.headers['x-forwarded-for'] + " " + req.method + " " + req.url + " " + this.evaluator.counter.getGlobalRequestCount()); 92 | }, 93 | 94 | // responder generator 95 | 96 | makeForward: function () { 97 | const _this = this; 98 | return function(req, res) { 99 | _this.proxyEvent.emit('forwarded', req); 100 | 101 | _this.proxy.web(req, res, { 102 | target: _this.settings.forwardUrl 103 | }); 104 | } 105 | }, 106 | 107 | makeReject: function(reason, errorCode) { 108 | const _this = this; 109 | errorCode = errorCode || 429; 110 | 111 | return function (req, res) { 112 | _this.proxyEvent.emit('rejected', req, errorCode, reason); 113 | 114 | const handled = _this.proxyEvent.emit('rejectRequest', req, res, errorCode, reason); 115 | if (!handled) { 116 | _this.rejectRequest(req, res, errorCode, reason); 117 | } 118 | } 119 | }, 120 | 121 | makeHealthcheck: function() { 122 | return function (req, res) { 123 | res.writeHead(200, "Rate-Limiter is running"); 124 | res.write("OK"); 125 | res.end(); 126 | } 127 | }, 128 | 129 | rejectRequest: function (req, res, errorCode, reason) { 130 | res.writeHead(errorCode, "Rejected by the rate limiter"); 131 | res.write("Request has been rejected by the rate limiter"); 132 | res.end(); 133 | }, 134 | 135 | terminate: function (value) { 136 | this.close(function () { 137 | logger.info("Old worker proxy process is terminated."); 138 | process.exit(value); 139 | }); 140 | }, 141 | 142 | close: function (done) { 143 | logger.info("Old worker proxy is being closed, will serve active requests but no new request will be accepted."); 144 | if (process.send) { 145 | logger.info("Old worker proxy sends 'offline' message."); 146 | process.send('offline'); 147 | } 148 | 149 | const _this = this; 150 | this.server.close(function () { 151 | logger.info("Old worker proxy is closed."); 152 | 153 | for (let listener in _this.processEventListeners) { 154 | if (_this.processEventListeners.hasOwnProperty(listener)) { 155 | process.removeListener(listener, _this.processEventListeners[listener]); 156 | } 157 | } 158 | done(); 159 | }); 160 | }, 161 | 162 | requestForwarded: function (proxyReq, req) { 163 | if ("bucket" in req) { 164 | proxyReq.setHeader(this.settings.bucketHeaderName, req.bucket.name); 165 | } 166 | }, 167 | 168 | requestServed: function (proxyRes, req, res) { 169 | this.proxyEvent.emit('served', req, res); 170 | }, 171 | 172 | requestFailed: function (err, req, res, options) { 173 | logger.error('proxy error', err); 174 | 175 | this.proxyEvent.emit('failed', err, req, res); 176 | 177 | if (res.headersSent) { 178 | logger.error('Headers are sent already, cannot change HTTP response by now'); 179 | } else { 180 | res.writeHead(500, { 181 | 'Content-Type': 'text/plain' 182 | }); 183 | } 184 | res.end('An internal error has occurred.'); 185 | } 186 | }; 187 | -------------------------------------------------------------------------------- /lib/reverse-proxy-rate-limiter/ipextractor.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const proxyaddr = require('proxy-addr'); 4 | 5 | function IPExtractor(forwardHeader){ 6 | this.internalFilter = proxyaddr.compile(forwardHeader['ignored_ip_ranges'] || []); 7 | } 8 | 9 | IPExtractor.prototype = { 10 | 11 | extractClientIP : function(header){ 12 | 13 | const ipExtractor = this; 14 | const extractedIPs = this.extractIPs(header); 15 | const filteredIPs = extractedIPs.filter(function (ip) { 16 | return !ipExtractor.isIgnoredIP(ip); 17 | }); 18 | 19 | if(filteredIPs.length){ 20 | return filteredIPs.pop(); 21 | } 22 | 23 | return extractedIPs.pop(); 24 | }, 25 | 26 | extractIPs: function(header){ 27 | return header.split(",").map(Function.prototype.call, String.prototype.trim); 28 | }, 29 | 30 | isIgnoredIP: function(ip){ 31 | return this.internalFilter(ip); 32 | } 33 | }; 34 | 35 | exports.IPExtractor = IPExtractor; 36 | -------------------------------------------------------------------------------- /lib/reverse-proxy-rate-limiter/ipresolver.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const log4js = require('log4js'), 4 | IPExtractor = require("./ipextractor").IPExtractor; 5 | 6 | const PROXY_HEADER = 'X-FORWARDED-FOR'.toLowerCase(); 7 | const ipExtractorMap = {}; 8 | 9 | // If the request only passed through external proxies, and maybe ELB, 10 | // the situation is a bit trickier. We have to go from the end of the 11 | // list, and take the first IP that doesn't belong to a private network. 12 | // More information can be found here: 13 | // http://serverfault.com/questions/314574/nginx-real-ip-header-and-x-forwarded-for-seems-wrong#answer-414166 14 | 15 | function IPResolver(forwardedHeadersFromSettings) { 16 | this.forwardedHeaders = forwardedHeadersFromSettings || {}; 17 | this.forwardedHeaders[PROXY_HEADER] = { 18 | "ignored_ip_ranges": ['127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12', '192.0.2.0/24', '192.168.0.0/16'] 19 | }; 20 | } 21 | 22 | IPResolver.prototype.resolve = function (req) { 23 | const remoteAddress = req.socket.remoteAddress; 24 | for (let key in this.forwardedHeaders) { 25 | if (this.forwardedHeaders.hasOwnProperty(key)) { 26 | const hdr = req.headers[key.toLowerCase()]; 27 | if(hdr){ 28 | return this.getExtractor(key).extractClientIP(hdr); 29 | } 30 | } 31 | } 32 | return remoteAddress; 33 | }; 34 | 35 | IPResolver.prototype.getExtractor = function (headerName){ 36 | if(!ipExtractorMap.hasOwnProperty(headerName)) 37 | ipExtractorMap[headerName]= new IPExtractor(this.forwardedHeaders[headerName]); 38 | return ipExtractorMap[headerName]; 39 | }; 40 | 41 | exports.IPResolver = IPResolver; 42 | exports.PROXY_HEADER = PROXY_HEADER; -------------------------------------------------------------------------------- /lib/reverse-proxy-rate-limiter/limits-config-schema.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Validator = require('jsonschema').Validator; 4 | const SchemaError = require('jsonschema').SchemaError; 5 | const Condition = require("./conditions/conditions").Condition; 6 | 7 | exports.validate = validate; 8 | 9 | function validate(limitsConfig) { 10 | const validator = new Validator(); 11 | validator.attributes.isCondition = validateCondition; 12 | validator.addSchema(bucket); 13 | validator.addSchema(bucketLimit); 14 | validator.addSchema(condition); 15 | return validator.validate(limitsConfig, limitsConfigSchema); 16 | } 17 | 18 | const limitsConfigSchema = { 19 | "id": "/LimitsConfig", 20 | "type": "object", 21 | "properties": { 22 | "version": {"type": "integer", "required": true}, 23 | "max_requests": {"type": "integer", "required": true, "minimum": 0}, 24 | "buffer_ratio": {"type": "double", "required": true, "minimum": 0.0}, 25 | "healthcheck_url": {"type": "string"}, 26 | "buckets": { 27 | "type": "array", 28 | "items": {"$ref": "/Buckets"}, 29 | "required": true 30 | } 31 | } 32 | }; 33 | 34 | const bucket = { 35 | "id": "/Buckets", 36 | "type": "object", 37 | "properties": { 38 | "name": {"type": "string", "required": true}, 39 | "conditions": { 40 | "type": "array", 41 | "items": {$ref: "/Condition"} 42 | }, 43 | "limits": {"$ref": "/BucketLimit"} 44 | } 45 | }; 46 | 47 | const condition = { 48 | "id": "/Condition", 49 | "type": "array", 50 | "items": {"type": "string"}, 51 | "isCondition": true 52 | }; 53 | 54 | const bucketLimit = { 55 | "id": "/BucketLimit", 56 | "type": "object", 57 | "properties": { 58 | "capacity_unit": { 59 | "type": "integer", 60 | "required": true, 61 | "minimum": 0 62 | }, 63 | "max_requests_per_ip": {"type": "integer", "minimum": 0} 64 | } 65 | }; 66 | 67 | function validateCondition(instance, schema) { 68 | if (typeof schema.isCondition !== 'boolean') { 69 | throw new SchemaError('"isCondition" expects a boolean', schema); 70 | } 71 | 72 | if (schema.isCondition) { 73 | try { 74 | new Condition(instance); 75 | } 76 | catch (e) { 77 | return e.toString(); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/reverse-proxy-rate-limiter/limits-config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require('fs'); 4 | const request = require("request"); 5 | const url = require("url"); 6 | const limitsConfigSchema = require('./limits-config-schema'); 7 | const Bucket = require("./bucket").Bucket; 8 | const log4js = require('log4js'); 9 | 10 | exports.isValidConfig = isValidConfig; 11 | exports.LimitsConfiguration = LimitsConfiguration; 12 | exports.LimitsConfigurationLoader = LimitsConfigurationLoader; 13 | 14 | exports.defaultLimitsConfig = { 15 | version: 1, 16 | max_requests: 0, // 0 means unlimited 17 | buffer_ratio: 0, 18 | buckets: [{name: "default"}] 19 | }; 20 | 21 | if (!isValidConfig(exports.defaultLimitsConfig)) { 22 | throw new Error("Invalid defaultLimitsConfig"); 23 | } 24 | 25 | const logger = log4js.getLogger(); 26 | 27 | 28 | function LimitsConfiguration(cfg) { 29 | this.buckets = []; 30 | this.bufferRatio = cfg.buffer_ratio; 31 | 32 | this.maxRequests = cfg.max_requests; 33 | this.maxRequestsWithoutBuffer = Math.floor(this.maxRequests * (1 - this.bufferRatio)); 34 | this.healthcheckUrl = cfg.healthcheck_url; 35 | 36 | this.buckets = this.initializeBuckets(cfg.buckets); 37 | 38 | const sumOfCapacityUnits = this.calculateTotalCapacityUnits(); 39 | this.buckets.forEach(function (bucket) { 40 | if (sumOfCapacityUnits === 0) { 41 | bucket.maxRequests = 0; 42 | } else { 43 | const bucketCapacityRatio = bucket.capacityUnit / sumOfCapacityUnits; 44 | bucket.maxRequests = Math.ceil(this.maxRequestsWithoutBuffer * bucketCapacityRatio); 45 | } 46 | }, this); 47 | } 48 | 49 | LimitsConfiguration.prototype = { 50 | initializeBuckets: function (buckets) { 51 | let defaultBucket = null; 52 | let initializedBuckets = []; 53 | 54 | buckets.forEach(function (bucketConfig) { 55 | const bucket = new Bucket(bucketConfig); 56 | 57 | if (bucket.isDefault()) { 58 | if (defaultBucket !== null) { 59 | throw new Error("There is more than one default buckets defined in the limits configuration."); 60 | } 61 | defaultBucket = bucket; // we will push it separately to be at the end of the array 62 | } else { 63 | initializedBuckets.push(bucket); 64 | } 65 | }); 66 | 67 | if (defaultBucket === null) { 68 | throw new Error("No default bucket was set."); 69 | } 70 | initializedBuckets.push(defaultBucket); 71 | 72 | return initializedBuckets; 73 | }, 74 | 75 | calculateTotalCapacityUnits: function () { 76 | let capacityUnits = 0; 77 | this.buckets.forEach(function (bucket) { 78 | capacityUnits += bucket.capacityUnit; 79 | }); 80 | return capacityUnits; 81 | } 82 | }; 83 | 84 | function LimitsConfigurationLoader(configEndpoint) { 85 | this.configEndpoint = configEndpoint; 86 | this.limitsConfigurationSource = null; 87 | 88 | } 89 | 90 | LimitsConfigurationLoader.prototype = { 91 | load: function (callback) { 92 | this.limitsConfigurationSource = new LimitsConfigurationSource(this.configEndpoint); 93 | const forwardValidConfigCallback = this.buildForwardValidConfigCallback(callback); 94 | 95 | if (this.limitsConfigurationSource.isUrl()) { 96 | this.loadFromURL(this.configEndpoint, forwardValidConfigCallback); 97 | } else if (this.limitsConfigurationSource.isFilePath()) { 98 | this.loadFromFile(this.limitsConfigurationSource.parsedConfigEndpoint.path, forwardValidConfigCallback); 99 | } else { 100 | throw new Error("Illegal url: " + this.configEndpoint); 101 | } 102 | }, 103 | 104 | buildForwardValidConfigCallback: function (callback) { 105 | const _this = this; 106 | return function (config) { 107 | if (isValidConfig(config)) { 108 | callback(config); 109 | } else { 110 | logger.error("Could not load config from " + _this.configEndpoint + ". Using the existing limits configuration..."); 111 | callback(null); 112 | } 113 | }; 114 | }, 115 | 116 | loadFromURL: function (url, forwardValidConfigCallback) { 117 | logger.debug("Loading config from URL: " + url); 118 | request({ 119 | url: url, 120 | json: true, 121 | // if no timeout is defined and the server is not responding then request objects can slowly leak resources 122 | timeout: 1500 123 | }, function (error, response, body) { 124 | if (!error && response.statusCode === 200) { 125 | logger.debug("Getting config from URL succeeded."); 126 | forwardValidConfigCallback(body); 127 | } else { 128 | logger.error("Could not load config from url. " + error + "; url: " + url); 129 | forwardValidConfigCallback(null); 130 | } 131 | }); 132 | }, 133 | 134 | loadFromFile: function (path, forwardValidConfigCallback) { 135 | logger.debug("Loading config from file: " + path); 136 | fs.readFile(path, {encoding: 'utf8'}, function (err, data) { 137 | if (err) { 138 | logger.error("Could not load config from file. " + err + "; file: " + path); 139 | } else { 140 | logger.debug("Getting config from file succeeded."); 141 | forwardValidConfigCallback(JSON.parse(data)); 142 | } 143 | }); 144 | } 145 | }; 146 | 147 | function LimitsConfigurationSource(configEndpoint) { 148 | this.parsedConfigEndpoint = url.parse(configEndpoint); 149 | } 150 | 151 | LimitsConfigurationSource.prototype = { 152 | isUrl: function () { 153 | return this.parsedConfigEndpoint.protocol === "https:" || this.parsedConfigEndpoint.protocol === "http:"; 154 | }, 155 | 156 | isFilePath: function () { 157 | return this.parsedConfigEndpoint.protocol === "file:"; 158 | } 159 | }; 160 | 161 | function isValidConfig(config) { 162 | const result = limitsConfigSchema.validate(config); 163 | if (result.errors.length > 0) { 164 | logger.error("Invalid config: " + result.errors); 165 | } 166 | return result.valid; 167 | } 168 | -------------------------------------------------------------------------------- /lib/reverse-proxy-rate-limiter/limits-evaluator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const IPResolver = require("./ipresolver").IPResolver, 4 | CounterStore = require("./counter").CounterStore, 5 | LimitsConfiguration = require("./limits-config").LimitsConfiguration, 6 | LimitsConfigurationLoader = require("./limits-config").LimitsConfigurationLoader, 7 | defaultLimitsConfig = require("./limits-config").defaultLimitsConfig, 8 | log4js = require('log4js'); 9 | 10 | module.exports = LimitsEvaluator; 11 | 12 | const logger = log4js.getLogger(); 13 | 14 | function LimitsEvaluator(settings, eventEmitter) { 15 | this.settings = settings; 16 | 17 | this.limitsConfiguration = null; 18 | this.onConfigurationUpdated = null; 19 | 20 | this.limitConfigurationLoader = new LimitsConfigurationLoader(settings.fullConfigEndpoint); 21 | this.counter = new CounterStore(); 22 | this.ipResolver = new IPResolver(settings.forwarded_headers); 23 | 24 | eventEmitter.on('forwarded', this.onRequestForwarded.bind(this)); 25 | eventEmitter.on('failed', this.onRequestFailed.bind(this)); 26 | eventEmitter.on('served', this.onRequestServed.bind(this)); 27 | 28 | this.updateConfig(defaultLimitsConfig); 29 | this.loadConfig(); 30 | 31 | if (this.settings.configRefreshInterval > 0) { 32 | if (this.settings.configRefreshInterval < 5000) { 33 | logger.warn("configRefreshInterval should be >= 5000 (5sec) -> automatic config update turned off"); 34 | } else { 35 | const _this = this; 36 | setInterval(function () { 37 | _this.loadConfig(); 38 | }, this.settings.configRefreshInterval); 39 | } 40 | } 41 | } 42 | 43 | LimitsEvaluator.prototype = { 44 | loadConfig: function () { 45 | try { 46 | const _this = this; 47 | this.limitConfigurationLoader.load(function (cfg) { 48 | if (cfg !== null) { 49 | _this.updateConfig(cfg); 50 | } else if (_this.limitsConfiguration === null) { 51 | _this.updateConfig(defaultLimitsConfig); 52 | } // else use the current limits config 53 | }); 54 | } catch (e) { 55 | logger.error("load/update config failed: " + e); 56 | } 57 | }, 58 | 59 | updateConfig: function (cfg) { 60 | this.limitsConfiguration = new LimitsConfiguration(cfg); 61 | 62 | if (typeof this.onConfigurationUpdated === "function") { 63 | this.onConfigurationUpdated(); 64 | } 65 | }, 66 | 67 | // event handlers 68 | 69 | onRequestForwarded: function(req) { 70 | const bucket = req.bucket; 71 | const ip = req.ip; 72 | 73 | if (bucket && ip) { 74 | this.counter.increment(bucket, ip); 75 | } 76 | }, 77 | 78 | onRequestServed: function(req, res) { 79 | this.counter.decrement(req.bucket, req.ip); 80 | }, 81 | 82 | onRequestFailed: function(err, req, res) { 83 | this.counter.decrement(req.bucket, req.ip); 84 | }, 85 | 86 | isRateLimiterHealthcheck: function (req) { 87 | return req.headers["x-rate-limiter"] === "healthcheck"; 88 | }, 89 | 90 | isServiceHealthcheck: function (req) { 91 | return req.url === this.limitsConfiguration.healthcheckUrl; 92 | }, 93 | 94 | isConfigEndpointRequested: function (req) { 95 | return req.url === this.getConfigEndpoint(); 96 | }, 97 | 98 | isMaxRequestsLimitReached: function () { 99 | const isMaxRequestsLimitSet = this.limitsConfiguration.maxRequests > 0; 100 | const isMaxRequestsLimitReached = this.counter.getGlobalRequestCount() >= this.limitsConfiguration.maxRequests; 101 | 102 | if (isMaxRequestsLimitSet && isMaxRequestsLimitReached) { 103 | logger.info("Rejected by global limit: " + this.counter.getGlobalRequestCount()); 104 | return true; 105 | } 106 | 107 | return false; 108 | }, 109 | 110 | isByIPLimitReached: function (bucket, ip) { 111 | const isByIPLimitSet = bucket.getMaxRequestsPerIp() > 0; 112 | const isByIPLimitReached = this.counter.getRequestCountForBucketAndIP(bucket, ip) >= bucket.getMaxRequestsPerIp(); 113 | 114 | if (isByIPLimitSet && isByIPLimitReached) { 115 | logger.info("Rejected by IP limit for bucket: ", bucket.name, ip); 116 | return true; 117 | } 118 | 119 | return false; 120 | }, 121 | 122 | isBelowMaxRequestsWithoutBuffer: function () { 123 | const isGlobalRequestsUnlimited = this.limitsConfiguration.maxRequestsWithoutBuffer === 0; 124 | const isBelowMaxRequestsWithoutBuffer = this.counter.getGlobalRequestCount() < this.limitsConfiguration.maxRequestsWithoutBuffer; 125 | return isGlobalRequestsUnlimited || isBelowMaxRequestsWithoutBuffer; 126 | }, 127 | 128 | isBucketFull: function (bucket, ip) { 129 | const isBucketCapacityLimited = bucket.getMaxRequests() > 0; 130 | const isBucketFull = this.counter.getRequestCountForBucket(bucket) >= this.calculateAvailableRequestsForBucket(bucket); 131 | 132 | if (isBucketCapacityLimited && isBucketFull) { 133 | logger.info("Rejected by bucket limit: ", bucket.name, ip); 134 | return true; 135 | } 136 | 137 | return false; 138 | }, 139 | 140 | evaluate: function (req, returnForward, returnReject, returnHealthcheck) { 141 | try { 142 | if (this.isRateLimiterHealthcheck(req)) { 143 | return returnHealthcheck(); 144 | } 145 | 146 | if (this.isServiceHealthcheck(req)) { 147 | return returnForward(); 148 | } 149 | 150 | if (this.isConfigEndpointRequested(req)) { 151 | return returnReject("config_endpoint_requested", 404); 152 | } 153 | 154 | if (this.isMaxRequestsLimitReached()) { 155 | return returnReject("global.request_limit_reached"); 156 | } 157 | 158 | const bucket = this.getMatchingBucket(req); 159 | req.bucket = bucket; 160 | 161 | const ip = this.ipResolver.resolve(req); 162 | req.ip = ip; 163 | 164 | if (this.isByIPLimitReached(bucket, ip)) { 165 | return returnReject(bucket.name + ".ip_limit_reached"); 166 | } 167 | 168 | if (this.isBelowMaxRequestsWithoutBuffer()) { 169 | return returnForward(); 170 | } 171 | 172 | if (this.isBucketFull(bucket, ip)) { 173 | return returnReject(bucket.name + ".request_limit_reached"); 174 | } 175 | 176 | return returnForward(); 177 | } catch (e) { 178 | logger.error("Evaluating limits failed:", e); 179 | // do nothing, let the request through 180 | } 181 | 182 | return returnForward(); 183 | }, 184 | 185 | getCompetingBuckets: function() { 186 | return this.limitsConfiguration.buckets.filter(function (bucket) { 187 | return this.counter.getRequestCountForBucket(bucket) >= bucket.getMaxRequests(); 188 | }, this); 189 | }, 190 | 191 | calculateAvailableRequestCount: function () { 192 | let remainingRequestCount = this.limitsConfiguration.maxRequestsWithoutBuffer; 193 | 194 | this.limitsConfiguration.buckets.forEach(function (bucket) { 195 | if (this.counter.getRequestCountForBucket(bucket) < bucket.getMaxRequests()) { 196 | remainingRequestCount -= this.counter.getRequestCountForBucket(bucket); 197 | } 198 | }, this); 199 | 200 | return remainingRequestCount; 201 | }, 202 | 203 | calculateTotalCapacityOfCompetingBuckets: function (competingBuckets) { 204 | let sumOfCapacityUnits = 0; 205 | 206 | competingBuckets.forEach(function (bucket) { 207 | sumOfCapacityUnits += bucket.capacityUnit; 208 | }); 209 | 210 | return sumOfCapacityUnits; 211 | }, 212 | 213 | calculateAvailableRequestsForBucket: function (bucket) { 214 | const competingBuckets = this.getCompetingBuckets(); 215 | const totalCapacityOfCompetingBuckets = this.calculateTotalCapacityOfCompetingBuckets(competingBuckets); 216 | const availableRequestCount = this.calculateAvailableRequestCount(); 217 | 218 | return Math.ceil(availableRequestCount / totalCapacityOfCompetingBuckets * bucket.capacityUnit); 219 | }, 220 | 221 | getMatchingBucket: function (req) { 222 | for (let i = 0; i < this.limitsConfiguration.buckets.length; i++) { 223 | if (this.limitsConfiguration.buckets[i].matches(req)) { 224 | return this.limitsConfiguration.buckets[i]; 225 | } 226 | } 227 | throw "Invalid state: no default bucket found"; 228 | }, 229 | 230 | getConfigEndpoint: function () { 231 | return this.settings.fullConfigEndpoint; 232 | }, 233 | 234 | getConfigRefreshInterval: function () { 235 | return this.settings.configRefreshInterval; 236 | } 237 | }; 238 | -------------------------------------------------------------------------------- /lib/reverse-proxy-rate-limiter/settings.js: -------------------------------------------------------------------------------- 1 | /* global require, process */ 2 | (function () { 3 | const os = require('os'), 4 | fs = require('fs'), 5 | _ = require('lodash'), 6 | url = require('url'); 7 | 8 | exports.init = init; 9 | exports.load = load; 10 | exports.updateDerivedSettings = updateDerivedSettings; 11 | 12 | function ConfigBuilder() { 13 | this.objects = []; 14 | } 15 | 16 | ConfigBuilder.prototype.addObj = function (obj) { 17 | this.objects.push(obj); 18 | }; 19 | 20 | ConfigBuilder.prototype.addFile = function (path) { 21 | this.objects.push(maybeLoadConfigFile(path)); 22 | }; 23 | 24 | ConfigBuilder.prototype.addDir = function (path) { 25 | this.objects.push(maybeLoadConfigFile(path + '/default.json')); 26 | if (process.env.NODE_ENV) { 27 | this.objects.push(maybeLoadConfigFile(path + '/' + process.env.NODE_ENV + '.json')); 28 | } 29 | }; 30 | 31 | ConfigBuilder.prototype.build = function () { 32 | return _.merge.apply(null, [{}].concat(this.objects)); 33 | }; 34 | 35 | function maybeLoadConfigFile(path) { 36 | let content; 37 | try { 38 | content = fs.readFileSync(path); 39 | } catch (e) { 40 | console.error("Tried to load settings file " + path + " but it doesn't exist."); 41 | return {}; 42 | } 43 | return JSON.parse(content); 44 | } 45 | 46 | function updateDerivedSettings(settings) { 47 | settings.forwardUrl = "http://" + settings.forwardHost + ":" + settings.forwardPort; 48 | 49 | let configUrl = url.parse(settings.configEndpoint); 50 | if (configUrl.protocol === null) { 51 | configUrl = url.parse(settings.forwardUrl); 52 | configUrl.pathname = settings.configEndpoint; 53 | settings.fullConfigEndpoint = url.format(configUrl); 54 | } else { 55 | settings.fullConfigEndpoint = settings.configEndpoint; 56 | } 57 | return settings; 58 | } 59 | 60 | function load(extraConfigFile, hook) { 61 | const builder = new ConfigBuilder(); 62 | builder.addObj(defaultConfig); 63 | builder.addDir("config"); 64 | if (hook) { hook(builder); } 65 | if (extraConfigFile) { 66 | builder.addFile(extraConfigFile); 67 | } 68 | 69 | const settings = builder.build(); 70 | return updateDerivedSettings(settings); 71 | } 72 | 73 | function init() { 74 | const opts = require('nomnom') 75 | .option('config', { 76 | abbr: 'c', 77 | default: null, 78 | help: 'Configuration file to use' 79 | }) 80 | .option('version', { 81 | abbr: 'v', 82 | flag: true, 83 | help: 'Print version and exit', 84 | callback: function () { 85 | return "reverse-proxy-rate-limiter version " + require('../../package.json').version; 86 | } 87 | }) 88 | .parse(); 89 | 90 | return this.load(opts.config); 91 | } 92 | 93 | const defaultConfig = { 94 | "log4js": { 95 | "appenders": { 96 | "console" : { 97 | "type": "console", 98 | "layout": { 99 | "type": "pattern", 100 | "pattern": "%d{ISO8601} %h %c %p %m%n" 101 | } 102 | } 103 | }, 104 | "categories": { 105 | "default": { 106 | "appenders": ["console"], 107 | "level": "info" 108 | } 109 | } 110 | }, 111 | "serviceName": "defaultService", 112 | "listenPort": 8001, 113 | "forwardPort": 8000, 114 | "forwardHost": "localhost", 115 | "configRefreshInterval": 0, 116 | "configEndpoint": "/rate-limiter/", 117 | "bucketHeaderName": "X-RateLimiter-Bucket" 118 | }; 119 | }()); 120 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reverse-proxy-rate-limiter", 3 | "version": "0.1.3", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.0.0", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", 10 | "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", 11 | "requires": { 12 | "@babel/highlight": "^7.0.0" 13 | } 14 | }, 15 | "@babel/highlight": { 16 | "version": "7.0.0", 17 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", 18 | "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", 19 | "requires": { 20 | "chalk": "^2.0.0", 21 | "esutils": "^2.0.2", 22 | "js-tokens": "^4.0.0" 23 | }, 24 | "dependencies": { 25 | "ansi-styles": { 26 | "version": "3.2.1", 27 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 28 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 29 | "requires": { 30 | "color-convert": "^1.9.0" 31 | } 32 | }, 33 | "chalk": { 34 | "version": "2.4.1", 35 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", 36 | "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", 37 | "requires": { 38 | "ansi-styles": "^3.2.1", 39 | "escape-string-regexp": "^1.0.5", 40 | "supports-color": "^5.3.0" 41 | } 42 | } 43 | } 44 | }, 45 | "ajv": { 46 | "version": "6.5.5", 47 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz", 48 | "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==", 49 | "requires": { 50 | "fast-deep-equal": "^2.0.1", 51 | "fast-json-stable-stringify": "^2.0.0", 52 | "json-schema-traverse": "^0.4.1", 53 | "uri-js": "^4.2.2" 54 | } 55 | }, 56 | "ansi-regex": { 57 | "version": "3.0.0", 58 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", 59 | "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" 60 | }, 61 | "ansi-styles": { 62 | "version": "1.0.0", 63 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", 64 | "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=" 65 | }, 66 | "arr-diff": { 67 | "version": "2.0.0", 68 | "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", 69 | "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", 70 | "requires": { 71 | "arr-flatten": "^1.0.1" 72 | } 73 | }, 74 | "arr-flatten": { 75 | "version": "1.1.0", 76 | "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", 77 | "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" 78 | }, 79 | "array-unique": { 80 | "version": "0.2.1", 81 | "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", 82 | "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" 83 | }, 84 | "asn1": { 85 | "version": "0.2.4", 86 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 87 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 88 | "requires": { 89 | "safer-buffer": "~2.1.0" 90 | } 91 | }, 92 | "assert-plus": { 93 | "version": "1.0.0", 94 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 95 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 96 | }, 97 | "asynckit": { 98 | "version": "0.4.0", 99 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 100 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 101 | }, 102 | "aws-sign2": { 103 | "version": "0.7.0", 104 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 105 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 106 | }, 107 | "aws4": { 108 | "version": "1.8.0", 109 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", 110 | "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" 111 | }, 112 | "balanced-match": { 113 | "version": "1.0.0", 114 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 115 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 116 | "dev": true 117 | }, 118 | "bcrypt-pbkdf": { 119 | "version": "1.0.2", 120 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 121 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 122 | "requires": { 123 | "tweetnacl": "^0.14.3" 124 | } 125 | }, 126 | "brace-expansion": { 127 | "version": "1.1.11", 128 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 129 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 130 | "dev": true, 131 | "requires": { 132 | "balanced-match": "^1.0.0", 133 | "concat-map": "0.0.1" 134 | } 135 | }, 136 | "braces": { 137 | "version": "1.8.5", 138 | "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", 139 | "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", 140 | "requires": { 141 | "expand-range": "^1.8.1", 142 | "preserve": "^0.2.0", 143 | "repeat-element": "^1.1.2" 144 | } 145 | }, 146 | "browser-stdout": { 147 | "version": "1.3.1", 148 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 149 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 150 | "dev": true 151 | }, 152 | "caseless": { 153 | "version": "0.12.0", 154 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 155 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 156 | }, 157 | "chalk": { 158 | "version": "0.4.0", 159 | "resolved": "http://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", 160 | "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", 161 | "requires": { 162 | "ansi-styles": "~1.0.0", 163 | "has-color": "~0.1.0", 164 | "strip-ansi": "~0.1.0" 165 | } 166 | }, 167 | "circular-json": { 168 | "version": "0.5.9", 169 | "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.5.9.tgz", 170 | "integrity": "sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ==" 171 | }, 172 | "color-convert": { 173 | "version": "1.9.3", 174 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 175 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 176 | "requires": { 177 | "color-name": "1.1.3" 178 | } 179 | }, 180 | "color-name": { 181 | "version": "1.1.3", 182 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 183 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 184 | }, 185 | "combined-stream": { 186 | "version": "1.0.7", 187 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", 188 | "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", 189 | "requires": { 190 | "delayed-stream": "~1.0.0" 191 | } 192 | }, 193 | "commander": { 194 | "version": "2.15.1", 195 | "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz", 196 | "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", 197 | "dev": true 198 | }, 199 | "concat-map": { 200 | "version": "0.0.1", 201 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 202 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 203 | "dev": true 204 | }, 205 | "core-util-is": { 206 | "version": "1.0.2", 207 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 208 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 209 | }, 210 | "dashdash": { 211 | "version": "1.14.1", 212 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 213 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 214 | "requires": { 215 | "assert-plus": "^1.0.0" 216 | } 217 | }, 218 | "date-format": { 219 | "version": "1.2.0", 220 | "resolved": "https://registry.npmjs.org/date-format/-/date-format-1.2.0.tgz", 221 | "integrity": "sha1-YV6CjiM90aubua4JUODOzPpuytg=" 222 | }, 223 | "debug": { 224 | "version": "3.1.0", 225 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 226 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 227 | "requires": { 228 | "ms": "2.0.0" 229 | } 230 | }, 231 | "delayed-stream": { 232 | "version": "1.0.0", 233 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 234 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 235 | }, 236 | "diff": { 237 | "version": "3.5.0", 238 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 239 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" 240 | }, 241 | "ecc-jsbn": { 242 | "version": "0.1.2", 243 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 244 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 245 | "requires": { 246 | "jsbn": "~0.1.0", 247 | "safer-buffer": "^2.1.0" 248 | } 249 | }, 250 | "escape-string-regexp": { 251 | "version": "1.0.5", 252 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 253 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 254 | }, 255 | "esutils": { 256 | "version": "2.0.2", 257 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", 258 | "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" 259 | }, 260 | "eventemitter3": { 261 | "version": "3.1.0", 262 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", 263 | "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==" 264 | }, 265 | "expand-brackets": { 266 | "version": "0.1.5", 267 | "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", 268 | "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", 269 | "requires": { 270 | "is-posix-bracket": "^0.1.0" 271 | } 272 | }, 273 | "expand-range": { 274 | "version": "1.8.2", 275 | "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", 276 | "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", 277 | "requires": { 278 | "fill-range": "^2.1.0" 279 | } 280 | }, 281 | "expect": { 282 | "version": "23.6.0", 283 | "resolved": "https://registry.npmjs.org/expect/-/expect-23.6.0.tgz", 284 | "integrity": "sha512-dgSoOHgmtn/aDGRVFWclQyPDKl2CQRq0hmIEoUAuQs/2rn2NcvCWcSCovm6BLeuB/7EZuLGu2QfnR+qRt5OM4w==", 285 | "requires": { 286 | "ansi-styles": "^3.2.0", 287 | "jest-diff": "^23.6.0", 288 | "jest-get-type": "^22.1.0", 289 | "jest-matcher-utils": "^23.6.0", 290 | "jest-message-util": "^23.4.0", 291 | "jest-regex-util": "^23.3.0" 292 | }, 293 | "dependencies": { 294 | "ansi-styles": { 295 | "version": "3.2.1", 296 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 297 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 298 | "requires": { 299 | "color-convert": "^1.9.0" 300 | } 301 | } 302 | } 303 | }, 304 | "extend": { 305 | "version": "3.0.2", 306 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 307 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 308 | }, 309 | "extglob": { 310 | "version": "0.3.2", 311 | "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", 312 | "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", 313 | "requires": { 314 | "is-extglob": "^1.0.0" 315 | } 316 | }, 317 | "extsprintf": { 318 | "version": "1.3.0", 319 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 320 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 321 | }, 322 | "fast-deep-equal": { 323 | "version": "2.0.1", 324 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", 325 | "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" 326 | }, 327 | "fast-json-stable-stringify": { 328 | "version": "2.0.0", 329 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 330 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" 331 | }, 332 | "filename-regex": { 333 | "version": "2.0.1", 334 | "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", 335 | "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=" 336 | }, 337 | "fill-range": { 338 | "version": "2.2.4", 339 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", 340 | "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", 341 | "requires": { 342 | "is-number": "^2.1.0", 343 | "isobject": "^2.0.0", 344 | "randomatic": "^3.0.0", 345 | "repeat-element": "^1.1.2", 346 | "repeat-string": "^1.5.2" 347 | } 348 | }, 349 | "follow-redirects": { 350 | "version": "1.5.9", 351 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.9.tgz", 352 | "integrity": "sha512-Bh65EZI/RU8nx0wbYF9shkFZlqLP+6WT/5FnA3cE/djNSuKNHJEinGGZgu/cQEkeeb2GdFOgenAmn8qaqYke2w==", 353 | "requires": { 354 | "debug": "=3.1.0" 355 | } 356 | }, 357 | "for-in": { 358 | "version": "1.0.2", 359 | "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", 360 | "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" 361 | }, 362 | "for-own": { 363 | "version": "0.1.5", 364 | "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", 365 | "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", 366 | "requires": { 367 | "for-in": "^1.0.1" 368 | } 369 | }, 370 | "forever-agent": { 371 | "version": "0.6.1", 372 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 373 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 374 | }, 375 | "form-data": { 376 | "version": "2.3.3", 377 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 378 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 379 | "requires": { 380 | "asynckit": "^0.4.0", 381 | "combined-stream": "^1.0.6", 382 | "mime-types": "^2.1.12" 383 | } 384 | }, 385 | "forwarded": { 386 | "version": "0.1.2", 387 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 388 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 389 | }, 390 | "fs.realpath": { 391 | "version": "1.0.0", 392 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 393 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 394 | "dev": true 395 | }, 396 | "getpass": { 397 | "version": "0.1.7", 398 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 399 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 400 | "requires": { 401 | "assert-plus": "^1.0.0" 402 | } 403 | }, 404 | "glob": { 405 | "version": "7.1.2", 406 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 407 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 408 | "dev": true, 409 | "requires": { 410 | "fs.realpath": "^1.0.0", 411 | "inflight": "^1.0.4", 412 | "inherits": "2", 413 | "minimatch": "^3.0.4", 414 | "once": "^1.3.0", 415 | "path-is-absolute": "^1.0.0" 416 | } 417 | }, 418 | "glob-base": { 419 | "version": "0.3.0", 420 | "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", 421 | "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", 422 | "requires": { 423 | "glob-parent": "^2.0.0", 424 | "is-glob": "^2.0.0" 425 | } 426 | }, 427 | "glob-parent": { 428 | "version": "2.0.0", 429 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", 430 | "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", 431 | "requires": { 432 | "is-glob": "^2.0.0" 433 | } 434 | }, 435 | "growl": { 436 | "version": "1.10.5", 437 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 438 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 439 | "dev": true 440 | }, 441 | "har-schema": { 442 | "version": "2.0.0", 443 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 444 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 445 | }, 446 | "har-validator": { 447 | "version": "5.1.2", 448 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.2.tgz", 449 | "integrity": "sha512-OFxb5MZXCUMx43X7O8LK4FKggEQx6yC5QPmOcBnYbJ9UjxEcMcrMbaR0af5HZpqeFopw2GwQRQi34ZXI7YLM5w==", 450 | "requires": { 451 | "ajv": "^6.5.5", 452 | "har-schema": "^2.0.0" 453 | } 454 | }, 455 | "has-color": { 456 | "version": "0.1.7", 457 | "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", 458 | "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=" 459 | }, 460 | "has-flag": { 461 | "version": "3.0.0", 462 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 463 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 464 | }, 465 | "he": { 466 | "version": "1.1.1", 467 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 468 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 469 | "dev": true 470 | }, 471 | "http-proxy": { 472 | "version": "1.17.0", 473 | "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", 474 | "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", 475 | "requires": { 476 | "eventemitter3": "^3.0.0", 477 | "follow-redirects": "^1.0.0", 478 | "requires-port": "^1.0.0" 479 | } 480 | }, 481 | "http-signature": { 482 | "version": "1.2.0", 483 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 484 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 485 | "requires": { 486 | "assert-plus": "^1.0.0", 487 | "jsprim": "^1.2.2", 488 | "sshpk": "^1.7.0" 489 | } 490 | }, 491 | "inflight": { 492 | "version": "1.0.6", 493 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 494 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 495 | "dev": true, 496 | "requires": { 497 | "once": "^1.3.0", 498 | "wrappy": "1" 499 | } 500 | }, 501 | "inherits": { 502 | "version": "2.0.3", 503 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 504 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 505 | }, 506 | "ipaddr.js": { 507 | "version": "1.8.0", 508 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", 509 | "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" 510 | }, 511 | "is-buffer": { 512 | "version": "1.1.6", 513 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 514 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" 515 | }, 516 | "is-dotfile": { 517 | "version": "1.0.3", 518 | "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", 519 | "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=" 520 | }, 521 | "is-equal-shallow": { 522 | "version": "0.1.3", 523 | "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", 524 | "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", 525 | "requires": { 526 | "is-primitive": "^2.0.0" 527 | } 528 | }, 529 | "is-extendable": { 530 | "version": "0.1.1", 531 | "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", 532 | "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" 533 | }, 534 | "is-extglob": { 535 | "version": "1.0.0", 536 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", 537 | "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" 538 | }, 539 | "is-glob": { 540 | "version": "2.0.1", 541 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", 542 | "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", 543 | "requires": { 544 | "is-extglob": "^1.0.0" 545 | } 546 | }, 547 | "is-number": { 548 | "version": "2.1.0", 549 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", 550 | "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", 551 | "requires": { 552 | "kind-of": "^3.0.2" 553 | } 554 | }, 555 | "is-posix-bracket": { 556 | "version": "0.1.1", 557 | "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", 558 | "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=" 559 | }, 560 | "is-primitive": { 561 | "version": "2.0.0", 562 | "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", 563 | "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" 564 | }, 565 | "is-typedarray": { 566 | "version": "1.0.0", 567 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 568 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 569 | }, 570 | "isarray": { 571 | "version": "1.0.0", 572 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 573 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 574 | }, 575 | "isobject": { 576 | "version": "2.1.0", 577 | "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", 578 | "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", 579 | "requires": { 580 | "isarray": "1.0.0" 581 | } 582 | }, 583 | "isstream": { 584 | "version": "0.1.2", 585 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 586 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 587 | }, 588 | "jest-diff": { 589 | "version": "23.6.0", 590 | "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-23.6.0.tgz", 591 | "integrity": "sha512-Gz9l5Ov+X3aL5L37IT+8hoCUsof1CVYBb2QEkOupK64XyRR3h+uRpYIm97K7sY8diFxowR8pIGEdyfMKTixo3g==", 592 | "requires": { 593 | "chalk": "^2.0.1", 594 | "diff": "^3.2.0", 595 | "jest-get-type": "^22.1.0", 596 | "pretty-format": "^23.6.0" 597 | }, 598 | "dependencies": { 599 | "ansi-styles": { 600 | "version": "3.2.1", 601 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 602 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 603 | "requires": { 604 | "color-convert": "^1.9.0" 605 | } 606 | }, 607 | "chalk": { 608 | "version": "2.4.1", 609 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", 610 | "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", 611 | "requires": { 612 | "ansi-styles": "^3.2.1", 613 | "escape-string-regexp": "^1.0.5", 614 | "supports-color": "^5.3.0" 615 | } 616 | } 617 | } 618 | }, 619 | "jest-get-type": { 620 | "version": "22.4.3", 621 | "resolved": "http://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", 622 | "integrity": "sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==" 623 | }, 624 | "jest-matcher-utils": { 625 | "version": "23.6.0", 626 | "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz", 627 | "integrity": "sha512-rosyCHQfBcol4NsckTn01cdelzWLU9Cq7aaigDf8VwwpIRvWE/9zLgX2bON+FkEW69/0UuYslUe22SOdEf2nog==", 628 | "requires": { 629 | "chalk": "^2.0.1", 630 | "jest-get-type": "^22.1.0", 631 | "pretty-format": "^23.6.0" 632 | }, 633 | "dependencies": { 634 | "ansi-styles": { 635 | "version": "3.2.1", 636 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 637 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 638 | "requires": { 639 | "color-convert": "^1.9.0" 640 | } 641 | }, 642 | "chalk": { 643 | "version": "2.4.1", 644 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", 645 | "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", 646 | "requires": { 647 | "ansi-styles": "^3.2.1", 648 | "escape-string-regexp": "^1.0.5", 649 | "supports-color": "^5.3.0" 650 | } 651 | } 652 | } 653 | }, 654 | "jest-message-util": { 655 | "version": "23.4.0", 656 | "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-23.4.0.tgz", 657 | "integrity": "sha1-F2EMUJQjSVCNAaPR4L2iwHkIap8=", 658 | "requires": { 659 | "@babel/code-frame": "^7.0.0-beta.35", 660 | "chalk": "^2.0.1", 661 | "micromatch": "^2.3.11", 662 | "slash": "^1.0.0", 663 | "stack-utils": "^1.0.1" 664 | }, 665 | "dependencies": { 666 | "ansi-styles": { 667 | "version": "3.2.1", 668 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 669 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 670 | "requires": { 671 | "color-convert": "^1.9.0" 672 | } 673 | }, 674 | "chalk": { 675 | "version": "2.4.1", 676 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", 677 | "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", 678 | "requires": { 679 | "ansi-styles": "^3.2.1", 680 | "escape-string-regexp": "^1.0.5", 681 | "supports-color": "^5.3.0" 682 | } 683 | } 684 | } 685 | }, 686 | "jest-regex-util": { 687 | "version": "23.3.0", 688 | "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-23.3.0.tgz", 689 | "integrity": "sha1-X4ZylUfCeFxAAs6qj4Sf6MpHG8U=" 690 | }, 691 | "js-tokens": { 692 | "version": "4.0.0", 693 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 694 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 695 | }, 696 | "jsbn": { 697 | "version": "0.1.1", 698 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 699 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 700 | }, 701 | "json-schema": { 702 | "version": "0.2.3", 703 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 704 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 705 | }, 706 | "json-schema-traverse": { 707 | "version": "0.4.1", 708 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 709 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 710 | }, 711 | "json-stringify-safe": { 712 | "version": "5.0.1", 713 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 714 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 715 | }, 716 | "jsonschema": { 717 | "version": "1.2.4", 718 | "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.4.tgz", 719 | "integrity": "sha512-lz1nOH69GbsVHeVgEdvyavc/33oymY1AZwtePMiMj4HZPMbP5OIKK3zT9INMWjwua/V4Z4yq7wSlBbSG+g4AEw==" 720 | }, 721 | "jsprim": { 722 | "version": "1.4.1", 723 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 724 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 725 | "requires": { 726 | "assert-plus": "1.0.0", 727 | "extsprintf": "1.3.0", 728 | "json-schema": "0.2.3", 729 | "verror": "1.10.0" 730 | } 731 | }, 732 | "kind-of": { 733 | "version": "3.2.2", 734 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", 735 | "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", 736 | "requires": { 737 | "is-buffer": "^1.1.5" 738 | } 739 | }, 740 | "lodash": { 741 | "version": "4.17.11", 742 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", 743 | "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" 744 | }, 745 | "log4js": { 746 | "version": "3.0.6", 747 | "resolved": "https://registry.npmjs.org/log4js/-/log4js-3.0.6.tgz", 748 | "integrity": "sha512-ezXZk6oPJCWL483zj64pNkMuY/NcRX5MPiB0zE6tjZM137aeusrOnW1ecxgF9cmwMWkBMhjteQxBPoZBh9FDxQ==", 749 | "requires": { 750 | "circular-json": "^0.5.5", 751 | "date-format": "^1.2.0", 752 | "debug": "^3.1.0", 753 | "rfdc": "^1.1.2", 754 | "streamroller": "0.7.0" 755 | } 756 | }, 757 | "math-random": { 758 | "version": "1.0.1", 759 | "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", 760 | "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=" 761 | }, 762 | "micromatch": { 763 | "version": "2.3.11", 764 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", 765 | "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", 766 | "requires": { 767 | "arr-diff": "^2.0.0", 768 | "array-unique": "^0.2.1", 769 | "braces": "^1.8.2", 770 | "expand-brackets": "^0.1.4", 771 | "extglob": "^0.3.1", 772 | "filename-regex": "^2.0.0", 773 | "is-extglob": "^1.0.0", 774 | "is-glob": "^2.0.1", 775 | "kind-of": "^3.0.2", 776 | "normalize-path": "^2.0.1", 777 | "object.omit": "^2.0.0", 778 | "parse-glob": "^3.0.4", 779 | "regex-cache": "^0.4.2" 780 | } 781 | }, 782 | "mime-db": { 783 | "version": "1.37.0", 784 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", 785 | "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" 786 | }, 787 | "mime-types": { 788 | "version": "2.1.21", 789 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", 790 | "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", 791 | "requires": { 792 | "mime-db": "~1.37.0" 793 | } 794 | }, 795 | "minimatch": { 796 | "version": "3.0.4", 797 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 798 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 799 | "dev": true, 800 | "requires": { 801 | "brace-expansion": "^1.1.7" 802 | } 803 | }, 804 | "minimist": { 805 | "version": "0.0.8", 806 | "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 807 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" 808 | }, 809 | "mkdirp": { 810 | "version": "0.5.1", 811 | "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 812 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 813 | "requires": { 814 | "minimist": "0.0.8" 815 | } 816 | }, 817 | "mocha": { 818 | "version": "5.2.0", 819 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", 820 | "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", 821 | "dev": true, 822 | "requires": { 823 | "browser-stdout": "1.3.1", 824 | "commander": "2.15.1", 825 | "debug": "3.1.0", 826 | "diff": "3.5.0", 827 | "escape-string-regexp": "1.0.5", 828 | "glob": "7.1.2", 829 | "growl": "1.10.5", 830 | "he": "1.1.1", 831 | "minimatch": "3.0.4", 832 | "mkdirp": "0.5.1", 833 | "supports-color": "5.4.0" 834 | } 835 | }, 836 | "ms": { 837 | "version": "2.0.0", 838 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 839 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 840 | }, 841 | "nomnom": { 842 | "version": "1.8.1", 843 | "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.8.1.tgz", 844 | "integrity": "sha1-IVH3Ikcrp55Qp2/BJbuMjy5Nwqc=", 845 | "requires": { 846 | "chalk": "~0.4.0", 847 | "underscore": "~1.6.0" 848 | } 849 | }, 850 | "normalize-path": { 851 | "version": "2.1.1", 852 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", 853 | "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", 854 | "requires": { 855 | "remove-trailing-separator": "^1.0.1" 856 | } 857 | }, 858 | "oauth-sign": { 859 | "version": "0.9.0", 860 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 861 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 862 | }, 863 | "object.omit": { 864 | "version": "2.0.1", 865 | "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", 866 | "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", 867 | "requires": { 868 | "for-own": "^0.1.4", 869 | "is-extendable": "^0.1.1" 870 | } 871 | }, 872 | "once": { 873 | "version": "1.4.0", 874 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 875 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 876 | "dev": true, 877 | "requires": { 878 | "wrappy": "1" 879 | } 880 | }, 881 | "parse-glob": { 882 | "version": "3.0.4", 883 | "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", 884 | "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", 885 | "requires": { 886 | "glob-base": "^0.3.0", 887 | "is-dotfile": "^1.0.0", 888 | "is-extglob": "^1.0.0", 889 | "is-glob": "^2.0.0" 890 | } 891 | }, 892 | "path-is-absolute": { 893 | "version": "1.0.1", 894 | "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 895 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 896 | "dev": true 897 | }, 898 | "performance-now": { 899 | "version": "2.1.0", 900 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 901 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 902 | }, 903 | "preserve": { 904 | "version": "0.2.0", 905 | "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", 906 | "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" 907 | }, 908 | "pretty-format": { 909 | "version": "23.6.0", 910 | "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", 911 | "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", 912 | "requires": { 913 | "ansi-regex": "^3.0.0", 914 | "ansi-styles": "^3.2.0" 915 | }, 916 | "dependencies": { 917 | "ansi-styles": { 918 | "version": "3.2.1", 919 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 920 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 921 | "requires": { 922 | "color-convert": "^1.9.0" 923 | } 924 | } 925 | } 926 | }, 927 | "process-nextick-args": { 928 | "version": "2.0.0", 929 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", 930 | "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" 931 | }, 932 | "proxy-addr": { 933 | "version": "2.0.4", 934 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", 935 | "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", 936 | "requires": { 937 | "forwarded": "~0.1.2", 938 | "ipaddr.js": "1.8.0" 939 | } 940 | }, 941 | "psl": { 942 | "version": "1.1.29", 943 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", 944 | "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" 945 | }, 946 | "punycode": { 947 | "version": "2.1.1", 948 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 949 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 950 | }, 951 | "qs": { 952 | "version": "6.5.2", 953 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 954 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 955 | }, 956 | "randomatic": { 957 | "version": "3.1.1", 958 | "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", 959 | "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", 960 | "requires": { 961 | "is-number": "^4.0.0", 962 | "kind-of": "^6.0.0", 963 | "math-random": "^1.0.1" 964 | }, 965 | "dependencies": { 966 | "is-number": { 967 | "version": "4.0.0", 968 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", 969 | "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" 970 | }, 971 | "kind-of": { 972 | "version": "6.0.2", 973 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", 974 | "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" 975 | } 976 | } 977 | }, 978 | "readable-stream": { 979 | "version": "2.3.6", 980 | "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", 981 | "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", 982 | "requires": { 983 | "core-util-is": "~1.0.0", 984 | "inherits": "~2.0.3", 985 | "isarray": "~1.0.0", 986 | "process-nextick-args": "~2.0.0", 987 | "safe-buffer": "~5.1.1", 988 | "string_decoder": "~1.1.1", 989 | "util-deprecate": "~1.0.1" 990 | } 991 | }, 992 | "regex-cache": { 993 | "version": "0.4.4", 994 | "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", 995 | "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", 996 | "requires": { 997 | "is-equal-shallow": "^0.1.3" 998 | } 999 | }, 1000 | "remove-trailing-separator": { 1001 | "version": "1.1.0", 1002 | "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", 1003 | "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" 1004 | }, 1005 | "repeat-element": { 1006 | "version": "1.1.3", 1007 | "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", 1008 | "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" 1009 | }, 1010 | "repeat-string": { 1011 | "version": "1.6.1", 1012 | "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", 1013 | "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" 1014 | }, 1015 | "request": { 1016 | "version": "2.88.0", 1017 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", 1018 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", 1019 | "requires": { 1020 | "aws-sign2": "~0.7.0", 1021 | "aws4": "^1.8.0", 1022 | "caseless": "~0.12.0", 1023 | "combined-stream": "~1.0.6", 1024 | "extend": "~3.0.2", 1025 | "forever-agent": "~0.6.1", 1026 | "form-data": "~2.3.2", 1027 | "har-validator": "~5.1.0", 1028 | "http-signature": "~1.2.0", 1029 | "is-typedarray": "~1.0.0", 1030 | "isstream": "~0.1.2", 1031 | "json-stringify-safe": "~5.0.1", 1032 | "mime-types": "~2.1.19", 1033 | "oauth-sign": "~0.9.0", 1034 | "performance-now": "^2.1.0", 1035 | "qs": "~6.5.2", 1036 | "safe-buffer": "^5.1.2", 1037 | "tough-cookie": "~2.4.3", 1038 | "tunnel-agent": "^0.6.0", 1039 | "uuid": "^3.3.2" 1040 | } 1041 | }, 1042 | "requires-port": { 1043 | "version": "1.0.0", 1044 | "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", 1045 | "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" 1046 | }, 1047 | "rfdc": { 1048 | "version": "1.1.2", 1049 | "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.2.tgz", 1050 | "integrity": "sha512-92ktAgvZhBzYTIK0Mja9uen5q5J3NRVMoDkJL2VMwq6SXjVCgqvQeVP2XAaUY6HT+XpQYeLSjb3UoitBryKmdA==" 1051 | }, 1052 | "safe-buffer": { 1053 | "version": "5.1.2", 1054 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1055 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 1056 | }, 1057 | "safer-buffer": { 1058 | "version": "2.1.2", 1059 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1060 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1061 | }, 1062 | "slash": { 1063 | "version": "1.0.0", 1064 | "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", 1065 | "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" 1066 | }, 1067 | "sshpk": { 1068 | "version": "1.15.2", 1069 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", 1070 | "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==", 1071 | "requires": { 1072 | "asn1": "~0.2.3", 1073 | "assert-plus": "^1.0.0", 1074 | "bcrypt-pbkdf": "^1.0.0", 1075 | "dashdash": "^1.12.0", 1076 | "ecc-jsbn": "~0.1.1", 1077 | "getpass": "^0.1.1", 1078 | "jsbn": "~0.1.0", 1079 | "safer-buffer": "^2.0.2", 1080 | "tweetnacl": "~0.14.0" 1081 | } 1082 | }, 1083 | "stack-utils": { 1084 | "version": "1.0.1", 1085 | "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.1.tgz", 1086 | "integrity": "sha1-1PM6tU6OOHeLDKXP07OvsS22hiA=" 1087 | }, 1088 | "streamroller": { 1089 | "version": "0.7.0", 1090 | "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-0.7.0.tgz", 1091 | "integrity": "sha512-WREzfy0r0zUqp3lGO096wRuUp7ho1X6uo/7DJfTlEi0Iv/4gT7YHqXDjKC2ioVGBZtE8QzsQD9nx1nIuoZ57jQ==", 1092 | "requires": { 1093 | "date-format": "^1.2.0", 1094 | "debug": "^3.1.0", 1095 | "mkdirp": "^0.5.1", 1096 | "readable-stream": "^2.3.0" 1097 | } 1098 | }, 1099 | "string_decoder": { 1100 | "version": "1.1.1", 1101 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 1102 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 1103 | "requires": { 1104 | "safe-buffer": "~5.1.0" 1105 | } 1106 | }, 1107 | "strip-ansi": { 1108 | "version": "0.1.1", 1109 | "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", 1110 | "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=" 1111 | }, 1112 | "supports-color": { 1113 | "version": "5.4.0", 1114 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", 1115 | "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", 1116 | "requires": { 1117 | "has-flag": "^3.0.0" 1118 | } 1119 | }, 1120 | "tough-cookie": { 1121 | "version": "2.4.3", 1122 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", 1123 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", 1124 | "requires": { 1125 | "psl": "^1.1.24", 1126 | "punycode": "^1.4.1" 1127 | }, 1128 | "dependencies": { 1129 | "punycode": { 1130 | "version": "1.4.1", 1131 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 1132 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 1133 | } 1134 | } 1135 | }, 1136 | "tunnel-agent": { 1137 | "version": "0.6.0", 1138 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 1139 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 1140 | "requires": { 1141 | "safe-buffer": "^5.0.1" 1142 | } 1143 | }, 1144 | "tweetnacl": { 1145 | "version": "0.14.5", 1146 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 1147 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 1148 | }, 1149 | "underscore": { 1150 | "version": "1.6.0", 1151 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", 1152 | "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" 1153 | }, 1154 | "uri-js": { 1155 | "version": "4.2.2", 1156 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 1157 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 1158 | "requires": { 1159 | "punycode": "^2.1.0" 1160 | } 1161 | }, 1162 | "util-deprecate": { 1163 | "version": "1.0.2", 1164 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1165 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 1166 | }, 1167 | "uuid": { 1168 | "version": "3.3.2", 1169 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 1170 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 1171 | }, 1172 | "verror": { 1173 | "version": "1.10.0", 1174 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 1175 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 1176 | "requires": { 1177 | "assert-plus": "^1.0.0", 1178 | "core-util-is": "1.0.2", 1179 | "extsprintf": "^1.2.0" 1180 | } 1181 | }, 1182 | "wrappy": { 1183 | "version": "1.0.2", 1184 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1185 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1186 | "dev": true 1187 | } 1188 | } 1189 | } 1190 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reverse-proxy-rate-limiter", 3 | "version": "0.1.3", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/prezi/reverse-proxy-rate-limiter.git" 7 | }, 8 | "bugs": { 9 | "url": "https://github.com/prezi/reverse-proxy-rate-limiter/issues" 10 | }, 11 | "description": "Reverse proxy written in Node.js that limits incoming requests based on their origin and the number of active concurrent requests.", 12 | "keywords": [ 13 | "reverse", 14 | "proxy", 15 | "rate", 16 | "limiter" 17 | ], 18 | "main": "index.js", 19 | "dependencies": { 20 | "http-proxy": "^1.17.0", 21 | "jsonschema": "^1.2.4", 22 | "lodash": "^4.17.11", 23 | "log4js": "^3.0.6", 24 | "nomnom": "1.8.1", 25 | "proxy-addr": "^2.0.4", 26 | "request": "^2.88.0" 27 | }, 28 | "devDependencies": { 29 | "expect": "^23.6.0", 30 | "mocha": "^5.2.0" 31 | }, 32 | "engines": { 33 | "node": "11.1.0" 34 | }, 35 | "scripts": { 36 | "start": "node start-rate-limiter.js", 37 | "test": "mocha --recursive" 38 | }, 39 | "license": "Apache-2.0", 40 | "jshintConfig": { 41 | "undef": true, 42 | "unused": true, 43 | "node": true, 44 | "globals": { 45 | "describe": false, 46 | "it": false, 47 | "before": false, 48 | "beforeEach": false, 49 | "after": false, 50 | "afterEach": false 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /start-rate-limiter.js: -------------------------------------------------------------------------------- 1 | /* global require */ 2 | const config = require('./lib/reverse-proxy-rate-limiter/settings').init(), 3 | rateLimiter = require('./lib/rate-limiter'); 4 | 5 | rateLimiter.createRateLimiter(config); 6 | -------------------------------------------------------------------------------- /test/bucket-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const expect = require('expect'); 4 | const fs = require('fs'); 5 | const LimitsEvaluator = require('../lib/reverse-proxy-rate-limiter/limits-evaluator'); 6 | const EventEmitter = require('events').EventEmitter; 7 | const Bucket = require('../lib/reverse-proxy-rate-limiter/bucket').Bucket; 8 | 9 | describe("Bucket tests", function () { 10 | const request = { 11 | "url": "/test" 12 | }; 13 | it("should match the bucket", function () { 14 | const bucket = new Bucket({ 15 | name: "testBucket", 16 | conditions: [ 17 | ["path", "eq", "/test"] 18 | ] 19 | }); 20 | 21 | expect(bucket.matches(request)).toBeTruthy(); 22 | }); 23 | 24 | it("should not match the bucket", function () { 25 | const bucket = new Bucket({ 26 | name: "testBucket", 27 | conditions: [ 28 | ["path", "eq", "/test2"] 29 | ] 30 | }); 31 | 32 | expect(bucket.matches(request)).toBeFalsy(); 33 | }); 34 | 35 | it("should not match if only one condition is met", function () { 36 | const bucket = new Bucket({ 37 | name: "testBucket", 38 | conditions: [ 39 | ["path", "eq", "/test"], 40 | ["path", "eq", "/test2"] 41 | ] 42 | }); 43 | 44 | expect(bucket.matches(request)).toBeFalsy(); 45 | }); 46 | 47 | let evaluator; 48 | 49 | it("should choose the reuse bucket", function () { 50 | const b = evaluator.getMatchingBucket({headers: {"x-prezi-client": "reuse-e5759ce4bb1c298b063f2d8aa1a334"}}); 51 | expect(b.name).toBe("reuse"); 52 | }); 53 | it("should choose the default bucket", function () { 54 | const b = evaluator.getMatchingBucket(); 55 | expect(b.name).toBe("default"); 56 | }); 57 | 58 | before(function () { 59 | evaluator = new LimitsEvaluator({}, new EventEmitter()); 60 | evaluator.updateConfig(JSON.parse(fs.readFileSync("./test/fixtures/example_configuration.json", 'utf8'))); 61 | }); 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /test/condition-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const expect = require('expect'); 4 | const Condition = require("../lib/reverse-proxy-rate-limiter/conditions/conditions").Condition; 5 | const Predicates = require("../lib/reverse-proxy-rate-limiter/conditions/predicates").Predicates; 6 | const SubjectTypes = require("../lib/reverse-proxy-rate-limiter/conditions/subject-types").SubjectTypes; 7 | 8 | describe("Condition parameter validation tests", function () { 9 | it("should throw an exception because parameter is not an array", function () { 10 | expect(function () { 11 | new Condition('not an array'); 12 | }).toThrow(/conditionArray parameter must be an array/); 13 | }); 14 | 15 | it("should throw 'invalid subject' exception", function () { 16 | expect(function () { 17 | new Condition(["invalid"]); 18 | }).toThrow(/Invalid subject: invalid/); 19 | }); 20 | 21 | it("should have a valid subject", function () { 22 | const condition = new Condition(["header", "test", "eq", "test"]); 23 | const subject = condition.subject; 24 | expect(subject.name).toBe("header"); 25 | expect(subject.parameterCount).toBe(1); 26 | expect(subject.predicates).toBe(SubjectTypes.header.predicates); 27 | }); 28 | 29 | it("should throw an exception because of wrong parameter array size", function () { 30 | expect(function () { 31 | new Condition(["header", "eq", "test"]); 32 | }).toThrow(/Expected conditionArray size is 4 but was 3/); 33 | }); 34 | 35 | it("should throw an exception because of invalid predicate", function () { 36 | expect(function () { 37 | new Condition(["header", "test", "invalid predicate", "test"]); 38 | }).toThrow(/Invalid predicate: invalid predicate/); 39 | }); 40 | 41 | it("should throw an exception because of not usable predicate", function () { 42 | expect(function () { 43 | new Condition(["header", "test", "gt", "test"]); 44 | }).toThrow(/Predicate gt not usable for subject header/); 45 | }); 46 | 47 | it("should be the following condition: header['testheader'] == 'test'", function () { 48 | const condition = new Condition(["header", "testheader", "eq", "testvalue"]); 49 | 50 | expect(condition.subject.name).toBe("header"); 51 | expect(condition.predicate).toBe(Predicates.eq); 52 | expect(condition.parameters.length).toBe(1); 53 | expect(condition.parameters[0]).toBe("testheader"); 54 | expect(condition.expectedValue).toBe("testvalue"); 55 | }); 56 | }); 57 | 58 | describe("Condition evaluation tests", function () { 59 | it("header[test] == FIXME shoud be true", function () { 60 | const condition = new Condition(["header", "test", "eq", "FIXME"]); 61 | expect(condition.evaluate({headers: {test: 'FIXME'}})).toBe(true); 62 | }); 63 | 64 | it("header[test] == 'some other value' should be false", function () { 65 | const condition = new Condition(["header", "test", "eq", "some other value"]); 66 | expect(condition.evaluate()).toBe(false); 67 | }); 68 | 69 | it("client_ip == 1.2.3.4 should be true", function () { 70 | const condition = new Condition(["client_ip", "eq", "1.2.3.4"]); 71 | expect(condition.evaluate()).toBe(true); 72 | }); 73 | 74 | it("path == '/test' should be true", function () { 75 | const condition = new Condition(["path", "eq", "/test"]); 76 | expect(condition.evaluate({url: "/test?a=b"})).toBe(true); 77 | }); 78 | 79 | it("true eq true should be true", function () { 80 | const condition = new Condition(["true", "eq", "true"]); 81 | expect(condition.evaluate()).toBe(true); 82 | }); 83 | 84 | it("simple regexp should match", function () { 85 | const condition = new Condition(["header", "test", "matches", "^FIXM.$"]); 86 | expect(condition.evaluate({headers: {test: 'FIXME'}})).toBe(true); 87 | }); 88 | 89 | it("regexp should not match 'undefined' if the header specified in condition is not present", function () { 90 | const condition = new Condition(["header", "test", "matches", ".*"]); 91 | expect(condition.evaluate({headers: {randomHeader: 'testValue'}})).toBe(false); 92 | }); 93 | 94 | it("simple regexp should not match", function () { 95 | const condition = new Condition(["header", "test", "matches", "^FIXM.{2}$"]); 96 | expect(condition.evaluate()).toBe(false); 97 | }); 98 | }); -------------------------------------------------------------------------------- /test/config-update-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const expect = require('expect'); 4 | const _ = require("lodash"); 5 | const assert = require('assert'); 6 | const helpers = require('./helpers'); 7 | const LimitsEvaluator = require("../lib/reverse-proxy-rate-limiter/limits-evaluator"); 8 | const EventEmitter = require('events').EventEmitter; 9 | const createTestLimitsEvaluator = require("./helpers").createTestLimitsEvaluator; 10 | const TestLimitsConfigurationLoader = require("./helpers").TestLimitsConfigurationLoader; 11 | 12 | describe("Initializing Ratelimiter with limitsConfiguration", function () { 13 | let evaluator; 14 | beforeEach(function (done) { 15 | const settings = require('../lib/reverse-proxy-rate-limiter/settings').load(); 16 | settings.fullConfigEndpoint = "file:./test/fixtures/example_configuration.json"; 17 | 18 | evaluator = new LimitsEvaluator(settings, new EventEmitter()); 19 | evaluator.onConfigurationUpdated = done; 20 | }); 21 | 22 | it("should load required parameters", function () { 23 | expect(evaluator.limitsConfiguration.maxRequests).toBe(30); 24 | expect(evaluator.limitsConfiguration.maxRequestsWithoutBuffer).toBe(27); 25 | expect(evaluator.limitsConfiguration.bufferRatio).toBe(0.1); 26 | expect(evaluator.limitsConfiguration.healthcheckUrl).toBe("/healthcheck/"); 27 | }); 28 | 29 | it("should load 3 buckets", function () { 30 | expect(Object.keys(evaluator.limitsConfiguration.buckets).length).toBe(3); 31 | }); 32 | 33 | it("should load default bucket", function () { 34 | const defaultBucket = helpers.getBucketByName(evaluator.limitsConfiguration.buckets, "default"); 35 | 36 | expect(defaultBucket.name).toBe("default"); 37 | expect(defaultBucket.capacityUnit).toBe(7); 38 | expect(defaultBucket.maxRequests).toBe(19); 39 | expect(defaultBucket.maxRequestsPerIp).toBe(5); 40 | }); 41 | 42 | it("should load and configure all the buckets' limits", function () { 43 | expect(helpers.getBucketByName(evaluator.limitsConfiguration.buckets, "default").maxRequests).toBe(19); 44 | expect(helpers.getBucketByName(evaluator.limitsConfiguration.buckets, "default").maxRequestsPerIp).toBe(5); 45 | 46 | expect(helpers.getBucketByName(evaluator.limitsConfiguration.buckets, "reuse").maxRequests).toBe(6); 47 | expect(helpers.getBucketByName(evaluator.limitsConfiguration.buckets, "backup").maxRequests).toBe(3); 48 | }); 49 | 50 | it("should fall back to default limits configuration if loading of configuration failed", function () { 51 | evaluator.limitConfigurationLoader = new TestLimitsConfigurationLoader("dumy_endpoint"); 52 | evaluator.limitsConfiguration = null; 53 | evaluator.onConfigurationUpdated = null; 54 | evaluator.loadConfig(); 55 | 56 | expect(evaluator.limitsConfiguration.maxRequests).toBe(0); 57 | expect(evaluator.limitsConfiguration.maxRequestsWithoutBuffer).toBe(0); 58 | expect(evaluator.limitsConfiguration.bufferRatio).toBe(0); 59 | expect(evaluator.limitsConfiguration.buckets.length).toBe(1); 60 | expect(evaluator.limitsConfiguration.buckets[0].name).toBe("default"); 61 | }); 62 | }); 63 | 64 | 65 | describe("Config change tests", function () { 66 | const cfg = { 67 | "version": 1, 68 | "max_requests": 10, 69 | "buffer_ratio": 0.1, 70 | "buckets": [{ 71 | "name": "default", 72 | "limits": { 73 | "capacity_unit": 2 74 | } 75 | }] 76 | }; 77 | 78 | function cloneConfig(cfg) { 79 | return _.cloneDeep(cfg); 80 | } 81 | 82 | function assertRequestCountsEqual(bucket, ip, expectedCounts) { 83 | assert.strictEqual(evaluator.counter.getGlobalRequestCount(), expectedCounts[0]); 84 | assert.strictEqual(evaluator.counter.getRequestCountForBucket(bucket), expectedCounts[1]); 85 | assert.strictEqual(evaluator.counter.getRequestCountForBucketAndIP(bucket, ip), expectedCounts[2]); 86 | } 87 | 88 | const cfgWith2Buckets = cloneConfig(cfg); 89 | cfgWith2Buckets.buckets.push({ 90 | "name": "test", 91 | "conditions": [["true", "eq", "true"]], 92 | "limits": { 93 | "capacity_unit": 3 94 | } 95 | }); 96 | 97 | let evaluator; 98 | beforeEach(function () { 99 | // the rateLimiter created by createTestLimitsEvaluator does not start a proxy so it doesn't need to be terminated 100 | evaluator = createTestLimitsEvaluator({}); 101 | evaluator.updateConfig(cfg); 102 | }); 103 | 104 | it("changing a bucket's config to have no limits it should have no limits", function () { 105 | const cfg2 = cloneConfig(cfg); 106 | assert.strictEqual(evaluator.limitsConfiguration.buckets[0].capacityUnit, 2); 107 | 108 | cfg2.buckets[0] = {name: "default"}; 109 | evaluator.updateConfig(cfg2); 110 | assert.strictEqual(evaluator.limitsConfiguration.buckets[0].capacityUnit, 0); 111 | }); 112 | 113 | it("changing the bucket's config should not change the request count", function () { 114 | 115 | evaluator.counter.increment(evaluator.limitsConfiguration.buckets[0], "dummy_ip"); 116 | 117 | assert.strictEqual(evaluator.limitsConfiguration.buckets[0].capacityUnit, 2); 118 | assertRequestCountsEqual(evaluator.limitsConfiguration.buckets[0], "dummy_ip", [1, 1, 1]); 119 | 120 | const cfg2 = cloneConfig(cfg); 121 | cfg2.buckets[0].limits.capacity_unit = 3; 122 | evaluator.updateConfig(cfg2); 123 | 124 | assert.strictEqual(evaluator.limitsConfiguration.buckets[0].capacityUnit, 3); 125 | assertRequestCountsEqual(evaluator.limitsConfiguration.buckets[0], "dummy_ip", [1, 1, 1]); 126 | }); 127 | 128 | it("adding and removing buckets", function () { 129 | evaluator.counter.increment(evaluator.limitsConfiguration.buckets[0], "dummy_ip"); 130 | assert.strictEqual(evaluator.limitsConfiguration.buckets.length, 1); 131 | assertRequestCountsEqual(evaluator.limitsConfiguration.buckets[0], "dummy_ip", [1, 1, 1]); 132 | 133 | evaluator.updateConfig(cfgWith2Buckets); 134 | assert.strictEqual(evaluator.limitsConfiguration.buckets.length, 2); 135 | assert.strictEqual(evaluator.limitsConfiguration.buckets[0].name, "test"); 136 | assert.strictEqual(evaluator.limitsConfiguration.buckets[1].name, "default"); 137 | 138 | evaluator.updateConfig(cfg); 139 | assert.strictEqual(evaluator.limitsConfiguration.buckets.length, 1); 140 | assert.strictEqual(evaluator.limitsConfiguration.buckets[0].name, "default"); 141 | assertRequestCountsEqual(evaluator.limitsConfiguration.buckets[0], "dummy_ip", [1, 1, 1]); 142 | }); 143 | 144 | }); 145 | -------------------------------------------------------------------------------- /test/config-validation-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require('fs'); 4 | const http = require('http'); 5 | const expect = require('expect'); 6 | const assert = require("assert"); 7 | const _ = require("lodash"); 8 | const limitsConfig = require("../lib/reverse-proxy-rate-limiter/limits-config"); 9 | const LimitsConfigurationLoader = require("../lib/reverse-proxy-rate-limiter/limits-config").LimitsConfigurationLoader; 10 | const schema = require("../lib/reverse-proxy-rate-limiter/limits-config-schema"); 11 | 12 | describe("Schema validator", function () { 13 | it("should accept valid config", function () { 14 | const validConfig = {"version": 1, "max_requests": 10, "buffer_ratio": 0.1, "buckets": [{"name": "default"}]}; 15 | expect(limitsConfig.isValidConfig(validConfig)).toBeTruthy(); 16 | }); 17 | 18 | it("should reject invalid config'", function () { 19 | const invalidConfig = {"version": "a", "max_requests": -1, "buffer_ratio": 2}; 20 | expect(limitsConfig.isValidConfig(invalidConfig)).toBeFalsy(); 21 | }); 22 | }); 23 | 24 | describe("Conditions", function () { 25 | const configWithValidCondition = { 26 | "version": 1, 27 | "max_requests": 10, 28 | "buffer_ratio": 0.1, 29 | "buckets": [ 30 | { 31 | "name": "reuse", 32 | "conditions": [ 33 | ["header", "X-Prezi-Client", "eq", "reuse"], 34 | ["header", "X-Prezi-Client", "eq", "backup"] 35 | ], 36 | "limits": { 37 | "capacity_unit": 2 38 | } 39 | } 40 | ] 41 | }; 42 | 43 | it("should be accepted with valid parameters", function () { 44 | expect(schema.validate(configWithValidCondition).valid).toBeTruthy(); 45 | }); 46 | 47 | it("should be rejected with invalid subject type", function () { 48 | const configWithInvalidSubjectType = configWithValidCondition; 49 | configWithInvalidSubjectType.buckets[0].conditions = [ 50 | ["unsupported_subject_type", "X-Prezi-Client", "eq", "reuse"] 51 | ]; 52 | 53 | const result = schema.validate(configWithInvalidSubjectType); 54 | 55 | expect(result.valid).toBeFalsy(); 56 | expect(_.find(result.errors, function (error) { 57 | return error.message.indexOf("Invalid subject") > -1; 58 | })); 59 | }); 60 | 61 | it("should be rejected with invalid predicate", function () { 62 | const configWithInvalidPredicate = configWithValidCondition; 63 | configWithInvalidPredicate.buckets[0].conditions = [ 64 | ["header", "X-Prezi-Client", "equals_not_eq", "reuse"] 65 | ]; 66 | 67 | const result = schema.validate(configWithInvalidPredicate); 68 | 69 | expect(result.valid).toBeFalsy(); 70 | expect(_.find(result.errors, function (error) { 71 | return error.message.indexOf("Invalid predicate") > -1; 72 | })); 73 | }); 74 | }); 75 | 76 | describe("Load config from url", function () { 77 | let httpServer; 78 | before(function (done) { 79 | httpServer = http.createServer(function (req, res) { 80 | let cfg; 81 | if (req.url === "/valid-config/") { 82 | cfg = fs.readFileSync(__dirname + "/fixtures/example_configuration.json"); 83 | } else if (req.url === "/invalid-config/") { 84 | cfg = "invalid json"; 85 | 86 | } else if (/\/\d{3}/.test(req.url)) { 87 | res.writeHead(parseInt(req.url.substring(1))); 88 | res.end(); 89 | return; 90 | 91 | } else { 92 | res.writeHead(404); 93 | res.end(); 94 | return; 95 | } 96 | 97 | res.writeHead(200, {'Content-Type': 'text/plain'}); 98 | res.write(cfg); 99 | res.end(); 100 | }).listen(9999, done); 101 | }); 102 | 103 | after(function (done) { 104 | httpServer.close(done); 105 | }); 106 | 107 | it("should load valid config", function (done) { 108 | const limitsConfigurationLoader = new LimitsConfigurationLoader("http://localhost:9999/valid-config/"); 109 | limitsConfigurationLoader.load(function (cfg) { 110 | assert.strictEqual(cfg.version, 1); 111 | assert.strictEqual(cfg.max_requests, 30); 112 | assert.strictEqual(cfg.buckets.length, 3); 113 | done(); 114 | }); 115 | }); 116 | 117 | it("should handle invalid config", function (done) { 118 | const limitsConfigurationLoader = new LimitsConfigurationLoader("http://localhost:9999/invalid-config/"); 119 | limitsConfigurationLoader.load(function (cfg) { 120 | assert.strictEqual(cfg, null); 121 | done(); 122 | }); 123 | }); 124 | 125 | it("should handle 404 not found", function (done) { 126 | const limitsConfigurationLoader = new LimitsConfigurationLoader("http://localhost:9999/404"); 127 | limitsConfigurationLoader.load(function (cfg) { 128 | assert.strictEqual(cfg, null); 129 | done(); 130 | }); 131 | }); 132 | 133 | it("should handle 500 internal server error", function (done) { 134 | const limitsConfigurationLoader = new LimitsConfigurationLoader("http://localhost:9999/500"); 135 | limitsConfigurationLoader.load(function (cfg) { 136 | assert.strictEqual(cfg, null); 137 | done(); 138 | }); 139 | }); 140 | 141 | }); -------------------------------------------------------------------------------- /test/counterstore-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const assert = require("assert"), 4 | CounterStore = require("../lib/reverse-proxy-rate-limiter/counter.js").CounterStore; 5 | 6 | describe("CounterStore tests", function () { 7 | let cs; 8 | beforeEach(function () { 9 | cs = new CounterStore(); 10 | }); 11 | 12 | const bucket1 = {name: "bucket1"}, bucket2 = {name: "bucket2"}; 13 | 14 | it("should increment the counters", function () { 15 | cs.increment(bucket1, "ip1"); 16 | cs.increment(bucket1, "ip1"); 17 | cs.increment(bucket1, "ip2"); 18 | cs.increment(bucket2, "ip1"); 19 | 20 | const counters = { 21 | global: cs.getGlobalRequestCount(), 22 | 23 | bucket1: cs.getRequestCountForBucket(bucket1, "ip1"), 24 | bucket1_ip1: cs.getRequestCountForBucketAndIP(bucket1, "ip1"), 25 | bucket1_ip2: cs.getRequestCountForBucketAndIP(bucket1, "ip2"), 26 | 27 | bucket2: cs.getRequestCountForBucket(bucket2), 28 | bucket2_ip1: cs.getRequestCountForBucketAndIP(bucket2, "ip1"), 29 | bucket2_ip2: cs.getRequestCountForBucketAndIP(bucket2, "ip2") 30 | }; 31 | 32 | assert.strictEqual(counters.global, 4); 33 | assert.strictEqual(counters.bucket1, 3); 34 | assert.strictEqual(counters.bucket1_ip1, 2); 35 | assert.strictEqual(counters.bucket1_ip2, 1); 36 | 37 | assert.strictEqual(counters.bucket2, 1); 38 | assert.strictEqual(counters.bucket2_ip1, 1); 39 | assert.strictEqual(counters.bucket2_ip2, 0); 40 | }); 41 | 42 | it("should increment and decrement the counters", function () { 43 | cs.increment(bucket1, "ip1"); 44 | cs.increment(bucket1, "ip1"); 45 | cs.increment(bucket1, "ip2"); 46 | 47 | cs.decrement(bucket1, "ip1"); 48 | cs.decrement(bucket1, "ip2"); 49 | 50 | const counters = { 51 | global: cs.getGlobalRequestCount(), 52 | 53 | bucket1: cs.getRequestCountForBucket(bucket1), 54 | bucket1_ip1: cs.getRequestCountForBucketAndIP(bucket1, "ip1"), 55 | bucket1_ip2: cs.getRequestCountForBucketAndIP(bucket1, "ip2") 56 | }; 57 | 58 | assert.strictEqual(counters.global, 1); 59 | assert.strictEqual(counters.bucket1, 1); 60 | assert.strictEqual(counters.bucket1_ip1, 1); 61 | assert.strictEqual(counters.bucket1_ip2, 0); 62 | }); 63 | 64 | it("should delete the nulled values", function () { 65 | cs.increment(bucket1, "ip1"); 66 | cs.decrement(bucket1, "ip1"); 67 | assert.strictEqual(Object.keys(cs.counters).length, 0); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/fixtures/example_configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "max_requests": 30, 4 | "buffer_ratio": 0.1, 5 | "healthcheck_url": "/healthcheck/", 6 | "buckets": [ 7 | { 8 | "name": "default", 9 | "limits": { 10 | "capacity_unit": 7, 11 | "max_requests_per_ip": 5 12 | } 13 | }, 14 | { 15 | "name": "reuse", 16 | "conditions": [ 17 | ["header", "X-Prezi-Client", "eq","reuse-e5759ce4bb1c298b063f2d8aa1a334"] 18 | ], 19 | "limits": { 20 | "capacity_unit": 2 21 | } 22 | }, 23 | { 24 | "name": "backup", 25 | "conditions": [ 26 | ["client_ip", "eq", "12.23.45.56"] 27 | ], 28 | "limits": { 29 | "capacity_unit": 1 30 | } 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const LimitsConfigurationLoader = require("../lib/reverse-proxy-rate-limiter/limits-config").LimitsConfigurationLoader; 4 | 5 | module.exports.createTestLimitsEvaluator = createTestLimitsEvaluator; 6 | module.exports.getBucketByName = getBucketByName; 7 | module.exports.TestLimitsConfigurationLoader = TestLimitsConfigurationLoader; 8 | 9 | 10 | function createTestLimitsEvaluator(settings) { 11 | const Evaluator = require("../lib/reverse-proxy-rate-limiter/limits-evaluator"); 12 | // returns a RateLimiter instance that neither initializes a config nor starts the proxy 13 | 14 | const EventEmitter = require('events').EventEmitter; 15 | 16 | function TestLimitsEvaluator(settings) { 17 | Evaluator.call(this, settings, new EventEmitter()); 18 | } 19 | 20 | TestLimitsEvaluator.prototype = Object.create(Evaluator.prototype); 21 | TestLimitsEvaluator.prototype.loadConfig = function () { 22 | }; 23 | TestLimitsEvaluator.prototype.initProxy = function () { 24 | }; 25 | 26 | return new TestLimitsEvaluator(settings); 27 | } 28 | 29 | function TestLimitsConfigurationLoader(fullConfigEndpoint) { 30 | 31 | function TestLimitsConfigurationLoader(settings) { 32 | LimitsConfigurationLoader.call(this, fullConfigEndpoint); 33 | } 34 | } 35 | 36 | TestLimitsConfigurationLoader.prototype = Object.create(LimitsConfigurationLoader.prototype); 37 | TestLimitsConfigurationLoader.prototype.load = function (callback) { callback(null); }; 38 | 39 | function getBucketByName(buckets, name) { 40 | const filteredBuckets = buckets.filter(function (bucket) { return bucket.name == name}); 41 | return filteredBuckets[0]; 42 | } 43 | -------------------------------------------------------------------------------- /test/integration/complex-limit-tests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const itUtils = require('./integration-utils'); 4 | 5 | itUtils.describe("Integration tests", function(tester) { 6 | 7 | const cfg2 = { 8 | version: 1, 9 | max_requests: 10, 10 | buffer_ratio: 0.2, 11 | buckets: [{ 12 | name: "default", 13 | limits: { 14 | capacity_unit: 4 15 | } 16 | }, { 17 | name: "reuse", 18 | limits: { 19 | capacity_unit: 3 20 | }, 21 | conditions: [ 22 | ["header", "bucket", "eq", "reuse"] 23 | ] 24 | }, { 25 | name: "backup", 26 | limits: { 27 | capacity_unit: 1 28 | }, 29 | conditions: [ 30 | ["header", "bucket", "eq", "backup"] 31 | ] 32 | }] 33 | }; 34 | const bucketReuse = { 35 | "bucket": "reuse" 36 | }; 37 | const bucketBackup = { 38 | "bucket": "backup" 39 | }; 40 | 41 | 42 | it("soft-hard limit testing", function(done) { 43 | tester.rateLimiter.evaluator.updateConfig(cfg2); 44 | 45 | tester.sendRequests(8, {}, function() { 46 | tester.sendRequest().onRejected(function() { 47 | tester.sendRequests(2, bucketReuse, function() { // hard limit reached, next will be rejected 48 | tester.sendRequest(bucketBackup).onRejected(function() { 49 | tester.serveRequests(-2).onServed(function() { // serve the first two default, still above the "soft" limit 50 | tester.sendRequest(bucketBackup).onForwarded(function() { 51 | tester.serveRequests().onServed(function() { 52 | done(); 53 | }); 54 | }); 55 | }); 56 | }); 57 | }); 58 | }); 59 | }); 60 | }); 61 | 62 | it("bucket ratio test", function(done) { 63 | tester.rateLimiter.evaluator.updateConfig(cfg2); 64 | 65 | tester.sendRequests(5, bucketBackup, function() { // 5 backup 66 | tester.sendRequests(5, bucketReuse, function() { 67 | // 5 backup, 5 reuse = 10 = hard limit 68 | // next request will be rejected 69 | tester.sendRequest(bucketBackup).onRejected(function() { // 70 | tester.serveRequests(-1).onServed(function() { 71 | // 4 backup, 5 reuse = 9 = soft limit 72 | // expected ratio is 2 backup : 6 reuse 73 | // next backup will be rejected but next reuse will be forwarded 74 | tester.sendRequest(bucketBackup).onRejected(function() { 75 | tester.sendRequest(bucketReuse).onForwarded(function() { 76 | done(); 77 | }); 78 | }); 79 | }); 80 | }); 81 | }); 82 | }); 83 | }); 84 | 85 | const maxRequestsPerIPConfig = { 86 | version: 1, 87 | max_requests: 10, 88 | buffer_ratio: 0.2, 89 | buckets: [{ 90 | name: "default", 91 | limits: { 92 | max_requests_per_ip: 2 93 | } 94 | }] 95 | }; 96 | const bucketDefault = { 97 | "bucket": "default" 98 | }; 99 | 100 | 101 | it("ip limit is enforced", function(done) { 102 | tester.rateLimiter.evaluator.updateConfig(maxRequestsPerIPConfig); 103 | 104 | tester.sendRequests(2, bucketDefault, function() { 105 | tester.sendRequest(bucketDefault).onRejected(function() { 106 | tester.serveRequests(2).onServed(function() { 107 | tester.sendRequest(bucketDefault).onForwarded(function() { 108 | done(); 109 | }); 110 | }); 111 | }); 112 | }); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /test/integration/counter-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const assert = require("assert"), 4 | helpers = require("./../helpers"), 5 | itUtils = require('./integration-utils'); 6 | 7 | itUtils.describe("counter consistency test", function(tester) { 8 | const cfg = { 9 | version: 1, 10 | max_requests: 3, 11 | buffer_ratio: 0, 12 | buckets: [ 13 | { 14 | "name": "A", 15 | "conditions": [["header", "Bucket", "eq", "A"]], 16 | "limits": {"capacity_unit": 1} 17 | }, { 18 | name: "default", 19 | limits: { 20 | "capacity_unit": 2 21 | } 22 | } 23 | ] 24 | }; 25 | 26 | beforeEach(function() { 27 | tester.rateLimiter.evaluator.updateConfig(cfg); 28 | }); 29 | 30 | function getCountForBucket(ratelimiter, bucketName) { 31 | const bucket = helpers.getBucketByName(tester.rateLimiter.evaluator.limitsConfiguration.buckets, bucketName); 32 | return ratelimiter.evaluator.counter.getRequestCountForBucket(bucket); 33 | } 34 | 35 | it("counter consistency: two in, two served", function (done) { 36 | tester.sendRequests(2, {}, function () { 37 | assert.strictEqual(2, tester.pendingRequestsCount()); 38 | assert.strictEqual(2, tester.rateLimiter.evaluator.counter.getGlobalRequestCount()); 39 | assert.strictEqual(2, getCountForBucket(tester.rateLimiter, "default")); 40 | tester.serveRequests().onServed(function () { 41 | assert.strictEqual(0, tester.pendingRequestsCount()); 42 | assert.strictEqual(0, tester.rateLimiter.evaluator.counter.getGlobalRequestCount()); 43 | assert.strictEqual(0, getCountForBucket(tester.rateLimiter, "default")); 44 | done(); 45 | }); 46 | }); 47 | }); 48 | 49 | it("counter consistency: four in, one rejected, three served", function (done) { 50 | tester.sendRequests(3, {}, function () { 51 | assert.strictEqual(3, tester.pendingRequestsCount()); 52 | assert.strictEqual(3, tester.rateLimiter.evaluator.counter.getGlobalRequestCount()); 53 | assert.strictEqual(3, getCountForBucket(tester.rateLimiter, "default")); 54 | 55 | tester.sendRequest().onRejected(function () { 56 | assert.strictEqual(3, tester.pendingRequestsCount()); 57 | assert.strictEqual(3, tester.rateLimiter.evaluator.counter.getGlobalRequestCount()); 58 | assert.strictEqual(3, getCountForBucket(tester.rateLimiter, "default")); 59 | tester.serveRequests().onServed(function () { 60 | assert.strictEqual(0, tester.pendingRequestsCount()); 61 | assert.strictEqual(0, tester.rateLimiter.evaluator.counter.getGlobalRequestCount()); 62 | assert.strictEqual(0, getCountForBucket(tester.rateLimiter, "default")); 63 | done(); 64 | }); 65 | }); 66 | }); 67 | }); 68 | 69 | it("counter consistency: one default bucket, one A bucket, both served", function (done) { 70 | tester.sendRequest().onForwarded(function () { 71 | assert.strictEqual(1, tester.pendingRequestsCount()); 72 | assert.strictEqual(1, tester.rateLimiter.evaluator.counter.getGlobalRequestCount()); 73 | assert.strictEqual(1, getCountForBucket(tester.rateLimiter, "default")); 74 | assert.strictEqual(0, getCountForBucket(tester.rateLimiter, "A")); 75 | 76 | tester.sendRequest({bucket: 'A'}).onForwarded(function () { 77 | assert.strictEqual(2, tester.pendingRequestsCount()); 78 | assert.strictEqual(2, tester.rateLimiter.evaluator.counter.getGlobalRequestCount()); 79 | assert.strictEqual(1, getCountForBucket(tester.rateLimiter, "default")); 80 | assert.strictEqual(1, getCountForBucket(tester.rateLimiter, "A")); 81 | 82 | tester.serveRequests().onServed(function () { 83 | assert.strictEqual(0, tester.pendingRequestsCount()); 84 | assert.strictEqual(0, tester.rateLimiter.evaluator.counter.getGlobalRequestCount()); 85 | assert.strictEqual(0, getCountForBucket(tester.rateLimiter, "default")); 86 | assert.strictEqual(0, getCountForBucket(tester.rateLimiter, "A")); 87 | done(); 88 | }); 89 | }); 90 | }); 91 | }); 92 | 93 | it("counter consistency: one A, two default, one rejected due to global limit", function (done) { 94 | tester.sendRequest({bucket: 'A'}).onForwarded(function () { 95 | assert.strictEqual(1, tester.pendingRequestsCount()); 96 | assert.strictEqual(1, tester.rateLimiter.evaluator.counter.getGlobalRequestCount()); 97 | assert.strictEqual(0, getCountForBucket(tester.rateLimiter, "default")); 98 | assert.strictEqual(1, getCountForBucket(tester.rateLimiter, "A")); 99 | 100 | tester.sendRequests(2, {}, function () { 101 | assert.strictEqual(3, tester.pendingRequestsCount()); 102 | assert.strictEqual(3, tester.rateLimiter.evaluator.counter.getGlobalRequestCount()); 103 | assert.strictEqual(2, getCountForBucket(tester.rateLimiter, "default")); 104 | assert.strictEqual(1, getCountForBucket(tester.rateLimiter, "A")); 105 | 106 | tester.sendRequest().onRejected(function () { 107 | assert.strictEqual(3, tester.pendingRequestsCount()); 108 | assert.strictEqual(3, tester.rateLimiter.evaluator.counter.getGlobalRequestCount()); 109 | assert.strictEqual(2, getCountForBucket(tester.rateLimiter, "default")); 110 | assert.strictEqual(1, getCountForBucket(tester.rateLimiter, "A")); 111 | 112 | tester.serveRequests().onServed(function () { 113 | assert.strictEqual(0, tester.pendingRequestsCount()); 114 | assert.strictEqual(0, tester.rateLimiter.evaluator.counter.getGlobalRequestCount()); 115 | assert.strictEqual(0, getCountForBucket(tester.rateLimiter, "default")); 116 | assert.strictEqual(0, getCountForBucket(tester.rateLimiter, "A")); 117 | done(); 118 | }); 119 | }); 120 | }); 121 | }); 122 | }); 123 | 124 | it("counter consistency: three A, one rejected due to global limit", function (done) { 125 | tester.sendRequests(3, {bucket: 'A'}, function () { 126 | assert.strictEqual(3, tester.pendingRequestsCount()); 127 | assert.strictEqual(3, tester.rateLimiter.evaluator.counter.getGlobalRequestCount()); 128 | assert.strictEqual(0, getCountForBucket(tester.rateLimiter, "default")); 129 | assert.strictEqual(3, getCountForBucket(tester.rateLimiter, "A")); 130 | 131 | tester.sendRequest({bucket: "A"}).onRejected(function () { 132 | assert.strictEqual(3, tester.pendingRequestsCount()); 133 | assert.strictEqual(3, tester.rateLimiter.evaluator.counter.getGlobalRequestCount()); 134 | assert.strictEqual(0, getCountForBucket(tester.rateLimiter, "default")); 135 | assert.strictEqual(3, getCountForBucket(tester.rateLimiter, "A")); 136 | 137 | tester.serveRequests().onServed(function () { 138 | assert.strictEqual(0, tester.pendingRequestsCount()); 139 | assert.strictEqual(0, tester.rateLimiter.evaluator.counter.getGlobalRequestCount()); 140 | assert.strictEqual(0, getCountForBucket(tester.rateLimiter, "default")); 141 | assert.strictEqual(0, getCountForBucket(tester.rateLimiter, "A")); 142 | done(); 143 | }); 144 | }); 145 | }); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /test/integration/error-tests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const assert = require("assert"), 4 | itUtils = require('./integration-utils'); 5 | 6 | itUtils.describe("Integration tests - error-tests", function (tester) { 7 | 8 | function changeConfig(key, value) { 9 | itUtils.changeConfig(tester, key, value); 10 | } 11 | 12 | it("should handle server errors: HPE_INVALID_CONSTANT", function (done) { 13 | changeConfig("max_requests", 1); 14 | 15 | tester.sendRequest().onForwarded(function () { 16 | assert.strictEqual(tester.rateLimiter.evaluator.counter.getGlobalRequestCount(), 1); 17 | tester.failRequestWithInvalidContentLength().onFailed(function () { 18 | assert.strictEqual(tester.rateLimiter.evaluator.counter.getGlobalRequestCount(), 0); 19 | done(); 20 | }); 21 | }); 22 | }); 23 | 24 | it("should not fail if a 4xx response is served", function (done) { 25 | tester.sendRequest({ 26 | "expectedStatusCode": 401 27 | }).onForwarded(function() { 28 | itUtils.checkPendingRequestsCount(tester, 1); 29 | tester.serveRequestWithStatusCode(401).onServed(function () { 30 | itUtils.checkPendingRequestsCount(tester, 0); 31 | done(); 32 | }); 33 | }); 34 | }); 35 | 36 | it("should not fail if a 5xx response is served", function (done) { 37 | tester.sendRequest({ 38 | "expectedStatusCode": 500 39 | }).onForwarded(function() { 40 | itUtils.checkPendingRequestsCount(tester, 1); 41 | tester.serveRequestWithStatusCode(500).onServed(function () { 42 | itUtils.checkPendingRequestsCount(tester, 0); 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | }); 49 | -------------------------------------------------------------------------------- /test/integration/eventhook-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const limitsConfig = require("../../lib/reverse-proxy-rate-limiter/limits-config"); 4 | const assert = require("assert"); 5 | const itUtils = require('./integration-utils'); 6 | 7 | itUtils.describe("Integration tests - from the hooks", function(tester) { 8 | 9 | const eventStat = bindEventHandlers(tester.rateLimiter.proxyEvent); 10 | 11 | function bindEventHandlers(proxyEvent) { 12 | const eventStat = {}; 13 | 14 | const e = function (name) { 15 | return function () { 16 | const v = eventStat[name] != undefined ? eventStat[name] : 0; 17 | eventStat[name] = v + 1; 18 | } 19 | }; 20 | 21 | proxyEvent.on('forwarded', e('forwarded')); 22 | proxyEvent.on('rejected', e('rejected')); 23 | proxyEvent.on('failed', e('failed')); 24 | proxyEvent.on('served', e('served')); 25 | 26 | proxyEvent.on('rejectRequest', function(req, res, errorCode, reason) { 27 | res.writeHead(404, "Rejected by the rate limiter"); 28 | res.write(JSON.stringify({"code": errorCode})); 29 | res.end(); 30 | 31 | e('rejectRequest')(); 32 | }); 33 | 34 | return eventStat; 35 | } 36 | 37 | function clearEventStat(eventStat) { 38 | delete eventStat['forwarded']; 39 | delete eventStat['rejected']; 40 | delete eventStat['failed']; 41 | delete eventStat['served']; 42 | delete eventStat['rejectRequest']; 43 | } 44 | 45 | function changeConfig(key, value) { 46 | itUtils.changeConfig(tester, key, value); 47 | } 48 | 49 | 50 | it("'served' should be called on served", function(done) { 51 | clearEventStat(eventStat); 52 | 53 | tester.sendRequest().onForwarded(function() { 54 | tester.serveRequests(1).onServed(function() { 55 | assert.strictEqual(eventStat['served'], 1); 56 | done(); 57 | }); 58 | }); 59 | }); 60 | 61 | it("'forwarded' should be called on forward", function(done) { 62 | clearEventStat(eventStat); 63 | 64 | tester.sendRequest().onForwarded(function() { 65 | assert.strictEqual(eventStat['forwarded'], 1); 66 | done(); 67 | }); 68 | }); 69 | 70 | it("'rejected' should be called on rejected request", function(done) { 71 | clearEventStat(eventStat); 72 | changeConfig("max_requests", 1); 73 | 74 | tester.sendRequest().onForwarded(function() { 75 | assert.strictEqual(eventStat['forwarded'], 1); 76 | tester.sendRequest().onRejected(function() { 77 | assert.strictEqual(eventStat['forwarded'], 1); 78 | assert.strictEqual(eventStat['rejected'], 1); 79 | done(); 80 | }); 81 | }); 82 | }); 83 | 84 | it("'rejectRequest' should be be able to set the response of a rejected request", function(done) { 85 | clearEventStat(eventStat); 86 | changeConfig("max_requests", 1); 87 | 88 | tester.sendRequest().onForwarded(function() { 89 | assert(eventStat['forwarded'], 1); 90 | tester.sendRequest().onRejected(function(response) { 91 | assert.strictEqual(eventStat['forwarded'], 1); 92 | assert.strictEqual(eventStat['rejected'], 1); 93 | assert.strictEqual(response.statusCode, 404); 94 | assert.strictEqual(JSON.parse(response.body).code, 429); 95 | tester.serveRequests(1).onServed(function() { 96 | assert.strictEqual(eventStat['forwarded'], 1); 97 | assert.strictEqual(eventStat['rejected'], 1); 98 | done(); 99 | }) 100 | }); 101 | }); 102 | }); 103 | 104 | it("'failed' should be called on failed", function(done) { 105 | clearEventStat(eventStat); 106 | 107 | tester.sendRequest().onForwarded(function() { 108 | tester.failRequestWithInvalidContentLength().onFailed(function() { 109 | assert.strictEqual(eventStat['forwarded'], 1); 110 | assert.strictEqual(eventStat['failed'], 1); 111 | done(); 112 | }); 113 | }); 114 | }); 115 | }); -------------------------------------------------------------------------------- /test/integration/healthcheck-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const request = require("request"); 4 | const assert = require("assert"); 5 | const itUtils = require('./integration-utils'); 6 | 7 | itUtils.describe("Healthcheck test", function(tester) { 8 | 9 | it("should return a healthcheck if healthcheck header is set", function(done) { 10 | request({ 11 | url: "http://localhost:" + tester.listenPort, 12 | headers: { 13 | 'x-rate-limiter': 'healthcheck' 14 | } 15 | }, function (error, response, body) { 16 | if (!error && response.statusCode === 200) { 17 | assert.strictEqual(body, "OK"); 18 | done(); 19 | } 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/integration/integration-tester.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const http = require("http"), 4 | rateLimiter = require("./../../index.js"), 5 | request = require("request"); 6 | let url = require('url'); 7 | 8 | const HTTP_OK = 200, 9 | HTTP_NOT_FOUND = 404, 10 | HTTP_TOO_MANY_REQUESTS = 429, 11 | HTTP_INTERNAL_SERVER_ERROR = 500, 12 | REQUEST_ID_HEADER = "X-RateLimiter-IntegrationTest-RequestId"; 13 | 14 | let port = 8080; 15 | 16 | function IntegrationTester() { 17 | this.requestBuffer = []; 18 | this.sentMessages = {}; 19 | this.requestId = 0; 20 | this.listenPort = port; 21 | this.forwardPort = port + 1; 22 | port += 2; 23 | 24 | const _this = this; 25 | this.testBackendServer = http.createServer(function (req, res) { 26 | const sentMessage = _this.sentMessages[getRequestId(req)]; 27 | 28 | _this.requestBuffer.push({ 29 | req: req, 30 | res: res, 31 | setOnServedCallback: sentMessage.setOnServedCallback, 32 | setOnFailedCallback: sentMessage.setOnFailedCallback 33 | }); 34 | sentMessage.onForwardedCallback(); 35 | 36 | }).listen(this.forwardPort); 37 | 38 | const settingsModule = require('../../lib/reverse-proxy-rate-limiter/settings'); 39 | let settings = settingsModule.load(); 40 | 41 | settings.listenPort = this.listenPort; 42 | settings.forwardPort = this.forwardPort; 43 | settings = settingsModule.updateDerivedSettings(settings); // update the derived values 44 | settings.fullConfigEndpoint = "file:./test/fixtures/example_configuration.json"; 45 | 46 | this.rateLimiter = rateLimiter.createRateLimiter(settings); 47 | } 48 | exports.IntegrationTester = IntegrationTester; 49 | 50 | IntegrationTester.prototype = { 51 | 52 | sendRequest: function (options) { 53 | return new SentMessage(this, ++this.requestId, options); 54 | }, 55 | 56 | sendRequests: function (count, options, callback) { 57 | if (typeof count === "undefined" || count === 0) { 58 | count = 1; 59 | } 60 | 61 | // simple countdown latch 62 | function CDL(countdown, completion) { 63 | this.signal = function() { 64 | if(--countdown < 1) completion(); 65 | }; 66 | } 67 | 68 | const latch = new CDL(count, function () { 69 | callback(); 70 | }); 71 | 72 | while (count-- > 0) { 73 | const message = new SentMessage(this, ++this.requestId, options); 74 | message.onForwarded(function () { 75 | latch.signal(); 76 | }); 77 | message.onRejected(function () { 78 | latch.signal(); 79 | }); 80 | message.onFailed(function () { 81 | latch.signal(); 82 | }); 83 | } 84 | }, 85 | 86 | serveRequests: function (howMany) { 87 | if (this.requestBuffer.length === 0) { 88 | throw "RequestBuffer empty, cannot serve any requests"; 89 | } 90 | 91 | if (typeof howMany === "undefined" || howMany === 0) { 92 | howMany = this.requestBuffer.length; 93 | } 94 | 95 | let fromFirst = false; 96 | if (howMany < 0) { 97 | fromFirst = true; 98 | howMany *= -1; 99 | } 100 | 101 | if (howMany > this.requestBuffer.length) { 102 | throw "cannot serve more requests than the size of the request buffer (" + howMany + " > " + this.requestBuffer.length + ")"; 103 | } 104 | 105 | let lastServedRequest; 106 | while (howMany-- > 0) { 107 | lastServedRequest = fromFirst ? this.requestBuffer.shift() : this.requestBuffer.pop(); 108 | flushSingleRequest(HTTP_OK, lastServedRequest.req, lastServedRequest.res); 109 | } 110 | 111 | return new ServedRequestWrapper(lastServedRequest); 112 | }, 113 | 114 | serveRequestWithStatusCode: function (statusCode) { 115 | if (this.requestBuffer.length === 0) { 116 | throw "RequestBuffer empty, cannot serve any requests"; 117 | } 118 | 119 | const servedRequest = this.requestBuffer.pop(); 120 | flushSingleRequest(statusCode, servedRequest.req, servedRequest.res); 121 | 122 | return new ServedRequestWrapper(servedRequest); 123 | }, 124 | 125 | failRequestWithInvalidContentLength: function () { 126 | const lastServedRequest = this.requestBuffer.pop(); 127 | const res = lastServedRequest.res; 128 | res.writeHead(200, { 129 | 'Content-Length': 0, 130 | 'Content-Type': 'text/plain' 131 | }); 132 | res.write('obviously-nonzero-body'); 133 | res.end(); 134 | return new FailedRequestWrapper(lastServedRequest); 135 | }, 136 | 137 | reset: function (done) { 138 | this.requestId = 0; 139 | if (typeof done !== "function") { 140 | done = function () { /* noop */ 141 | }; 142 | } 143 | if (this.requestBuffer.length > 0) { 144 | this.serveRequests().onServed(done); 145 | } else { 146 | done(); 147 | } 148 | }, 149 | 150 | closeTestBackendServer: function (done) { 151 | this.testBackendServer.close(done); 152 | }, 153 | 154 | pendingRequestsCount: function () { 155 | return this.requestBuffer.length; 156 | } 157 | }; 158 | 159 | function flushSingleRequest(statusCode, request, response) { 160 | response.writeHead(statusCode, {'Content-Type': 'text/plain'}); 161 | response.write("Hello ratelimiter!"); 162 | response.end(); 163 | } 164 | 165 | function getRequestId(request) { 166 | return request.headers[REQUEST_ID_HEADER.toLowerCase()]; 167 | } 168 | 169 | function ServedRequestWrapper(servedRequest) { 170 | this.onServed = function (callback) { 171 | servedRequest.setOnServedCallback(callback); 172 | }; 173 | } 174 | 175 | function FailedRequestWrapper(failedRequest) { 176 | this.onFailed = function (callback) { 177 | failedRequest.setOnFailedCallback(callback); 178 | }; 179 | } 180 | 181 | function SentMessage(it, requestId, options) { 182 | let onForwardedCallback, onRejectedCallback, onFailedCallback; 183 | let onServedCallback = defaultOnServedCallback; 184 | let expectedStatusCode; 185 | 186 | const headers = {}; 187 | headers[REQUEST_ID_HEADER] = requestId; 188 | url = "http://localhost:" + it.listenPort + "/"; 189 | 190 | if (options) { 191 | if (options.bucket) { 192 | headers.Bucket = options.bucket; 193 | } 194 | 195 | if ("expectedStatusCode" in options) { 196 | expectedStatusCode = options.expectedStatusCode; 197 | } 198 | 199 | if (options.path) { 200 | url = url + options.path; 201 | } 202 | } 203 | 204 | request({ 205 | url: url, 206 | headers: headers 207 | }, function (error, response) { 208 | if (response && response.statusCode === HTTP_OK) { 209 | onServedCallback(); 210 | } else if (expectedStatusCode && response.statusCode === expectedStatusCode) { 211 | onServedCallback(); 212 | } else if (response && response.statusCode === HTTP_NOT_FOUND) { 213 | onRejectedCallback(response); 214 | } else if (response && response.statusCode === HTTP_TOO_MANY_REQUESTS) { 215 | onRejectedCallback(response); 216 | } else if (error || (response && response.statusCode === HTTP_INTERNAL_SERVER_ERROR)) { 217 | onFailedCallback(); 218 | } 219 | }); 220 | 221 | this.onForwarded = function (callback) { 222 | it.sentMessages[requestId] = { 223 | onForwardedCallback: callback, 224 | setOnFailedCallback: function (c) { 225 | onFailedCallback = c; 226 | }, 227 | setOnServedCallback: function (c) { 228 | onServedCallback = c; 229 | } 230 | }; 231 | }; 232 | 233 | this.onRejected = function (callback) { 234 | onRejectedCallback = callback; 235 | }; 236 | 237 | this.onFailed = function (callback) { 238 | onFailedCallback = callback; 239 | }; 240 | 241 | this.onForwarded(defaultOnForwarded); 242 | this.onRejected(defaultOnRejected); 243 | this.onFailed(defaultOnFailedCallback); 244 | 245 | function defaultOnRejected() { 246 | if (onForwardedCallback !== defaultOnForwarded) { 247 | throw new Error("Unexpected: request rejected by the ratelimiter"); 248 | } 249 | } 250 | 251 | function defaultOnForwarded() { 252 | if (onRejectedCallback !== defaultOnRejected) { 253 | throw new Error("Unexpected: request forwarded by the ratelimiter"); 254 | } 255 | } 256 | 257 | function defaultOnServedCallback() { 258 | } 259 | 260 | function defaultOnFailedCallback() { 261 | throw new Error("Unexpected: request failed on the service side"); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /test/integration/integration-utils.js: -------------------------------------------------------------------------------- 1 | const IntegrationTester = require("./integration-tester").IntegrationTester, 2 | _ = require("lodash"), 3 | assert = require("assert"); 4 | 5 | const cfg = { 6 | "version": 1, 7 | "max_requests": 10, 8 | "healthcheck_url": "/healthcheck/", 9 | "buffer_ratio": 0.1, 10 | "buckets": [{ 11 | "name": "default" 12 | }] 13 | }; 14 | 15 | module.exports.changeConfig = function(tester, key, value) { 16 | const _cfg = _.cloneDeep(cfg); 17 | _cfg[key] = value; 18 | tester.rateLimiter.evaluator.updateConfig(_cfg); 19 | }; 20 | 21 | module.exports.describe = function(name, testingFunction) { 22 | 23 | describe(name, function() { 24 | 25 | const tester = new IntegrationTester(); 26 | 27 | after(function(done) { 28 | tester.closeTestBackendServer(function() { 29 | tester.rateLimiter.close(done); 30 | }); 31 | }); 32 | 33 | beforeEach(function(done) { 34 | tester.rateLimiter.evaluator.onConfigurationUpdated = function() { 35 | tester.rateLimiter.evaluator.onConfigurationUpdated = null; 36 | done(); 37 | }; 38 | tester.rateLimiter.evaluator.updateConfig(cfg); 39 | }); 40 | 41 | afterEach(function(done) { 42 | tester.reset(done); 43 | }); 44 | 45 | testingFunction(tester); 46 | }); 47 | }; 48 | 49 | module.exports.checkPendingRequestsCount = function(tester, expectedRequestsCount) { 50 | assert.strictEqual(tester.pendingRequestsCount(), expectedRequestsCount); 51 | }; -------------------------------------------------------------------------------- /test/integration/simple-limit-tests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const limitsConfig = require("../../lib/reverse-proxy-rate-limiter/limits-config"), 4 | itUtils = require('./integration-utils'); 5 | 6 | itUtils.describe("Integration tests", function(tester) { 7 | 8 | function changeConfig(key, value) { 9 | itUtils.changeConfig(tester, key, value); 10 | } 11 | 12 | it("should limit per bucket", function(done) { 13 | const buckets = [{ 14 | "name": "default" 15 | }, { 16 | "name": "A", 17 | "conditions": [ 18 | ["header", "Bucket", "eq", "A"] 19 | ], 20 | "limits": { 21 | "capacity_unit": 2 22 | } 23 | }]; 24 | 25 | changeConfig("buckets", buckets); 26 | changeConfig("max_requests", 2); 27 | const options = { 28 | "bucket": "A" 29 | }; 30 | tester.sendRequests(2, options, function() { 31 | tester.sendRequest(options).onRejected(function() { 32 | done(); 33 | }); 34 | }); 35 | }); 36 | 37 | it("should update bucket limits when requests are served", function(done) { 38 | const buckets = [{ 39 | "name": "default" 40 | }, { 41 | "name": "A", 42 | "conditions": [ 43 | ["header", "Bucket", "eq", "A"] 44 | ], 45 | "limits": { 46 | "capacity_unit": 2 47 | } 48 | }]; 49 | 50 | changeConfig("buckets", buckets); 51 | changeConfig("max_requests", 2); 52 | const options = { 53 | "bucket": "A" 54 | }; 55 | tester.sendRequests(2, options, function() { 56 | tester.sendRequest(options).onRejected(function() { 57 | tester.serveRequests(2).onServed(function() { 58 | tester.sendRequest(options).onForwarded(function() { 59 | done(); 60 | }); 61 | }); 62 | }); 63 | }); 64 | }); 65 | 66 | it("let everything in with default limitsConfiguration", function(done) { 67 | tester.rateLimiter.evaluator.updateConfig(limitsConfig.defaultLimitsConfig); 68 | 69 | tester.sendRequests(100, {}, function() { 70 | done(); 71 | }); 72 | 73 | }); 74 | 75 | 76 | }); 77 | -------------------------------------------------------------------------------- /test/integration/simple-tests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const assert = require("assert"); 4 | const itUtils = require('./integration-utils'); 5 | 6 | itUtils.describe("Integration tests - simple tests", function(tester) { 7 | 8 | function changeConfig(key, value) { 9 | itUtils.changeConfig(tester, key, value); 10 | } 11 | 12 | it("should start with an empty buffer", function() { 13 | itUtils.checkPendingRequestsCount(tester, 0); 14 | }); 15 | 16 | it("should have one element in the buffer", function(done) { 17 | tester.sendRequest().onForwarded(function() { 18 | itUtils.checkPendingRequestsCount(tester, 1); 19 | done(); 20 | }); 21 | }); 22 | 23 | it("should consume all the requests in two steps", function(done) { 24 | tester.sendRequests(3, {}, function() { 25 | itUtils.checkPendingRequestsCount(tester, 3); 26 | 27 | tester.serveRequests(2).onServed(function() { 28 | itUtils.checkPendingRequestsCount(tester, 1); 29 | 30 | tester.serveRequests().onServed(function() { 31 | itUtils.checkPendingRequestsCount(tester, 0); 32 | done(); 33 | }); 34 | }); 35 | }); 36 | }); 37 | 38 | it("should not let in more requests than the global limit", function(done) { 39 | changeConfig("max_requests", 1); 40 | tester.sendRequest().onForwarded(function() { 41 | tester.sendRequest().onRejected(function() { 42 | itUtils.checkPendingRequestsCount(tester, 1); 43 | done(); 44 | }); 45 | }); 46 | 47 | }); 48 | 49 | it("Limit 1, first forwarded, next rejected, pending served, next forwarded", function(done) { 50 | changeConfig("max_requests", 1); 51 | 52 | tester.sendRequest().onForwarded(function() { 53 | tester.sendRequest().onRejected(function() { 54 | itUtils.checkPendingRequestsCount(tester, 1); 55 | 56 | tester.serveRequests().onServed(function() { 57 | itUtils.checkPendingRequestsCount(tester, 0); 58 | 59 | tester.sendRequest().onForwarded(function() { 60 | itUtils.checkPendingRequestsCount(tester, 1); 61 | done(); 62 | }); 63 | }); 64 | }); 65 | }); 66 | 67 | }); 68 | 69 | it("should always allow /healthcheck/", function(done) { 70 | changeConfig("max_requests", 1); 71 | tester.sendRequest().onForwarded(function() { 72 | tester.sendRequest().onRejected(function() { 73 | tester.sendRequest({ 74 | "path": "healthcheck/" 75 | }).onForwarded(function() { 76 | assert.strictEqual(tester.rateLimiter.evaluator.counter.getGlobalRequestCount(), 1); 77 | done(); 78 | }); 79 | }); 80 | }); 81 | }); 82 | 83 | it("should not proxy requests to the configEndpoint", function(done) { 84 | const oldEndpoint = tester.rateLimiter.settings.fullConfigEndpoint; 85 | const testEndpoint = "test-config-endpoint"; 86 | tester.rateLimiter.settings.fullConfigEndpoint = "/" + testEndpoint; 87 | 88 | tester.sendRequest({ 89 | "path": testEndpoint 90 | }).onRejected(function(res) { 91 | itUtils.checkPendingRequestsCount(tester, 0); 92 | assert.strictEqual(res.statusCode, 404); 93 | 94 | tester.rateLimiter.settings.fullConfigEndpoint = oldEndpoint; 95 | done(); 96 | }); 97 | 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /test/ipextractor-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const IPExtractor = require("../lib/reverse-proxy-rate-limiter/ipextractor").IPExtractor; 4 | const assert = require('assert'); 5 | 6 | describe("IP Extracting",function(){ 7 | 8 | let ipextractor; 9 | beforeEach(function(){ 10 | 11 | ipextractor = new IPExtractor({ 12 | "ignored_ip_ranges": [ 13 | "127.0.0.0/8", 14 | "10.0.0.0/8", 15 | "172.16.0.0/12", 16 | "192.0.2.0/24", 17 | "192.168.0.0/16", 18 | "193.45.0.0/16" 19 | ] 20 | }); 21 | }); 22 | 23 | 24 | it("should return the only ip", function () { 25 | const result = ipextractor.extractClientIP('41.168.1.1'); 26 | assert.strictEqual(result,'41.168.1.1'); 27 | }); 28 | 29 | it("should return the last ip", function () { 30 | const result = ipextractor.extractClientIP(' 41.168.1.1, 41.168.1.2 '); 31 | assert.strictEqual(result,'41.168.1.2'); 32 | }); 33 | 34 | it("should return last not ignored ip", function () { 35 | const result = ipextractor.extractClientIP(' 41.168.1.1, 192.168.1.2'); 36 | assert.strictEqual(result,'41.168.1.1'); 37 | }); 38 | 39 | it("should return the last ip if no configuration", function () { 40 | ipextractor = new IPExtractor({}); 41 | const result = ipextractor.extractClientIP(' 41.168.1.1, 192.168.1.2'); 42 | assert.strictEqual(result,'192.168.1.2'); 43 | }); 44 | 45 | it("should return the last ip if no public ip present", function () { 46 | const result = ipextractor.extractClientIP(' 192.168.1.1, 192.168.1.2'); 47 | assert.strictEqual(result,'192.168.1.2'); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/ipresolver-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const http = require('http'); 4 | const assert = require('assert'); 5 | const helpers = require('./helpers'); 6 | const ipResolver = require('../lib/reverse-proxy-rate-limiter/ipresolver'); 7 | 8 | const EXPECTED = "expected"; 9 | const ACTUAL = "actual"; 10 | const HOST = "localhost"; 11 | const PORT = "9876"; 12 | 13 | const TEST_FORWARD_HEADERS = { 14 | "X-TEST-FORWARDED-FOR": { 15 | "ignored_ip_ranges": [ 16 | "127.0.0.0/8", 17 | "10.0.0.0/8", 18 | "172.16.0.0/12", 19 | "192.0.2.0/24", 20 | "12.34.0.0/16" 21 | ] 22 | } 23 | }; 24 | 25 | const TEST_FORWARD_HEADER = 'X-TEST-FORWARDED-FOR'; 26 | 27 | const CLIENT_IP = "12.34.56.78"; 28 | const LOCAL_HOST = "127.0.0.1"; 29 | const SOME_IP = "11.22.33.44"; 30 | const IP_LIST = CLIENT_IP + ", " + LOCAL_HOST; 31 | const LONG_IP_LIST = SOME_IP + " ," + IP_LIST; 32 | 33 | function createRequest(headers, expected, done) { 34 | headers[EXPECTED] = expected; 35 | const options = { 36 | hostname: HOST, 37 | path: '/', 38 | port: PORT, 39 | headers: headers 40 | }; 41 | const req = http.request(options, function (response) { 42 | assert.strictEqual(response.headers[ACTUAL], expected); 43 | done(); 44 | }); 45 | req.end(); 46 | } 47 | 48 | describe("Client IP tests", function () { 49 | let headers; 50 | let httpServer; 51 | 52 | before(function (done) { 53 | httpServer = http.createServer(function (req, res) { 54 | const ip = new ipResolver.IPResolver(TEST_FORWARD_HEADERS).resolve(req); 55 | res.setHeader(ACTUAL, ip); 56 | res.setHeader("Connection", "close"); 57 | res.writeHead(200, {'Content-Type': 'text/plain'}); 58 | res.end(); 59 | }).listen(PORT, HOST, done); 60 | }); 61 | 62 | after(function (done) { 63 | httpServer.close(function () { 64 | done(); 65 | }); 66 | }); 67 | 68 | beforeEach(function () { 69 | headers = {}; 70 | }); 71 | 72 | it("CUSTOM_HEADER: if available, takes precedence", function (done) { 73 | headers[TEST_FORWARD_HEADER] = CLIENT_IP; 74 | headers[ipResolver.PROXY_HEADER] = LOCAL_HOST; 75 | createRequest(headers, CLIENT_IP, done); 76 | }); 77 | 78 | it("CUSTOM_HEADER: the last ip in the list is always correct", function (done) { 79 | headers[TEST_FORWARD_HEADER] = IP_LIST; 80 | createRequest(headers, LOCAL_HOST, done); 81 | }); 82 | 83 | it("CUSTOM_HEADER: custom header can ignore any ip ranges", function (done) { 84 | headers[TEST_FORWARD_HEADER] = LONG_IP_LIST; 85 | createRequest(headers, SOME_IP, done); 86 | }); 87 | 88 | it("PROXY_HEADER: if no custom header, PROXY_HEADER takes precedence", function (done) { 89 | headers[ipResolver.PROXY_HEADER] = CLIENT_IP; 90 | createRequest(headers, CLIENT_IP, done); 91 | }); 92 | 93 | it("PROXY_HEADER: the last public ip is the one we need", function (done) { 94 | headers[ipResolver.PROXY_HEADER] = LONG_IP_LIST; 95 | createRequest(headers, CLIENT_IP, done); 96 | }); 97 | 98 | it("PROXY_HEADER: if there is no public ip return private", function (done) { 99 | headers[ipResolver.PROXY_HEADER] = LOCAL_HOST; 100 | createRequest(headers, LOCAL_HOST, done); 101 | }); 102 | 103 | it("REMOTE_ADDR: no proxies involved, use what we get from server", function (done) { 104 | createRequest(headers, LOCAL_HOST, done); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /test/rate-limiter-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const expect = require('expect'), 4 | assert = require('assert'), 5 | rateLimiter = require("../lib/reverse-proxy-rate-limiter/"), 6 | settings = require("../lib/reverse-proxy-rate-limiter/settings"), 7 | createTestRateLimiter = require('./helpers').createTestLimitsEvaluator; 8 | 9 | describe("Default settings values", function () { 10 | let rl; 11 | before(function() { 12 | // the rateLimiter created by createTestLimitsEvaluator does not start a proxy so it doesn't need to be terminated 13 | const s = settings.load(); 14 | rl = createTestRateLimiter(s); 15 | }); 16 | 17 | it("config endpoint should be set to valid URL", function () { 18 | expect(rl.getConfigEndpoint()).toBe("http://localhost:8000/rate-limiter/"); 19 | }); 20 | 21 | it("refresh interval should be 0", function () { 22 | expect(rl.getConfigRefreshInterval()).toBe(0); 23 | }); 24 | 25 | it("should validate the options", function () { 26 | const testSettings = { 27 | forwardHost: "example.com", 28 | forwardPort: 9001, 29 | configEndpoint: "test_endpoint" 30 | }; 31 | expect(settings.updateDerivedSettings(testSettings).fullConfigEndpoint).toBe("http://example.com:9001/test_endpoint"); 32 | 33 | const testSettings1 = { 34 | forwardHost: "example.com", 35 | forwardPort: 9001, 36 | configEndpoint: "/test_endpoint" 37 | }; 38 | expect(settings.updateDerivedSettings(testSettings1).fullConfigEndpoint).toBe("http://example.com:9001/test_endpoint"); 39 | 40 | const testSettings2 = { 41 | forwardHost: "example.com/", 42 | forwardPort: 9001, 43 | configEndpoint: "/test_endpoint" 44 | }; 45 | expect(settings.updateDerivedSettings(testSettings2).fullConfigEndpoint).toBe("http://example.com/test_endpoint"); 46 | 47 | const testSettings3 = { 48 | forwardHost: "localhost", 49 | forwardPort: "9001", 50 | configEndpoint: "test_endpoint" 51 | }; 52 | expect(settings.updateDerivedSettings(testSettings3).fullConfigEndpoint).toBe("http://localhost:9001/test_endpoint"); 53 | 54 | const testSettings4 = { 55 | forwardHost: "localhost", 56 | forwardPort: "test", 57 | configEndpoint: "test_endpoint" 58 | }; 59 | expect(settings.updateDerivedSettings(testSettings4).fullConfigEndpoint).toBe("http://localhost/test_endpoint"); 60 | }); 61 | }); 62 | --------------------------------------------------------------------------------