Skip to content

Commit

Permalink
rate limiting progress
Browse files Browse the repository at this point in the history
  • Loading branch information
10A7 committed Feb 2, 2018
1 parent 6c42df1 commit a63813c
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 37 deletions.
2 changes: 2 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const glob = require('glob');
const cluster = require('cluster');
const redis = require('redis');

process.env.EGS_APP_ROOT = __dirname;

if (cluster.isMaster) {
let processes = require('os').cpus().length;
if (process.env.WORKER_PROCESSES) {
Expand Down
39 changes: 4 additions & 35 deletions lib/RateLimiter.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ const limiter = require('express-rate-limit');
const limiterRedisStore = require('rate-limit-redis');

const settings = require('../lib/EGSSettings');

const redis_helper = require('../lib/helpers/redis');

// TODO: this code could use a refactor/cleanup now that it is in its own file

var bootstrapLimiter = function() {

// initialize rate limiting
Expand All @@ -20,37 +19,7 @@ var bootstrapLimiter = function() {
let APIRateLimiter = null;
let limiterOptions = {};
try {
let redisProtocol = settings.getSetting('redis', 'protocol');
if (settings.getSetting('redis', 'protocol') === 'redis') {
// XXX abstract to helper
let connString = 'redis://';
try {
let password = settings.getSetting('password');
connString += ':' + password + '@';
}
catch(e) {
console.warn("RateLimiter: Redis password not set. Not authenticating.");
}
connString += settings.getSetting('redis', 'hostname');
try {
let port = settings.getSetting('redis', 'port');
connString += ':' + port;
}
catch(e) {
console.warn("RateLimiter: Redis port not set. Defaulting to 6379");
connString += ':6379/';
}
console.warn("RateLimiter: Redis at " + connString);
redisClient = redis.createClient(connString);
}
else if (redisProtocol === 'unix') {
// TODO: finish socket
redisClient = redis.createClient({
path: settings.getSetting('redis', 'path')
});
} else {
throw new Error("Bad Redis protocol.");
}
let redisClient = redis_helper.getRedisClient();
limiterOptions = {
store: new limiterRedisStore({
client: redisClient
Expand All @@ -70,10 +39,10 @@ var bootstrapLimiter = function() {
}

// add proper keying for proxy
if (settings.getSetting('api', 'rate_limit_reverse_proxy') === true) {
if (settings.getSetting('api', 'behind_reverse_proxy') === true) {
limiterOptions['key'] = (req) => {
try {
let real_ip_header = settings.getSetting('api', 'rate_limit_real_ip_header');
let real_ip_header = settings.getSetting('api', 'proxy_real_ip_header');
let real_header_value = req.header(real_ip_header);
if (real_header_value) {
return real_header_value;
Expand Down
43 changes: 43 additions & 0 deletions lib/helpers/redis.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Various Redis code used in a lot of places.
*/

var RedisHelpers = {

getRedisClient: function() {
let redisProtocol = settings.getSetting('redis', 'protocol');
if (settings.getSetting('redis', 'protocol') === 'redis') {
let connString = 'redis://';
try {
let password = settings.getSetting('password');
connString += ':' + password + '@';
}
catch(e) {
console.warn("getRedisClient: Redis password not set. Not authenticating.");
}
connString += settings.getSetting('redis', 'hostname');
try {
let port = settings.getSetting('redis', 'port');
connString += ':' + port;
}
catch(e) {
console.warn("getRedisClient: Redis port not set. Defaulting to 6379");
connString += ':6379/';
}
console.warn("RateLimiter: Redis at " + connString);
redisClient = redis.createClient(connString);
}
else if (redisProtocol === 'unix') {
// TODO: finish socket
redisClient = redis.createClient({
path: settings.getSetting('redis', 'path')
});
} else {
throw new Error("Bad Redis protocol.");
}
return redisClient;
}

};

module.exports = RedisHelpers;
142 changes: 142 additions & 0 deletions lib/middleware/Banhammer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* Banhammer
* IP Blacklisting
**/

const redis = require('redis');
const path = require('path');
const fs = require('fs');

const settings = require('../EGSSettings');
const redis_helper = require('../helpers/redis');

class Banhammer {

constructor (options) {
this.defaults = {
backingStore: 'redis',
redisPrefix: 'banhammer_'
}
this.options = Object.assign({}, this.defaults, options);
this.proxyEnabled = (settings.getSetting('api', 'behind_reverse_proxy') === true);
if (this.proxyEnabled) {
this.proxyHeader = settings.getSetting('api', 'proxy_real_ip_header');
}

switch (this.backingStore) {
case 'redis':
this.redis = redis_helper.getRedisClient();
break;

case 'fs':
this.api_key_file = settings.getSetting('api', 'banhammer_api_key_file');
this.ip_addr_file = settings.getSetting('api', 'banhammer_ip_addr_file');
if (!fs.existsSync(this.api_key_file)) {
console.warn("Banhammer: no API key file found, creating");
fs.closeSync(fs.openSync(this.api_key_file, 'w'));
}
if (!fs.existsSync(this.ip_addr_file)) {
// attempt to create?
console.warn("Banhammer: no IP file found, creating");
fs.closeSync(fs.openSync(this.ip_addr_file, 'w'));
}
break;

default:
throw new Error("Unsupported backing store.");
}
}

/**
* Express.js middleware filter.
*
* @param {*} req
* @param {*} res
* @param {*} next
*/
filter (req, res, next) {
let userIP = this._getClientIP(req);
if (this._isBannedAddress(userIP)) {
res.status(401);
res.json({
status: 'error',
msg: 'IP address blocked.'
});
} else if (req.api_key &&
this._isBannedAPIKey(req.api_key)) {
res.status(401);
res.json({
status: 'error',
msg: 'API key blocked.'
});
} else {
next();
}
}

// TODO: add logging infrastructure.
// attempts by banned addresses should be logged.
_isBannedAddress (ip_addr) {
switch (this.backingStore) {
case 'redis':
return this._isBannedRedis(ip_addr);
case 'fs':
return this._isBannedAddressFS(ip_addr);
default:
throw new Error("Banhammer: Undefined backing store.");
}
}

_isBannedAPIKey (api_key) {
switch (this.backingStore) {
case 'redis':
return this._isBannedRedis(api_key);
case 'fs':
return this._isBannedAPIKeyFS(api_key);
default:
throw new Error("Banhammer: Undefined backing store.");
}
}

_isBannedAddressFS (ip_addr) {
return this._existsInFile(this.ip_addr_file, ip_addr);
}

_isBannedAPIKeyFS (api_key) {
return this._existsInFile(this.api_key_file, api_key);
}

_existsInFile (filepath, string) {
let res = fs.readFileSync(filepath, 'utf-8');
let entries = res.split("\n");
entries.forEach((possible_match) => {
if (string === possible_match) {
return true;
}
});
return false;
}

_isBannedRedis (ip_addr_or_api_key) {
return this.redis.exists(this._getRedisKey(ip_addr_or_api_key)) >= 1;
}

_getRedisKey (data) {
return this.options.redisPrefix + data.toString();
}

_getClientIP (req) {
if (this.proxyEnabled) {
let ip = req.headers[this.proxyHeader];
if (ip === '') {
throw new Error("Banhammer: Cannot filter, proxy header not set by upstream");
}
return ip;
} else {
return req.ip;
}
}

}

module.exports = Banhammer;
21 changes: 19 additions & 2 deletions settings.docker.conf
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,25 @@
; The API layer, ethgasstation-api, uses these settings as its defaults.
; You will want to change them if you are running a public API.
[api]
; reverse proxy info
; if the application is behind a load balancer or cloudflare,
; set "behind_reverse_proxy" to true.
behind_reverse_proxy = false
; proxy servers send the client IP via a different header, such
; as x-real-ip or x-forwarded-for.
proxy_real_ip_header = x-forwarded-for

; ip rate limiting
; this rate limits IP addresses to stop bad (or stupid) actors
rate_limit_disable = false
rate_limit_max_requests = 120
rate_limit_request_window_seconds = 60
rate_limit_reverse_proxy = false
rate_limit_real_ip_header = x-forwarded-for

; ip or api key banning
; used to permanently blacklist specific IPs
banhammer_disable = false
banhammer_backing_store = redis
; if you aren't running redis, you can use flat files.
; this isn't recommended because it will be slow.
banhammer_api_key_file = /tmp/api_ban.txt
banhammer_ip_addr_file = /tmp/ip_ban.txt

0 comments on commit a63813c

Please sign in to comment.