├── .gitignore ├── AUTHORS ├── LICENSE ├── README.md ├── bin └── throttleproxy.js ├── index.js ├── package.json ├── src └── throttle.js └── test └── throttle_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | node_modules 3 | npm-debug.log 4 | tmp 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Tiago Quelhas 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Tiago Quelhas. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | * The names of the authors and contributors may not be used to endorse or 14 | promote products derived from this software without specific prior 15 | written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 19 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 20 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 21 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 23 | OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 26 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stream-throttle # 2 | 3 | A rate limiter for Node.js streams. 4 | 5 | ## API usage 6 | 7 | This module exports two classes, `Throttle` and `ThrottleGroup`. 8 | 9 | `Throttle` creates a single throttled stream, based on `stream.Transform`. It accepts an `opts` parameter with the following keys: 10 | 11 | * `opts.rate` is the throttling rate, in bytes per second. 12 | * `opts.chunksize` (optional) is the maximum chunk size into which larger writes are decomposed; the default is `opts.rate`/10. 13 | 14 | The `opts` object may also contain options to be passed to the `stream.Transform` constructor. 15 | 16 | For example, the following code throttles stdin to stdout at 10 bytes per second: 17 | 18 | process.stdin.pipe(new Throttle({rate: 10})).pipe(process.stdout) 19 | 20 | `ThrottleGroup` allows the creation of a group of streams whose aggregate bandwidth is throttled. The constructor accepts the same `opts` argument as for `Throttle`. Call `throttle` on a `ThrottleGroup` object to create a new throttled stream belonging to the group. 21 | 22 | For example, the following code creates two HTTP connections to `www.google.com:80`, and throttles their aggregate (downstream) bandwidth to 10 KB/s: 23 | 24 | var addr = { host: 'www.google.com', port: 80 }; 25 | var tg = new ThrottleGroup({rate: 10240}); 26 | 27 | var conn1 = net.createConnection(addr), 28 | conn2 = net.createConnection(addr); 29 | 30 | var thr1 = conn1.pipe(tg.throttle()), 31 | thr2 = conn2.pipe(tg.throttle()); 32 | 33 | // Reads from thr1 and thr2 are throttled to 10 KB/s in aggregate 34 | 35 | ## Command line usage 36 | 37 | This package installs a `throttleproxy` binary which implements a command-line utility for throttling connections. Run `throttleproxy -h` for instructions. 38 | 39 | ## Contributing 40 | 41 | Feel free to open an issue or send a pull request. 42 | 43 | ## License 44 | 45 | BSD-style. See the LICENSE file. 46 | 47 | ## Author 48 | 49 | Copyright © 2013 Tiago Quelhas. Contact me at ``. 50 | -------------------------------------------------------------------------------- /bin/throttleproxy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var net = require('net'); 4 | var opts = require('commander'); 5 | var Throttle = require('../src/throttle.js').Throttle; 6 | 7 | function parseAddr(addr) { 8 | var result = /^(([^:]*):)?(\d+)$/.exec(addr); 9 | if (!result) 10 | return null; 11 | return { 12 | host: result[2], 13 | port: result[3] 14 | }; 15 | } 16 | 17 | function parseInteger(s) { 18 | if (!/^\d+$/.test(s)) 19 | return undefined; 20 | return parseInt(s, 10); 21 | } 22 | 23 | function runProxy(localAddr, remoteAddr, downRate, upRate) { 24 | var server = net.createServer(function(local) { 25 | 26 | var remote = net.createConnection(remoteAddr); 27 | 28 | var localThrottle = new Throttle({rate: upRate}); 29 | var remoteThrottle = new Throttle({rate: downRate}); 30 | 31 | local.pipe(localThrottle).pipe(remote); 32 | local.on('error', function() { 33 | remote.destroy(); 34 | local.destroy(); 35 | }); 36 | 37 | remote.pipe(remoteThrottle).pipe(local); 38 | remote.on('error', function() { 39 | local.destroy(); 40 | remote.destroy(); 41 | }); 42 | }); 43 | 44 | server.listen(localAddr.port, localAddr.host); 45 | } 46 | 47 | function main() { 48 | var localAddr, remoteAddr, downRate, upRate; 49 | 50 | opts 51 | .option('-l, --localaddr ', 'local address, default 0.0.0.0:8080') 52 | .option('-r, --remoteaddr ', 'remote address, default localhost:80') 53 | .option('-d, --downstream ', 'downstream bandwidth', parseInteger) 54 | .option('-u, --upstream ', 'upstream bandwidth, default equal to downstream', parseInteger) 55 | .parse(process.argv); 56 | 57 | if (opts.localaddr !== undefined) { 58 | localAddr = parseAddr(opts.localaddr); 59 | if (!localAddr) 60 | opts.help(); 61 | } else 62 | localAddr = {host: undefined, port: 8080}; 63 | 64 | if (opts.remoteaddr !== undefined) { 65 | remoteAddr = parseAddr(opts.remoteaddr); 66 | if (!remoteAddr) 67 | opts.help(); 68 | } else 69 | remoteAddr = {host: undefined, port: 80}; 70 | 71 | if (opts.downstream === undefined) 72 | opts.help(); 73 | downRate = opts.downstream; 74 | 75 | if (opts.upstream !== undefined) 76 | upRate = opts.upstream; 77 | else 78 | upRate = downRate; 79 | 80 | runProxy(localAddr, remoteAddr, downRate, upRate); 81 | } 82 | 83 | main(); 84 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/throttle.js'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stream-throttle", 3 | "description": "A rate limiter for Node.js streams.", 4 | "version": "0.1.3", 5 | "author": "Tiago Quelhas ", 6 | "license": "BSD-3-Clause", 7 | "keywords": [ 8 | "streams", 9 | "throttling", 10 | "ratelimit" 11 | ], 12 | "engines": { 13 | "node": ">= 0.10.0" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "http://github.com/tjgq/node-stream-throttle.git" 18 | }, 19 | "main": "./index.js", 20 | "scripts": { 21 | "test": "nodeunit test" 22 | }, 23 | "bin" : { 24 | "throttleproxy" : "./bin/throttleproxy.js" 25 | }, 26 | "dependencies": { 27 | "commander": "^2.2.0", 28 | "limiter": "^1.0.5" 29 | }, 30 | "devDependencies": { 31 | "async": "^0.6.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/throttle.js: -------------------------------------------------------------------------------- 1 | var inherits = require('util').inherits; 2 | var Transform = require('stream').Transform; 3 | var TokenBucket = require('limiter').TokenBucket; 4 | 5 | /* 6 | * Throttle is a throttled stream implementing the stream.Transform interface. 7 | * Options: 8 | * rate (mandatory): the throttling rate in bytes per second. 9 | * chunksize (optional): the maximum chunk size into which larger writes are decomposed. 10 | * Any other options are passed to stream.Transform. 11 | */ 12 | function Throttle(opts, group) { 13 | if (group === undefined) 14 | group = new ThrottleGroup(opts); 15 | this.bucket = group.bucket; 16 | this.chunksize = group.chunksize; 17 | Transform.call(this, opts); 18 | } 19 | inherits(Throttle, Transform); 20 | 21 | Throttle.prototype._transform = function(chunk, encoding, done) { 22 | process(this, chunk, 0, done); 23 | }; 24 | 25 | function process(self, chunk, pos, done) { 26 | var slice = chunk.slice(pos, pos + self.chunksize); 27 | if (!slice.length) { 28 | // chunk fully consumed 29 | done(); 30 | return; 31 | } 32 | self.bucket.removeTokens(slice.length, function(err) { 33 | if (err) { 34 | done(err); 35 | return; 36 | } 37 | self.push(slice); 38 | process(self, chunk, pos + self.chunksize, done); 39 | }); 40 | } 41 | 42 | /* 43 | * ThrottleGroup throttles an aggregate of streams. 44 | * Options are the same as for Throttle. 45 | */ 46 | function ThrottleGroup(opts) { 47 | if (!(this instanceof ThrottleGroup)) 48 | return new ThrottleGroup(opts); 49 | 50 | opts = opts || {}; 51 | if (opts.rate === undefined) 52 | throw new Error('throttle rate is a required argument'); 53 | if (typeof opts.rate !== 'number' || opts.rate <= 0) 54 | throw new Error('throttle rate must be a positive number'); 55 | if (opts.chunksize !== undefined && (typeof opts.chunksize !== 'number' || opts.chunksize <= 0)) { 56 | throw new Error('throttle chunk size must be a positive number'); 57 | } 58 | 59 | this.rate = opts.rate; 60 | this.chunksize = opts.chunksize || this.rate/10; 61 | this.bucket = new TokenBucket(this.rate, this.rate, 'second', null); 62 | } 63 | 64 | /* 65 | * Create a new stream in the throttled group and returns it. 66 | * Any supplied options are passed to the Throttle constructor. 67 | */ 68 | ThrottleGroup.prototype.throttle = function(opts) { 69 | return new Throttle(opts, this); 70 | }; 71 | 72 | module.exports = { 73 | Throttle: Throttle, 74 | ThrottleGroup: ThrottleGroup 75 | }; -------------------------------------------------------------------------------- /test/throttle_test.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | var Throttle = require('../index.js').Throttle; 3 | var ThrottleGroup = require('../index.js').ThrottleGroup; 4 | 5 | var sendStr = (function() { 6 | var s = '0123456789xyzXYZ?!\0åéîõü$£€*<>'; // 30 characters 7 | for (var i = 0, str = ''; i < 1000; i++) 8 | str += s; 9 | return str; // 30K characters 10 | })(); 11 | 12 | var opts = {rate: 100000}; // 100 KiB/s 13 | 14 | var testSendRecv = function(t, cb) { 15 | var recvStr = ''; 16 | t.on('data', function(chunk) { 17 | recvStr += chunk; 18 | }); 19 | t.on('end', function() { 20 | cb(sendStr == recvStr); 21 | }); 22 | t.write(sendStr, function() { 23 | t.end(); 24 | }); 25 | }; 26 | 27 | exports.testThrottle = function(test) { 28 | var t = new Throttle(opts); 29 | 30 | test.expect(1); 31 | testSendRecv(t, function(ok) { 32 | test.ok(ok, "received string should equal sent string"); 33 | test.done(); 34 | }); 35 | }; 36 | 37 | exports.testGroupThrottle = function(test) { 38 | var tg = new ThrottleGroup(opts); 39 | 40 | test.expect(3); 41 | async.each([1, 2, 3], function(i, done) { 42 | testSendRecv(tg.throttle(), function(ok) { 43 | test.ok(ok, "received string should equal sent string"); 44 | done(); 45 | }); 46 | }, function() { 47 | test.done(); 48 | }); 49 | }; 50 | --------------------------------------------------------------------------------