├── .gitignore ├── .jshintrc ├── Gruntfile.js ├── LICENSE ├── README.md ├── app.js ├── bin └── start-bagarino-daemon ├── etc └── bagarino.conf ├── lib ├── conf.js ├── const.js ├── contexts.js ├── gc.js ├── policies.js ├── stats.js ├── tickets.js └── utils.js ├── package-lock.json ├── package.json ├── private └── PUTHEREYOURSSLKEYS ├── renovate.json └── test ├── Tickets Tests.jmx ├── contexts_test.js ├── cors_test.js ├── payloads_test.js ├── status_test.js ├── tickets_1_test.js ├── tickets_2_test.js ├── tickets_3_test.js ├── tickets_4_test.js ├── tickets_5_test.js ├── tickets_6_test.js └── tickets_policy_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | 16 | node_modules/ 17 | reports/ 18 | 19 | .watsonrc 20 | 21 | private/*.crt 22 | private/*.pem 23 | private/*.csr 24 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": true, 11 | "boss": true, 12 | "eqnull": true, 13 | "node": true, 14 | "esversion": 6, 15 | 16 | "laxbreak": true 17 | } 18 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const request = require('request'); 5 | const sleep = require('sleep').sleep; 6 | 7 | 8 | module.exports = function(grunt) 9 | { 10 | // show elapsed time at the end 11 | require('time-grunt')(grunt); 12 | 13 | // load all grunt tasks 14 | require('load-grunt-tasks')(grunt); 15 | 16 | grunt.loadNpmTasks('grunt-plato'); 17 | 18 | const reloadPort = 35729; 19 | let files; 20 | 21 | grunt.initConfig( 22 | { 23 | pkg: grunt.file.readJSON('package.json'), 24 | 25 | nodeunit: 26 | { 27 | files: ['test/**/*_test.js'] 28 | }, 29 | 30 | jshint: 31 | { 32 | options: 33 | { 34 | jshintrc: '.jshintrc', 35 | reporter: require('jshint-stylish') 36 | }, 37 | gruntfile: 38 | { 39 | src: 'Gruntfile.js' 40 | }, 41 | main: 42 | { 43 | src: ['bin/start-bagarino-daemon*', './app.js'] 44 | }, 45 | lib: 46 | { 47 | src: ['lib/**/*.js'] 48 | }, 49 | test: 50 | { 51 | src: ['test/**/*.js'] 52 | } 53 | }, 54 | 55 | plato: { 56 | analyze_all: { 57 | options : { 58 | jshint : grunt.file.readJSON('.jshintrc') 59 | }, 60 | 61 | files: { 62 | 'reports': ['bin/**/*.js', 'lib/**/*.js'] 63 | } 64 | } 65 | }, 66 | 67 | watch: { 68 | options: { 69 | nospawn: true, 70 | livereload: reloadPort 71 | }, 72 | server: { 73 | files: [ 74 | 'bin/start-bagarino-daemon', 75 | './app.js' 76 | ], 77 | tasks: ['develop', 'delayed-livereload'] 78 | } 79 | } 80 | }); 81 | 82 | grunt.config.requires('watch.server.files'); 83 | files = grunt.config('watch.server.files'); 84 | files = grunt.file.expand(files); 85 | 86 | // Not using arrow-syntax functions because this.async will loose meaning: 87 | grunt.registerTask('delayed-livereload', 'Live reload after the node server has restarted.', function() 88 | { 89 | const done = this.async(); 90 | 91 | setTimeout( () => 92 | { 93 | request.get('http://localhost:' + reloadPort + '/changed?files=' + files.join(','), (err, res) => 94 | { 95 | const reloaded = !err && res.statusCode === 200; 96 | 97 | if (reloaded) 98 | { 99 | grunt.log.ok('Delayed live reload successful.'); 100 | } 101 | else 102 | { 103 | grunt.log.error('Unable to make a delayed live reload.'); 104 | } 105 | 106 | done(reloaded); 107 | }); 108 | 109 | }, 500); 110 | }); 111 | 112 | // Not using arrow-syntax functions because this.async will loose meaning: 113 | grunt.registerTask('warmup', 'Check system preconditions before starting the systems', function() 114 | { 115 | const done = this.async(); 116 | 117 | // Check whether Redis exists 118 | const redis = require("redis"); 119 | const client = redis.createClient(); 120 | 121 | client.on("error", err => 122 | { 123 | grunt.log.writeln('Redis error: ' + err); 124 | done(false); 125 | }); 126 | 127 | client.on("ready", () => 128 | { 129 | grunt.log.writeln("Everything's fine"); 130 | done(true); 131 | }); 132 | }); 133 | 134 | grunt.registerTask('startserver', 'Start the service', () => 135 | { 136 | grunt.task.requires('warmup'); 137 | 138 | grunt.log.writeln('Running bagarino server from "%s"...', process.cwd()); 139 | 140 | const fork = require('child_process').fork; 141 | 142 | fork('bin/start-bagarino-daemon', ['--dev'], {detached: true, cwd: process.cwd(), env: process.env}); 143 | }); 144 | 145 | grunt.registerTask('wait', 'Wait N seconds', () => 146 | { 147 | const secs = 1; 148 | 149 | grunt.log.writeln('Waiting %d second(s) before continuing...', secs); 150 | 151 | sleep(secs); 152 | }); 153 | 154 | grunt.registerTask('start', ['warmup', 'startserver']); 155 | 156 | grunt.registerTask('test', ['jshint', 'start', 'wait', 'nodeunit', 'wait', 'plato']); 157 | 158 | grunt.registerTask('default', ['start', 'watch']); 159 | }; 160 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LICENSE - Apache License v2 2 | --------------------------- 3 | Copyright (c) 2018 Nicola Orritos 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use these files except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![NPM Downloads][npmdt-image]][npmdt-url] 3 | [![NPM Version][npmv-image]][npmv-url] 4 | [![GitHub Tag][ghtag-image]][ghtag-url] 5 | [![GitHub License][ghlic-image]][ghlic-url] 6 | [![Dependencies Status][david-image]][david-url] 7 | 8 | bagarino 9 | ======== 10 | _Bagarino_ (means _"scalper"_ in Italian) generates and validates alphanumeric tickets using a number of different expiration policies. 11 | _Bagarino_ can tell a real ticket from a fake one. Simple, fast and RESTful. 12 | Ask it for a new ticket and it'll give you. Then ask it whether a ticket is still valid or expired. Or whether it is a fake. It'll know for sure. 13 | When tickets expire simply ask bagarino for new ones. 14 | 15 | _Bagarino_ can be used as a support for a licensing server and as an helper to other systems in an authentication/authorization scenario. 16 | 17 | 18 | ## Table of Contents 19 | - [Install](#install) 20 | - [Usage](#usage) 21 | - [Configuration](#configuration) 22 | - [Tickets](#tickets) 23 | - [New Tickets](#new-tickets) 24 | - [Valid Tickets](#valid-tickets) 25 | - [Expired Tickets](#expired-tickets) 26 | - [Forcible Manual Expiration](#forcible-manual-expiration) 27 | - [Mass-creation of Tickets](#mass-creation-of-tickets) 28 | - [Tickets Contexts](#tickets-contexts) 29 | - [Auto-renewing Tickets](#auto-renewing-tickets) 30 | - [Tickets Generation Speed](#tickets-generation-speed) 31 | - [Lightweight Validation](#lightweight-validation) 32 | - [Retrieve Tickets Policy](#retrieve-tickets-policy) 33 | - [Payloads](#payloads) 34 | - [Status Check](#status-check) 35 | - [Statistics](#statistics) 36 | - [Garbage Collection](#garbage-collection) 37 | - [License](#license) 38 | 39 | 40 | ## Install 41 | npm install -g bagarino 42 | 43 | ## Usage 44 | _Bagarino_ needs Redis ([redis.io](http://redis.io/)) to be installed and running in order to work. 45 | To run bagarino use the following command: 46 | 47 | sudo bagarino 48 | 49 | _Bagarino_ is now up and running, listening for requests on port 8124. 50 | 51 | ## Configuration 52 | Right out of the box _bagarino_ is configured to run with default settings that make it listen on port 8124, protocol _http_, and log to _/var/log_. 53 | These settings can be easily overridden by placing a file named _"bagarino.conf"_ under _/etc_. 54 | This file must contain a valid JSON, organized as follows: 55 | ```js 56 | { 57 | "ENVIRONMENT": "production", 58 | 59 | "PORT": 8124, 60 | "HTTPS_PORT": 8443, 61 | 62 | "SERVER_TYPE": { 63 | "HTTPS": { 64 | "ENABLED": false, 65 | "KEY": "private/key.pem", 66 | "CERT": "private/cert.crt" 67 | }, 68 | "HTTP": { 69 | "ENABLED": true 70 | } 71 | }, 72 | 73 | "LOGGING": { 74 | "ENABLED": true, 75 | "PATH": "/var/log" 76 | }, 77 | 78 | "REDIS": { 79 | "HOST": "localhost", 80 | "PORT": 6379, 81 | "DB": 3 82 | }, 83 | 84 | "SECONDS_TO_REMEMBER_TICKETS_UNTIL": 864000, 85 | 86 | "CORS": 87 | { 88 | "ENABLED": false, 89 | "ORIGINS": [] 90 | } 91 | } 92 | ``` 93 | 94 | This file can be generated by calling `sudo bagarino initconf`. 95 | 96 | The **"ENVIRONMENT"** key is passed to Nodejs and tells it whether to start in _production_ or _development_ mode. 97 | The two keys **"PORT"** and **"HTTPS_PORT"** set on which port the server will be listening for incoming requests. 98 | The **"SERVER_TYPE"** key enables one of the two modes _bagarino_ can be started in, either simple HTTP or HTTPS. 99 | The **"HTTPS"** sub-key has some more configuration in it as the paths to the key and certificate files must be provided. 100 | The **"LOGGING"** key establishes under which folder the logs will be placed. 101 | The **"REDIS"** key tells _bagarino_ where is the Redis instance that will be used to save tickets. 102 | Finally, the **"CORS"** key can be used to enable [_CORS_](https://www.w3.org/TR/cors/) requests on the _bagarino_ service. 103 | When **"ENABLED"** is true the **"ORIGINS"** sub-key must be populated with one or more hosts, like this: 104 | ```"ORIGINS": ["http://google.com", "http://twitter.com", "https://abc.xyz"]``` 105 | When _CORS_ is enabled but no origin is specified it will be assumed the "*" value, meaning _"all origins"_. 106 | 107 | 108 | ## Tickets 109 | Here's a detailed guide on how to submit requests for creating new tickets and/or validating old ones. 110 | 111 | ### New tickets 112 | Obtain a new ticket: 113 | 114 | GET http://localhost:8124/tickets/new?policy=requests_based 115 | 200 OK {"result":"OK","ticket":"7fd88ab09e40f99767e17df27a723d05562d573b","expires_in":100,"policy":"requests_based"} 116 | 117 | See the status of the newly created ticket: 118 | 119 | GET http://localhost:8124/tickets/7fd88ab09e40f99767e17df27a723d05562d573b/status 120 | 200 OK {"status":"VALID","expires_in":99,"policy":"requests_based"} 121 | 122 | After some requests (99 more in this case) the ticket expires. Then, asking for it again will result in the following response: 123 | 124 | 200 OK {"status": "EXPIRED"} 125 | 126 | Asking for a non-existent ticket results in the following: 127 | 128 | GET http://localhost:8124/tickets/321somenonsense123/status 129 | 404 Not Found {"status":"ERROR","cause":"not_found"} 130 | 131 | By default new tickets have a time-based expire policy and a time-to-live of 60 seconds. 132 | A different policy can be used by specifying the _"policy"_ parameter in query-string: 133 | * **policy=time_based** is the default one. Add "seconds=300" to make the ticket expire after the non-default delay of 5 minutes. 134 | * **policy=requests_based** makes the ticket expire after a certain amount of requests of its status you do to bagarino. By default it's 100 requests, but you can otherwise specify e.g. "requests=500" to make it last for 500 requests. 135 | * **policy=cascading** makes the ticket _depend_ on another one: once the _dependency_ ticket expires the _dependent_ one does as well. 136 | * **policy=manual_expiration** makes the ticket perpetual, unless you make it expire manually by calling the _"expire"_ verb (explained some lines below). 137 | * **policy=bandwidth_based** makes the ticket perpetual as well, but the number of requests for it that can be done within a minute is limited. 138 | 139 | Let's see some requests that create tickets with different expiration policies: 140 | 141 | GET http://localhost:8124/tickets/new?policy=requests_based&requests=5 142 | 200 OK {"result":"OK","ticket":"62a315cd7bdae5e84567cad9620f82b5defd3ef0","expires_in":5,"policy":"requests_based"} 143 | 144 | GET http://localhost:8124/tickets/new?policy=requests_based 145 | 200 OK {"result":"OK","ticket":"0b4e20ce63f7de9a4a77910e7f909e5dba4538f3","expires_in":100,"policy":"requests_based"} 146 | 147 | GET http://localhost:8124/tickets/new?policy=time_based&seconds=120 148 | 200 OK {"result":"OK","ticket":"50ab14d6f5dd082e8ed343f7adb5f916fa76188a","expires_in":120,"policy":"time_based"} 149 | 150 | GET http://localhost:8124/tickets/new?policy=cascading&depends_on=f073145dfdf45a6e85d0f758f78fd627fa301983 151 | 200 OK {"result":"OK","ticket":"9ae23360fb4e9b3348917eb5e9b8a8e725b0dcb0","depends_on":"f073145dfdf45a6e85d0f758f78fd627fa301983","policy":"cascading"} 152 | 153 | GET http://localhost:8124/tickets/new?policy=manual_expiration 154 | 200 OK {"result":"OK","ticket":"f57d75c23f6a49951a6e886bbc60de74bc02ef33","policy":"manual_expiration"} 155 | 156 | When using the manual expiration policy you must call an appropriate verb to make the ticket expire: 157 | 158 | GET http://localhost:8124/tickets/f57d75c23f6a49951a6e886bbc60de74bc02ef33/expire 159 | 200 OK {"status":"EXPIRED"} 160 | 161 | Subsequent requests for that ticket will give an "EXPIRED" status. 162 | 163 | Finally, bandwidth-based tickets can be created with the following requests: 164 | 165 | GET http://localhost:8124/tickets/new?policy=bandwidth_based&reqs_per_minute=100 166 | 200 OK {"result": "OK", "ticket": "2966c1fc73a0d78c96bdc18fb67ed99af1356b8a", "requests_per_minute": 100, "policy": "bandwidth_based"} 167 | 168 | 169 | ### Valid tickets 170 | Asking for a ticket status is all you can do with a newly created ticket. _bagarino_ will answer with three different statuses: 171 | * **VALID** 172 | * **EXPIRED** 173 | * **NOT_VALID** 174 | 175 | The answer will carry some more info when the ticket is still valid: 176 | 177 | GET http://localhost:8124/tickets/0b4e20ce63f7de9a4a77910e7f909e5dba4538f3/status 178 | 200 OK {"status":"VALID","expires_in":99,"policy":"requests_based"} 179 | 180 | In the previous example the expiration policy and the TTL (Time-To-Live) of the ticket are returned, as well as its status. 181 | The parameter *"expires_in"* has to be read based on the policy of the ticket: 182 | * When the policy is **time_based** then *"expires_in"* is the number of seconds before the ticket expires 183 | * When the policy is **requests_based** the value of *"expires_in"* is the number of requests before the ticket expires 184 | 185 | 186 | ### Expired tickets 187 | Expired tickets are kept in memory by _bagarino_ for 10 days. After that time a call to their status will return "NOT_VALID" as it would for a ticket that didn't exist in the first place. 188 | 189 | 190 | ### Forcible Manual Expiration 191 | Even tickets with a policy other than *"manual_expiration"* can be forcibly ended by calling the *expire* verb, provided that they had been created with an ad-hoc option, *"can\_force\_expiration"*: 192 | 193 | GET http://localhost:8124/tickets/new?policy=requests_based&can_force_expiration=true 194 | 200 OK {"result": "OK", "ticket": "d81d9b01e323510ba919c0f54fbfba5b7903e326", "expires_in": 100, "policy": "requests_based"} 195 | 196 | The result will look identical to any other *requests_based*-policied ticket but the *can\_force\_expiration* option enables the call to the *expire* verb to successfully end this ticket life: 197 | 198 | GET http://localhost:8124/tickets/d81d9b01e323510ba919c0f54fbfba5b7903e326/expire 199 | 200 OK {"status": "EXPIRED"} 200 | 201 | Creating the ticket without this option and subsequently calling *expire* would have produced the following error: 202 | 203 | 400 Bad Request {"status": "ERROR", "cause": "different_policy"} 204 | 205 | 206 | ### Mass-creation of Tickets 207 | It's possible to create more tickets at once by adding the paramenter "count" to the query-string of the verb _new_, followed by the number of tickets to be created. 208 | The maximum number of tickets that can be created this way is capped to prevent overloading the system. 209 | Here's a typical request for mass-creation of tickets: 210 | 211 | GET http://localhost:8124/tickets/new?count=4 212 | 200 OK {"result":"OK","tickets":["9c7800ec9cf053e60674042533710c556fe22949","3cd5da62c2ba6d2b6b8973016264282f61f4afdd","7207c7effb2bd8fd97b885a4f72492a97e79babf","75a6cf2ba0454dfe74a4d6ce8baa80881fb76005"],"expire_in":60,"policy":"time_based"} 213 | 214 | 215 | ### Tickets Contexts 216 | Sometimes it may be useful to bound one or more tickets to a "context" so they only acquire a meaning under certain conditions. 217 | In _bagarino_ this is done by attaching a textual context to the ticket during the "new" operation: 218 | 219 | GET http://localhost:8124/tickets/new?policy=requests_based&context=mysweetlittlecontext 220 | 200 OK {"result":"OK","ticket":"7486f1dcf4fc4d3c4ef257230060aea531d42758","expires_in":100,"policy":"requests_based"} 221 | 222 | Once it's scoped this way requests for that ticket status that don't specify the context won't be able to retrieve it, resulting in a "not_found" error, the same given when asking for a non-existent ticket: 223 | 224 | GET http://localhost:8124/tickets/7486f1dcf4fc4d3c4ef257230060aea531d42758/status 225 | 404 Not Found {"status":"ERROR","cause":"not_found"} 226 | 227 | The way to ask for a context-bound token is as follows: 228 | 229 | GET http://localhost:8124/tickets/7486f1dcf4fc4d3c4ef257230060aea531d42758/status?context=mysweetlittlecontext 230 | 200 OK {"status":"VALID","expires_in":99,"policy":"requests_based"} 231 | 232 | 233 | ### Auto-renewing Tickets 234 | A ticket created with the option _autorenew=true_ automatically generates a new one right before expiring. 235 | Only requests-based ones can be decorated at creation with the additional option _"autorenew"_. 236 | When this option is `true` _bagarino_ automatically spawns a new ticket when the old one's expiration is one request away, 237 | returning this newly created one alongside the validity/expiration info of a _"status"_ request. 238 | The new ticket's policy and initial TTL will be the same as the old one's. 239 | 240 | Here's how an autorenew ticket is created: 241 | 242 | GET http://localhost:8124/tickets/new?policy=requests_based&requests=10&autorenew=true 243 | 200 OK {"result":"OK","expires_in":10,"ticket":"0cca33a81e4ce168f218d74692e096c676af2a25","policy":"requests_based"} 244 | 245 | After asking 9 times for this ticket validity here's what happens asking one more time: 246 | 247 | GET http://localhost:8124/tickets/0cca33a81e4ce168f218d74692e096c676af2a25/status 248 | 200 OK {"status":"VALID","expires_in":0,"policy":"requests_based","next_ticket":"c7433c48f56bd224de43b232657165842609690b"} 249 | 250 | A new ticket, _c7433c48f56bd224de43b232657165842609690b_, is born, right when the old one expires and with the same policy and initial TTL (i.e. 10 requests). 251 | 252 | 253 | ### Tickets Generation Speed 254 | Generating a ticket takes some CPU time and, under certain circumstances, this may be an issue. To arbitrarily reduce generation time a feature is present in _bagarino_ that can be activated by passing certain values to the optional _**"generation_speed"**_ parameter. 255 | 256 | GET http://localhost:8124/tickets/new?policy=time_based&seconds=30&generation_speed=slow 257 | 200 OK 258 | {"result":"OK","expires_in":30,"ticket":"e7e0dc24544cf038daf1e5f32ff0451a65a04661","policy":"time_based"} 259 | 260 | GET http://localhost:8124/tickets/new?policy=time_based&seconds=30&generation_speed=fast 261 | 200 OK 262 | {"result":"OK","expires_in":30,"ticket":"BgvPnLoxr","policy":"time_based"} 263 | 264 | GET http://localhost:8124/tickets/new?policy=time_based&seconds=30&generation_speed=faster 265 | 200 OK 266 | {"result":"OK","expires_in":30,"ticket":"1437313717902","policy":"time_based" 267 | 268 | Notice how the format of the tickets is different for every approach: that's a direct consequence of the speed the tickets are generated. 269 | **When no generation speed is specified the default _slow_ one is used.** 270 | It's almost superfluous to note that faster generation speeds are more subject to _weak_ tickets 271 | that can conflict across an eventual _multi-bagarino-s_ environment. 272 | Viceversa, slower generation speeds are more CPU-demanding although giving birth to _strong_ tickets that are almost unique. 273 | 274 | 275 | ### Lightweight Validation 276 | Sometimes checking a ticket validity directly influences its status: in particular requests- or bandwidth-based tickets 277 | have policies that put a direct correlation between the number of times a "status" check is called for them and their validity itself. 278 | 279 | There may be times when it's needed to check whether a ticket with one of these policies is valid or not, 280 | without affecting its status. 281 | At those times a "status" call can be expanded with a "light" parameter, like this: 282 | 283 | GET http://localhost:8124/tickets/7ed46ccc3606ca87ce71071e4abd894abd53b972/status?light=true 284 | 200 OK {"status":"VALID","expires_in":100,"policy":"requests_based"} 285 | 286 | The net result, in this case for a requests-based ticket, is the call not affecting the remaining number of times the "status" call can be made for this ticket. 287 | I.e. Calling status on it again will show the same number of remaining "status" checks: 288 | 289 | GET http://localhost:8124/tickets/7ed46ccc3606ca87ce71071e4abd894abd53b972/status?light=true 290 | 200 OK {"status":"VALID","expires_in":100,"policy":"requests_based"} 291 | 292 | Almost the same applies to bandwidth-based tickets, except that, for them, the number of "status" checks resets every minute. 293 | 294 | 295 | ### Retrieve Tickets Policy 296 | In _bagarino_ version 1.10.2 a new utility call has been added, that can be used to retrieve which policy a ticket responds to: 297 | 298 | GET http://localhost:8124/tickets/7ed46ccc3606ca87ce71071e4abd894abd53b972/policy 299 | 200 OK {"policy":"**requests_based**","more":{"autorenew":false,"generation_speed":"slow","can_force_expiration":false}} 300 | 301 | This way the policy for that ticket can be retrieved without the need to issue a "status" call on it. 302 | You can notice that the response to a "policy" call carries some additional info about other parameters driving the ticket behavior. 303 | In fact, the "more" object contains a list of settings for this ticket other than the policy type. 304 | For explanations about any of them see the paragraphs above in this same guide. 305 | 306 | 307 | ### Payloads 308 | In _bagarino_ version 2.4.0 a new feature has been added: **creating tickets with a JSON payload**. 309 | POST-ing a request for a new ticket, with some data and to the route `/tickets/new/withpayload` 310 | will trigger the creation of a _traditional_ ticket which will be saved alongside those data. 311 | Data are saved and accessible until the ticket expires; once expired they will be deleted and won't be accessible anymore. 312 | Some limitations apply, mostly to avoid abusing of this feature: 313 | - No mass-creation allowed; only one ticket carrying a payload will be created each time the route is called. 314 | - The payload can be max 1MB in size 315 | 316 | Here's how such tickets can be created: 317 | ``` Bash 318 | $ curl -H "Content-Type: application/json" -X POST -d '{"payloadField":"This is a payload"}' http://localhost:8124/tickets/new/withpayload?policy=manual_expiration 319 | {"result":"OK","ticket":"cfeb196b51e47f1234e4a02e52edaf45a3acde99","policy":"manual_expiration"} 320 | ``` 321 | 322 | And this is the call that retrieves the payload of a ticket still valid: 323 | 324 | GET http://localhost:8124/tickets/cfeb196b51e47f1234e4a02e52edaf45a3acde99/payload 325 | 200 OK "{\"payloadField\": \"This is a payload\"}" 326 | 327 | Tickets carrying a payload behave exactly as dictated by their expiration policies, 328 | only their payload can be treated in particular ways depending on some of these policies: 329 | - Auto-renewing tickets aren't allowed to carry a payload 330 | - Payload requests to bandwidth-based tickets **affect** the bandwidth count 331 | - Payload requests to requests-based tickets **decrease** the number of remaining requests 332 | 333 | 334 | ### Status Check 335 | An endpoint is available to check the status of the _bagarino_ service. 336 | The `/status` endpoint returns a simple JSON document containing some useful information about the server, like memory, node version and other. 337 | It returns `200 OK` if everything is fine: 338 | 339 | GET http://localhost:8124/status 340 | 200 OK {"status":"OK","memory":{"rss":"~40MB","heapTotal":"~22MB","heapUsed":"~14MB"},"uptime":12.144,"node-version":"v6.3.1"} 341 | 342 | **NOTE: Do not abuse this endpoint because it's not throttled** 343 | 344 | 345 | ## Statistics 346 | By running `bagarino stats` we can collect some statistics about the current population of tickets: 347 | ```Bash 348 | $ bagarino stats 349 | { 350 | "tickets": { 351 | "total": 282, 352 | "ignored": 0, 353 | "orphans": 3, 354 | "policies": { 355 | "requests_based": 178, 356 | "time_based": 0, 357 | "manual_expiration": 50, 358 | "bandwidth_based": 54, 359 | "cascading": 0 360 | }, 361 | "exploration": "valid-based" 362 | }, 363 | "duration": "0.033s" 364 | } 365 | ``` 366 | Orphan (or _"stale"_) tickets are the ones that got somehow forgotten by bagarino. They tipically are very old and can be safely deleted by performing a garbage collection (see [Garbage Collection](#garbage-collection)). 367 | Some tickets may be ignored during the collection, mainly because they have been modified by hand in the Redis instance. 368 | The _"exploration"_ field may be safely ignored; it's only used for debug purposes at the moment. 369 | The _"duration"_ field is the total time used by bagarino to collect the stats. 370 | 371 | **NOTE: It may take quite some time to collect the statistics altough we won't degrade Redis performances by doing it** 372 | NOTE: The tickets being analyzed are the ones stored inside the Redis instance currently configured (see [Configuration](#configuration)) 373 | 374 | 375 | ## Garbage Collection 376 | Under some circumstances it may happen that one or more old tickets become _stale_ and continue to be tracked by bagarino even if they aren't active anymore. 377 | A command-line switch can be used to remove them all at once, but pay attention to some potential issues: 378 | - stale tickets can't be recovered after they got deleted by a garbage collection 379 | - a big number of stale tickets (> 100K) may cause the garbage collection to degrade bagarino performances until the cleanup ends 380 | 381 | Here's the command-line that activates the garbage collection: 382 | ```Bash 383 | bagarino gc 384 | ``` 385 | 386 | Or: 387 | ```Bash 388 | bagarino gcv 389 | ``` 390 | 391 | The latter is much (very much!) verbose, reporting progress for every stale ticket being deleted, so be careful when using it. 392 | 393 | Here's an example response from `bagarino gc`: 394 | ```Bash 395 | Starting garbage collection... 396 | Got 12 key(s) to analyze... 397 | Garbage Collection performed correctly. 398 | 1 stale ticket(s) cleaned. 399 | ``` 400 | 401 | **Please note that garbage-collection of tickets with payloads destroys such payloads as well.** 402 | 403 | ## License 404 | 405 | Copyright (c) 2016 Nicola Orritos 406 | Licensed under the Apache-2 license. 407 | 408 | 409 | 410 | [npmdt-image]: https://img.shields.io/npm/dt/bagarino.svg "NPM Downloads" 411 | [npmdt-url]: https://www.npmjs.com/package/bagarino 412 | [npmv-image]: https://img.shields.io/npm/v/bagarino.svg "NPM Version" 413 | [npmv-url]: https://www.npmjs.com/package/bagarino 414 | [ghtag-image]: https://img.shields.io/github/tag/NicolaOrritos/bagarino.svg "GitHub Tag" 415 | [ghtag-url]: https://github.com/NicolaOrritos/bagarino/releases 416 | [ghlic-image]: https://img.shields.io/github/license/NicolaOrritos/bagarino.svg "GitHub License" 417 | [ghlic-url]: https://github.com/NicolaOrritos/bagarino/blob/master/LICENSE 418 | [david-image]: https://img.shields.io/david/NicolaOrritos/bagarino.svg "David-dm.org Dependencies Check" 419 | [david-url]: https://david-dm.org/NicolaOrritos/bagarino 420 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 2 | // [todo] - Tune logging subsystem and use it thoroughly 3 | 4 | 'use strict'; 5 | 6 | const fs = require('fs'); 7 | const CORS = require('restify-cors-middleware'); 8 | const cluster = require('cluster'); 9 | const restify = require('restify'); 10 | const Log = require('log'); 11 | const utils = require('./lib/utils'); 12 | const CONF = require('./lib/conf'); 13 | const CONST = require('./lib/const'); 14 | 15 | 16 | // Initialize logging 17 | if (CONST.ENV.DEVELOPMENT === CONF.ENVIRONMENT) 18 | { 19 | global.log = new Log('debug'); 20 | } 21 | else 22 | { 23 | global.log = new Log('info'); 24 | } 25 | 26 | 27 | const routes = 28 | { 29 | 'tickets' : require('./lib/tickets'), 30 | 'contexts': require('./lib/contexts'), 31 | 'utils' : require('./lib/utils') 32 | }; 33 | 34 | function initAndStart(server, port) 35 | { 36 | if (server && port) 37 | { 38 | server.use (utils.toobusy); // Reject requests when too busy 39 | 40 | server.on ('NotFound', routes.utils.notpermitted); 41 | server.on ('MethodNotAllowed', routes.utils.notpermitted); 42 | server.on ('uncaughtException', routes.utils.notpermitted); 43 | 44 | server.get ('/status', routes.utils.status); 45 | 46 | server.use (restify.plugins.queryParser()); 47 | 48 | server.get ('/tickets/new', routes.tickets.new); 49 | server.get ('/tickets/:ticket/status', routes.tickets.status); 50 | server.get ('/tickets/:ticket/policy', routes.tickets.policy); 51 | server.get ('/tickets/:ticket/expire', routes.tickets.expire); 52 | server.get ('/contexts/:context/expireall', routes.contexts.expireall); 53 | 54 | server.use (restify.plugins.acceptParser('application/json')); 55 | server.use (restify.plugins.bodyParser({maxBodySize: CONST.ONE_MiB})); 56 | 57 | server.post('/tickets/new/withpayload', routes.tickets.withpayload); 58 | server.get ('/tickets/:ticket/payload', routes.tickets.payload); 59 | 60 | 61 | if (CONF.CORS && CONF.CORS.ENABLED) 62 | { 63 | const origins = (CONF.CORS.ORIGINS && CONF.CORS.ORIGINS.length) ? CONF.CORS.ORIGINS : ['*']; 64 | const headers = 'Accept, Accept-Version, Content-Type, Api-Version, Origin, X-Requested-With, ' 65 | + 'Authorization, Withcredentials, X-Requested-With, X-Forwarded-For, X-Real-Ip, ' 66 | + 'X-Customheader, User-Agent, Keep-Alive, Host, Accept, Connection, Upgrade, ' 67 | + 'Content-Type, If-Modified-Since, Cache-Control'; 68 | 69 | const cors = CORS({ origins, allowHeaders: headers }); 70 | 71 | server.pre(cors.preflight); 72 | server.use(cors.actual); 73 | 74 | server.opts(/\.*/, routes.tickets.cors); 75 | } 76 | 77 | 78 | server.listen(port, () => 79 | { 80 | let workerID = 'test'; 81 | 82 | if (cluster && cluster.worker) 83 | { 84 | workerID = cluster.worker.id; 85 | } 86 | 87 | global.log.info('BAGARINO server listening on port %d in %s mode [worker is %s]', 88 | port, 89 | CONF.ENVIRONMENT, 90 | workerID); 91 | }); 92 | 93 | // Gracefully handle SIGTERM 94 | process.on('SIGTERM', () => 95 | { 96 | server.close(function() 97 | { 98 | // Exit after server is closed 99 | process.exit(0); 100 | }); 101 | }); 102 | } 103 | } 104 | 105 | 106 | if (CONF.SERVER_TYPE.HTTP.ENABLED) 107 | { 108 | const httpServer = restify.createServer(); 109 | 110 | initAndStart(httpServer, CONF.PORT); 111 | } 112 | 113 | if (CONF.SERVER_TYPE.HTTPS.ENABLED) 114 | { 115 | if (CONF.SERVER_TYPE.HTTP.ENABLED && CONF.PORT === CONF.HTTPS_PORT) 116 | { 117 | global.log.error('Could not start bagarino HTTP and HTTPS server on the same port (%s)! Exiting...', CONF.PORT); 118 | 119 | process.exit(1); 120 | } 121 | else 122 | { 123 | const privateKey = fs.readFileSync(CONF.SERVER_TYPE.HTTPS.KEY, 'utf8'); 124 | const certificate = fs.readFileSync(CONF.SERVER_TYPE.HTTPS.CERT, 'utf8'); 125 | 126 | const credentials = {key: privateKey, certificate: certificate}; 127 | 128 | const httpsServer = restify.createServer(credentials); 129 | 130 | initAndStart(httpsServer, CONF.HTTPS_PORT); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /bin/start-bagarino-daemon: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | 6 | // Parsing some options: 7 | const docopt = require('docopt').docopt; 8 | 9 | const options = 'Start/configure the bagarino daemon or perform a Garbage Collection of stale tickets or collect stats about tickets \n' 10 | + ' \n' 11 | + 'Usage: \n' 12 | + ' bagarino [--dev] \n' 13 | + ' bagarino gc \n' 14 | + ' bagarino gcv \n' 15 | + ' bagarino stats \n' 16 | + ' bagarino initconf \n' 17 | + ' bagarino -h | --help \n' 18 | + ' \n' 19 | + 'Options: \n' 20 | + ' gc Perform a Garbage Collection of stale tickets (without starting the daemon) \n' 21 | + ' gcv Perform Garbage Collection and be verbose about it (without starting the daemon) \n' 22 | + ' stats Collect and print (as JSON) statistics about the current population of tickets (without starting the daemon) \n' 23 | + ' initconf Create (or replace) the configuration file "/etc/bagarino.conf" using the default one (without starting the daemon) \n' 24 | + ' --dev Starts bagarino in development-mode, only spawning one worker \n' 25 | + ' --h --help Show this help \n' 26 | + ' \n' 27 | + 'Example: \n' 28 | + ' echo "Starting bagarino in production mode..." \n' 29 | + ' sudo bagarino \n'; 30 | 31 | const cmd = docopt(options); 32 | 33 | 34 | if (cmd && (cmd['gc'] || cmd['gcv'])) 35 | { 36 | const gc = require('../lib/gc.js'); 37 | 38 | const verbose = cmd['gcv'] ? true : false; 39 | 40 | gc.run(verbose) 41 | .then( count => 42 | { 43 | console.log('Garbage Collection performed correctly.'); 44 | console.log('%s stale ticket(s) cleaned.', count); 45 | 46 | process.exit(); 47 | }) 48 | .catch( err => 49 | { 50 | console.log('Could not perform Garbage Collection. %s', err.stack); 51 | 52 | process.exit(1); 53 | }); 54 | } 55 | else if (cmd && cmd['stats']) 56 | { 57 | const stats = require('../lib/stats.js'); 58 | 59 | stats.run() 60 | .then( result => 61 | { 62 | console.log(JSON.stringify(result, null, 4)); 63 | 64 | process.exit(); 65 | }) 66 | .catch( err => 67 | { 68 | console.log('Could not collect statistics. %s', err.stack); 69 | 70 | process.exit(1); 71 | }); 72 | } 73 | else if (cmd && cmd['initconf']) 74 | { 75 | const fs = require('fs'); 76 | 77 | const readStream = fs.createReadStream(__dirname + '/../etc/bagarino.conf'); 78 | const writeStream = fs.createWriteStream('/etc/bagarino.conf'); 79 | 80 | readStream.on('end', () => 81 | { 82 | console.log('Created file "/etc/bagarino.conf"'); 83 | 84 | process.exit(); 85 | }); 86 | 87 | readStream.on( 'error', err => console.log('Error initializing the configuration. %s', err) ); 88 | writeStream.on('error', err => console.log('Error initializing the configuration. %s', err) ); 89 | 90 | readStream.pipe(writeStream); 91 | } 92 | else 93 | { 94 | const probiotic = require('probiotic'); 95 | const CONF = require(__dirname + '/../lib/conf'); 96 | 97 | const devMode = (cmd['--dev']) || (CONF.ENVIRONMENT === 'development') ? true : false; 98 | const workers = devMode ? 1 : 'auto'; 99 | 100 | probiotic.run({ 101 | name: 'bagarino', 102 | main: '../app.js', 103 | workers: workers, 104 | logsBasePath: CONF.LOGGING.PATH 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /etc/bagarino.conf: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "ENVIRONMENT": "production", 4 | 5 | "PORT": 8124, 6 | "HTTPS_PORT": 8443, 7 | 8 | "SERVER_TYPE": { 9 | "HTTPS": { 10 | "ENABLED": false, 11 | "KEY": "private/key.pem", 12 | "CERT": "private/cert.crt" 13 | }, 14 | "HTTP": { 15 | "ENABLED": true 16 | } 17 | }, 18 | 19 | "LOGGING": { 20 | "ENABLED": true, 21 | "PATH": "/var/log" 22 | }, 23 | 24 | "REDIS": { 25 | "HOST": "localhost", 26 | "PORT": 6379, 27 | "DB": 3 28 | }, 29 | 30 | "SECONDS_TO_REMEMBER_TICKETS_UNTIL": 864000, 31 | 32 | "CORS": 33 | { 34 | "ENABLED": false, 35 | "ORIGINS": ["*"] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sjl = require('sjl'); 4 | 5 | 6 | const defaults = 7 | { 8 | 'ENVIRONMENT': 'production', 9 | 10 | 'PORT': 8124, 11 | 'HTTPS_PORT': 8443, 12 | 13 | 'SERVER_TYPE': { 14 | 'HTTPS': { 15 | 'ENABLED': false, 16 | 'KEY': 'private/key.pem', 17 | 'CERT': 'private/cert.crt' 18 | }, 19 | 'HTTP': { 20 | 'ENABLED': true 21 | } 22 | }, 23 | 24 | 'LOGGING': { 25 | 'ENABLED': true, 26 | 'PATH': '/var/log' 27 | }, 28 | 29 | "REDIS": { 30 | "HOST": "localhost", 31 | "PORT": 6379, 32 | "DB": 3 33 | }, 34 | 35 | 'SECONDS_TO_REMEMBER_TICKETS_UNTIL': 864000 36 | }; 37 | 38 | let result = sjl('/etc/bagarino.conf', defaults); 39 | 40 | 41 | // Backward compatibility: 42 | result.REDIS = result.REDIS || defaults.REDIS; 43 | 44 | 45 | module.exports = result; 46 | -------------------------------------------------------------------------------- /lib/const.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 4 | { 5 | ENV: { 6 | PRODUCTION: 'production', 7 | DEVELOPMENT: 'development' 8 | }, 9 | 10 | OK: 'OK', 11 | NOT_OK: 'NOT_OK', 12 | ERROR: 'ERROR', 13 | 14 | ERRORS: { 15 | UNKNOWN: 'unknown', 16 | DIFFERENT_POLICY: 'different_policy', 17 | MALFORMED_TICKET: 'malformed_ticket', 18 | TOO_MUCH_TICKETS: 'too_much_tickets', 19 | WRONG_POLICY: 'wrong_policy', 20 | NOT_FOUND: 'not_found', 21 | EMPTY_REQUEST: 'empty_request', 22 | CONTEXT_NOT_FOUND: 'context_not_found', 23 | MALFORMED_REQUEST: 'malformed_request', 24 | PAYLOAD_NOT_FOUND: 'payload_not_found' 25 | }, 26 | 27 | POLICIES: { 28 | REQUESTS_BASED: 'requests_based', 29 | TIME_BASED: 'time_based', 30 | MANUAL_EXPIRATION: 'manual_expiration', 31 | BANDWIDTH_BASED: 'bandwidth_based', 32 | CASCADING: 'cascading' 33 | }, 34 | 35 | VALID_TICKET: 'VALID', 36 | VALID_PREFIX: 'VALID:', 37 | 38 | EXPIRED_TICKET: 'EXPIRED', 39 | EXPIRED_PREFIX: 'EXPIRED:', 40 | 41 | CONTEXTS_PREFIX: 'contexts:', 42 | 43 | DEFAULT_EXPIRES_IN_SECONDS : 60, 44 | DEFAULT_EXPIRES_IN_REQUESTS: 100, 45 | DEFAULT_REQUESTS_PER_MINUTE: 60, 46 | DEFAULT_REMEMBER_UNTIL: 60 * 60 * 24 * 10, // Ten days = 864000 47 | 48 | MAX_TICKETS_PER_TIME: 200, 49 | 50 | SPEED: { 51 | SLOW: 'slow', 52 | FAST: 'fast', 53 | FASTER: 'faster' 54 | }, 55 | 56 | ONE_MiB: 1048576 57 | }; 58 | -------------------------------------------------------------------------------- /lib/contexts.js: -------------------------------------------------------------------------------- 1 | 2 | // [todo] - Add documentation for contexts-based multi-ticket expiration 3 | 4 | 'use strict'; 5 | 6 | const redis = require('redis'); 7 | const CONST = require('./const'); 8 | const CONF = require('./conf'); 9 | 10 | 11 | const client = redis.createClient({ 12 | host: CONF.REDIS.HOST, 13 | port: CONF.REDIS.PORT, 14 | db: CONF.REDIS.DB 15 | }); 16 | 17 | client.on('error', err => 18 | { 19 | global.log.error('Got an error from the Redis client: ' + err.stack); 20 | }); 21 | 22 | 23 | function removeTicket(context, ticket) 24 | { 25 | return new Promise( (resolve, reject) => 26 | { 27 | if (ticket && context) 28 | { 29 | client.hget(CONST.VALID_PREFIX + ticket, 'policy', (error, policy_str) => 30 | { 31 | if (policy_str) 32 | { 33 | const policy = JSON.parse(policy_str); 34 | 35 | if ( policy.manual_expiration === true 36 | || policy.can_force_expiration === true) 37 | { 38 | // Save the 'expired' counterpart when manually expiring: 39 | client.set(CONST.EXPIRED_PREFIX + ticket, CONST.EXPIRED_TICKET); 40 | client.expire(CONST.EXPIRED_PREFIX + ticket, policy.remember_until); 41 | 42 | // Finally delete valid ticket 43 | client.del(CONST.VALID_PREFIX + ticket); 44 | 45 | client.lrem(context, '1', ticket, (err, removed) => 46 | { 47 | if (err) 48 | { 49 | console.log('Could not remove ticket "%s" from context map "%s". Cause: %s', ticket, context, err); 50 | 51 | reject(err); 52 | } 53 | else 54 | { 55 | resolve(removed); 56 | } 57 | }); 58 | } 59 | else 60 | { 61 | resolve(false); 62 | } 63 | } 64 | else 65 | { 66 | // Malformed ticket in the DB: delete 67 | client.del(CONST.VALID_PREFIX + ticket, err => 68 | { 69 | if (err) 70 | { 71 | console.log('Could not delete supposedly-malformed ticket. Cause: %s', err); 72 | 73 | reject(err); 74 | } 75 | 76 | client.del(CONST.EXPIRED_PREFIX + ticket, err2 => 77 | { 78 | if (err2) 79 | { 80 | console.log('Could not fully delete supposedly-malformed ticket. Cause: %s', err2); 81 | 82 | reject(err2); 83 | } 84 | 85 | client.lrem(context, '1', ticket, (err3, removed) => 86 | { 87 | if (err3) 88 | { 89 | console.log('Could not remove supposedly-malformed ticket "%s" from context map "%s". Cause: %s', ticket, context, err3); 90 | 91 | reject(err3); 92 | } 93 | else 94 | { 95 | resolve(removed); 96 | } 97 | }); 98 | }); 99 | }); 100 | } 101 | }); 102 | } 103 | else 104 | { 105 | reject(); 106 | } 107 | }); 108 | } 109 | 110 | 111 | exports.expireall = function(req, res, next) 112 | { 113 | const reply = {'status': CONST.ERROR}; 114 | 115 | let context = req.params.context; 116 | 117 | if (context) 118 | { 119 | context = CONST.CONTEXTS_PREFIX + context; 120 | 121 | client.lrange(context, '0', '-1', (err, tickets) => 122 | { 123 | if (err) 124 | { 125 | console.log('Error when retrieving tickets for context "%s": %s', context, err); 126 | 127 | reply.status = CONST.ERROR; 128 | reply.cause = err; 129 | 130 | res.send(500, reply); 131 | 132 | next(); 133 | } 134 | else 135 | { 136 | if (tickets.length > 0) 137 | { 138 | const promises = tickets.map( ticket => 139 | { 140 | return removeTicket(context, ticket); 141 | }); 142 | 143 | Promise.all(promises) 144 | .then( deletedTickets => 145 | { 146 | let deletedCount = deletedTickets.reduce( (total, deleted) => 147 | { 148 | if (isNaN(total)) 149 | { 150 | total = 0; 151 | } 152 | 153 | if (deleted) 154 | { 155 | total++; 156 | } 157 | 158 | return total; 159 | }); 160 | 161 | // When only one ticket is processed, 'reduce' gets skipped... 162 | if (deletedCount === true) 163 | { 164 | deletedCount = 1; 165 | } 166 | else if (deletedCount === false) 167 | { 168 | deletedCount = 0; 169 | } 170 | 171 | reply.status = CONST.OK; 172 | reply.expired = deletedCount; 173 | 174 | res.send(reply); 175 | 176 | next(); 177 | }); 178 | } 179 | else 180 | { 181 | reply.status = CONST.NOT_OK; 182 | reply.cause = CONST.ERRORS.CONTEXT_NOT_FOUND; 183 | 184 | res.send(404, reply); 185 | 186 | next(); 187 | } 188 | } 189 | }); 190 | } 191 | else 192 | { 193 | reply.status = CONST.ERROR; 194 | reply.err = CONST.ERRORS.EMPTY_REQUEST; 195 | 196 | res.send(400, reply); 197 | 198 | next(); 199 | } 200 | }; 201 | -------------------------------------------------------------------------------- /lib/gc.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | const redis = require('redis'); 5 | const CONST = require('./const'); 6 | const CONF = require('./conf'); 7 | 8 | 9 | module.exports = 10 | { 11 | run: function(verbose) 12 | { 13 | return new Promise( (resolve, reject) => 14 | { 15 | console.log('Starting garbage collection' + (verbose ? ', with "verbose" option' : '') + '...'); 16 | 17 | 18 | const client = redis.createClient({ 19 | host: CONF.REDIS.HOST, 20 | port: CONF.REDIS.PORT, 21 | db: CONF.REDIS.DB 22 | }); 23 | 24 | client.on('error', reject); 25 | 26 | 27 | let cleaned = 0; 28 | 29 | /* jshint -W116 */ 30 | 31 | function scan(cursor) 32 | { 33 | if (cursor === undefined || cursor === null) 34 | { 35 | cursor = 0; 36 | } 37 | 38 | client.scan(cursor, 39 | 'MATCH', CONST.VALID_PREFIX + '*', 40 | 'COUNT', 128, 41 | (err2, res) => 42 | { 43 | if (err2) 44 | { 45 | reject(err2); 46 | } 47 | else 48 | { 49 | cursor = parseInt(res[0]); 50 | 51 | 52 | const keys = res[1]; 53 | 54 | const total = keys.length; 55 | let processed = 0; 56 | 57 | if (verbose) 58 | { 59 | console.log('Cursor is now "%s"...', cursor); 60 | console.log('Got %s key(s) to analyze...', total); 61 | } 62 | 63 | const deleted = function(count) 64 | { 65 | cleaned += count; 66 | processed++; 67 | 68 | if (processed === total) 69 | { 70 | if (cursor === 0) 71 | { 72 | resolve(cleaned); 73 | } 74 | else 75 | { 76 | scan(cursor); 77 | } 78 | } 79 | }; 80 | 81 | 82 | if (keys.length === 0) 83 | { 84 | if (cursor === 0) 85 | { 86 | resolve(cleaned); 87 | } 88 | else 89 | { 90 | scan(cursor); 91 | } 92 | } 93 | else 94 | { 95 | keys.forEach( key => 96 | { 97 | if (key) 98 | { 99 | const ticket = key.slice(CONST.VALID_PREFIX.length); 100 | 101 | 102 | if (verbose) console.log('Analyzing ticket "%s" (from key "%s")...', ticket, key); 103 | 104 | if (ticket) 105 | { 106 | client.hget(CONST.VALID_PREFIX + ticket, 'policy', (err3, policy_str) => 107 | { 108 | if (err3) 109 | { 110 | if (verbose) console.log('Could not get "%s"\'s policy. %s', CONST.VALID_PREFIX + ticket, err3); 111 | 112 | deleted(0); 113 | } 114 | else if (policy_str) 115 | { 116 | const policy = JSON.parse(policy_str); 117 | 118 | if (policy.requests_based) 119 | { 120 | client.get(CONST.EXPIRED_PREFIX + ticket, (err4, exists) => 121 | { 122 | if (err4) 123 | { 124 | if (verbose) console.log('Could not get "%s". %s', CONST.EXPIRED_PREFIX + ticket, err4); 125 | 126 | deleted(0); 127 | } 128 | else if (exists) 129 | { 130 | if (verbose) console.log('"%s"\'s not a stale ticket', ticket); 131 | 132 | deleted(0); 133 | } 134 | else 135 | { 136 | // Stale ticket, delete 137 | client.del(CONST.VALID_PREFIX + ticket, err5 => 138 | { 139 | if (err5) 140 | { 141 | if (verbose) console.log('Could not delete stale ticket "%s"\'. %s', CONST.VALID_PREFIX + ticket, err5); 142 | 143 | deleted(0); 144 | } 145 | else 146 | { 147 | if (verbose) console.log('Deleted stale ticket "%s"\'', ticket); 148 | 149 | deleted(1); 150 | } 151 | }); 152 | } 153 | }); 154 | } 155 | else 156 | { 157 | if (verbose) console.log('Skipping non "requests_based" "%s" ticket...', ticket); 158 | 159 | deleted(0); 160 | } 161 | } 162 | else 163 | { 164 | if (verbose) console.log('Could not get "%s"\'s policy. Unknown reason...', CONST.VALID_PREFIX + ticket); 165 | 166 | deleted(0); 167 | } 168 | }); 169 | } 170 | else 171 | { 172 | if (verbose) console.log('Could not find ticket "%s" anymore', key); 173 | 174 | deleted(0); 175 | } 176 | } 177 | else 178 | { 179 | if (verbose) console.log('Skipping empty key...'); 180 | 181 | deleted(0); 182 | } 183 | }); 184 | } 185 | } 186 | }); 187 | } 188 | 189 | 190 | if (verbose) console.log('Opened DB "%s"...', CONF.REDIS.DB); 191 | 192 | /* jshint +W116 */ 193 | 194 | scan(); 195 | }); 196 | } 197 | }; 198 | -------------------------------------------------------------------------------- /lib/policies.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | const redis = require('redis'); 5 | const CONST = require('./const'); 6 | const CONF = require('./conf'); 7 | 8 | 9 | const client = redis.createClient({ 10 | host: CONF.REDIS.HOST, 11 | port: CONF.REDIS.PORT, 12 | db: CONF.REDIS.DB 13 | }); 14 | 15 | client.on('error', (err) => 16 | { 17 | global.log.error('Got an error from the Redis client: ' + err); 18 | }); 19 | 20 | 21 | module.exports = 22 | { 23 | detectRequestsBased: function(query_string, policy) 24 | { 25 | return new Promise( (resolve, reject) => 26 | { 27 | if (query_string && policy) 28 | { 29 | policy.requests_based = true; 30 | 31 | if (query_string.requests) 32 | { 33 | const reqs = parseInt(query_string.requests); 34 | 35 | // Added "reqs < 0" condition to fix bug #10 36 | if (isNaN(reqs) || reqs < 0) 37 | { 38 | policy.expires_in = CONST.DEFAULT_EXPIRES_IN_REQUESTS; 39 | } 40 | else 41 | { 42 | policy.expires_in = reqs; 43 | } 44 | } 45 | else 46 | { 47 | policy.expires_in = CONST.DEFAULT_EXPIRES_IN_REQUESTS; 48 | } 49 | 50 | if (policy.autorenew) 51 | { 52 | policy.original_expires_in = policy.expires_in; 53 | } 54 | 55 | resolve(policy); 56 | } 57 | else 58 | { 59 | reject(new Error('Missing query-string and/or policy')); 60 | } 61 | }); 62 | }, 63 | 64 | detectManual: function(query_string, policy) 65 | { 66 | return new Promise( (resolve, reject) => 67 | { 68 | if (query_string && policy) 69 | { 70 | policy.manual_expiration = true; 71 | 72 | resolve(policy); 73 | } 74 | else 75 | { 76 | reject(new Error('Missing query-string and/or policy')); 77 | } 78 | }); 79 | }, 80 | 81 | detectTimeBased: function(query_string, policy) 82 | { 83 | return new Promise( (resolve, reject) => 84 | { 85 | if (query_string && policy) 86 | { 87 | policy.time_based = true; 88 | 89 | if (query_string.seconds) 90 | { 91 | const secs = parseInt(query_string.seconds); 92 | 93 | if (isNaN(secs)) 94 | { 95 | policy.expires_in = CONST.DEFAULT_EXPIRES_IN_SECONDS; 96 | } 97 | else 98 | { 99 | policy.expires_in = secs; 100 | } 101 | } 102 | else 103 | { 104 | policy.expires_in = CONST.DEFAULT_EXPIRES_IN_SECONDS; 105 | } 106 | 107 | 108 | resolve(policy); 109 | } 110 | else 111 | { 112 | reject(new Error('Missing query-string and/or policy')); 113 | } 114 | }); 115 | }, 116 | 117 | detectCascading: function(query_string, policy) 118 | { 119 | return new Promise( (resolve, reject) => 120 | { 121 | if (query_string && policy) 122 | { 123 | policy.cascading = true; 124 | 125 | const dep_ticket = query_string.depends_on; 126 | 127 | global.log.debug('Creating cascading-policy ticket dependent on ticket "%s"...', dep_ticket); 128 | 129 | if (dep_ticket) 130 | { 131 | client.exists(CONST.VALID_PREFIX + dep_ticket, (error, exists) => 132 | { 133 | if (exists) 134 | { 135 | global.log.debug('Dependency ticket "%s" exists', dep_ticket); 136 | 137 | policy.depends_on = dep_ticket; 138 | 139 | global.log.debug('Resulting policy for cascading ticket is: ' + JSON.stringify(policy)); 140 | } 141 | else 142 | { 143 | global.log.debug('Dependency ticket "%s" DOES NOT exists', dep_ticket); 144 | 145 | policy = undefined; 146 | } 147 | 148 | 149 | resolve(policy); 150 | }); 151 | } 152 | else 153 | { 154 | policy = undefined; 155 | 156 | reject(new Error('No dependent ticket found')); 157 | } 158 | } 159 | else 160 | { 161 | reject(new Error('Missing query-string and/or policy')); 162 | } 163 | }); 164 | }, 165 | 166 | detectBandwithBased: function(query_string, policy) 167 | { 168 | return new Promise( (resolve, reject) => 169 | { 170 | if (query_string && policy) 171 | { 172 | policy.bandwidth_based = true; 173 | 174 | if (query_string.reqs_per_minute) 175 | { 176 | const reqsPerMin = parseInt(query_string.reqs_per_minute); 177 | 178 | if (isNaN(reqsPerMin)) 179 | { 180 | policy.expires_in = CONST.DEFAULT_REQUESTS_PER_MINUTE; 181 | } 182 | else 183 | { 184 | policy.expires_in = reqsPerMin; 185 | } 186 | } 187 | else 188 | { 189 | policy.expires_in = CONST.DEFAULT_REQUESTS_PER_MINUTE; 190 | } 191 | 192 | 193 | resolve(policy); 194 | } 195 | else 196 | { 197 | reject(new Error('Missing query-string and/or policy')); 198 | } 199 | }); 200 | } 201 | }; 202 | -------------------------------------------------------------------------------- /lib/stats.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | const redis = require('redis'); 5 | const CONST = require('./const'); 6 | const CONF = require('./conf'); 7 | 8 | 9 | module.exports = 10 | { 11 | run: function() 12 | { 13 | return new Promise( (resolve, reject) => 14 | { 15 | const started = Date.now(); 16 | 17 | 18 | const client = redis.createClient({ 19 | host: CONF.REDIS.HOST, 20 | port: CONF.REDIS.PORT, 21 | db: CONF.REDIS.DB 22 | }); 23 | 24 | client.on('error', reject); 25 | 26 | let ignored = 0; 27 | let orphans = 0; 28 | let grandTotal = 0; 29 | 30 | const policies = {}; 31 | policies[CONST.POLICIES.REQUESTS_BASED] = 0; 32 | policies[CONST.POLICIES.TIME_BASED] = 0; 33 | policies[CONST.POLICIES.MANUAL_EXPIRATION] = 0; 34 | policies[CONST.POLICIES.BANDWIDTH_BASED] = 0; 35 | policies[CONST.POLICIES.CASCADING] = 0; 36 | 37 | /* jshint -W116 */ 38 | 39 | function scan(cursor) 40 | { 41 | if (cursor === undefined || cursor === null) 42 | { 43 | cursor = 0; 44 | } 45 | 46 | client.scan(cursor, 47 | 'MATCH', CONST.VALID_PREFIX + '*', 48 | 'COUNT', 128, 49 | (err2, res) => 50 | { 51 | if (err2) 52 | { 53 | reject(err2); 54 | } 55 | else 56 | { 57 | cursor = parseInt(res[0]); 58 | 59 | 60 | const keys = res[1]; 61 | 62 | const total = keys.length; 63 | let processed = 0; 64 | 65 | const areWeFinished = function(instructions) 66 | { 67 | if (instructions && instructions.ignored) 68 | { 69 | ignored++; 70 | } 71 | 72 | processed++; 73 | 74 | if (processed === total) 75 | { 76 | grandTotal += processed; 77 | 78 | const duration = ((Date.now() - started) / 1000).toPrecision(2) + 's'; 79 | 80 | if (cursor === 0) 81 | { 82 | const result = 83 | { 84 | tickets: 85 | { 86 | total: grandTotal, 87 | ignored, 88 | orphans, 89 | policies, 90 | 91 | exploration: 'valid-based' 92 | }, 93 | 94 | duration 95 | }; 96 | 97 | resolve(result); 98 | } 99 | else 100 | { 101 | scan(cursor); 102 | } 103 | } 104 | }; 105 | 106 | 107 | if (keys.length === 0) 108 | { 109 | if (cursor === 0) 110 | { 111 | resolve(); 112 | } 113 | else 114 | { 115 | scan(cursor); 116 | } 117 | } 118 | else 119 | { 120 | keys.forEach( key => 121 | { 122 | if (key) 123 | { 124 | const ticket = key.slice(CONST.VALID_PREFIX.length); 125 | 126 | if (ticket) 127 | { 128 | client.hget(CONST.VALID_PREFIX + ticket, 'policy', (err3, policy_str) => 129 | { 130 | if (err3) 131 | { 132 | areWeFinished({ignore: true}); 133 | } 134 | else if (policy_str) 135 | { 136 | const policy = JSON.parse(policy_str); 137 | 138 | for (let policyName in policies) 139 | { 140 | if (policy[policyName] === true) 141 | { 142 | policies[policyName]++; 143 | } 144 | } 145 | 146 | if (policy.requests_based) 147 | { 148 | client.get(CONST.EXPIRED_PREFIX + ticket, (err4, exists) => 149 | { 150 | if (!err4 && !exists) 151 | { 152 | // Stale ticket 153 | orphans++; 154 | } 155 | 156 | areWeFinished(); 157 | }); 158 | } 159 | else 160 | { 161 | areWeFinished(); 162 | } 163 | } 164 | else 165 | { 166 | areWeFinished({ignore: true}); 167 | } 168 | }); 169 | } 170 | else 171 | { 172 | areWeFinished({ignore: true}); 173 | } 174 | } 175 | else 176 | { 177 | areWeFinished({ignore: true}); 178 | } 179 | }); 180 | } 181 | } 182 | }); 183 | } 184 | 185 | /* jshint +W116 */ 186 | 187 | scan(); 188 | }); 189 | } 190 | }; 191 | -------------------------------------------------------------------------------- /lib/tickets.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | // [todo] - Add a clarification about bandwidth-based tickets: they never expire, they simply can be spent a fixed amount of times within a minute 5 | // [todo] - Add new docs for auto-renewable tickets 6 | // [todo] - Add new docs for can_force_expiration switch 7 | // [todo] - What happens to a requests-based 10-requests-long ticket expired counterpart after DEFAULT_REMEMBER_UNTIL if I didn't ask for it never once? 8 | 9 | const hash = require('node_hash'); 10 | const redis = require('redis'); 11 | const restify = require('restify'); 12 | const Hashids = require('hashids'); 13 | const CONF = require('./conf'); 14 | const CONST = require('./const'); 15 | const policies = require('./policies'); 16 | 17 | const lighthash = new Hashids('bagarino'); 18 | 19 | 20 | const client = redis.createClient({ 21 | host: CONF.REDIS.HOST, 22 | port: CONF.REDIS.PORT, 23 | db: CONF.REDIS.DB 24 | }); 25 | 26 | client.on('error', err => 27 | { 28 | global.log.error('Got an error from the Redis client: ' + err); 29 | }); 30 | 31 | 32 | // [todo] - Add bandwidth-based policy 33 | function calculateExpirationPolicy(req) 34 | { 35 | return new Promise( (resolve, reject) => 36 | { 37 | if (req && req.query) 38 | { 39 | const query_string = req.query; 40 | let alreadyRejected = false; 41 | 42 | let policy = 43 | { 44 | // Policies available for tickets: 45 | time_based: false, 46 | requests_based: false, 47 | manual_expiration: false, 48 | cascading: false, 49 | bandwidth_based: false, 50 | 51 | // When the ticket has a cascading policy this field tracks the one it depends on: 52 | depends_on: undefined, 53 | 54 | // Track an optional context for this ticket 55 | context: undefined, 56 | 57 | // Auto-renewable ticket? 58 | autorenew: false, 59 | 60 | // Can expiration be forced? 61 | can_force_expiration: false, 62 | 63 | // Generation speed: 64 | generation_speed: CONST.SPEED.SLOW, 65 | 66 | // Parameter-driven max age for a 'valid' ticket, despite its policy: 67 | last_max: undefined, 68 | 69 | // Number of seconds/requests until this ticket expires 70 | expires_in: undefined, 71 | 72 | // Whether it carries a payload or not: 73 | payload: false, 74 | 75 | // How much time before discarding the 'expired' countepart 76 | remember_until: CONF.SECONDS_TO_REMEMBER_TICKETS_UNTIL || CONST.DEFAULT_REMEMBER_UNTIL 77 | }; 78 | 79 | 80 | // The policy may contain a "context": 81 | if (query_string.context) 82 | { 83 | policy.context = query_string.context; 84 | } 85 | 86 | if (query_string.autorenew) 87 | { 88 | policy.autorenew = (query_string.autorenew === true || query_string.autorenew === 'true'); 89 | } 90 | 91 | if (query_string.can_force_expiration) 92 | { 93 | policy.can_force_expiration = (query_string.can_force_expiration === true || query_string.can_force_expiration === 'true'); 94 | } 95 | 96 | if (query_string.generation_speed) 97 | { 98 | const speed = query_string.generation_speed.trim ? query_string.generation_speed.trim().toLowerCase() : CONST.SPEED.SLOW; 99 | 100 | if (speed === CONST.SPEED.FAST) 101 | { 102 | policy.generation_speed = CONST.SPEED.FAST; 103 | } 104 | else if (speed === CONST.SPEED.FASTER) 105 | { 106 | policy.generation_speed = CONST.SPEED.FASTER; 107 | } 108 | else 109 | { 110 | policy.generation_speed = CONST.SPEED.SLOW; 111 | } 112 | 113 | global.log.debug('Creating ticket with generation speed "%s"...', policy.generation_speed); 114 | } 115 | 116 | if (query_string.last_max) 117 | { 118 | const last_max = parseInt(query_string.last_max); 119 | 120 | if (isNaN(last_max) || last_max <= 0) 121 | { 122 | alreadyRejected = true; 123 | 124 | reject(new Error('When provided, "last_max" parameter must be a positive integer, greater than 0')); 125 | } 126 | else 127 | { 128 | policy.last_max = last_max; 129 | } 130 | } 131 | 132 | if (req.body && !policy.autorenew) 133 | { 134 | policy.payload = true; 135 | } 136 | 137 | if (alreadyRejected) 138 | { 139 | // Skip everything below... 140 | } 141 | else if (query_string.policy === CONST.POLICIES.REQUESTS_BASED) 142 | { 143 | Promise.resolve(policies.detectRequestsBased(query_string, policy)) 144 | .then(resolve) 145 | .catch(reject); 146 | } 147 | else if (query_string.policy === CONST.POLICIES.MANUAL_EXPIRATION) 148 | { 149 | Promise.resolve(policies.detectManual(query_string, policy)) 150 | .then(resolve) 151 | .catch(reject); 152 | } 153 | else if (query_string.policy === CONST.POLICIES.TIME_BASED) 154 | { 155 | Promise.resolve(policies.detectTimeBased(query_string, policy)) 156 | .then(resolve) 157 | .catch(reject); 158 | } 159 | else if (query_string.policy === CONST.POLICIES.CASCADING) 160 | { 161 | Promise.resolve(policies.detectCascading(query_string, policy)) 162 | .then(resolve) 163 | .catch(reject); 164 | } 165 | else if (query_string.policy === CONST.POLICIES.BANDWIDTH_BASED) 166 | { 167 | Promise.resolve(policies.detectBandwithBased(query_string, policy)) 168 | .then(resolve) 169 | .catch(reject); 170 | } 171 | else 172 | { 173 | policy = undefined; 174 | 175 | resolve(policy); 176 | } 177 | } 178 | else 179 | { 180 | reject(); 181 | } 182 | }); 183 | } 184 | 185 | function createNewTicket_faster() 186 | { 187 | const ticket = (new Date()).getTime().toString(); 188 | return ticket; 189 | } 190 | 191 | function createNewTicket_fast() 192 | { 193 | const now = (new Date()).getTime(); 194 | 195 | const ticket = lighthash.encode(now); 196 | 197 | return ticket; 198 | } 199 | 200 | function createNewTicket_slow() 201 | { 202 | let now = (new Date()).getTime().toString(); 203 | 204 | now += Math.random(); 205 | 206 | const ticket = hash.sha1(now); 207 | 208 | return ticket; 209 | } 210 | 211 | function createNewTicket(speed) 212 | { 213 | if (speed === CONST.SPEED.FASTER) 214 | { 215 | return createNewTicket_faster(); 216 | } 217 | else if (speed === CONST.SPEED.FAST) 218 | { 219 | return createNewTicket_fast(); 220 | } 221 | else 222 | { 223 | return createNewTicket_slow(); 224 | } 225 | } 226 | 227 | function isAutorenewable(policy) 228 | { 229 | let result = false; 230 | 231 | if (policy) 232 | { 233 | result = (policy.autorenew === true || policy.autorenew === 'true'); 234 | } 235 | 236 | return result; 237 | } 238 | 239 | function handleTimeBasedTicketResponse(ticket_base, res, next) 240 | { 241 | client.ttl(CONST.VALID_PREFIX + ticket_base, (err, ttl) => 242 | { 243 | const reply = {'status': CONST.ERROR}; 244 | 245 | if (err) 246 | { 247 | reply.cause = err; 248 | 249 | res.send(500, reply); 250 | } 251 | else 252 | { 253 | reply.status = CONST.VALID_TICKET; 254 | reply.expires_in = ttl; 255 | reply.policy = CONST.POLICIES.TIME_BASED; 256 | 257 | res.send(reply); 258 | } 259 | 260 | next(); 261 | }); 262 | } 263 | 264 | function handleRequestsBasedTicketResponse(ticket_base, res, next, light) 265 | { 266 | if (ticket_base) 267 | { 268 | res = res || {send: function(){}}; 269 | next = next || function(){}; 270 | 271 | client.hget(CONST.VALID_PREFIX + ticket_base, 'policy', (err, policy_str) => 272 | { 273 | const reply = {'status': CONST.ERROR}; 274 | 275 | if (policy_str) 276 | { 277 | const policy = JSON.parse(policy_str); 278 | 279 | if (policy.requests_based) 280 | { 281 | const isLight = (light === true || light === 'true'); 282 | 283 | if (policy.expires_in === 0 && !isLight) 284 | { 285 | reply.status = CONST.EXPIRED_TICKET; 286 | 287 | res.send(reply); 288 | 289 | client.del(CONST.VALID_PREFIX + ticket_base); 290 | 291 | // Begin the expiration countdown for the "expired" counterpart: 292 | client.expire(CONST.EXPIRED_PREFIX + ticket_base, policy.remember_until); 293 | } 294 | else 295 | { 296 | if (!isLight) 297 | { 298 | policy.expires_in -= 1; 299 | 300 | client.hset(CONST.VALID_PREFIX + ticket_base, 'policy', JSON.stringify(policy)); 301 | } 302 | 303 | reply.status = CONST.VALID_TICKET; 304 | reply.expires_in = policy.expires_in; 305 | reply.policy = CONST.POLICIES.REQUESTS_BASED; 306 | 307 | if ( isAutorenewable(policy) 308 | && !isLight 309 | && (policy.expires_in === 0 || policy.expires_in === '0')) 310 | { 311 | // Create a new ticket and serve it alongside the other info 312 | const newTicket = createNewTicket(policy.generation_speed); 313 | const valid_ticket = CONST.VALID_PREFIX + newTicket; 314 | const expired_ticket = CONST.EXPIRED_PREFIX + newTicket; 315 | 316 | const newPolicy = policy; 317 | newPolicy.expires_in = newPolicy.original_expires_in = policy.original_expires_in; 318 | 319 | // First save the "next" ticket (named "valid_ticket"): 320 | client.hset(valid_ticket, 'content', CONST.VALID_TICKET); 321 | client.hset(valid_ticket, 'policy', JSON.stringify(newPolicy)); 322 | 323 | if (policy.payload) 324 | { 325 | client.hget(CONST.VALID_PREFIX + ticket_base, 'payload', (err, payload) => 326 | { 327 | if (payload) 328 | { 329 | client.hset(valid_ticket, 'payload', payload); 330 | } 331 | }); 332 | } 333 | 334 | 335 | // Then save its "to-be-expired" counterpart, but without the expiration date set on it: 336 | client.set(expired_ticket, CONST.EXPIRED_TICKET); 337 | 338 | reply.expires_in = 0; 339 | reply.next_ticket = newTicket; 340 | } 341 | 342 | 343 | res.send(reply); 344 | 345 | next(); 346 | } 347 | } 348 | else 349 | { 350 | reply.status = CONST.ERROR; 351 | reply.cause = CONST.ERRORS.DIFFERENT_POLICY; 352 | 353 | res.send(400, reply); 354 | 355 | next(); 356 | } 357 | } 358 | else 359 | { 360 | // Malformed ticket in the DB: delete 361 | client.del(CONST.VALID_PREFIX + ticket_base, err => 362 | { 363 | if (err) 364 | { 365 | reply.cause = err; 366 | } 367 | else 368 | { 369 | reply.cause = CONST.ERRORS.MALFORMED_TICKET; 370 | } 371 | 372 | res.send(500, reply); 373 | 374 | next(); 375 | }); 376 | } 377 | }); 378 | } 379 | else 380 | { 381 | throw new Error('Missing "ticket_base" parameter'); 382 | } 383 | } 384 | 385 | function handleManualTicketResponse(ticket_base, res, next) 386 | { 387 | const reply = {'status': CONST.VALID_TICKET, 'policy': CONST.POLICIES.MANUAL_EXPIRATION}; 388 | 389 | res.send(reply); 390 | 391 | next(); 392 | } 393 | 394 | function handleCascadingTicketResponse(ticket_base, res, next) 395 | { 396 | client.hget(CONST.VALID_PREFIX + ticket_base, 'policy', (err, policy_str) => 397 | { 398 | if (policy_str) 399 | { 400 | const policy = JSON.parse(policy_str); 401 | 402 | if (policy.cascading) 403 | { 404 | const dep_ticket = policy.depends_on; 405 | 406 | if (dep_ticket) 407 | { 408 | client.exists(CONST.VALID_PREFIX + dep_ticket, (err, exists) => 409 | { 410 | const reply = {'status': CONST.ERROR}; 411 | 412 | if (err) 413 | { 414 | reply.cause = err; 415 | 416 | res.send(reply); 417 | } 418 | else if (exists) 419 | { 420 | reply.status = CONST.VALID_TICKET; 421 | reply.policy = CONST.POLICIES.CASCADING; 422 | reply.depends_on = dep_ticket; 423 | 424 | res.send(reply); 425 | 426 | next(); 427 | } 428 | else 429 | { 430 | client.exists(CONST.EXPIRED_PREFIX + dep_ticket, (error2) => 431 | { 432 | if (error2) 433 | { 434 | reply.cause = error2; 435 | res.send(500, reply); 436 | } 437 | else 438 | { 439 | /* The ticket this one depends on has expired 440 | * since the last time we checked. 441 | * We must mark this one as expired too. */ 442 | 443 | // Early reply 444 | reply.status = CONST.EXPIRED_TICKET; 445 | 446 | res.send(reply); 447 | 448 | client.del(CONST.VALID_PREFIX + ticket_base); 449 | 450 | // Begin the expiration countdown for the "expired" counterpart: 451 | client.expire(CONST.EXPIRED_PREFIX + ticket_base, policy.remember_until); 452 | } 453 | 454 | next(); 455 | }); 456 | } 457 | }); 458 | } 459 | } 460 | } 461 | }); 462 | } 463 | 464 | function handleBandwidthTicketResponse(ticket_base, res, next, light) 465 | { 466 | if (ticket_base) 467 | { 468 | res = res || {send: function(){}}; 469 | next = next || function(){}; 470 | 471 | client.hget(CONST.VALID_PREFIX + ticket_base, 'policy', (err, policy_str) => 472 | { 473 | const reply = {'status': CONST.ERROR}; 474 | 475 | if (policy_str) 476 | { 477 | const policy = JSON.parse(policy_str); 478 | 479 | const isLight = light === true || light === 'true'; 480 | 481 | if (policy.bandwidth_based) 482 | { 483 | // Last time we checked this ticket with "/status": 484 | const last_check = policy.last_check; 485 | 486 | // Times the ticket has been checked in the last minute: 487 | let count = policy.checks_count; 488 | let now = (new Date()).getTime(); 489 | const timeDiff = now - last_check; 490 | 491 | 492 | if ( last_check 493 | && timeDiff < 60 * 1000 ) // 60 seconds 494 | { 495 | if (count < policy.expires_in || isLight) 496 | { 497 | if (!isLight) 498 | { 499 | count++; 500 | } 501 | 502 | reply.status = CONST.VALID_TICKET; 503 | reply.expires_in = policy.expires_in - count; 504 | reply.policy = CONST.POLICIES.BANDWIDTH_BASED; 505 | } 506 | else 507 | { 508 | // Since it's expired reset the counter: 509 | count = 0; 510 | 511 | reply.status = CONST.EXPIRED_TICKET; 512 | } 513 | } 514 | else 515 | { 516 | /* First time this ticket has been checked 517 | * or a minute from the last check has already passed */ 518 | 519 | /* Also, first ticket of the bandwidth-based policy 520 | * isn't considered for the "light" option. 521 | * Only the ones following it are. */ 522 | 523 | count = 1; 524 | 525 | reply.status = CONST.VALID_TICKET; 526 | reply.expires_in = policy.expires_in - count; 527 | reply.policy = CONST.POLICIES.BANDWIDTH_BASED; 528 | } 529 | 530 | res.send(reply); 531 | 532 | 533 | // Update the checks count and the check time on the ticket policy: 534 | policy.checks_count = count; 535 | policy.last_check = now; 536 | 537 | client.hset(CONST.VALID_PREFIX + ticket_base, 'policy', JSON.stringify(policy)); 538 | } 539 | else 540 | { 541 | reply.cause = CONST.ERRORS.DIFFERENT_POLICY; 542 | 543 | res.send(400, reply); 544 | } 545 | } 546 | else 547 | { 548 | // Malformed ticket in the DB: early-reply and delete 549 | reply.cause = CONST.ERRORS.MALFORMED_TICKET; 550 | 551 | res.send(500, reply); 552 | 553 | client.del(CONST.VALID_PREFIX + ticket_base, err => 554 | { 555 | global.log.error('Could not delete supposedly-malformed ticket "%s". Cause: %s', ticket_base, err); 556 | }); 557 | } 558 | 559 | next(); 560 | }); 561 | } 562 | else 563 | { 564 | throw new Error('Missing "ticket_base" parameter'); 565 | } 566 | } 567 | 568 | function addToContextMap(context, ticket) 569 | { 570 | if (context && ticket) 571 | { 572 | context = CONST.CONTEXTS_PREFIX + context; 573 | 574 | client.lpush(context, ticket, err => 575 | { 576 | if (err) 577 | { 578 | global.log.error('Could not save "%s" to context "%s"', ticket, context); 579 | } 580 | }); 581 | } 582 | } 583 | 584 | function getPolicyString(policy) 585 | { 586 | let result; 587 | 588 | if (policy.requests_based) 589 | { 590 | result = CONST.POLICIES.REQUESTS_BASED; 591 | } 592 | else if (policy.manual_expiration) 593 | { 594 | result = CONST.POLICIES.MANUAL_EXPIRATION; 595 | } 596 | else if (policy.time_based) 597 | { 598 | result = CONST.POLICIES.TIME_BASED; 599 | } 600 | else if (policy.cascading) 601 | { 602 | result = CONST.POLICIES.CASCADING; 603 | } 604 | else if (policy.bandwidth_based) 605 | { 606 | result = CONST.POLICIES.BANDWIDTH_BASED; 607 | } 608 | 609 | return result; 610 | } 611 | 612 | 613 | function checkExpired(ticket, res, next, reply) 614 | { 615 | if (ticket && res && next && reply) 616 | { 617 | client.exists(CONST.EXPIRED_PREFIX + ticket, (err, expired) => 618 | { 619 | global.log.debug('[checkExpired] expired returned: %s', expired); 620 | global.log.debug('[checkExpired] error was: %s', err); 621 | 622 | if (expired) 623 | { 624 | reply.status = CONST.EXPIRED_TICKET; 625 | 626 | res.send(reply); 627 | 628 | next(); 629 | } 630 | else 631 | { 632 | reply.cause = CONST.ERRORS.NOT_FOUND; 633 | 634 | res.send(404, reply); 635 | 636 | next(); 637 | } 638 | }); 639 | } 640 | } 641 | 642 | 643 | exports.new = function(req, res, next) 644 | { 645 | const reply = {'result': CONST.NOT_OK}; 646 | 647 | calculateExpirationPolicy(req) 648 | .then( policy => 649 | { 650 | let count = 1; 651 | 652 | if (req.query.count) 653 | { 654 | count = req.query.count; 655 | } 656 | 657 | if (count > CONST.MAX_TICKETS_PER_TIME) 658 | { 659 | reply.cause = CONST.ERRORS.TOO_MUCH_TICKETS; 660 | reply.message = 'Try lowering your "count" request to <' + CONST.MAX_TICKETS_PER_TIME; 661 | 662 | res.send(400, reply); 663 | } 664 | else 665 | { 666 | const tickets = []; 667 | 668 | reply.result = CONST.OK; 669 | reply.expires_in = policy.expires_in; 670 | 671 | for (let a=0; a 1) 904 | { 905 | if (reply.result !== CONST.NOT_OK) 906 | { 907 | reply.tickets = tickets; 908 | } 909 | 910 | res.send(reply); 911 | } 912 | 913 | next(); 914 | } 915 | }) 916 | .catch( () => 917 | { 918 | // Return an error: 919 | reply.cause = CONST.ERRORS.WRONG_POLICY; 920 | 921 | res.send(400, reply); 922 | 923 | next(); 924 | }); 925 | }; 926 | 927 | exports.status = function(req, res, next) 928 | { 929 | const reply = {'status': CONST.ERROR}; 930 | 931 | const ticket_base = req.params.ticket; 932 | 933 | if (ticket_base) 934 | { 935 | global.log.debug('[tickets.status] asking status of ticket "%s"...', ticket_base); 936 | 937 | client.exists(CONST.VALID_PREFIX + ticket_base, (err, exists) => 938 | { 939 | global.log.debug('[tickets.status] exists returned: %s', exists); 940 | global.log.debug('[tickets.status] error was: %s', err); 941 | 942 | if (exists) 943 | { 944 | client.hget(CONST.VALID_PREFIX + ticket_base, 'policy', (err, policy_str) => 945 | { 946 | if (policy_str) 947 | { 948 | global.log.debug('[tickets.status] policy string is %s', policy_str); 949 | 950 | const policy = JSON.parse(policy_str); 951 | 952 | let can_go_on = true; 953 | 954 | // If the ticket was created with a context check it: 955 | if ( policy.context 956 | && req.query.context !== policy.context) 957 | { 958 | can_go_on = false; 959 | } 960 | 961 | if (can_go_on) 962 | { 963 | if (policy.time_based) 964 | { 965 | handleTimeBasedTicketResponse(ticket_base, res, next); 966 | } 967 | else if (policy.requests_based) 968 | { 969 | handleRequestsBasedTicketResponse(ticket_base, res, next, req.query.light); 970 | } 971 | else if (policy.manual_expiration) 972 | { 973 | handleManualTicketResponse(ticket_base, res, next); 974 | } 975 | else if (policy.cascading) 976 | { 977 | handleCascadingTicketResponse(ticket_base, res, next); 978 | } 979 | else if (policy.bandwidth_based) 980 | { 981 | handleBandwidthTicketResponse(ticket_base, res, next, req.query.light); 982 | } 983 | 984 | 985 | // Additionally, rewrite the expire counterpart of the ticket everytime we find a valid one [to avoid issue #8] 986 | client.set(CONST.EXPIRED_PREFIX + ticket_base, CONST.EXPIRED_TICKET); 987 | } 988 | else 989 | { 990 | reply.cause = CONST.ERRORS.NOT_FOUND; 991 | 992 | res.send(404, reply); 993 | 994 | next(); 995 | } 996 | } 997 | else 998 | { 999 | // Malformed ticket in the DB: early-reply and delete 1000 | reply.cause = CONST.ERRORS.MALFORMED_TICKET; 1001 | 1002 | res.send(500, reply); 1003 | 1004 | next(); 1005 | 1006 | client.del(CONST.VALID_PREFIX + ticket_base, err => 1007 | { 1008 | global.log.error('Could not delete supposedly-malformed ticket "%s". Cause: %s', ticket_base, err); 1009 | }); 1010 | } 1011 | }); 1012 | } 1013 | else 1014 | { 1015 | // Check whether it expired: 1016 | checkExpired(ticket_base, res, next, reply); 1017 | } 1018 | }); 1019 | } 1020 | else 1021 | { 1022 | reply.cause = CONST.ERRORS.EMPTY_REQUEST; 1023 | 1024 | res.send(400, reply); 1025 | 1026 | next(); 1027 | } 1028 | }; 1029 | 1030 | exports.policy = function(req, res, next) 1031 | { 1032 | const reply = {'status': CONST.ERROR}; 1033 | 1034 | const ticket_base = req.params.ticket; 1035 | 1036 | if (ticket_base) 1037 | { 1038 | client.exists(CONST.VALID_PREFIX + ticket_base, (err, exists) => 1039 | { 1040 | if (exists) 1041 | { 1042 | client.hget(CONST.VALID_PREFIX + ticket_base, 'policy', (error, policy_str) => 1043 | { 1044 | if (policy_str) 1045 | { 1046 | const policy = JSON.parse(policy_str); 1047 | 1048 | delete reply.status; 1049 | 1050 | reply.policy = getPolicyString(policy); 1051 | 1052 | reply.more = {}; 1053 | reply.more.context = policy.context; 1054 | reply.more.autorenew = policy.autorenew; 1055 | reply.more.depends_on = policy.depends_on; 1056 | reply.more.generation_speed = policy.generation_speed; 1057 | reply.more.can_force_expiration = policy.can_force_expiration; 1058 | 1059 | res.send(200, reply); 1060 | 1061 | next(); 1062 | } 1063 | else 1064 | { 1065 | // Malformed ticket in the DB: early-reply and delete 1066 | reply.cause = CONST.ERRORS.MALFORMED_TICKET; 1067 | 1068 | res.send(500, reply); 1069 | 1070 | next(); 1071 | 1072 | client.del(CONST.VALID_PREFIX + ticket_base, err => 1073 | { 1074 | global.log.error('Could not delete supposedly-malformed ticket "%s". Cause: %s', ticket_base, err); 1075 | }); 1076 | } 1077 | }); 1078 | } 1079 | else 1080 | { 1081 | // Check whether it expired: 1082 | checkExpired(ticket_base, res, next, reply); 1083 | } 1084 | }); 1085 | } 1086 | else 1087 | { 1088 | reply.cause = CONST.ERRORS.EMPTY_REQUEST; 1089 | 1090 | res.send(400, reply); 1091 | 1092 | next(); 1093 | } 1094 | }; 1095 | 1096 | exports.expire = function(req, res, next) 1097 | { 1098 | const reply = {'status': CONST.ERROR}; 1099 | 1100 | const ticket_base = req.params.ticket; 1101 | 1102 | if (ticket_base) 1103 | { 1104 | client.exists(CONST.VALID_PREFIX + ticket_base, (err, exists) => 1105 | { 1106 | if (exists) 1107 | { 1108 | client.hget(CONST.VALID_PREFIX + ticket_base, 'policy', (error, policy_str) => 1109 | { 1110 | if (policy_str) 1111 | { 1112 | const policy = JSON.parse(policy_str); 1113 | 1114 | if ( policy.manual_expiration === true 1115 | || policy.can_force_expiration === true) 1116 | { 1117 | reply.status = CONST.EXPIRED_TICKET; 1118 | 1119 | res.send(reply); 1120 | 1121 | // Set expiration on the "expired" counterpart when manually expiring: 1122 | client.expire(CONST.EXPIRED_PREFIX + ticket_base, policy.remember_until); 1123 | 1124 | // Finally delete valid ticket 1125 | client.del(CONST.VALID_PREFIX + ticket_base); 1126 | } 1127 | else 1128 | { 1129 | reply.cause = CONST.ERRORS.DIFFERENT_POLICY; 1130 | 1131 | res.send(400, reply); 1132 | 1133 | next(); 1134 | } 1135 | } 1136 | else 1137 | { 1138 | // Malformed ticket in the DB: early-reply and delete 1139 | reply.cause = CONST.ERRORS.MALFORMED_TICKET; 1140 | 1141 | res.send(500, reply); 1142 | 1143 | next(); 1144 | 1145 | client.del(CONST.VALID_PREFIX + ticket_base, err => 1146 | { 1147 | global.log.error('Could not delete supposedly-malformed ticket "%s". Cause: %s', ticket_base, err); 1148 | }); 1149 | } 1150 | }); 1151 | } 1152 | else 1153 | { 1154 | // Check whether it expired: 1155 | checkExpired(ticket_base, res, next, reply); 1156 | } 1157 | }); 1158 | } 1159 | else 1160 | { 1161 | reply.cause = CONST.ERRORS.EMPTY_REQUEST; 1162 | 1163 | res.send(400, reply); 1164 | 1165 | next(); 1166 | } 1167 | }; 1168 | 1169 | exports.withpayload = function(req, res, next) 1170 | { 1171 | // Let's keep count to 1: 1172 | req.query.count = 1; 1173 | 1174 | // Then redirect to 'new()': 1175 | return exports.new(req, res, next); 1176 | }; 1177 | 1178 | exports.payload = function(req, res, next) 1179 | { 1180 | const reply = {'status': CONST.ERROR}; 1181 | 1182 | const ticket_base = req.params.ticket; 1183 | 1184 | if (ticket_base) 1185 | { 1186 | global.log.debug('[tickets.payload] asking status of ticket "%s"...', ticket_base); 1187 | 1188 | client.exists(CONST.VALID_PREFIX + ticket_base, (err, exists) => 1189 | { 1190 | global.log.debug('[tickets.payload] exists returned: %s', exists); 1191 | global.log.debug('[tickets.payload] error was: %s', err); 1192 | 1193 | if (err) 1194 | { 1195 | global.log.error('Could not search for ticket "%s". %s', ticket_base, err); 1196 | 1197 | reply.cause = CONST.ERRORS.PAYLOAD_NOT_FOUND; 1198 | 1199 | res.send(404, reply); 1200 | 1201 | next(); 1202 | } 1203 | else if (exists) 1204 | { 1205 | client.hgetall(CONST.VALID_PREFIX + ticket_base, (err, metadata) => 1206 | { 1207 | if (err) 1208 | { 1209 | global.log.error('Could not load payload for ticket "%s". %s', ticket_base, err); 1210 | 1211 | reply.cause = CONST.ERRORS.PAYLOAD_NOT_FOUND; 1212 | 1213 | res.send(404, reply); 1214 | 1215 | next(); 1216 | } 1217 | else 1218 | { 1219 | const payload = metadata.payload; 1220 | 1221 | let result = {}; 1222 | 1223 | if (payload) 1224 | { 1225 | result = JSON.parse(payload); 1226 | } 1227 | 1228 | res.send(result); 1229 | 1230 | // Now decrease the ticket counters (if bandwidth- or requests-based): 1231 | metadata.policy = JSON.parse(metadata.policy); 1232 | 1233 | if (metadata.policy.requests_based) 1234 | { 1235 | handleRequestsBasedTicketResponse(ticket_base, null, next, req.query.light); 1236 | 1237 | // TODO: Payloads can't be used for auto-renewables... 1238 | } 1239 | else if (metadata.policy.bandwidth_based) 1240 | { 1241 | handleBandwidthTicketResponse(ticket_base, null, next, req.query.light); 1242 | } 1243 | } 1244 | }); 1245 | } 1246 | else 1247 | { 1248 | // Check whether it expired: 1249 | checkExpired(ticket_base, res, next, reply); 1250 | } 1251 | }); 1252 | } 1253 | else 1254 | { 1255 | reply.cause = CONST.ERRORS.EMPTY_REQUEST; 1256 | 1257 | res.send(400, reply); 1258 | 1259 | next(); 1260 | } 1261 | }; 1262 | 1263 | exports.cors = function(req, res, next) 1264 | { 1265 | res.header("Access-Control-Allow-Headers", restify.CORS.ALLOW_HEADERS.join(", ")); 1266 | res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); 1267 | res.header("Access-Control-Allow-Origin", req.headers.origin); 1268 | res.header("Access-Control-Max-Age", 0); 1269 | res.header("Content-type", "text/plain charset=UTF-8"); 1270 | res.header("Content-length", 0); 1271 | 1272 | res.send(204); 1273 | 1274 | return next(); 1275 | }; 1276 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const toobusy = require('toobusy-js'); 4 | const restify = require('restify'); 5 | const CONST = require('./const'); 6 | 7 | 8 | const answer = 9 | { 10 | 'status': CONST.ERROR, 11 | 'cause' : CONST.ERRORS.MALFORMED_REQUEST 12 | }; 13 | 14 | 15 | module.exports = 16 | { 17 | notpermitted: function(req, res, err) 18 | { 19 | global.log.error('Got an error for URL "%s": %s', req.url, err); 20 | 21 | if (err) 22 | { 23 | global.log.error(err.stack); 24 | } 25 | 26 | res.send(400, answer); 27 | }, 28 | 29 | toobusy: function(req, res, next) 30 | { 31 | if (toobusy()) 32 | { 33 | next(new restify.ServiceUnavailableError('The server is too busy right now')); 34 | } 35 | else 36 | { 37 | next(); 38 | } 39 | }, 40 | 41 | status: function(req, res, next) 42 | { 43 | // Get info about this process memory usage: 44 | const usage = process.memoryUsage(); 45 | 46 | // Use human-readable memory sizes: 47 | usage.rss = '~' + parseInt(usage.rss / 1024 / 1024) + 'MB'; 48 | usage.heapTotal = '~' + parseInt(usage.heapTotal / 1024 / 1024) + 'MB'; 49 | usage.heapUsed = '~' + parseInt(usage.heapUsed / 1024 / 1024) + 'MB'; 50 | 51 | // Get up-time of this process: 52 | const uptime = process.uptime(); 53 | 54 | // Get info about NodeJS version: 55 | const nodeVersion = process.version; 56 | 57 | // Then return 200 if everything is OK: 58 | res.send({status: CONST.OK, memory: usage, uptime: uptime, 'node-version': nodeVersion}); 59 | 60 | next(); 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bagarino", 3 | "version": "2.9.2", 4 | "license": "apache-2.0", 5 | "private": false, 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/NicolaOrritos/bagarino" 9 | }, 10 | "bin": { 11 | "bagarino": "bin/start-bagarino-daemon" 12 | }, 13 | "files": [ 14 | "bin", 15 | "etc", 16 | "lib", 17 | "app.js" 18 | ], 19 | "engines": { 20 | "node": ">=6.9.2" 21 | }, 22 | "dependencies": { 23 | "docopt": "0.6.2", 24 | "hashids": "1.1.4", 25 | "hiredis": "0.5.0", 26 | "log": "1.4.0", 27 | "node_hash": "0.2.0", 28 | "probiotic": "1.0.3", 29 | "redis": "2.8.0", 30 | "restify": "7.2.0", 31 | "restify-cors-middleware": "1.1.0", 32 | "sjl": "0.4.0", 33 | "toobusy-js": "0.5.1" 34 | }, 35 | "devDependencies": { 36 | "grunt": "0.4.5", 37 | "grunt-contrib-jshint": "0.12.0", 38 | "grunt-contrib-nodeunit": "0.4.1", 39 | "grunt-contrib-watch": "0.6.1", 40 | "grunt-develop": "0.4.0", 41 | "grunt-plato": "1.4.0", 42 | "jshint-stylish": "2.2.1", 43 | "load-grunt-tasks": "3.5.2", 44 | "request": "2.85.0", 45 | "sleep": "5.1.1", 46 | "time-grunt": "1.4.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /private/PUTHEREYOURSSLKEYS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NicolaOrritos/bagarino/96ffb8a9e7a47e8f3187784d5f4da8d76f35dc21/private/PUTHEREYOURSSLKEYS -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | 6 | "packageRules": [ 7 | { 8 | "excludePackageNames": ["sjl"] 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /test/Tickets Tests.jmx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | false 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | continue 16 | 17 | false 18 | 1 19 | 20 | 6000 21 | 1 22 | 1377468520000 23 | 1377468520000 24 | false 25 | 26 | 27 | 28 | 29 | 30 | 60 31 | 20.0 32 | 33 | 34 | 35 | 36 | 37 | 38 | 10.67.68.133 39 | 8124 40 | 41 | 42 | 43 | 44 | 45 | 4 46 | 47 | 48 | 49 | 50 | 51 | 52 | false 53 | requests_based 54 | = 55 | true 56 | policy 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | tickets/new 67 | GET 68 | true 69 | false 70 | true 71 | false 72 | false 73 | 74 | 75 | 76 | 77 | false 78 | 79 | saveConfig 80 | 81 | 82 | true 83 | true 84 | true 85 | 86 | true 87 | true 88 | true 89 | true 90 | false 91 | true 92 | true 93 | false 94 | false 95 | false 96 | false 97 | false 98 | false 99 | false 100 | false 101 | 0 102 | true 103 | 104 | 105 | 106 | 107 | 108 | 109 | false 110 | 111 | saveConfig 112 | 113 | 114 | true 115 | true 116 | true 117 | 118 | true 119 | true 120 | true 121 | true 122 | false 123 | true 124 | true 125 | false 126 | false 127 | false 128 | false 129 | false 130 | false 131 | false 132 | false 133 | 0 134 | true 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /test/contexts_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('request'); 4 | const CONST = require('../lib/const'); 5 | 6 | /* 7 | ======== A Handy Little Nodeunit Reference ======== 8 | https://github.com/caolan/nodeunit 9 | 10 | Test methods: 11 | test.expect(numAssertions) 12 | test.done() 13 | Test assertions: 14 | test.ok(value, [message]) 15 | test.equal(actual, expected, [message]) 16 | test.notEqual(actual, expected, [message]) 17 | test.deepEqual(actual, expected, [message]) 18 | test.notDeepEqual(actual, expected, [message]) 19 | test.strictEqual(actual, expected, [message]) 20 | test.notStrictEqual(actual, expected, [message]) 21 | test.throws(block, [error], [message]) 22 | test.doesNotThrow(block, [error], [message]) 23 | test.ifError(value) 24 | */ 25 | 26 | const existingContext = 'thisisacontext'; 27 | 28 | exports.read = 29 | { 30 | setUp: function(done) 31 | { 32 | request.get('http://localhost:8124/tickets/new?policy=manual_expiration&context=' + existingContext, err => 33 | { 34 | if (err) 35 | { 36 | console.log('Error setting the test up: %s', err); 37 | } 38 | 39 | done(); 40 | }); 41 | }, 42 | 43 | tearDown: function(done) 44 | { 45 | // Cleanup the context: 46 | request.get('http://localhost:8124/contexts/' + existingContext + '/expireall', err => 47 | { 48 | if (err) 49 | { 50 | console.log('Error tearing the test down: %s', err); 51 | } 52 | 53 | done(); 54 | }); 55 | }, 56 | 57 | 'Contexts wrong routes': function(test) 58 | { 59 | test.expect(8); 60 | 61 | const context = existingContext; 62 | 63 | request.get('http://localhost:8124/contexts/' + context, (err, res) => 64 | { 65 | test.ifError(err); 66 | 67 | test.equal(res.statusCode, 400); 68 | 69 | let result = JSON.parse(res.body); 70 | 71 | test.equal(result.status, CONST.ERROR); 72 | test.equal(result.cause, CONST.ERRORS.MALFORMED_REQUEST); 73 | 74 | 75 | request.get('http://localhost:8124/contexts/' + context + '/wrongoperation', (err2, res2) => 76 | { 77 | test.ifError(err2); 78 | test.equal(res2.statusCode, 400); 79 | 80 | result = JSON.parse(res2.body); 81 | 82 | test.equal(result.status, CONST.ERROR); 83 | test.equal(result.cause, CONST.ERRORS.MALFORMED_REQUEST); 84 | 85 | test.done(); 86 | }); 87 | }); 88 | }, 89 | 90 | 'Contexts route usage': function(test) 91 | { 92 | test.expect(14); 93 | 94 | const context = "nonexistentcontext"; 95 | 96 | request.get('http://localhost:8124/contexts/' + context + '/expireall', (err, res) => 97 | { 98 | test.ifError(err); 99 | 100 | test.equal(res.statusCode, 404); 101 | 102 | let result = JSON.parse(res.body); 103 | 104 | test.equal(result.status, CONST.NOT_OK); 105 | test.equal(result.cause, CONST.ERRORS.CONTEXT_NOT_FOUND); 106 | 107 | 108 | request.get('http://localhost:8124/contexts/expireall', (err2, res2) => 109 | { 110 | test.ifError(err2); 111 | test.equal(res2.statusCode, 400); 112 | 113 | request.get('http://localhost:8124/contexts/' + existingContext + '/expireall', (err3, res3) => 114 | { 115 | test.ifError(err3); 116 | 117 | test.equal(res3.statusCode, 200); 118 | 119 | result = JSON.parse(res3.body); 120 | 121 | test.strictEqual(result.expired, 1); 122 | 123 | 124 | request.get('http://localhost:8124/tickets/new?policy=manual_expiration&context=' + existingContext, err4 => 125 | { 126 | test.ifError(err4); 127 | 128 | request.get('http://localhost:8124/tickets/new?policy=manual_expiration&context=' + existingContext, err5 => 129 | { 130 | test.ifError(err5); 131 | 132 | request.get('http://localhost:8124/contexts/' + existingContext + '/expireall', (err6, res6) => 133 | { 134 | test.ifError(err6); 135 | 136 | test.equal(res6.statusCode, 200); 137 | 138 | result = JSON.parse(res6.body); 139 | 140 | test.strictEqual(result.expired, 2); 141 | 142 | 143 | test.done(); 144 | }); 145 | }); 146 | }); 147 | }); 148 | }); 149 | }); 150 | } 151 | }; 152 | -------------------------------------------------------------------------------- /test/cors_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('request'); 4 | 5 | /* 6 | ======== A Handy Little Nodeunit Reference ======== 7 | https://github.com/caolan/nodeunit 8 | 9 | Test methods: 10 | test.expect(numAssertions) 11 | test.done() 12 | Test assertions: 13 | test.ok(value, [message]) 14 | test.equal(actual, expected, [message]) 15 | test.notEqual(actual, expected, [message]) 16 | test.deepEqual(actual, expected, [message]) 17 | test.notDeepEqual(actual, expected, [message]) 18 | test.strictEqual(actual, expected, [message]) 19 | test.notStrictEqual(actual, expected, [message]) 20 | test.throws(block, [error], [message]) 21 | test.doesNotThrow(block, [error], [message]) 22 | test.ifError(value) 23 | */ 24 | 25 | exports.read = 26 | { 27 | 'OPTIONS call': function(test) 28 | { 29 | test.expect(2); 30 | 31 | request({url: 'http://localhost:8124/', method: 'OPTIONS', headers: {'Origin': 'localhost'}}, 32 | (err, res) => 33 | { 34 | test.ifError(err); 35 | 36 | test.equal(res.statusCode, 204); 37 | 38 | let headers = res.headers; 39 | 40 | console.log('Headers received: %s', headers); 41 | 42 | 43 | test.done(); 44 | }); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /test/payloads_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('request'); 4 | const CONST = require('../lib/const'); 5 | 6 | /* 7 | ======== A Handy Little Nodeunit Reference ======== 8 | https://github.com/caolan/nodeunit 9 | 10 | Test methods: 11 | test.expect(numAssertions) 12 | test.done() 13 | Test assertions: 14 | test.ok(value, [message]) 15 | test.equal(actual, expected, [message]) 16 | test.notEqual(actual, expected, [message]) 17 | test.deepEqual(actual, expected, [message]) 18 | test.notDeepEqual(actual, expected, [message]) 19 | test.strictEqual(actual, expected, [message]) 20 | test.notStrictEqual(actual, expected, [message]) 21 | test.throws(block, [error], [message]) 22 | test.doesNotThrow(block, [error], [message]) 23 | test.ifError(value) 24 | */ 25 | 26 | 27 | exports.read = 28 | { 29 | setUp: function(done) 30 | { 31 | done(); 32 | }, 33 | 34 | tearDown: function(done) 35 | { 36 | done(); 37 | }, 38 | 39 | 'Tickets with payloads wrong routes': test => 40 | { 41 | test.expect(4); 42 | 43 | request.get('http://localhost:8124/tickets/new/withpayload', (err, res) => 44 | { 45 | test.ifError(err); 46 | test.equal(res.statusCode, 400); 47 | 48 | request.post('http://localhost:8124/tickets/new', (err2, res2) => 49 | { 50 | test.ifError(err2); 51 | test.equal(res2.statusCode, 400); 52 | 53 | test.done(); 54 | }); 55 | }); 56 | }, 57 | 58 | 'Tickets with payloads - creation and payload retrieval - manual_expiration': test => 59 | { 60 | test.expect(15); 61 | 62 | const payload = {'key': 'value'}; 63 | 64 | request.post('http://localhost:8124/tickets/new/withpayload?policy=manual_expiration&can_force_expiration=true', 65 | {body: payload, json: true}, 66 | (err, res) => 67 | { 68 | test.ifError(err); 69 | test.equal(res.statusCode, 200); 70 | 71 | let result = res.body; 72 | 73 | test.equal(result.result, CONST.OK); 74 | test.equal(result.policy, 'manual_expiration'); 75 | test.ok(result.ticket); 76 | 77 | const ticket = result.ticket; 78 | 79 | request.get('http://localhost:8124/tickets/' + ticket + '/payload', 80 | (err2, res2) => 81 | { 82 | test.ifError(err2); 83 | test.equal(res2.statusCode, 200); 84 | 85 | result = JSON.parse(res2.body); 86 | 87 | test.ok(result); 88 | test.equal(result.key, 'value'); 89 | 90 | request.get('http://localhost:8124/tickets/' + ticket + '/expire', (err) => 91 | { 92 | test.ifError(err); 93 | 94 | request.get('http://localhost:8124/tickets/' + ticket + '/payload', 95 | (err, res) => 96 | { 97 | test.ifError(err); 98 | test.ok(res); 99 | test.equal(res.statusCode, 200); 100 | 101 | result = JSON.parse(res.body); 102 | 103 | test.ok(result); 104 | test.equal(result.status, CONST.EXPIRED_TICKET); 105 | 106 | 107 | test.done(); 108 | }); 109 | }); 110 | }); 111 | }); 112 | }, 113 | 114 | 'Tickets with payloads - auto-renewables - payload "migration"': test => 115 | { 116 | test.expect(18); 117 | 118 | const payload = {'key': 'value'}; 119 | 120 | request.post('http://localhost:8124/tickets/new/withpayload?policy=requests_based&requests=1&autorenew=true', 121 | {body: payload, json: true}, 122 | (err, res) => 123 | { 124 | test.ifError(err); 125 | test.equal(res.statusCode, 200); 126 | 127 | const result = res.body; 128 | 129 | test.equal(result.result, CONST.OK); 130 | test.equal(result.policy, 'requests_based'); 131 | test.ok(result.ticket); 132 | 133 | let ticket = result.ticket; 134 | 135 | request.get('http://localhost:8124/tickets/' + ticket + '/payload', 136 | (err, res) => 137 | { 138 | test.ifError(err); 139 | test.equal(res.statusCode, 200); 140 | 141 | const result = JSON.parse(res.body); 142 | 143 | test.ok(result); 144 | test.equal(result.key, 'value'); 145 | 146 | request.get('http://localhost:8124/tickets/' + ticket + '/status', 147 | (err, res) => 148 | { 149 | test.ifError(err); 150 | test.equal(res.statusCode, 200); 151 | 152 | const result = JSON.parse(res.body); 153 | 154 | test.ok(result); 155 | 156 | test.deepEqual(result.expires_in, 0); 157 | test.ok(result.next_ticket); 158 | 159 | // The autorenewed new ticket: 160 | ticket = result.next_ticket; 161 | 162 | request.get('http://localhost:8124/tickets/' + ticket + '/payload', 163 | (err, res) => 164 | { 165 | test.ifError(err); 166 | test.equal(res.statusCode, 200); 167 | 168 | const result = JSON.parse(res.body); 169 | 170 | test.ok(result); 171 | test.equal(result.key, 'value'); 172 | 173 | 174 | test.done(); 175 | }); 176 | }); 177 | }); 178 | }); 179 | }, 180 | 181 | 'Tickets with payloads - auto-renewables - payload affecting the number of requests': test => 182 | { 183 | test.expect(18); 184 | 185 | const payload = {'key': 'value'}; 186 | 187 | request.post('http://localhost:8124/tickets/new/withpayload?policy=requests_based&requests=2', 188 | {body: payload, json: true}, 189 | (err, res) => 190 | { 191 | test.ifError(err); 192 | test.equal(res.statusCode, 200); 193 | 194 | const result = res.body; 195 | 196 | test.equal(result.result, CONST.OK); 197 | test.equal(result.policy, 'requests_based'); 198 | test.ok(result.ticket); 199 | 200 | let ticket = result.ticket; 201 | 202 | request.get('http://localhost:8124/tickets/' + ticket + '/payload', 203 | (err, res) => 204 | { 205 | test.ifError(err); 206 | test.equal(res.statusCode, 200); 207 | 208 | const result = JSON.parse(res.body); 209 | 210 | test.ok(result); 211 | test.equal(result.key, 'value'); 212 | 213 | 214 | request.get('http://localhost:8124/tickets/' + ticket + '/status', 215 | (err, res) => 216 | { 217 | test.ifError(err); 218 | test.equal(res.statusCode, 200); 219 | 220 | const result = JSON.parse(res.body); 221 | 222 | test.ok(result); 223 | 224 | test.deepEqual(result.status, CONST.VALID_TICKET); 225 | test.deepEqual(result.expires_in, 0); 226 | 227 | 228 | request.get('http://localhost:8124/tickets/' + ticket + '/status', 229 | (err, res) => 230 | { 231 | test.ifError(err); 232 | test.equal(res.statusCode, 200); 233 | 234 | const result = JSON.parse(res.body); 235 | 236 | test.ok(result); 237 | test.equal(result.status, CONST.EXPIRED_TICKET); 238 | 239 | 240 | test.done(); 241 | }); 242 | }); 243 | }); 244 | }); 245 | } 246 | }; 247 | -------------------------------------------------------------------------------- /test/status_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('request'); 4 | const CONST = require('../lib/const'); 5 | 6 | /* 7 | ======== A Handy Little Nodeunit Reference ======== 8 | https://github.com/caolan/nodeunit 9 | 10 | Test methods: 11 | test.expect(numAssertions) 12 | test.done() 13 | Test assertions: 14 | test.ok(value, [message]) 15 | test.equal(actual, expected, [message]) 16 | test.notEqual(actual, expected, [message]) 17 | test.deepEqual(actual, expected, [message]) 18 | test.notDeepEqual(actual, expected, [message]) 19 | test.strictEqual(actual, expected, [message]) 20 | test.notStrictEqual(actual, expected, [message]) 21 | test.throws(block, [error], [message]) 22 | test.doesNotThrow(block, [error], [message]) 23 | test.ifError(value) 24 | */ 25 | 26 | exports.read = 27 | { 28 | 'Status call answering OK': function(test) 29 | { 30 | test.expect(9); 31 | 32 | request.get('http://localhost:8124/status', (err, res) => 33 | { 34 | test.ifError(err); 35 | 36 | test.equal(res.statusCode, 200); 37 | 38 | let result = JSON.parse(res.body); 39 | 40 | test.equal(result.status, CONST.OK); 41 | 42 | test.ok(result.memory); 43 | test.ok(result.memory.rss); 44 | test.ok(result.memory.heapTotal); 45 | test.ok(result.memory.heapUsed); 46 | 47 | test.ok(!isNaN(result.uptime)); 48 | 49 | test.ok(result['node-version']); 50 | 51 | 52 | test.done(); 53 | }); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /test/tickets_1_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('request'); 4 | const CONST = require('../lib/const'); 5 | 6 | /* 7 | ======== A Handy Little Nodeunit Reference ======== 8 | https://github.com/caolan/nodeunit 9 | 10 | Test methods: 11 | test.expect(numAssertions) 12 | test.done() 13 | Test assertions: 14 | test.ok(value, [message]) 15 | test.equal(actual, expected, [message]) 16 | test.notEqual(actual, expected, [message]) 17 | test.deepEqual(actual, expected, [message]) 18 | test.notDeepEqual(actual, expected, [message]) 19 | test.strictEqual(actual, expected, [message]) 20 | test.notStrictEqual(actual, expected, [message]) 21 | test.throws(block, [error], [message]) 22 | test.doesNotThrow(block, [error], [message]) 23 | test.ifError(value) 24 | */ 25 | 26 | exports.read = 27 | { 28 | setUp: function(done) 29 | { 30 | done(); 31 | }, 32 | 33 | 'Tickets route - Part 1': function(test) 34 | { 35 | test.expect(15); 36 | 37 | request.get('http://localhost:8124/tickets/new?policy=manual_expiration', (err, res) => 38 | { 39 | test.ifError(err); 40 | test.equal(res.statusCode, 200); 41 | 42 | let result = JSON.parse(res.body); 43 | 44 | test.equal(result.result, CONST.OK); 45 | 46 | 47 | request.get('http://localhost:8124/tickets/' + result.ticket + '/expire', (err2, res2) => 48 | { 49 | test.ifError(err2); 50 | test.equal(res2.statusCode, 200); 51 | 52 | result = JSON.parse(res2.body); 53 | 54 | test.equal(result.status, CONST.EXPIRED_TICKET); 55 | 56 | 57 | request.get('http://localhost:8124/tickets/new?policy=requests_based&requests=1', (err3, res3) => 58 | { 59 | test.ifError(err3); 60 | test.equal(res3.statusCode, 200); 61 | 62 | result = JSON.parse(res3.body); 63 | 64 | test.equal(result.result, CONST.OK); 65 | test.equal(result.expires_in, 1); 66 | test.ok(result.ticket); 67 | 68 | 69 | request.get('http://localhost:8124/tickets/' + result.ticket + '/status', (err4, res4) => 70 | { 71 | test.ifError(err4); 72 | test.equal(res4.statusCode, 200); 73 | 74 | result = JSON.parse(res4.body); 75 | 76 | test.equal(result.status, CONST.VALID_TICKET); 77 | test.deepEqual(result.expires_in, 0); 78 | 79 | test.done(); 80 | }); 81 | }); 82 | }); 83 | }); 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /test/tickets_2_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('request'); 4 | const CONST = require('../lib/const'); 5 | 6 | /* 7 | ======== A Handy Little Nodeunit Reference ======== 8 | https://github.com/caolan/nodeunit 9 | 10 | Test methods: 11 | test.expect(numAssertions) 12 | test.done() 13 | Test assertions: 14 | test.ok(value, [message]) 15 | test.equal(actual, expected, [message]) 16 | test.notEqual(actual, expected, [message]) 17 | test.deepEqual(actual, expected, [message]) 18 | test.notDeepEqual(actual, expected, [message]) 19 | test.strictEqual(actual, expected, [message]) 20 | test.notStrictEqual(actual, expected, [message]) 21 | test.throws(block, [error], [message]) 22 | test.doesNotThrow(block, [error], [message]) 23 | test.ifError(value) 24 | */ 25 | 26 | exports.read = 27 | { 28 | setUp: function(done) 29 | { 30 | done(); 31 | }, 32 | 'Tickets route - Part 2': function(test) 33 | { 34 | test.expect(11); 35 | 36 | const seconds = 2; 37 | 38 | request.get('http://localhost:8124/tickets/new?policy=time_based&seconds=' + seconds, (err, res) => 39 | { 40 | test.ifError(err); 41 | test.equal(res.statusCode, 200); 42 | 43 | let result = JSON.parse(res.body); 44 | 45 | test.equal(result.result, CONST.OK); 46 | test.ok(result.expires_in > (seconds / 2)); 47 | 48 | const ticket = result.ticket; 49 | 50 | test.ok(ticket); 51 | 52 | 53 | request.get('http://localhost:8124/tickets/' + ticket + '/status', (err2, res2) => 54 | { 55 | test.ifError(err2); 56 | test.equal(res2.statusCode, 200); 57 | 58 | result = JSON.parse(res2.body); 59 | 60 | test.equal(result.status, CONST.VALID_TICKET); 61 | 62 | 63 | setTimeout( () => 64 | { 65 | request.get('http://localhost:8124/tickets/' + ticket + '/status', (err3, res3) => 66 | { 67 | test.ifError(err3); 68 | test.equal(res3.statusCode, 200); 69 | 70 | result = JSON.parse(res3.body); 71 | 72 | test.equal(result.status, CONST.EXPIRED_TICKET); 73 | 74 | 75 | test.done(); 76 | }); 77 | 78 | }, (seconds * 1000)); 79 | }); 80 | }); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /test/tickets_3_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('request'); 4 | const CONST = require('../lib/const'); 5 | 6 | /* 7 | ======== A Handy Little Nodeunit Reference ======== 8 | https://github.com/caolan/nodeunit 9 | 10 | Test methods: 11 | test.expect(numAssertions) 12 | test.done() 13 | Test assertions: 14 | test.ok(value, [message]) 15 | test.equal(actual, expected, [message]) 16 | test.notEqual(actual, expected, [message]) 17 | test.deepEqual(actual, expected, [message]) 18 | test.notDeepEqual(actual, expected, [message]) 19 | test.strictEqual(actual, expected, [message]) 20 | test.notStrictEqual(actual, expected, [message]) 21 | test.throws(block, [error], [message]) 22 | test.doesNotThrow(block, [error], [message]) 23 | test.ifError(value) 24 | */ 25 | 26 | exports.read = 27 | { 28 | setUp: function(done) 29 | { 30 | done(); 31 | }, 32 | 33 | 'Tickets route - Part 3': function(test) 34 | { 35 | test.expect(17); 36 | 37 | let requests = 2; 38 | 39 | request.get('http://localhost:8124/tickets/new?policy=requests_based&requests=' + requests, (err, res) => 40 | { 41 | test.ifError(err); 42 | test.equal(res.statusCode, 200); 43 | 44 | let result = JSON.parse(res.body); 45 | 46 | test.equal(result.result, CONST.OK); 47 | test.equal(result.expires_in, requests); 48 | 49 | const ticket = result.ticket; 50 | 51 | test.ok(ticket); 52 | 53 | 54 | request.get('http://localhost:8124/tickets/' + ticket + '/status', (err2, res2) => 55 | { 56 | requests--; 57 | 58 | test.ifError(err2); 59 | test.equal(res2.statusCode, 200); 60 | 61 | result = JSON.parse(res2.body); 62 | 63 | if (requests >= 0) 64 | { 65 | test.equal(result.status, CONST.VALID_TICKET); 66 | test.deepEqual(result.expires_in, requests); 67 | } 68 | else 69 | { 70 | test.equal(result.status, CONST.EXPIRED_TICKET); 71 | test.deepEqual(result.expires_in, undefined); 72 | } 73 | 74 | 75 | request.get('http://localhost:8124/tickets/' + ticket + '/status', (err3, res3) => 76 | { 77 | requests--; 78 | 79 | test.ifError(err3); 80 | test.equal(res3.statusCode, 200); 81 | 82 | result = JSON.parse(res3.body); 83 | 84 | if (requests >= 0) 85 | { 86 | test.equal(result.status, CONST.VALID_TICKET); 87 | test.deepEqual(result.expires_in, requests); 88 | } 89 | else 90 | { 91 | test.equal(result.status, CONST.EXPIRED_TICKET); 92 | test.deepEqual(result.expires_in, undefined); 93 | } 94 | 95 | 96 | request.get('http://localhost:8124/tickets/' + ticket + '/status', (err4, res4) => 97 | { 98 | requests--; 99 | 100 | test.ifError(err4); 101 | test.equal(res4.statusCode, 200); 102 | 103 | result = JSON.parse(res4.body); 104 | 105 | if (requests >= 0) 106 | { 107 | test.equal(result.status, CONST.VALID_TICKET); 108 | test.deepEqual(result.expires_in, requests); 109 | } 110 | else 111 | { 112 | test.equal(result.status, CONST.EXPIRED_TICKET); 113 | test.deepEqual(result.expires_in, undefined); 114 | } 115 | 116 | 117 | test.done(); 118 | }); 119 | }); 120 | }); 121 | }); 122 | } 123 | }; 124 | -------------------------------------------------------------------------------- /test/tickets_4_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('request'); 4 | const CONST = require('../lib/const'); 5 | 6 | /* 7 | ======== A Handy Little Nodeunit Reference ======== 8 | https://github.com/caolan/nodeunit 9 | 10 | Test methods: 11 | test.expect(numAssertions) 12 | test.done() 13 | Test assertions: 14 | test.ok(value, [message]) 15 | test.equal(actual, expected, [message]) 16 | test.notEqual(actual, expected, [message]) 17 | test.deepEqual(actual, expected, [message]) 18 | test.notDeepEqual(actual, expected, [message]) 19 | test.strictEqual(actual, expected, [message]) 20 | test.notStrictEqual(actual, expected, [message]) 21 | test.throws(block, [error], [message]) 22 | test.doesNotThrow(block, [error], [message]) 23 | test.ifError(value) 24 | */ 25 | 26 | exports.read = 27 | { 28 | setUp: function(done) 29 | { 30 | done(); 31 | }, 32 | 33 | 'Tickets generation speeds': function(test) 34 | { 35 | test.expect(12); 36 | 37 | let seconds = 2; 38 | 39 | let genspeed = CONST.SPEED.FASTER; 40 | 41 | request.get('http://localhost:8124/tickets/new?policy=time_based&seconds=' + seconds + '&generation_speed=' + genspeed, (err, res) => 42 | { 43 | test.ifError(err); 44 | test.equal(res.statusCode, 200); 45 | 46 | let result = JSON.parse(res.body); 47 | 48 | test.equal(result.result, CONST.OK); 49 | 50 | let ticket = result.ticket; 51 | 52 | test.ok(ticket); 53 | 54 | 55 | genspeed = CONST.SPEED.FAST; 56 | 57 | request.get('http://localhost:8124/tickets/new?policy=time_based&seconds=' + seconds + '&generation_speed=' + genspeed, (err2, res2) => 58 | { 59 | test.ifError(err2); 60 | test.equal(res2.statusCode, 200); 61 | 62 | result = JSON.parse(res2.body); 63 | 64 | test.equal(result.result, CONST.OK); 65 | 66 | ticket = result.ticket; 67 | 68 | test.ok(ticket); 69 | 70 | 71 | genspeed = CONST.SPEED.SLOW; 72 | 73 | request.get('http://localhost:8124/tickets/new?policy=time_based&seconds=' + seconds + '&generation_speed=' + genspeed, (err3, res3) => 74 | { 75 | test.ifError(err3); 76 | test.equal(res3.statusCode, 200); 77 | 78 | result = JSON.parse(res3.body); 79 | 80 | test.equal(result.result, CONST.OK); 81 | 82 | ticket = result.ticket; 83 | 84 | test.ok(ticket); 85 | 86 | 87 | test.done(); 88 | }); 89 | }); 90 | }); 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /test/tickets_5_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('request'); 4 | const CONST = require('../lib/const'); 5 | 6 | /* 7 | ======== A Handy Little Nodeunit Reference ======== 8 | https://github.com/caolan/nodeunit 9 | 10 | Test methods: 11 | test.expect(numAssertions) 12 | test.done() 13 | Test assertions: 14 | test.ok(value, [message]) 15 | test.equal(actual, expected, [message]) 16 | test.notEqual(actual, expected, [message]) 17 | test.deepEqual(actual, expected, [message]) 18 | test.notDeepEqual(actual, expected, [message]) 19 | test.strictEqual(actual, expected, [message]) 20 | test.notStrictEqual(actual, expected, [message]) 21 | test.throws(block, [error], [message]) 22 | test.doesNotThrow(block, [error], [message]) 23 | test.ifError(value) 24 | */ 25 | 26 | exports.read = 27 | { 28 | setUp: function(done) 29 | { 30 | done(); 31 | }, 32 | 33 | 'Bandwidth-based tickets': function(test) 34 | { 35 | test.expect(35); 36 | 37 | let requests = 4; 38 | 39 | request.get('http://localhost:8124/tickets/new?policy=bandwidth_based&reqs_per_minute=' + requests, (err, res) => 40 | { 41 | test.ifError(err); 42 | test.equal(res.statusCode, 200); 43 | 44 | let result = JSON.parse(res.body); 45 | 46 | test.equal(result.result, CONST.OK); 47 | 48 | let ticket = result.ticket; 49 | 50 | test.ok(ticket); 51 | 52 | 53 | request.get('http://localhost:8124/tickets/' + ticket + '/status', (err2, res2) => 54 | { 55 | test.ifError(err2); 56 | test.equal(res2.statusCode, 200); 57 | 58 | result = JSON.parse(res2.body); 59 | 60 | test.equal(result.status, CONST.VALID_TICKET); 61 | test.equal(result.expires_in, requests - 1); 62 | 63 | 64 | // Consume the requests and get an "expired" status 65 | request.get('http://localhost:8124/tickets/' + ticket + '/status', (err3, res3) => 66 | { 67 | test.ifError(err3); 68 | test.equal(res3.statusCode, 200); 69 | 70 | result = JSON.parse(res3.body); 71 | 72 | test.equal(result.status, CONST.VALID_TICKET); 73 | test.equal(result.expires_in, requests - 2); 74 | 75 | request.get('http://localhost:8124/tickets/' + ticket + '/status', (err4, res4) => 76 | { 77 | test.ifError(err4); 78 | test.equal(res4.statusCode, 200); 79 | 80 | result = JSON.parse(res4.body); 81 | 82 | test.equal(result.status, CONST.VALID_TICKET); 83 | test.equal(result.expires_in, requests - 3); 84 | 85 | 86 | // Then wait a bit more of a minute to reset the counter and try the lightweight parameter 87 | const wait = 61 * 1000; 88 | console.log('\n\n Waiting %s seconds for the bandwith check to reset... \n\n', wait / 1000); 89 | 90 | setTimeout( () => 91 | { 92 | request.get('http://localhost:8124/tickets/' + ticket + '/status', (err5, res5) => 93 | { 94 | test.ifError(err5); 95 | test.equal(res5.statusCode, 200); 96 | 97 | result = JSON.parse(res5.body); 98 | 99 | test.equal(result.status, CONST.VALID_TICKET); 100 | test.equal(result.expires_in, requests - 1); 101 | 102 | request.get('http://localhost:8124/tickets/' + ticket + '/status', (err6, res6) => 103 | { 104 | test.ifError(err6); 105 | test.equal(res6.statusCode, 200); 106 | 107 | result = JSON.parse(res6.body); 108 | 109 | test.equal(result.status, CONST.VALID_TICKET); 110 | test.equal(result.expires_in, requests - 2); 111 | 112 | request.get('http://localhost:8124/tickets/' + ticket + '/status', (err7, res7) => 113 | { 114 | test.ifError(err7); 115 | test.equal(res7.statusCode, 200); 116 | 117 | result = JSON.parse(res7.body); 118 | 119 | test.equal(result.status, CONST.VALID_TICKET); 120 | test.equal(result.expires_in, requests - 3); 121 | 122 | 123 | request.get('http://localhost:8124/tickets/' + ticket + '/status', (err8, res8) => 124 | { 125 | test.ifError(err8); 126 | test.equal(res8.statusCode, 200); 127 | 128 | result = JSON.parse(res8.body); 129 | 130 | test.equal(result.status, CONST.VALID_TICKET); 131 | test.equal(result.expires_in, requests - 4); 132 | 133 | 134 | request.get('http://localhost:8124/tickets/' + ticket + '/status', (err9, res9) => 135 | { 136 | test.ifError(err9); 137 | test.equal(res9.statusCode, 200); 138 | 139 | result = JSON.parse(res9.body); 140 | 141 | test.equal(result.status, CONST.EXPIRED_TICKET); 142 | 143 | 144 | test.done(); 145 | }); 146 | }); 147 | }); 148 | }); 149 | }); 150 | 151 | }, wait); 152 | }); 153 | }); 154 | }); 155 | }); 156 | } 157 | }; 158 | -------------------------------------------------------------------------------- /test/tickets_6_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('request'); 4 | const CONST = require('../lib/const'); 5 | 6 | /* 7 | ======== A Handy Little Nodeunit Reference ======== 8 | https://github.com/caolan/nodeunit 9 | 10 | Test methods: 11 | test.expect(numAssertions) 12 | test.done() 13 | Test assertions: 14 | test.ok(value, [message]) 15 | test.equal(actual, expected, [message]) 16 | test.notEqual(actual, expected, [message]) 17 | test.deepEqual(actual, expected, [message]) 18 | test.notDeepEqual(actual, expected, [message]) 19 | test.strictEqual(actual, expected, [message]) 20 | test.notStrictEqual(actual, expected, [message]) 21 | test.throws(block, [error], [message]) 22 | test.doesNotThrow(block, [error], [message]) 23 | test.ifError(value) 24 | */ 25 | 26 | exports.read = 27 | { 28 | setUp: function(done) 29 | { 30 | done(); 31 | }, 32 | 33 | 'Tickets "lightweight" status check, for requests-based tickets': function(test) 34 | { 35 | test.expect(16); 36 | 37 | let requests = 4; 38 | 39 | request.get('http://localhost:8124/tickets/new?policy=requests_based&requests=' + requests, (err, res) => 40 | { 41 | test.ifError(err); 42 | test.equal(res.statusCode, 200); 43 | 44 | let result = JSON.parse(res.body); 45 | 46 | test.equal(result.result, CONST.OK); 47 | 48 | let ticket = result.ticket; 49 | 50 | test.ok(ticket); 51 | 52 | 53 | request.get('http://localhost:8124/tickets/' + ticket + '/status', (err2, res2) => 54 | { 55 | test.ifError(err2); 56 | test.equal(res2.statusCode, 200); 57 | 58 | result = JSON.parse(res2.body); 59 | 60 | test.equal(result.status, CONST.VALID_TICKET); 61 | test.equal(result.expires_in, requests - 1); 62 | 63 | 64 | request.get('http://localhost:8124/tickets/' + ticket + '/status?light=true', (err3, res3) => 65 | { 66 | test.ifError(err3); 67 | test.equal(res3.statusCode, 200); 68 | 69 | result = JSON.parse(res3.body); 70 | 71 | test.equal(result.status, CONST.VALID_TICKET); 72 | test.equal(result.expires_in, requests - 1); 73 | 74 | 75 | // Notice the "light" parameter with an unsupported value: the parameter MUST be ignored 76 | request.get('http://localhost:8124/tickets/' + ticket + '/status?light=somethingstupid', (err4, res4) => 77 | { 78 | test.ifError(err4); 79 | test.equal(res4.statusCode, 200); 80 | 81 | result = JSON.parse(res4.body); 82 | 83 | test.equal(result.status, CONST.VALID_TICKET); 84 | test.equal(result.expires_in, requests - 2); 85 | 86 | 87 | test.done(); 88 | }); 89 | }); 90 | }); 91 | }); 92 | }, 93 | 94 | 'Tickets "lightweight" status check, for bandwidth-based tickets': function(test) 95 | { 96 | test.expect(25); 97 | 98 | let requests = 2; 99 | 100 | request.get('http://localhost:8124/tickets/new?policy=bandwidth_based&reqs_per_minute=' + requests, (err, res) => 101 | { 102 | test.ifError(err); 103 | test.equal(res.statusCode, 200); 104 | 105 | let result = JSON.parse(res.body); 106 | 107 | test.equal(result.result, CONST.OK); 108 | 109 | let ticket = result.ticket; 110 | 111 | test.ok(ticket); 112 | 113 | 114 | request.get('http://localhost:8124/tickets/' + ticket + '/status', (err2, res2) => 115 | { 116 | test.ifError(err2); 117 | test.equal(res2.statusCode, 200); 118 | 119 | result = JSON.parse(res2.body); 120 | 121 | test.equal(result.status, CONST.VALID_TICKET); 122 | 123 | 124 | // Consume the requests and get an "expired" status 125 | request.get('http://localhost:8124/tickets/' + ticket + '/status', () => 126 | { 127 | request.get('http://localhost:8124/tickets/' + ticket + '/status', (err3, res3) => 128 | { 129 | test.ifError(err3); 130 | test.equal(res3.statusCode, 200); 131 | 132 | result = JSON.parse(res3.body); 133 | 134 | test.equal(result.status, CONST.EXPIRED_TICKET); 135 | 136 | 137 | // Then wait a bit more of a minute to reset the counter and try the lightweight parameter 138 | const wait = 61 * 1000; 139 | console.log('\n\n Waiting %s seconds for the bandwith check to reset... \n\n', wait / 1000); 140 | 141 | setTimeout( () => 142 | { 143 | request.get('http://localhost:8124/tickets/' + ticket + '/status', (err4, res4) => 144 | { 145 | test.ifError(err4); 146 | test.equal(res4.statusCode, 200); 147 | 148 | result = JSON.parse(res4.body); 149 | 150 | test.equal(result.status, CONST.VALID_TICKET); 151 | test.equal(result.expires_in, requests - 1); 152 | 153 | 154 | // Notice the applied "light" parameter: 155 | request.get('http://localhost:8124/tickets/' + ticket + '/status?light=true', (err5, res5) => 156 | { 157 | test.ifError(err5); 158 | test.equal(res5.statusCode, 200); 159 | 160 | result = JSON.parse(res5.body); 161 | 162 | test.equal(result.status, CONST.VALID_TICKET); 163 | test.equal(result.expires_in, requests - 1); 164 | 165 | 166 | request.get('http://localhost:8124/tickets/' + ticket + '/status', (err6, res6) => 167 | { 168 | test.ifError(err6); 169 | test.equal(res6.statusCode, 200); 170 | 171 | result = JSON.parse(res6.body); 172 | 173 | test.equal(result.status, CONST.VALID_TICKET); 174 | test.equal(result.expires_in, requests - 2); 175 | 176 | 177 | request.get('http://localhost:8124/tickets/' + ticket + '/status', (err7, res7) => 178 | { 179 | test.ifError(err7); 180 | test.equal(res7.statusCode, 200); 181 | 182 | result = JSON.parse(res7.body); 183 | 184 | test.equal(result.status, CONST.EXPIRED_TICKET); 185 | 186 | 187 | test.done(); 188 | }); 189 | }); 190 | }); 191 | }); 192 | 193 | }, wait); 194 | }); 195 | }); 196 | }); 197 | }); 198 | } 199 | }; 200 | -------------------------------------------------------------------------------- /test/tickets_policy_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('request'); 4 | const CONST = require('../lib/const'); 5 | 6 | /* 7 | ======== A Handy Little Nodeunit Reference ======== 8 | https://github.com/caolan/nodeunit 9 | 10 | Test methods: 11 | test.expect(numAssertions) 12 | test.done() 13 | Test assertions: 14 | test.ok(value, [message]) 15 | test.equal(actual, expected, [message]) 16 | test.notEqual(actual, expected, [message]) 17 | test.deepEqual(actual, expected, [message]) 18 | test.notDeepEqual(actual, expected, [message]) 19 | test.strictEqual(actual, expected, [message]) 20 | test.notStrictEqual(actual, expected, [message]) 21 | test.throws(block, [error], [message]) 22 | test.doesNotThrow(block, [error], [message]) 23 | test.ifError(value) 24 | */ 25 | 26 | exports.read = 27 | { 28 | setUp: function(done) 29 | { 30 | done(); 31 | }, 32 | 33 | 'Tickets policy retrieval': function(test) 34 | { 35 | request.get('http://localhost:8124/tickets/new?policy=time_based&seconds=2', (err, res) => 36 | { 37 | test.ifError(err); 38 | test.equal(res.statusCode, 200); 39 | 40 | let result = JSON.parse(res.body); 41 | 42 | test.equal(result.result, CONST.OK); 43 | test.equal(result.policy, 'time_based'); 44 | 45 | const ticket = result.ticket; 46 | 47 | test.ok(ticket); 48 | 49 | 50 | request.get('http://localhost:8124/tickets/' + ticket + '/policy', (err2, res2) => 51 | { 52 | test.ifError(err2); 53 | test.equal(res2.statusCode, 200); 54 | 55 | result = JSON.parse(res2.body); 56 | 57 | test.equal(result.policy, 'time_based'); 58 | 59 | test.ok(result.more); 60 | test.equal(result.more.context, undefined); 61 | test.equal(result.more.autorenew, false); 62 | test.equal(result.more.depends_on, undefined); 63 | test.equal(result.more.generation_speed, CONST.SPEED.SLOW); 64 | test.equal(result.more.can_force_expiration, false); 65 | 66 | 67 | test.done(); 68 | }); 69 | }); 70 | } 71 | }; 72 | --------------------------------------------------------------------------------