Skip to content

Commit cee765f

Browse files
feat(api): Rate Limits
1 parent 3f9fbcd commit cee765f

File tree

5 files changed

+89
-1
lines changed

5 files changed

+89
-1
lines changed

packages/bitcore-node/src/config.ts

+5
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ const Config = function(): ConfigType {
5858
dbPort: process.env.DB_PORT || '27017',
5959
numWorkers: cpus().length,
6060
api: {
61+
rateLimiter: {
62+
whitelist: [
63+
'::ffff:127.0.0.1'
64+
]
65+
},
6166
wallets: {
6267
allowCreationBeforeCompleteSync: false,
6368
allowUnauthenticatedCalls: false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { BaseModel } from './base';
2+
import { ObjectID } from 'mongodb';
3+
4+
export type IRateLimit = {
5+
_id?: ObjectID;
6+
identifier: string;
7+
method: string;
8+
period: string;
9+
count: number;
10+
time?: Date;
11+
expireAt?: Date;
12+
value?: any;
13+
};
14+
15+
export class RateLimit extends BaseModel<IRateLimit> {
16+
constructor() {
17+
super('ratelimits');
18+
}
19+
allowedPaging = [];
20+
21+
onConnect() {
22+
this.collection.createIndex({ identifier: 1, time: 1, method: 1, count: 1 }, { background: true });
23+
this.collection.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0, background: true });
24+
}
25+
26+
incrementAndCheck(identifier: string, method: string) {
27+
return Promise.all([
28+
this.collection.findOneAndUpdate(
29+
{ identifier, method, period: 'second', time: {$gt: new Date(Date.now() - 1000)} },
30+
{
31+
$setOnInsert: { time: new Date(), expireAt: new Date(Date.now() + 10 * 1000) },
32+
$inc: { count: 1 }
33+
},
34+
{ upsert: true, returnOriginal: false }
35+
),
36+
this.collection.findOneAndUpdate(
37+
{ identifier, method, period: 'minute', time: { $gt: new Date(Date.now() - 60 * 1000) } },
38+
{
39+
$setOnInsert: { time: new Date(), expireAt: new Date(Date.now() + 2 * 60 * 1000) },
40+
$inc: { count: 1 }
41+
},
42+
{ upsert: true, returnOriginal: false }
43+
),
44+
this.collection.findOneAndUpdate(
45+
{ identifier, method, period: 'hour', time: { $gt: new Date(Date.now() - 60 * 60 * 1000) } },
46+
{
47+
$setOnInsert: { time: new Date(), expireAt: new Date(Date.now() + 2 * 60 * 1000) },
48+
$inc: { count: 1 }
49+
},
50+
{ upsert: true, returnOriginal: false }
51+
),
52+
]);
53+
}
54+
}
55+
56+
export let RateLimitModel = new RateLimit();

packages/bitcore-node/src/routes/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import config from '../config';
22
import { Request, Response } from 'express';
33
import express from 'express';
44
import cors from 'cors';
5-
import { LogRequest } from "./middleware";
5+
import { LogRequest, RateLimiter } from "./middleware";
66

77
const app = express();
88
const bodyParser = require('body-parser');
9+
app.use(RateLimiter('GLOBAL', 10, 200, 4000));
910
app.use(bodyParser.json());
1011
app.use(
1112
bodyParser.raw({

packages/bitcore-node/src/routes/middleware.ts

+23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import logger from '../logger';
22
import * as express from 'express';
3+
import { RateLimitModel } from '../models/rateLimit';
4+
import config from '../config';
35

46
type TimedRequest = {
57
startTime?: Date;
@@ -42,3 +44,24 @@ export function LogRequest(req: TimedRequest, res: express.Response, next: expre
4244
res.on('close', LogPhase('CLOSED'));
4345
next();
4446
}
47+
48+
export function RateLimiter(method: string, perSecond: number, perMinute: number, perHour: number) {
49+
return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
50+
try {
51+
const identifier = req.header('CF-Connecting-IP') || req.socket.remoteAddress || '';
52+
if (config.api.rateLimiter.whitelist.includes(identifier)) {
53+
return next();
54+
}
55+
let [perSecondResult, perMinuteResult, perHourResult] = await RateLimitModel.incrementAndCheck(identifier, method);
56+
if (
57+
(perSecondResult.value as any).count > perSecond ||
58+
(perMinuteResult.value as any).count > perMinute ||
59+
(perHourResult.value as any).count > perHour) {
60+
return res.status(429).send('Rate Limited');
61+
}
62+
} catch (err) {
63+
logger.error('Rate Limiter failed');
64+
}
65+
return next();
66+
}
67+
}

packages/bitcore-node/src/types/Config.ts

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ export default interface Config {
1010
[currency: string]: any;
1111
};
1212
api: {
13+
rateLimiter: {
14+
whitelist: [string];
15+
},
1316
wallets: {
1417
allowCreationBeforeCompleteSync?: boolean;
1518
allowUnauthenticatedCalls?: boolean;

0 commit comments

Comments
 (0)