UNPKG

cds-rate-limit

Version:

enabled rate limit pattern for CAP NodeJS Runtime

131 lines 6.55 kB
"use strict"; 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