Skip to content

Commit

Permalink
feat(api): Rate Limits
Browse files Browse the repository at this point in the history
  • Loading branch information
nitsujlangston committed Dec 23, 2018
1 parent 3f9fbcd commit cee765f
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 1 deletion.
5 changes: 5 additions & 0 deletions packages/bitcore-node/src/config.ts
Expand Up @@ -58,6 +58,11 @@ const Config = function(): ConfigType {
dbPort: process.env.DB_PORT || '27017',
numWorkers: cpus().length,
api: {
rateLimiter: {
whitelist: [
'::ffff:127.0.0.1'
]
},
wallets: {
allowCreationBeforeCompleteSync: false,
allowUnauthenticatedCalls: false
Expand Down
56 changes: 56 additions & 0 deletions packages/bitcore-node/src/models/rateLimit.ts
@@ -0,0 +1,56 @@
import { BaseModel } from './base';
import { ObjectID } from 'mongodb';

export type IRateLimit = {
_id?: ObjectID;
identifier: string;
method: string;
period: string;
count: number;
time?: Date;
expireAt?: Date;
value?: any;
};

export class RateLimit extends BaseModel<IRateLimit> {
constructor() {
super('ratelimits');
}
allowedPaging = [];

onConnect() {
this.collection.createIndex({ identifier: 1, time: 1, method: 1, count: 1 }, { background: true });
this.collection.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0, background: true });
}

incrementAndCheck(identifier: string, method: string) {
return Promise.all([
this.collection.findOneAndUpdate(
{ identifier, method, period: 'second', time: {$gt: new Date(Date.now() - 1000)} },
{
$setOnInsert: { time: new Date(), expireAt: new Date(Date.now() + 10 * 1000) },
$inc: { count: 1 }
},
{ upsert: true, returnOriginal: false }
),
this.collection.findOneAndUpdate(
{ identifier, method, period: 'minute', time: { $gt: new Date(Date.now() - 60 * 1000) } },
{
$setOnInsert: { time: new Date(), expireAt: new Date(Date.now() + 2 * 60 * 1000) },
$inc: { count: 1 }
},
{ upsert: true, returnOriginal: false }
),
this.collection.findOneAndUpdate(
{ identifier, method, period: 'hour', time: { $gt: new Date(Date.now() - 60 * 60 * 1000) } },
{
$setOnInsert: { time: new Date(), expireAt: new Date(Date.now() + 2 * 60 * 1000) },
$inc: { count: 1 }
},
{ upsert: true, returnOriginal: false }
),
]);
}
}

export let RateLimitModel = new RateLimit();
3 changes: 2 additions & 1 deletion packages/bitcore-node/src/routes/index.ts
Expand Up @@ -2,10 +2,11 @@ import config from '../config';
import { Request, Response } from 'express';
import express from 'express';
import cors from 'cors';
import { LogRequest } from "./middleware";
import { LogRequest, RateLimiter } from "./middleware";

const app = express();
const bodyParser = require('body-parser');
app.use(RateLimiter('GLOBAL', 10, 200, 4000));
app.use(bodyParser.json());
app.use(
bodyParser.raw({
Expand Down
23 changes: 23 additions & 0 deletions packages/bitcore-node/src/routes/middleware.ts
@@ -1,5 +1,7 @@
import logger from '../logger';
import * as express from 'express';
import { RateLimitModel } from '../models/rateLimit';
import config from '../config';

type TimedRequest = {
startTime?: Date;
Expand Down Expand Up @@ -42,3 +44,24 @@ export function LogRequest(req: TimedRequest, res: express.Response, next: expre
res.on('close', LogPhase('CLOSED'));
next();
}

export function RateLimiter(method: string, perSecond: number, perMinute: number, perHour: number) {
return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
try {
const identifier = req.header('CF-Connecting-IP') || req.socket.remoteAddress || '';
if (config.api.rateLimiter.whitelist.includes(identifier)) {
return next();
}
let [perSecondResult, perMinuteResult, perHourResult] = await RateLimitModel.incrementAndCheck(identifier, method);
if (
(perSecondResult.value as any).count > perSecond ||
(perMinuteResult.value as any).count > perMinute ||
(perHourResult.value as any).count > perHour) {
return res.status(429).send('Rate Limited');
}
} catch (err) {
logger.error('Rate Limiter failed');
}
return next();
}
}
3 changes: 3 additions & 0 deletions packages/bitcore-node/src/types/Config.ts
Expand Up @@ -10,6 +10,9 @@ export default interface Config {
[currency: string]: any;
};
api: {
rateLimiter: {
whitelist: [string];
},
wallets: {
allowCreationBeforeCompleteSync?: boolean;
allowUnauthenticatedCalls?: boolean;
Expand Down

0 comments on commit cee765f

Please sign in to comment.