cds-rate-limit
Version:
enabled rate limit pattern for CAP NodeJS Runtime
131 lines • 6.55 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.applyRateLimit = exports.DEFAULT_OPTIONS = void 0;
const constants_1 = require("./constants");
const key_1 = require("./key");
const limiters_1 = require("./limiters");
const options_1 = require("./options");
const utils_1 = require("./utils");
exports.DEFAULT_OPTIONS = {
impl: "memory", // use in-memory
keyParts: ["tenant"], // generate key from tenant
keyPrefix: constants_1.GLOBAL_RATE_LIMITER_PREFIX, // default prefix
duration: 60, // 60 seconds
points: 200 * 60, // 200 requests per seconds
// for anonymous requests
anonymous: {
// per seconds per remote ip allow 1000 requests
keyPrefix: constants_1.GLOBAL_ANONYMOUS_RATE_LIMITER_PREFIX,
duration: 10,
points: 10 * 1000,
},
};
/**
* attach headers to evt
*
* @param response express response
* @param total
* @param rateLimitRes
*/
const attachHeaders = (response, total, rateLimitRes) => {
var _a;
if (response !== undefined) {
(_a = response === null || response === void 0 ? void 0 : response.set) === null || _a === void 0 ? void 0 : _a.call(response, {
[constants_1.RATE_LIMIT_HEADERS["Retry-After"]]: Math.floor(rateLimitRes.msBeforeNext / 1000),
[constants_1.RATE_LIMIT_HEADERS["X-RateLimit-Limit"]]: total,
[constants_1.RATE_LIMIT_HEADERS["X-RateLimit-Remaining"]]: rateLimitRes.remainingPoints,
[constants_1.RATE_LIMIT_HEADERS["X-RateLimit-Reset"]]: Math.floor((Date.now() + rateLimitRes.msBeforeNext) / 1000)
});
}
};
/**
* apply rate limitation for cds
*
* @param cds
* @param defaultOptions
*/
const applyRateLimit = (cds) => {
var _a, _b;
const globalOptions = Object.assign({}, exports.DEFAULT_OPTIONS, (_b = (_a = cds.env.config) === null || _a === void 0 ? void 0 : _a.rateLimit) !== null && _b !== void 0 ? _b : {});
if (globalOptions.impl === "redis") {
const Redis = require("ioredis");
globalOptions.storeClient = new Redis(globalOptions.redisOptions);
}
cds.once("bootstrap", createBootStrapListener(cds, globalOptions));
cds.on("serving", createServiceListener(cds, globalOptions));
};
exports.applyRateLimit = applyRateLimit;
function createServiceListener(cds, globalOptions) {
return (service) => {
if (service instanceof cds.ApplicationService) { // only application services
const logger = cds.log(service === null || service === void 0 ? void 0 : service.name);
const createKeyExtractor = (0, key_1.keyExtractorCreatorBuilder)(globalOptions.keyExtractors);
service.prepend((srv) => {
srv.before("*", async (evt) => {
var _a, _b, _c, _d;
const eventKey = (0, utils_1.formatEventKey)(srv, evt);
// only affect HTTP requests
if (evt instanceof cds.Request) {
// if this event has been measured
if (cds.context[constants_1.FLAG_RATE_LIMIT_PERFORMED] === true) {
logger.debug("event", eventKey, "is triggered by internal communication (has been measured by first event), ignored");
return;
}
cds.context[constants_1.FLAG_RATE_LIMIT_PERFORMED] = true;
const options = await (0, options_1.parseOptions)(srv, evt, globalOptions);
const rateLimiter = (0, limiters_1.provisionRateLimiter)(options);
const keyExtractor = createKeyExtractor(options.keyParts);
const key = keyExtractor(evt);
try {
const response = await rateLimiter.consume(key);
logger.debug("rate limit consume successful:", key, response);
attachHeaders((_b = (_a = evt === null || evt === void 0 ? void 0 : evt._) === null || _a === void 0 ? void 0 : _a.req) === null || _b === void 0 ? void 0 : _b.res, options.points, response);
return;
}
catch (response) {
logger.error("rate limit consume failed:", key, response);
attachHeaders((_d = (_c = evt === null || evt === void 0 ? void 0 : evt._) === null || _c === void 0 ? void 0 : _c.req) === null || _d === void 0 ? void 0 : _d.res, options.points, response);
return evt.reject(429, `Rate limit exceed, please retry after ${Math.floor(response.msBeforeNext / 1000)} seconds`);
}
}
else {
logger.debug("event", eventKey, "is not from http request, ignored");
}
});
});
}
};
}
function createBootStrapListener(cds, globalOptions) {
return (app) => {
if (globalOptions.anonymous !== false) {
const logger = cds.log("AnonymousRateLimiter");
app.use(async (req) => {
// without authorization header
if (req.get("authorization") === undefined) {
// TODO: provide `anonymous` options from remote
const options = Object.assign({}, globalOptions, globalOptions.anonymous);
const rateLimiter = (0, limiters_1.provisionRateLimiter)(options);
try {
const response = await rateLimiter.consume(req.ip);
logger.debug("rate limit consume successful:", req.ip, response);
attachHeaders(req.res, options.points, response);
return req.next();
}
catch (response) {
logger.error("rate limit consume failed:", req.ip, response);
attachHeaders(req.res, options.points, response);
return req.res.status(429).json({
error: {
code: "429",
message: `Rate limit exceed, please retry after ${Math.floor(response.msBeforeNext / 1000)} seconds`
}
});
}
}
return req.next();
});
}
};
}
//# sourceMappingURL=support.js.map