├── stopcat.jpg ├── .gitignore ├── .travis.yml ├── lib ├── koa.js ├── defaults.js └── index.js ├── setupHelper.js ├── test ├── test-maxcount.js ├── test-localhost.js ├── test-koa.js ├── test-stop.js ├── test-hapi.js ├── test-hapi17.js ├── test-post.js ├── test-express.js └── test-options.js ├── LICENSE ├── package.json ├── index.js └── README.md /stopcat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rook2pawn/node-ddos/HEAD/stopcat.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .coveralls.yml 3 | 4 | ## Directory-based project format: 5 | .idea/ 6 | coverage/ 7 | .nyc_output 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | after_success: 5 | - COVERALLS_REPO_TOKEN=$coveralls_repo_token npm run coverage -------------------------------------------------------------------------------- /lib/koa.js: -------------------------------------------------------------------------------- 1 | const koa_handler = function(params, table, handle) { 2 | return function(ctx, next) { 3 | var req = ctx.req; 4 | var res = ctx.res; 5 | 6 | return handle(params, table, req); 7 | }; 8 | }; 9 | 10 | module.exports = exports = koa_handler; 11 | -------------------------------------------------------------------------------- /setupHelper.js: -------------------------------------------------------------------------------- 1 | var Ddos = require('./') 2 | const opener = require("opener"); 3 | var express = require('express') 4 | var ddos = new Ddos({ 5 | burst:4, 6 | limit:4, 7 | testmode:true 8 | }); 9 | var app = express(); 10 | app.use(ddos.express); 11 | app.get("/", (req,res,next) => { 12 | console.log("Beep"); 13 | res.end("Boop"); 14 | }) 15 | app.listen(5150, () => { 16 | opener("http://127.0.0.1:5150"); 17 | }); 18 | -------------------------------------------------------------------------------- /lib/defaults.js: -------------------------------------------------------------------------------- 1 | 2 | function genDefaults () { 3 | const defaults = {}; 4 | // burst, maxexpiry, checkinterval is in seconds 5 | defaults.maxcount = 30; 6 | defaults.burst = 5; 7 | defaults.checkinterval = 1; 8 | 9 | // limit is the maximum count 10 | defaults.limit = defaults.burst * 4; 11 | 12 | defaults.maxexpiry = 120; 13 | defaults.trustProxy = true; 14 | defaults.includeUserAgent = true; 15 | defaults.whitelist = []; 16 | defaults.errormessage = "Error"; 17 | defaults.testmode = false; 18 | defaults.responseStatus = 429; 19 | return defaults; 20 | } 21 | 22 | module.exports = exports = genDefaults; 23 | -------------------------------------------------------------------------------- /test/test-maxcount.js: -------------------------------------------------------------------------------- 1 | const tape = require("tape"); 2 | const Ddos = require("../"); 3 | const request = require("supertest-light"); 4 | const express = require("express"); 5 | 6 | tape("maxcount ", function(t) { 7 | t.plan(1); 8 | const ddos = new Ddos({ burst: 3, limit: 2 }); 9 | const app = express(); 10 | app.use(ddos.express); 11 | app.get("/user", (req, res) => { 12 | res.status(200).json({ name: "john" }); 13 | }); 14 | 15 | const doCall = function() { 16 | return request(app).get("/user"); 17 | }; 18 | 19 | doCall() 20 | .then(() => { 21 | return doCall(); 22 | }) 23 | .then(() => { 24 | return doCall(); 25 | }) 26 | .then(() => { 27 | return doCall(); 28 | }) 29 | .then(() => { 30 | return doCall(); 31 | }) 32 | .then(res => { 33 | ddos.end(); 34 | t.pass(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/test-localhost.js: -------------------------------------------------------------------------------- 1 | const tape = require("tape"); 2 | const Ddos = require("../"); 3 | const request = require("supertest-light"); 4 | const express = require("express"); 5 | 6 | // https://github.com/rook2pawn/node-ddos/issues/31 7 | 8 | tape("localhost ", function(t) { 9 | t.plan(4); 10 | 11 | const ddos = new Ddos({ burst: 3, limit: 2 }); 12 | t.equals("::ffff:127.0.0.1".match(ddos.ipv4re)[2], "127.0.0.1"); 13 | t.equals("127.0.0.1".match(ddos.ipv4re)[2], "127.0.0.1"); 14 | t.equals("32.45.32.65:12568".match(ddos.ipv4re)[2], "32.45.32.65:12568"); 15 | 16 | const app = express(); 17 | app.use(ddos.express); 18 | app.get("/user", (req, res) => { 19 | res.status(200).json({ name: "john" }); 20 | }); 21 | 22 | const doCall = function() { 23 | return request(app) 24 | .set("x-forwarded-for", "::1") 25 | .get("/user"); 26 | }; 27 | 28 | doCall() 29 | .then(() => { 30 | t.equals( 31 | Object.keys(ddos.table)[0].match(/127\.0\.0\.1/)[0], 32 | "127.0.0.1" 33 | ); 34 | }) 35 | .then(() => { 36 | ddos.end(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2014-2017] David Wee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/test-koa.js: -------------------------------------------------------------------------------- 1 | var semver = require("semver"); 2 | 3 | if (semver.gte(process.version, "7.6.0")) { 4 | var tape = require("tape"); 5 | var koa = require("koa"); 6 | var request = require("supertest-light"); 7 | var DDOS = require("../"); 8 | var ddos = new DDOS({ burst: 3, limit: 4, testmode: true }); 9 | 10 | const Router = require("koa-router"); 11 | const router = new Router(); 12 | var app = new koa(); 13 | 14 | router.get("/todos", ctx => { 15 | ctx.status = 200; 16 | ctx.body = [ 17 | { 18 | id: 1, 19 | text: "Switch to Koa", 20 | completed: true 21 | }, 22 | { 23 | id: 2, 24 | text: "???", 25 | completed: true 26 | }, 27 | { 28 | id: 3, 29 | text: "Profit", 30 | completed: true 31 | } 32 | ]; 33 | }); 34 | app.use(ddos.koa().bind(ddos)); 35 | app.use(router.routes()); 36 | 37 | tape("table test", function(t) { 38 | t.plan(3); 39 | request(app.callback()) 40 | .get("/todos") 41 | .then(res => { 42 | t.equal(res.statusCode, 200); 43 | t.equal(res.headers["content-length"], "131"); 44 | var key = Object.keys(ddos.table)[0]; 45 | t.deepEqual(ddos.table[key], { count: 1, expiry: 1 }); 46 | ddos.stop(); 47 | }); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ddos", 3 | "version": "0.2.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "setup-helper": "node setupHelper.js", 8 | "coverage": "nyc report --reporter=text-lcov | coveralls", 9 | "test": "nyc tape test/test-*.js | tap-spec" 10 | }, 11 | "nyc": { 12 | "exclude": [ 13 | "lib/koa.js", 14 | "test" 15 | ] 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git://github.com/rook2pawn/node-ddos.git" 20 | }, 21 | "keywords": [ 22 | "ddos", 23 | "rate", 24 | "limiting", 25 | "koa", 26 | "express", 27 | "hapijs", 28 | "hapi" 29 | ], 30 | "author": "David Wee (http://rook2pawn.com)", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/rook2pawn/node-ddos/issues" 34 | }, 35 | "homepage": "https://github.com/rook2pawn/node-ddos", 36 | "devDependencies": { 37 | "body-parser": "", 38 | "coveralls": "^3.0.1", 39 | "express": "", 40 | "istanbul": "^0.4.5", 41 | "koa": "", 42 | "koa-router": "^7.4.0", 43 | "npm-install-version": "^6.0.2", 44 | "nyc": "^12.0.1", 45 | "opener": "^1.5.1", 46 | "queuelib": "", 47 | "request": "", 48 | "semver": "^5.3.0", 49 | "supertest-light": "", 50 | "tap-spec": "^4.1.2", 51 | "tape": "" 52 | }, 53 | "dependencies": {} 54 | } 55 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const lib = require("./lib"); 2 | const defaultParams = require("./lib/defaults"); 3 | 4 | const ddos = function(params) { 5 | if (!params) params = {}; 6 | 7 | params = Object.assign({}, defaultParams(), params); 8 | params.maxcount = params.limit * 2; 9 | 10 | if (params.testmode) { 11 | console.log("ddos: starting params: ", params); 12 | } 13 | 14 | this.table = {}; 15 | this.timer = setInterval(this.update.bind(this), params.checkinterval * 1000); 16 | this.express = this.handle.bind(this); 17 | this.middleware = this.handle.bind(this); 18 | this.params = params; 19 | }; 20 | 21 | 22 | ddos.prototype.addWhitelist = lib.addWhitelist; 23 | ddos.prototype.stop = lib.stop; 24 | ddos.prototype.end = ddos.prototype.stop; 25 | ddos.prototype.update = lib.update; 26 | ddos.prototype.handle = lib.handle; 27 | ddos.prototype.express = lib.handle; 28 | ddos.prototype.koa = function() { 29 | return function(ctx, next) { 30 | var req = ctx.req; 31 | var res = ctx.res; 32 | 33 | return lib._handle(this.params,this.table, req) 34 | .then(() => { 35 | return next() 36 | }) 37 | }; 38 | }; 39 | 40 | ddos.prototype.hapi17 = function (request, h) { 41 | const req = request.raw.req; 42 | const params = this.params; 43 | const table = this.table; 44 | 45 | 46 | return lib._handle(params, table, req) 47 | .then(() => { 48 | return h.continue 49 | }) 50 | .catch((e) => { 51 | if (e.action === "respond") { 52 | const response = h.response(e.message); 53 | response.takeover(); 54 | response.code(e.code); 55 | return response; 56 | } 57 | }) 58 | 59 | 60 | } 61 | ddos.prototype.hapi = function(request, reply) { 62 | const req = request.raw.req; 63 | const res = reply; 64 | const table = this.table; 65 | const params = this.params; 66 | 67 | return lib._handle(params, table, req) 68 | .then(() => { 69 | return reply.continue(); 70 | }) 71 | .catch((e) => { 72 | if (e.action === "respond") { 73 | return res(e.message).code(e.code); 74 | } 75 | }) 76 | 77 | }; 78 | ddos.prototype.ipv4re = lib.ipv4re; 79 | 80 | module.exports = exports = ddos; 81 | -------------------------------------------------------------------------------- /test/test-stop.js: -------------------------------------------------------------------------------- 1 | var http = require("http"); 2 | var express = require("express"); 3 | var bodyParser = require("body-parser"); 4 | var request = require("request"); 5 | var QL = require("queuelib"); 6 | var Ddos = require("../"); 7 | var ddos = new Ddos(); 8 | var tape = require("tape"); 9 | 10 | tape("stop test", function(t) { 11 | var app = express(); 12 | app.use(ddos.express); 13 | app.use(bodyParser.json()); 14 | var q = new QL(); 15 | var server = http.createServer(app); 16 | server.listen(5050); 17 | 18 | var a = function(req, res, next) { 19 | next(); 20 | }; 21 | var b = function(req, res, next) { 22 | // some more random middleware 23 | next(); 24 | }; 25 | var c = function(req, res, next) { 26 | var num = req.body.num * 2; 27 | res.end(JSON.stringify({ foo: num })); 28 | }; 29 | app.post("/article", a, b, c); 30 | 31 | t.plan(6); 32 | q.series([ 33 | function(lib) { 34 | request.post( 35 | { url: "http://localhost:5050/article", json: true, body: { num: 42 } }, 36 | function(err, resp, body) { 37 | var key = Object.keys(ddos.table)[0]; 38 | t.deepEqual(ddos.table[key], { count: 1, expiry: 1 }); 39 | t.deepEqual(body, { foo: 84 }); 40 | lib.done(); 41 | } 42 | ); 43 | }, 44 | function(lib) { 45 | request.post( 46 | { url: "http://localhost:5050/article", json: true, body: { num: 42 } }, 47 | function(err, resp, body) { 48 | var key = Object.keys(ddos.table)[0]; 49 | t.deepEqual(ddos.table[key], { count: 2, expiry: 1 }); 50 | t.deepEqual(body, { foo: 84 }); 51 | ddos.stop(); 52 | lib.done(); 53 | } 54 | ); 55 | }, 56 | function(lib) { 57 | request.post( 58 | { url: "http://localhost:5050/article", json: true, body: { num: 42 } }, 59 | function(err, resp, body) { 60 | var key = Object.keys(ddos.table)[0]; 61 | t.deepEqual(ddos.table[key], { count: 2, expiry: 1 }); 62 | t.deepEqual(body, { foo: 84 }); 63 | lib.done(); 64 | } 65 | ); 66 | }, 67 | function(lib) { 68 | lib.done(); 69 | t.end(); 70 | server.close(); 71 | ddos.stop(); 72 | } 73 | ]); 74 | }); 75 | -------------------------------------------------------------------------------- /test/test-hapi.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"); 2 | 3 | tape("count and expiry test", function(t) { 4 | const niv = require("npm-install-version"); 5 | niv.install("hapi@16"); 6 | var Hapi = require("hapi@16"); 7 | var request = require("request"); 8 | var QL = require("queuelib"); 9 | 10 | var Ddos = require("../"); 11 | 12 | t.plan(11); 13 | var q = new QL(); 14 | var ddos = new Ddos({ burst: 3, limit: 4 }); 15 | 16 | const server = new Hapi.Server(); 17 | server.connection({ port: 3000, host: "localhost" }); 18 | server.route({ 19 | method: "GET", 20 | path: "/", 21 | handler: function(request, reply) { 22 | reply("Hello, world!"); 23 | } 24 | }); 25 | server.ext("onRequest", ddos.hapi.bind(ddos)); 26 | server.start(err => { 27 | q.series( 28 | [ 29 | lib => { 30 | request("http://localhost:3000/", (err, res, body) => { 31 | var key = Object.keys(ddos.table)[0]; 32 | t.deepEqual(ddos.table[key], { count: 1, expiry: 1 }); 33 | t.equal(res.statusCode, 200); 34 | lib.done(); 35 | }); 36 | }, 37 | lib => { 38 | request("http://localhost:3000/", (err, res, body) => { 39 | var key = Object.keys(ddos.table)[0]; 40 | t.deepEqual(ddos.table[key], { count: 2, expiry: 1 }); 41 | t.equal(res.statusCode, 200); 42 | lib.done(); 43 | }); 44 | }, 45 | lib => { 46 | request("http://localhost:3000/", (err, res, body) => { 47 | var key = Object.keys(ddos.table)[0]; 48 | t.deepEqual(ddos.table[key], { count: 3, expiry: 1 }); 49 | t.equal(res.statusCode, 200); 50 | lib.done(); 51 | }); 52 | }, 53 | lib => { 54 | request("http://localhost:3000/", (err, res, body) => { 55 | var key = Object.keys(ddos.table)[0]; 56 | t.deepEqual(ddos.table[key], { count: 4, expiry: 2 }); 57 | t.equal(res.statusCode, 200); 58 | lib.done(); 59 | }); 60 | }, 61 | lib => { 62 | request("http://localhost:3000/", (err, res, body) => { 63 | var key = Object.keys(ddos.table)[0]; 64 | t.deepEqual(ddos.table[key], { count: 5, expiry: 4 }); 65 | t.equal(res.statusCode, 429); 66 | lib.done(); 67 | }); 68 | } 69 | ], 70 | function() { 71 | t.pass("ok"); 72 | ddos.end(); 73 | server.stop(); 74 | } 75 | ); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test/test-hapi17.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"); 2 | 3 | 4 | tape("count and expiry test", function(t) { 5 | var request = require("request"); 6 | var QL = require("queuelib"); 7 | 8 | var Ddos = require("../"); 9 | const niv = require("npm-install-version"); 10 | niv.install("hapi@17"); 11 | var Hapi = require("hapi@17"); 12 | 13 | 14 | t.plan(11); 15 | var q = new QL(); 16 | var ddos = new Ddos({ burst: 3, limit: 4 }); 17 | 18 | const server = Hapi.server({ 19 | port: 3000, 20 | host: "localhost" 21 | }); 22 | 23 | server.route({ 24 | method: "GET", 25 | path: "/", 26 | handler: (request, h) => { 27 | return "Hello, world!"; 28 | } 29 | }); 30 | 31 | server.route({ 32 | method: "GET", 33 | path: "/{name}", 34 | handler: (request, h) => { 35 | return "Hello, " + encodeURIComponent(request.params.name) + "!"; 36 | } 37 | }); 38 | server.ext("onRequest", ddos.hapi17.bind(ddos)); 39 | 40 | server.start().then(() => { 41 | q.series( 42 | [ 43 | lib => { 44 | request("http://localhost:3000/", (err, res, body) => { 45 | var key = Object.keys(ddos.table)[0]; 46 | t.deepEqual(ddos.table[key], { count: 1, expiry: 1 }); 47 | t.equal(res.statusCode, 200); 48 | lib.done(); 49 | }); 50 | }, 51 | lib => { 52 | request("http://localhost:3000/", (err, res, body) => { 53 | var key = Object.keys(ddos.table)[0]; 54 | t.deepEqual(ddos.table[key], { count: 2, expiry: 1 }); 55 | t.equal(res.statusCode, 200); 56 | lib.done(); 57 | }); 58 | }, 59 | lib => { 60 | request("http://localhost:3000/", (err, res, body) => { 61 | var key = Object.keys(ddos.table)[0]; 62 | t.deepEqual(ddos.table[key], { count: 3, expiry: 1 }); 63 | t.equal(res.statusCode, 200); 64 | lib.done(); 65 | }); 66 | }, 67 | lib => { 68 | request("http://localhost:3000/", (err, res, body) => { 69 | var key = Object.keys(ddos.table)[0]; 70 | t.deepEqual(ddos.table[key], { count: 4, expiry: 2 }); 71 | t.equal(res.statusCode, 200); 72 | lib.done(); 73 | }); 74 | }, 75 | lib => { 76 | request("http://localhost:3000/", (err, res, body) => { 77 | var key = Object.keys(ddos.table)[0]; 78 | t.deepEqual(ddos.table[key], { count: 5, expiry: 4 }); 79 | t.equal(res.statusCode, 429); 80 | lib.done(); 81 | }); 82 | } 83 | ], 84 | function() { 85 | t.pass("ok"); 86 | ddos.end(); 87 | server.stop(); 88 | } 89 | ); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/test-post.js: -------------------------------------------------------------------------------- 1 | var http = require("http"); 2 | var express = require("express"); 3 | var bodyParser = require("body-parser"); 4 | var request = require("request"); 5 | var Ddos = require("../"); 6 | var ddos = new Ddos({ burst: 3, limit: 4, testmode: true }); 7 | var app = express(); 8 | app.use(ddos.express); 9 | app.use(bodyParser.json()); 10 | var server = http.createServer(app); 11 | 12 | var QL = require("queuelib"); 13 | var q = new QL(); 14 | 15 | server.listen(5050); 16 | 17 | var a = function(req, res, next) { 18 | next(); 19 | }; 20 | var b = function(req, res, next) { 21 | // some more random middleware 22 | next(); 23 | }; 24 | var c = function(req, res, next) { 25 | var num = req.body.num * 2; 26 | res.end(JSON.stringify({ foo: num })); 27 | }; 28 | app.post("/article", a, b, c); 29 | 30 | var tape = require("tape"); 31 | tape("post test", function(t) { 32 | t.plan(11); 33 | q.series([ 34 | function(lib) { 35 | request.post( 36 | { url: "http://localhost:5050/article", json: true, body: { num: 42 } }, 37 | function(err, resp, body) { 38 | var key = Object.keys(ddos.table)[0]; 39 | t.deepEqual(ddos.table[key], { count: 1, expiry: 1 }); 40 | t.deepEqual(body, { foo: 84 }); 41 | lib.done(); 42 | } 43 | ); 44 | }, 45 | function(lib) { 46 | request.post( 47 | { url: "http://localhost:5050/article", json: true, body: { num: 42 } }, 48 | function(err, resp, body) { 49 | var key = Object.keys(ddos.table)[0]; 50 | t.deepEqual(ddos.table[key], { count: 2, expiry: 1 }); 51 | t.deepEqual(body, { foo: 84 }); 52 | lib.done(); 53 | } 54 | ); 55 | }, 56 | function(lib) { 57 | request.post( 58 | { url: "http://localhost:5050/article", json: true, body: { num: 42 } }, 59 | function(err, resp, body) { 60 | var key = Object.keys(ddos.table)[0]; 61 | t.deepEqual(ddos.table[key], { count: 3, expiry: 1 }); 62 | t.deepEqual(body, { foo: 84 }); 63 | lib.done(); 64 | } 65 | ); 66 | }, 67 | function(lib) { 68 | request.post( 69 | { url: "http://localhost:5050/article", json: true, body: { num: 42 } }, 70 | function(err, resp, body) { 71 | var key = Object.keys(ddos.table)[0]; 72 | t.deepEqual(ddos.table[key], { count: 4, expiry: 2 }); 73 | t.deepEqual(body, { foo: 84 }); 74 | lib.done(); 75 | } 76 | ); 77 | }, 78 | function(lib) { 79 | request.post( 80 | { url: "http://localhost:5050/article", json: true, body: { num: 42 } }, 81 | function(err, resp, body) { 82 | var key = Object.keys(ddos.table)[0]; 83 | t.deepEqual(ddos.table[key], { count: 5, expiry: 4 }); 84 | t.equal(resp.statusCode, 429, "should be 429"); 85 | t.equal(body.count, 5, "should be 5"); 86 | lib.done(); 87 | } 88 | ); 89 | }, 90 | function(lib) { 91 | lib.done(); 92 | t.end(); 93 | server.close(); 94 | ddos.stop(); 95 | } 96 | ]); 97 | }); 98 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const addWhitelist = function(ip) { 2 | return this.params.whitelist.push(ip); 3 | } 4 | exports.addWhitelist = addWhitelist; 5 | 6 | const update = function() { 7 | var keys = Object.keys(this.table); 8 | for (var i = 0; i < keys.length; i++) { 9 | var key = keys[i]; 10 | this.table[key].expiry -= this.params.checkinterval; 11 | if (this.table[key].expiry <= 0) delete this.table[key]; 12 | } 13 | if (this.params.testmode) { 14 | console.log(this.table); 15 | } 16 | }; 17 | exports.update = update; 18 | 19 | const stop = function() { 20 | clearInterval(this.timer); 21 | this.params.stop = true; 22 | }; 23 | 24 | exports.stop = stop; 25 | 26 | const ipv4re = new RegExp(/(::ffff:)?(\d+\.\d+.\d+\.\d+(:\d+)?)/); 27 | exports.ipv4re = ipv4re; 28 | 29 | 30 | const getAddress = (options, req) => { 31 | let address = options.trustProxy 32 | ? req.headers["x-forwarded-for"] || req.connection.remoteAddress 33 | : req.connection.remoteAddress; 34 | if (address === "::1") { 35 | address="127.0.0.1" 36 | } else { 37 | let result = address.match(ipv4re) 38 | if (result && result[2]) 39 | address = result[2]; 40 | else 41 | address = "127.0.0.1"; 42 | } 43 | return address; 44 | } 45 | 46 | const _handle = function(options, table, req) { 47 | return new Promise((resolve, reject) => { 48 | if (options.stop) { 49 | return reject({action:"nothing", message:"stopped"}) 50 | } 51 | if (options.testmode) { 52 | console.log("ddos: handle: beginning:", table); 53 | } 54 | let host = getAddress(options, req); 55 | if (options.testmode) { 56 | console.log("host:", host); 57 | } 58 | if (options.whitelist.indexOf(host) != -1) { 59 | return resolve(); 60 | } 61 | if (options.includeUserAgent) 62 | host = host.concat("#" + req.headers["user-agent"]); 63 | if (!table[host]) 64 | table[host] = { count: 1, expiry: 1 }; 65 | else { 66 | table[host].count++; 67 | if (table[host].count > options.maxcount) 68 | table[host].count = options.maxcount; 69 | if ((table[host].count > options.burst) && (table[host].expiry <= options.maxexpiry)) { 70 | table[host].expiry = Math.min( 71 | options.maxexpiry, 72 | table[host].expiry * 2 73 | ); 74 | } else { 75 | table[host].expiry = 1; 76 | } 77 | } 78 | if (options.testmode) { 79 | console.log("ddos: handle: end:", table); 80 | } 81 | 82 | if (table[host].count > options.limit) { 83 | (!options.testmode) && (console.log("ddos: denied: entry:", host, table[host])); 84 | if (options.testmode) { 85 | return reject({action:'respond', code:429, message:JSON.stringify(table[host])}); 86 | } else { 87 | return reject({action:'respond', code:options.responseStatus, message:options.errormessage}) 88 | } 89 | } else { 90 | return resolve(); 91 | } 92 | }) 93 | }; 94 | exports._handle = _handle; 95 | 96 | exports.handle = function (req, res, next) { 97 | return _handle(this.params, this.table, req) 98 | .then(() => next()) 99 | .catch((e) => { 100 | if (e.action === "nothing") { 101 | return next(); 102 | } 103 | if (e.action === "respond") { 104 | if (this.params.onDenial) { 105 | this.params.onDenial(req) 106 | } 107 | 108 | res.writeHead(e.code, {'Content-Type':'application/json'}); 109 | return res.end(e.message); 110 | } 111 | }) 112 | }; 113 | -------------------------------------------------------------------------------- /test/test-express.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"); 2 | var express = require("express"); 3 | var request = require("supertest-light"); 4 | var QL = require("queuelib"); 5 | 6 | var Ddos = require("../"); 7 | 8 | tape("count and expiry test", function(t) { 9 | t.plan(14); 10 | 11 | var ddos = new Ddos({ burst: 3, limit: 4 }); 12 | var app = express(); 13 | app.use(ddos.express); 14 | var a = function(req, res, next) { 15 | next(); 16 | }; 17 | var b = function(req, res, next) { 18 | // some more random middleware 19 | next(); 20 | }; 21 | var c = function(req, res, next) { 22 | res.writeHead(200, { "Content-Type": "application/json" }); 23 | res.end(JSON.stringify({ foo: "bar" })); 24 | }; 25 | app.get("/article", a, b, c); 26 | 27 | var q = new QL(); 28 | q.series( 29 | [ 30 | lib => { 31 | request(app) 32 | .get("/article") 33 | .then(res => { 34 | t.equal(res.statusCode, 200); 35 | var key = Object.keys(ddos.table)[0]; 36 | t.deepEqual(ddos.table[key], { count: 1, expiry: 1 }); 37 | lib.done(); 38 | }); 39 | }, 40 | lib => { 41 | request(app) 42 | .get("/article") 43 | .then(res => { 44 | t.equal(res.statusCode, 200); 45 | var key = Object.keys(ddos.table)[0]; 46 | t.deepEqual(ddos.table[key], { count: 2, expiry: 1 }); 47 | lib.done(); 48 | }); 49 | }, 50 | lib => { 51 | request(app) 52 | .get("/article") 53 | .then(res => { 54 | t.equal(res.statusCode, 200); 55 | var key = Object.keys(ddos.table)[0]; 56 | t.deepEqual(ddos.table[key], { count: 3, expiry: 1 }); 57 | lib.done(); 58 | }); 59 | }, 60 | lib => { 61 | request(app) 62 | .get("/article") 63 | .then(res => { 64 | t.equal(res.statusCode, 200); 65 | var key = Object.keys(ddos.table)[0]; 66 | t.deepEqual(ddos.table[key], { count: 4, expiry: 2 }); 67 | lib.done(); 68 | }); 69 | }, 70 | lib => { 71 | request(app) 72 | .get("/article") 73 | .then(res => { 74 | t.equal(res.statusCode, 429); 75 | var key = Object.keys(ddos.table)[0]; 76 | t.deepEqual(ddos.table[key], { count: 5, expiry: 4 }); 77 | lib.done(); 78 | }); 79 | }, 80 | lib => { 81 | setTimeout(() => { 82 | request(app) 83 | .get("/article") 84 | .then(res => { 85 | t.equal(res.statusCode, 429); 86 | var key = Object.keys(ddos.table)[0]; 87 | // should start at {count:5, expiry:1} since 4 - 3 = 1 88 | // after a request, it should penalize for being over burst, which means 89 | // expiry goes to 2 90 | t.deepEqual(ddos.table[key], { count: 6, expiry: 2 }); 91 | lib.done(); 92 | }); 93 | }, 3100); 94 | }, 95 | lib => { 96 | setTimeout(() => { 97 | request(app) 98 | .get("/article") 99 | .then(res => { 100 | t.equal(res.statusCode, 200); 101 | var key = Object.keys(ddos.table)[0]; 102 | t.deepEqual(ddos.table[key], { count: 1, expiry: 1 }); 103 | lib.done(); 104 | }); 105 | }, 2100); 106 | } 107 | ], 108 | function() { 109 | ddos.end(); 110 | } 111 | ); 112 | }); 113 | -------------------------------------------------------------------------------- /test/test-options.js: -------------------------------------------------------------------------------- 1 | const tape = require("tape"); 2 | const Ddos = require("../"); 3 | const request = require("supertest-light"); 4 | const express = require("express"); 5 | 6 | tape("options - whitelist ", function(t) { 7 | t.plan(4); 8 | const ddos = new Ddos({ limit: 1, whitelist: ["127.0.0.1"] }); 9 | const app = express(); 10 | let count = 0; 11 | app.use(ddos.express); 12 | app.get("/user", (req, res) => { 13 | count++; 14 | return res.status(200).json({ name: "john" }); 15 | }); 16 | 17 | const doCall = function(code) { 18 | return request(app) 19 | .get("/user") 20 | .then(res => { 21 | t.equal(res.statusCode, code); 22 | }) 23 | .catch(e => { 24 | t.fail(); 25 | }); 26 | }; 27 | 28 | doCall(200) 29 | .then(() => doCall(200)) 30 | .then(() => doCall(200)) 31 | .then(() => { 32 | t.equals(count, 3); 33 | ddos.end(); 34 | }); 35 | }); 36 | 37 | tape("method - addwhitelist ", function(t) { 38 | t.plan(5); 39 | const ddos = new Ddos({ limit: 1 }); 40 | const app = express(); 41 | let count = 0; 42 | app.use(ddos.express); 43 | app.get("/user", (req, res) => { 44 | count++; 45 | return res.status(200).json({ name: "john" }); 46 | }); 47 | 48 | const doCall = function(code) { 49 | return request(app) 50 | .get("/user") 51 | .then(res => { 52 | t.equal(res.statusCode, code); 53 | }) 54 | .catch(e => { 55 | t.fail(); 56 | }); 57 | }; 58 | 59 | doCall(200) 60 | .then(() => doCall(429)) 61 | .then(() => { 62 | t.equals(count, 1); 63 | }) 64 | .then(() => { 65 | ddos.addWhitelist("127.0.0.1"); 66 | return doCall(200); 67 | }) 68 | .then(() => { 69 | t.equals(count, 2); 70 | ddos.end(); 71 | }); 72 | }); 73 | 74 | tape("options - includeUserAgent ", function(t) { 75 | t.plan(4); 76 | const ddos = new Ddos({ includeUserAgent: false, burst: 3, limit: 4 }); 77 | const app = express(); 78 | app.use(ddos.express); 79 | app.get("/user", (req, res) => { 80 | res.status(200).json({ name: "john" }); 81 | }); 82 | 83 | request(app) 84 | .get("/user") 85 | .then(res => { 86 | t.equals(res.headers["content-type"], "application/json; charset=utf-8"); 87 | t.equals(res.headers["content-length"], "15"); 88 | t.equals(res.statusCode, 200); 89 | }) 90 | .then(() => { 91 | ddos.end(); 92 | t.pass(); 93 | }); 94 | }); 95 | 96 | tape("options - trustProxy ", function(t) { 97 | t.plan(4); 98 | const ddos = new Ddos({ trustProxy: false, burst: 3, limit: 4 }); 99 | const app = express(); 100 | app.use(ddos.express); 101 | app.get("/user", (req, res) => { 102 | res.status(200).json({ name: "john" }); 103 | }); 104 | 105 | request(app) 106 | .get("/user") 107 | .then(res => { 108 | t.equals(res.headers["content-type"], "application/json; charset=utf-8"); 109 | t.equals(res.headers["content-length"], "15"); 110 | t.equals(res.statusCode, 200); 111 | }) 112 | .then(() => { 113 | ddos.end(); 114 | t.pass(); 115 | }); 116 | }); 117 | 118 | tape("options - onDenial ", function(t) { 119 | t.plan(1); 120 | let count = 0; 121 | const onDenial = function(req) { 122 | count++; 123 | }; 124 | const ddos = new Ddos({ limit: 1, onDenial }); 125 | const app = express(); 126 | app.use(ddos.express); 127 | app.get("/user", (req, res) => { 128 | console.log("reply"); 129 | res.status(200).json({ name: "john" }); 130 | }); 131 | 132 | const doCall = function() { 133 | return request(app).get("/user"); 134 | }; 135 | 136 | doCall() 137 | .then(() => doCall()) 138 | .then(() => doCall()) 139 | .then(res => { 140 | t.equals(count, 2); 141 | ddos.end(); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Configurable Denial-Of-Service prevention for http services 2 | 3 | 4 | 5 | [![Build Status](https://travis-ci.org/rook2pawn/node-ddos.svg?branch=master)](https://travis-ci.org/rook2pawn/node-ddos) 6 | 7 | [![Coverage Status](https://coveralls.io/repos/github/rook2pawn/node-ddos/badge.svg?branch=master)](https://coveralls.io/github/rook2pawn/node-ddos?branch=master) 8 | 9 | # install 10 | 11 | ``` 12 | npm install --save ddos 13 | ``` 14 | 15 | # setup helper (new!) 16 | 17 | ``` 18 | npm run setup-helper 19 | ``` 20 | 21 | Run `npm run setup-helper` and place the console side by side with your browser window and reload a few times and see how `burst` and `limit` are separate 22 | concepts. `burst` controls the expiry timer, and `limit` is what governs the actual denial. I made a [video tutorial](https://youtu.be/yx2T0oaF2T0) on this, which should 23 | give you an intuitive sense of what's going on. Play with the limit and burst in the `setupHelper.js`. 24 | 25 | 26 | 27 | # A Quick Overview 28 | 29 | ```js 30 | var Ddos = require('ddos') 31 | var express = require('express') 32 | var ddos = new Ddos({burst:2, limit:4}) 33 | var app = express(); 34 | app.use(ddos.express); 35 | ``` 36 | 37 | * **Rule 1** Every request per user increments an internal **count**. When the count exceeds the **limit**, the requests are denied with a HTTP 429 Too Many Requests. 38 | 39 | * **Rule 2** The *only* way for count to go away, is for an internal expiration time to expire, called the **expiry**, and is measured in seconds. Every second, the expiry time will go down by one. 40 | 41 | The first request comes in and the expiry is set to 1 second. If 1 second passes and no additional requests are made, then the entry is removed 42 | from the internal table. In fact, there can be up to **burst** amount of requests made and the **expiry time will not change**. 43 | The only way the expiry goes up is when a request comes, the count goes up, and then if the count *exceeds* the burst amount (greater than, not greater than or equal to), then the expiry goes up to twice its previous value. 44 | 45 | Every time the table is checked (defaults to 1 second, configurable by the **checkinterval** setting), the expiry goes down by that amount of time. 46 | Now we loop back to **Rule 2** when that when expiry is less than or equal to 0, then that entry is removed along with the count. 47 | 48 | 49 | ## Features 50 | 51 | [![Join the chat at https://gitter.im/rook2pawn/node-ddos](https://badges.gitter.im/rook2pawn/node-ddos.svg)](https://gitter.im/rook2pawn/node-ddos?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 52 | 53 | * support the X-Forwarded-For header in a reverse proxy request 54 | 55 | ## Supports 56 | 57 | * HapiJS 17+ 58 | * HapiJS 16 and before 59 | * Express 4+ 60 | * Koa 61 | 62 | ### With [Express](https://github.com/expressjs/expressjs.com "Express") 63 | 64 | ```js 65 | var Ddos = require('ddos') 66 | var express = require('express') 67 | var ddos = new Ddos; 68 | var app = express(); 69 | app.use(ddos.express) 70 | ``` 71 | 72 | or with a router 73 | 74 | ```js 75 | const router = express.Router(); 76 | 77 | router.use(ddos.express); 78 | router.get("/", (req,res,next) => { 79 | console.log("Beep"); 80 | res.end("Boop"); 81 | }) 82 | app.use(router); 83 | ``` 84 | This way, all paths defined on the router will be protected. 85 | 86 | You can also place it **only on sensitive database write paths or cpu/disk intensive operations** : 87 | 88 | ```js 89 | app.post('/user', ddos.express, ); 90 | ``` 91 | 92 | ### With [HapiJS 17+](https://hapijs.com/ "HapiJS") 93 | 94 | ```js 95 | var Ddos = require('ddos') 96 | var Hapi = require('hapi'); 97 | 98 | var ddos = new Ddos; 99 | const server = Hapi.server({ 100 | port: 3000, 101 | host: "localhost" 102 | }); 103 | server.route({ 104 | method: "GET", 105 | path: "/", 106 | handler: (request, h) => { 107 | return "Hello, world!"; 108 | } 109 | }); 110 | server.ext("onRequest", ddos.hapi17.bind(ddos)); 111 | 112 | server.start() 113 | .then(() => { 114 | 115 | }) 116 | 117 | ``` 118 | 119 | ### With [HapiJS 16 and before](https://hapijs.com/ "HapiJS") 120 | 121 | ```js 122 | var Ddos = require('ddos') 123 | var Hapi = require('hapi'); 124 | 125 | var ddos = new Ddos; 126 | const server = new Hapi.Server(); 127 | server.ext('onRequest', ddos.hapi.bind(ddos)); 128 | ``` 129 | 130 | ### With [Koa](http://koajs.com "KoaJS") 131 | 132 | ```js 133 | var Ddos = require('ddos') 134 | var koa = require('koa') 135 | var ddos = new Ddos; 136 | 137 | var app = new koa; 138 | app.use(ddos.koa().bind(ddos)) // be sure to bind ddos as koa rebinds the context 139 | ``` 140 | 141 | ### With [Router-Middleware](https://github.com/rook2pawn/router-middleware "Router Middleware") 142 | 143 | ```js 144 | var Router = require('router-middleware'); 145 | var Ddos = require('ddos') 146 | 147 | var ddos = new Ddos; 148 | var app = Router(); 149 | app.use(ddos); 150 | ``` 151 | 152 | ## How does this ddos prevention module work? 153 | 154 | Every request marks the internal table and increments the `count`. 155 | This is how an entry in the table managed by this module looks 156 | 157 | { host : , count: 1, expiry: 1 } 158 | 159 | When a second request is made 160 | 161 | { host : , count: 2, expiry: 1 } 162 | 163 | and the third 164 | 165 | { host : , count: 3, expiry: 1 } 166 | 167 | and so on. If the count exceeds the configurable **burst** amount, then the expiry goes up by twice the previous expiry, 1, 2, 4, 8, 16, etc. 168 | 169 | When count exceeds the **limit**, then the request is denied, otherwise, the request is permitted. 170 | 171 | Every time the internal table is checked, the expiration goes down by the time elapsed. 172 | 173 | The only way for a user who has denied requests to continue is for them to let the expiration time pass, and when expiration hits 0, the entry is deleted from the table, and new requests are allowed like normal. 174 | 175 | ## Processing and Memory Usage by this module 176 | 177 | There is only ONE table, and within it only one small entry per IP, and that entry is transient and will be deleted within normal parameters. The table itself is combed over at the configurable **checkinterval** in seconds. 178 | 179 | ## Yes, this will not deal with distributed denial-of-service attacks 180 | 181 | But it will deal with simple DOS ones, but the concept is associated with DDOS whereas DOS is about the classic operating system from the 90's. 182 | 183 | 184 | ## Let's review Configuration 185 | 186 | To override any configuration option, simply specify it at construction time. 187 | 188 | ```js 189 | var Ddos = require('ddos'); 190 | var ddos = new Ddos({burst:3,limit:4,testmode:true,whitelist:['74.125.224.72']}); 191 | ``` 192 | 193 | Let's go over the configuration options to help illustrate how this module works. 194 | All of the configurations default to the following: 195 | 196 | params.maxcount = 30; 197 | params.burst = 5; 198 | params.limit = _params.burst * 4; 199 | params.maxexpiry = 120; 200 | params.checkinterval = 1; 201 | params.trustProxy = true; 202 | params.includeUserAgent = true; 203 | params.whitelist = []; 204 | params.errormessage = 'Error'; 205 | params.testmode = false; 206 | params.responseStatus = 429; 207 | 208 | ### testmode 209 | 210 | `testmode` allows you to see exactly how your setup is functioning. 211 | 212 | ### limit 213 | 214 | `limit` is the number of maximum counts allowed (do not confuse that with maxcount). `count` increments with each request. 215 | If the `count` exceeds the `limit`, then the request is denied. Recommended limit is to use a multiple of the number of bursts. 216 | 217 | 218 | ### maxcount 219 | 220 | When the `count` exceeds the `limit` and then the `maxcount`, the count is reduced to the `maxcount`. The maxcount is simply is the maximum amount of "punishment" that could be applied to a denial time-out. 221 | 222 | 223 | ### burst 224 | 225 | Burst is the number or amount of allowable burst requests before the client starts being penalized. 226 | When the client is penalized, the expiration is increased by twice the previous expiration. 227 | 228 | 229 | ### maxexpiry 230 | 231 | maxexpiry is the seconds of maximum amount of expiration time. 232 | In order for the user to use whatever service you are providing again, they have to wait through the expiration time. 233 | 234 | 235 | ### checkinterval 236 | 237 | checkinterval is the seconds between updating the internal table. 238 | 239 | ### trustProxy 240 | 241 | Defaults to true. If true then we use the x-forwarded-for header, otherwise we use the remote address. 242 | 243 | ```js 244 | var host = _params.trustProxy ? (req.headers['x-forwarded-for'] || req.connection.remoteAddress) : req.connection.remoteAddress 245 | ``` 246 | 247 | ### includeUserAgent 248 | 249 | Defaults to true. If true we include the user agent as part of identifying a unique user. If false, then we only use IP. If set to false 250 | this can lead to an entire block being banned unintentionally. Included to leave it up to the developer how they want to use it. 251 | 252 | 253 | ### whitelist 254 | 255 | Defaults to empty list. Specify the IP's or addresses you would like to whitelist 256 | 257 | ```js 258 | var Ddos = require('ddos'); 259 | var ddos = new Ddos({whitelist:['74.125.224.72', '216.239.63.255']}); 260 | ``` 261 | 262 | Whitelisted IP's bypass all table checks. If the address in question is in IPV6 form, simply enable testmode 263 | 264 | ```js 265 | var ddos = new Ddos({whitelist:['74.125.224.72', '216.239.63.255'], testmode:true}); 266 | ``` 267 | 268 | and see the exact form of the address you want to whitelist. See this [link on stackoverflow about IPv6 addresses](http://stackoverflow.com/questions/29411551/express-js-req-ip-is-returning-ffff127-0-0-1) 269 | 270 | ### .addWhitelist(ip) 271 | 272 | Update whitelist while running. 273 | 274 | ```js 275 | ddos.addWhitelist('74.125.224.72') 276 | ``` 277 | 278 | ### errormessage 279 | 280 | When a request is denied, the user receives a 429 and the error message. 281 | 282 | ### responseStatus 283 | 284 | By default HTTP status code 429 (Too Many Requests) are sent in response. 285 | 286 | ### onDenial 287 | 288 | If this callback is specified, it will be called with the `req` object on a denial. Useful for logging. 289 | 290 | ```js 291 | const onDenial = function(req) { 292 | // log it 293 | } 294 | const ddos = new Ddos({ limit: 2, onDenial }); 295 | ``` 296 | 297 | Contribute 298 | ========== 299 | 300 | Contributions welcome! 301 | 302 | 303 | LICENSE 304 | ======= 305 | 306 | MIT 307 | --------------------------------------------------------------------------------