Skip to content

Commit fd4dfff

Browse files
authored
Merge 2cc16b4 into 0f1979f
2 parents 0f1979f + 2cc16b4 commit fd4dfff

File tree

7 files changed

+90
-0
lines changed

7 files changed

+90
-0
lines changed

package-lock.json

+18
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"pg-monitor": "2.0.0",
5454
"pg-promise": "11.3.0",
5555
"pluralize": "8.0.0",
56+
"rate-limit-redis": "3.0.1",
5657
"redis": "4.0.6",
5758
"semver": "7.3.8",
5859
"subscriptions-transport-ws": "0.11.0",

spec/RateLimit.spec.js

+30
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const RedisCacheAdapter = require('../lib/Adapters/Cache/RedisCacheAdapter').default;
12
describe('rate limit', () => {
23
it('can limit cloud functions', async () => {
34
Parse.Cloud.define('test', () => 'Abc');
@@ -388,4 +389,33 @@ describe('rate limit', () => {
388389
})
389390
).toBeRejectedWith(`Invalid rate limit option "path"`);
390391
});
392+
describe_only(() => {
393+
return process.env.PARSE_SERVER_TEST_CACHE === 'redis';
394+
})('with RedisCache', function () {
395+
it('does work with cache', async () => {
396+
await reconfigureServer({
397+
rateLimit: [
398+
{
399+
requestPath: '/classes/*',
400+
requestTimeWindow: 10000,
401+
requestCount: 1,
402+
errorResponseMessage: 'Too many requests',
403+
includeInternalRequests: true,
404+
redisUrl: 'redis://localhost:6379',
405+
},
406+
],
407+
});
408+
const obj = new Parse.Object('Test');
409+
await obj.save();
410+
await expectAsync(obj.save()).toBeRejectedWith(
411+
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
412+
);
413+
const cache = new RedisCacheAdapter();
414+
await cache.connect();
415+
const value = await cache.get('rl:127.0.0.1');
416+
expect(value).toEqual(2);
417+
const ttl = await cache.client.ttl('rl:127.0.0.1');
418+
expect(ttl).toEqual(10);
419+
});
420+
});
391421
});

src/Options/Definitions.js

+5
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,11 @@ module.exports.RateLimitOptions = {
557557
action: parsers.booleanParser,
558558
default: false,
559559
},
560+
redisUrl: {
561+
env: 'PARSE_SERVER_RATE_LIMIT_REDIS_URL',
562+
help:
563+
'Optional, the URL of the Redis server to store rate limit data. This allows to rate limit requests for multiple servers by calculating the sum of all requests across all servers. This is useful if multiple servers are processing requests behind a load balancer. For example, the limit of 10 requests is reached if each of 2 servers processed 5 requests.',
564+
},
560565
requestCount: {
561566
env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_COUNT',
562567
help:

src/Options/docs.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Options/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,9 @@ export interface RateLimitOptions {
320320
/* Optional, if `true` the rate limit will also apply to requests that are made in by Cloud Code, default is `false`. Note that a public Cloud Code function that triggers internal requests may circumvent rate limiting and be vulnerable to attacks.
321321
:DEFAULT: false */
322322
includeInternalRequests: ?boolean;
323+
/* Optional, the URL of the Redis server to store rate limit data. This allows to rate limit requests for multiple servers by calculating the sum of all requests across all servers. This is useful if multiple servers are processing requests behind a load balancer. For example, the limit of 10 requests is reached if each of 2 servers processed 5 requests.
324+
*/
325+
redisUrl: ?string;
323326
}
324327

325328
export interface SecurityOptions {

src/middlewares.js

+32
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import rateLimit from 'express-rate-limit';
1111
import { RateLimitOptions } from './Options/Definitions';
1212
import pathToRegexp from 'path-to-regexp';
1313
import ipRangeCheck from 'ip-range-check';
14+
import RedisStore from 'rate-limit-redis';
15+
import { createClient } from 'redis';
1416

1517
export const DEFAULT_ALLOWED_HEADERS =
1618
'X-Parse-Master-Key, X-Parse-REST-API-Key, X-Parse-Javascript-Key, X-Parse-Application-Id, X-Parse-Client-Version, X-Parse-Session-Token, X-Requested-With, X-Parse-Revocable-Session, X-Parse-Request-Id, Content-Type, Pragma, Cache-Control';
@@ -476,6 +478,35 @@ export const addRateLimit = (route, config) => {
476478
if (!config.rateLimits) {
477479
config.rateLimits = [];
478480
}
481+
const redisStore = {
482+
connectionPromise: Promise.resolve(),
483+
store: null,
484+
connected: false,
485+
};
486+
if (route.redisrUrl) {
487+
const client = createClient({
488+
url: route.redisrUrl,
489+
});
490+
redisStore.connectionPromise = async () => {
491+
if (redisStore.connected) {
492+
return;
493+
}
494+
try {
495+
await client.connect();
496+
redisStore.connected = true;
497+
} catch (e) {
498+
const log = config?.loggerController || defaultLogger;
499+
log.error(`Could not connect to redisURL in rate limit: ${e}`);
500+
}
501+
};
502+
redisStore.connectionPromise();
503+
redisStore.store = new RedisStore({
504+
sendCommand: async (...args) => {
505+
await redisStore.connectionPromise();
506+
return client.sendCommand(args);
507+
},
508+
});
509+
}
479510
config.rateLimits.push({
480511
path: pathToRegexp(route.requestPath),
481512
handler: rateLimit({
@@ -512,6 +543,7 @@ export const addRateLimit = (route, config) => {
512543
keyGenerator: request => {
513544
return request.config.ip;
514545
},
546+
store: redisStore.store,
515547
}),
516548
});
517549
Config.put(config);

0 commit comments

Comments
 (0)