├── .travis.yml ├── test ├── GeoLite2-Country.mmdb └── index.js ├── .gitignore ├── package.json ├── LICENSE ├── index.js └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.12" -------------------------------------------------------------------------------- /test/GeoLite2-Country.mmdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilich/node-ipgeoblock/HEAD/test/GeoLite2-Country.mmdb -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-ipgeoblock", 3 | "version": "0.1.4", 4 | "description": "Middleware to allow or block requests based on origin country.", 5 | "scripts": { 6 | "test": "mocha" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/ilich/node-ipgeoblock.git" 11 | }, 12 | "keywords": [ 13 | "security", 14 | "express", 15 | "connect", 16 | "firewall", 17 | "ip", 18 | "maxmind", 19 | "geoip2", 20 | "geoip" 21 | ], 22 | "author": "Ilya Verbitskiy", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/ilich/node-ipgeoblock/issues" 26 | }, 27 | "homepage": "https://github.com/ilich/node-ipgeoblock#readme", 28 | "devDependencies": { 29 | "connect": "^3.4.0", 30 | "mocha": "^2.2.5", 31 | "supertest": "^1.0.1" 32 | }, 33 | "dependencies": { 34 | "mmdb-reader": "0.0.4", 35 | "request-ip": "^1.1.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ilya Verbitskiy 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 | 23 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require("fs"); 4 | var requestIp = require("request-ip"); 5 | var MMDBReader = require("mmdb-reader"); 6 | 7 | module.exports = function (options, accessDenied) { 8 | 9 | accessDenied = accessDenied || function (req, res) { 10 | res.statusCode = 403; 11 | res.end("Forbidden"); 12 | }; 13 | 14 | options = options || {}; 15 | verifyOptions(options); 16 | 17 | var mmdb = new MMDBReader(options.geolite2); 18 | 19 | function verifyOptions() { 20 | if (!options.geolite2) { 21 | throw new Error("options.geolite2 is not set"); 22 | } 23 | 24 | // Check that geolite2 exists (fs.exists is deprecated) 25 | var geo2 = fs.openSync(options.geolite2, "r"); 26 | fs.close(geo2); 27 | 28 | options.blocked = options.blocked || []; 29 | options.blockedCountries = options.blockedCountries || []; 30 | options.allowedCountries = options.allowedCountries || []; 31 | 32 | if (options.blockedCountries.length > 0 && options.allowedCountries.length > 0) { 33 | throw new Error("You have to choose only allowed contries or only blocked countries"); 34 | } 35 | } 36 | 37 | function getIP(req) { 38 | var ip = requestIp.getClientIp(req); 39 | if (ip !== null) { 40 | ip = ip.split(":"); 41 | ip = ip[ip.length - 1]; 42 | } 43 | 44 | return ip; 45 | } 46 | 47 | function isBlocked(ip, req, res) { 48 | req.location = req.location || {}; 49 | req.location.country = { 50 | data: null, 51 | isoCode: "" 52 | }; 53 | 54 | // 1. Check that IP address is blocked 55 | if (options.blocked.indexOf(ip) > -1) { 56 | return true; 57 | } 58 | 59 | var blocked = false; 60 | var query = mmdb.lookup(ip); 61 | if (options.blockedCountries.length > 0) { 62 | 63 | // 2. If user added country to Blocked Countries collection then only those countries 64 | // are blocked 65 | 66 | blocked = query !== null && options.blockedCountries.indexOf(query.country.iso_code) > -1; 67 | } else if (options.allowedCountries.length > 0) { 68 | 69 | // 3. If user added country to Allowed Countries collecction then all countries except allowed 70 | // are blocked 71 | 72 | blocked = query === null || options.allowedCountries.indexOf(query.country.iso_code) === -1; 73 | } 74 | 75 | if (!blocked && query !== null) { 76 | req.location.country.data = query; 77 | req.location.country.isoCode = query.country.iso_code; 78 | } 79 | 80 | return blocked; 81 | } 82 | 83 | return function (req, res, next) { 84 | var ip = getIP(req); 85 | if (isBlocked(ip, req, res)) { 86 | accessDenied(req, res); 87 | return; 88 | } 89 | 90 | next(); 91 | }; 92 | 93 | }; -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var ipgeoblock = require(".."); 4 | var assert = require("assert"); 5 | var connect = require("connect"); 6 | var request = require("supertest"); 7 | 8 | describe("node-ipgeoblock", function () { 9 | 10 | function createApp(isWhitelist, accessDenied) { 11 | var blacklist = isWhitelist ? [] : ["FR"]; 12 | var whitelist = isWhitelist ? ["FR"] : []; 13 | 14 | var filterOption = { 15 | geolite2: "./test/GeoLite2-Country.mmdb", 16 | blocked: ["192.168.0.1", "192.168.0.3", "192.168.0.4"], 17 | blockedCountries: blacklist, 18 | allowedCountries: whitelist 19 | }; 20 | 21 | var app = connect(); 22 | 23 | if (accessDenied) { 24 | app.use(ipgeoblock(filterOption, accessDenied)); 25 | } else { 26 | app.use(ipgeoblock(filterOption)); 27 | } 28 | 29 | app.use(function (req, res) { 30 | res.setHeader("x-test-country", req.location.country.isoCode); 31 | res.end("OK"); 32 | }); 33 | 34 | return app; 35 | } 36 | 37 | var app = createApp(false); 38 | 39 | it("block specific IP address with custom error handler", function (done) { 40 | var app = createApp(false, function (req, res) { 41 | res.statusCode = 500; 42 | res.end("Internal Server Error"); 43 | }); 44 | 45 | request(app).get("/") 46 | .set("x-client-ip", "192.168.0.1") 47 | .expect(500) 48 | .expect("Internal Server Error", done); 49 | }); 50 | 51 | it("block specific IP address", function (done) { 52 | request(app).get("/") 53 | .set("x-client-ip", "192.168.0.1") 54 | .expect(403) 55 | .expect("Forbidden", done); 56 | }); 57 | 58 | it("allow IP address when IP blacklist is used", function (done) { 59 | request(app).get("/") 60 | .set("x-client-ip", "192.168.0.2") 61 | .expect("x-test-country", "") 62 | .expect(200) 63 | .expect("OK", done); 64 | }); 65 | 66 | it("block France IP address when only France is in blackist", function (done) { 67 | request(app).get("/") 68 | .set("x-client-ip", "213.128.63.255") 69 | .expect(403) 70 | .expect("Forbidden", done); 71 | }); 72 | 73 | it("allow Greece IP address when ony only France is in blacklist", function (done) { 74 | request(app).get("/") 75 | .set("x-client-ip", "46.246.128.0") 76 | .expect("x-test-country", "GR") 77 | .expect(200) 78 | .expect("OK", done); 79 | }); 80 | 81 | it("allow unknown IP address when ony only France is in blacklist", function (done) { 82 | request(app).get("/") 83 | .set("x-client-ip", "192.168.0.2") 84 | .expect("x-test-country", "") 85 | .expect(200) 86 | .expect("OK", done); 87 | }); 88 | 89 | it("allow France IP address when only France is in whitelist", function (done) { 90 | var appWhitelist = createApp(true); 91 | 92 | request(appWhitelist).get("/") 93 | .set("x-client-ip", "213.128.63.255") 94 | .expect("x-test-country", "FR") 95 | .expect(200) 96 | .expect("OK", done); 97 | }); 98 | 99 | it("block Greece IP address when ony only France is in whitelist", function (done) { 100 | var appWhitelist = createApp(true); 101 | 102 | request(appWhitelist).get("/") 103 | .set("x-client-ip", "46.246.128.0") 104 | .expect(403) 105 | .expect("Forbidden", done); 106 | }); 107 | 108 | 109 | it("block unknows IP address when ony only France is in whitelist", function (done) { 110 | var appWhitelist = createApp(true); 111 | 112 | request(appWhitelist).get("/") 113 | .set("x-client-ip", "192.168.0.2") 114 | .expect(403) 115 | .expect("Forbidden", done); 116 | }); 117 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-ipgeoblock 2 | =============== 3 | 4 | [![Build Status](https://travis-ci.org/ilich/node-ipgeoblock.svg?branch=master)](https://travis-ci.org/ilich/node-ipgeoblock) 5 | 6 | Node-ipgeoblock helps you secure your Express application by intoducing the blacklist of IPs, the blacklist of countries or the whitelist of countries. 7 | 8 | This product includes GeoLite2 data created by MaxMind, available from [http://www.maxmind.com](http://www.maxmind.com). 9 | 10 | Installation 11 | ------------ 12 | 13 | First, run `npm install node-ipgeoblock` for your application. 14 | 15 | Then download the latest version of [MaxMind GeoLite2 Country Database](https://dev.maxmind.com/geoip/geoip2/geolite2/) and save it somewhere in the way that you application can access it. Make sure you download the database in MaxMind DB format. We will use *./GeoLite2-Country.mmdb* in the following examples. 16 | 17 | The last step is to register node-ipgeoblock middleware in your Express (or Connect) application. 18 | 19 | IPs blacklist 20 | --------------------- 21 | 22 | ```javascript 23 | var ipgeoblock = require("node-ipgeoblock"); 24 | 25 | var app = express(); 26 | 27 | app.use(ipgeoblock({ 28 | geolite2: "./GeoLite2-Country.mmdb", 29 | blocked: ["192.168.0.1", "192.168.0.3", "192.168.0.4"] 30 | })); 31 | ``` 32 | 33 | Countries blacklist 34 | -------------------------- 35 | 36 | ```javascript 37 | var ipgeoblock = require("node-ipgeoblock"); 38 | 39 | var app = express(); 40 | 41 | app.use(ipgeoblock({ 42 | geolite2: "./GeoLite2-Country.mmdb", 43 | blockedCountries: [ "FR", "GB", "DE" ] 44 | })); 45 | ``` 46 | 47 | When Countries Blackist is used you allow access for all IPs except IPs assigned to the countres from the blacklist. The Country Code **MUST** be upper case [ISO 3166-2](https://en.wikipedia.org/wiki/ISO_3166-2) Code. 48 | 49 | Countries whitelist 50 | -------------------------- 51 | 52 | ```javascript 53 | var ipgeoblock = require("node-ipgeoblock"); 54 | 55 | var app = express(); 56 | 57 | app.use(ipgeoblock({ 58 | geolite2: "./GeoLite2-Country.mmdb", 59 | allowedCountries: [ "FR", "GB", "DE" ] 60 | })); 61 | ``` 62 | 63 | When Countries Whitelist is uses you restict access to the application only for the IPs assigned to the countries in the whitelist. The Country Code **MUST** be upper case [ISO 3166-2](https://en.wikipedia.org/wiki/ISO_3166-2) Code. 64 | 65 | Combine IPs and countries blacklists 66 | ------------------------------------------ 67 | 68 | You can specify only countries blacklist or whitelist. You cannot use both at the same time. But you can use IPs blacklist and countries blacklist or whitelist. 69 | 70 | ```javascript 71 | var ipgeoblock = require("node-ipgeoblock"); 72 | 73 | var app = express(); 74 | 75 | app.use(ipgeoblock({ 76 | geolite2: "./GeoLite2-Country.mmdb", 77 | blocked: ["192.168.0.1", "192.168.0.3", "192.168.0.4"], 78 | blockedCountries: [ "FR", "GB", "DE" ] 79 | })); 80 | ``` 81 | 82 | Get country information for a request 83 | -------------------------------------------- 84 | 85 | Node-ipgeoblock adds IP country information to the request object. 86 | 87 | ```javascript 88 | app.use(function (req, res) { 89 | // MaxMind GeoLite2 country object 90 | console.log(JSON.stringify(req.location.country.data)); 91 | 92 | // Country ISO 3166-2 code 93 | console.log(req.location.country.isoCode); 94 | }); 95 | ``` 96 | 97 | Custom access denied error hander 98 | -------------------------------------------- 99 | 100 | ```javascript 101 | var ipgeoblock = require("node-ipgeoblock"); 102 | 103 | var app = express(); 104 | 105 | app.use(ipgeoblock({ 106 | geolite2: "./GeoLite2-Country.mmdb", 107 | blocked: ["192.168.0.1", "192.168.0.3", "192.168.0.4"] 108 | }, function (req, res) { 109 | res.statusCode = 500; 110 | res.end("Internal Server Error"); 111 | })); 112 | ``` --------------------------------------------------------------------------------