@riocrypto/common-server 1.0.2277 → 1.0.2279
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -25,8 +25,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
26
|
exports.createDynamicRateLimiter = void 0;
|
|
27
27
|
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
|
|
28
|
-
// Removed path-to-regexp import
|
|
29
|
-
// Adjust the import path according to your project structure
|
|
30
28
|
const custom_rate_limit_1 = require("../models/custom-rate-limit");
|
|
31
29
|
// Cache for rate limiter instances
|
|
32
30
|
const limiterInstanceCache = new Map();
|
|
@@ -72,18 +70,12 @@ function refreshCustomIpRulesCache(CustomRateLimit, filePathForLog) {
|
|
|
72
70
|
});
|
|
73
71
|
}
|
|
74
72
|
function createDynamicRateLimiter(options) {
|
|
75
|
-
const { windowMs: defaultWindowMs,
|
|
76
|
-
max: defaultMax, // Rename for clarity
|
|
77
|
-
mongooseConnection, cacheTtlMs = DEFAULT_CACHE_TTL_MS,
|
|
78
|
-
// Capture other express-rate-limit options passed in
|
|
79
|
-
message, standardHeaders, legacyHeaders, skip, requestPropertyName } = options,
|
|
80
|
-
// Add any other valid RateLimitOptions you might pass
|
|
81
|
-
otherRateLimitOptions = __rest(options, ["windowMs", "max", "mongooseConnection", "cacheTtlMs", "message", "standardHeaders", "legacyHeaders", "skip", "requestPropertyName"]) // Use a specific variable if you know exactly what else you pass
|
|
82
|
-
;
|
|
73
|
+
const { windowMs: defaultWindowMs, max: defaultMax, mongooseConnection, cacheTtlMs = DEFAULT_CACHE_TTL_MS, message, standardHeaders, legacyHeaders, skip, requestPropertyName } = options, otherRateLimitOptions = __rest(options, ["windowMs", "max", "mongooseConnection", "cacheTtlMs", "message", "standardHeaders", "legacyHeaders", "skip", "requestPropertyName"]);
|
|
83
74
|
if (!mongooseConnection) {
|
|
84
75
|
throw new Error("[DynamicRateLimiter] Mongoose connection is required.");
|
|
85
76
|
}
|
|
86
77
|
const CustomRateLimit = (0, custom_rate_limit_1.buildCustomRateLimit)(mongooseConnection);
|
|
78
|
+
// Initial cache load (regex compilation happens here too)
|
|
87
79
|
refreshCustomIpRulesCache(CustomRateLimit, "initialization");
|
|
88
80
|
return (req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
89
81
|
try {
|
|
@@ -92,49 +84,78 @@ function createDynamicRateLimiter(options) {
|
|
|
92
84
|
(customIpRulesCache.size === 0 && lastCacheRefreshTime === 0)) {
|
|
93
85
|
yield refreshCustomIpRulesCache(CustomRateLimit, req.path);
|
|
94
86
|
}
|
|
95
|
-
|
|
96
|
-
let
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
customRule = customIpRulesCache.get(clientIp);
|
|
87
|
+
// Determine the client's IP address, prioritizing Cloudflare/proxy headers
|
|
88
|
+
let rawClientIp;
|
|
89
|
+
const cfConnectingIp = req.headers["cf-connecting-ip"];
|
|
90
|
+
const xForwardedForHeader = req.headers["x-forwarded-for"];
|
|
91
|
+
if (cfConnectingIp) {
|
|
92
|
+
rawClientIp = cfConnectingIp;
|
|
102
93
|
}
|
|
103
|
-
if (
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// Test the path against the regex
|
|
121
|
-
return regex.test(req.path);
|
|
122
|
-
});
|
|
123
|
-
if (appliesToCurrentRoute) {
|
|
124
|
-
currentLimit = customRule.limit;
|
|
125
|
-
currentWindowMs = customRule.windowMs;
|
|
126
|
-
ruleIdentifier = `custom_ip:${clientIp}`;
|
|
94
|
+
else if (xForwardedForHeader) {
|
|
95
|
+
// X-Forwarded-For can be a comma-separated list (client, proxy1, proxy2)
|
|
96
|
+
// The first IP is the original client IP
|
|
97
|
+
rawClientIp = xForwardedForHeader.split(",")[0].trim();
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
rawClientIp = req.ip; // Fallback to Express's req.ip
|
|
101
|
+
}
|
|
102
|
+
let normalizedIp;
|
|
103
|
+
if (rawClientIp) {
|
|
104
|
+
// Normalize IPv4-mapped IPv6 addresses (e.g., ::ffff:192.0.2.1 -> 192.0.2.1)
|
|
105
|
+
// Also handles direct IPv4 and standard IPv6 addresses
|
|
106
|
+
if (rawClientIp.startsWith("::ffff:")) {
|
|
107
|
+
normalizedIp = rawClientIp.substring(7);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
normalizedIp = rawClientIp;
|
|
127
111
|
}
|
|
128
112
|
}
|
|
129
|
-
|
|
113
|
+
let currentLimit = defaultMax;
|
|
114
|
+
let currentWindowMs = defaultWindowMs;
|
|
115
|
+
let ruleIdentifier = "default"; // Start with default identifier
|
|
116
|
+
let customRuleDoc;
|
|
117
|
+
let matchingRule = undefined;
|
|
118
|
+
if (normalizedIp) {
|
|
119
|
+
customRuleDoc = customIpRulesCache.get(normalizedIp);
|
|
120
|
+
}
|
|
121
|
+
if (customRuleDoc &&
|
|
122
|
+
customRuleDoc.routes &&
|
|
123
|
+
customRuleDoc.routes.length > 0) {
|
|
124
|
+
// Find the first specific rule that matches the request path and method
|
|
125
|
+
matchingRule = customRuleDoc.routes.find((rule) => {
|
|
126
|
+
const methodMatch = rule.action === "*" || rule.action === req.method.toUpperCase();
|
|
127
|
+
if (!methodMatch)
|
|
128
|
+
return false;
|
|
129
|
+
const regex = regexCache.get(rule.path);
|
|
130
|
+
if (!regex)
|
|
131
|
+
return false; // Should have been compiled on refresh
|
|
132
|
+
return regex.test(req.path);
|
|
133
|
+
});
|
|
134
|
+
if (matchingRule) {
|
|
135
|
+
// Apply limit and window from the *specific matching rule*
|
|
136
|
+
currentLimit = matchingRule.limit;
|
|
137
|
+
currentWindowMs = matchingRule.windowMs;
|
|
138
|
+
// Create a more specific identifier including the matched rule path/action
|
|
139
|
+
ruleIdentifier = `custom_ip:${normalizedIp}_rule:${matchingRule.action}:${matchingRule.path}`;
|
|
140
|
+
}
|
|
141
|
+
// If no specific rule in the array matches, we just fall through
|
|
142
|
+
// and the defaults (currentLimit = defaultMax etc.) will be used.
|
|
143
|
+
}
|
|
144
|
+
// If customRuleDoc is undefined or routes array is empty, defaults apply.
|
|
145
|
+
// Generate cache key for the limiter instance. This will be unique for:
|
|
146
|
+
// - Default limit (if no rule found/matched)
|
|
147
|
+
// - Each specific matching rule (due to unique ruleIdentifier)
|
|
130
148
|
const limiterCacheKey = `${currentLimit}-${currentWindowMs}-${ruleIdentifier}`;
|
|
131
149
|
let selectedLimiter = limiterInstanceCache.get(limiterCacheKey);
|
|
132
150
|
if (!selectedLimiter) {
|
|
151
|
+
// Create a new limiter instance with the determined limit/window
|
|
133
152
|
selectedLimiter = (0, express_rate_limit_1.default)(Object.assign({ windowMs: currentWindowMs, max: currentLimit, keyGenerator: (request) => {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
153
|
+
let ipForKey = request.ip;
|
|
154
|
+
if (ipForKey === null || ipForKey === void 0 ? void 0 : ipForKey.startsWith("::ffff:")) {
|
|
155
|
+
ipForKey = ipForKey.substring(7);
|
|
156
|
+
}
|
|
157
|
+
return ipForKey || "unknown_ip_for_rate_limit";
|
|
158
|
+
}, message: message, standardHeaders: standardHeaders, legacyHeaders: legacyHeaders, skip: skip, requestPropertyName: requestPropertyName }, otherRateLimitOptions));
|
|
138
159
|
limiterInstanceCache.set(limiterCacheKey, selectedLimiter);
|
|
139
160
|
}
|
|
140
161
|
return selectedLimiter(req, res, next);
|
|
@@ -2,19 +2,17 @@ import mongoose from "mongoose";
|
|
|
2
2
|
interface RouteRule {
|
|
3
3
|
path: string;
|
|
4
4
|
action: string;
|
|
5
|
+
limit: number;
|
|
6
|
+
windowMs: number;
|
|
5
7
|
}
|
|
6
8
|
interface CustomRateLimitAttrs {
|
|
7
|
-
createdAt
|
|
9
|
+
createdAt: Date;
|
|
8
10
|
ipAddress: string;
|
|
9
|
-
limit: number;
|
|
10
|
-
windowMs: number;
|
|
11
11
|
routes: RouteRule[];
|
|
12
12
|
}
|
|
13
13
|
interface CustomRateLimitDoc extends mongoose.Document {
|
|
14
14
|
createdAt: Date;
|
|
15
15
|
ipAddress: string;
|
|
16
|
-
limit: number;
|
|
17
|
-
windowMs: number;
|
|
18
16
|
routes: RouteRule[];
|
|
19
17
|
}
|
|
20
18
|
interface CustomRateLimitModel extends mongoose.Model<CustomRateLimitDoc> {
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildCustomRateLimit = void 0;
|
|
4
4
|
const buildCustomRateLimit = (mongoose) => {
|
|
5
|
-
// if model is already defined, return it
|
|
6
5
|
if (mongoose.models.CustomRateLimit) {
|
|
7
6
|
return mongoose.model("CustomRateLimit");
|
|
8
7
|
}
|
|
9
|
-
// Define the schema for the nested RouteRule object
|
|
8
|
+
// Updated: Define the schema for the nested RouteRule object including limits
|
|
10
9
|
const RouteRuleSchema = new mongoose.Schema({
|
|
11
10
|
path: {
|
|
12
11
|
type: String,
|
|
@@ -15,32 +14,36 @@ const buildCustomRateLimit = (mongoose) => {
|
|
|
15
14
|
action: {
|
|
16
15
|
type: String,
|
|
17
16
|
required: [true, "Action (HTTP method) is required for a route rule"],
|
|
18
|
-
uppercase: true,
|
|
19
|
-
//
|
|
20
|
-
// enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', '*'] // Example enum
|
|
17
|
+
uppercase: true,
|
|
18
|
+
// enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', '*']
|
|
21
19
|
},
|
|
22
|
-
|
|
20
|
+
limit: {
|
|
21
|
+
type: Number,
|
|
22
|
+
required: [true, "Limit is required for a specific route rule"],
|
|
23
|
+
min: [0, "Limit cannot be negative"], // Allow 0 for blocking?
|
|
24
|
+
},
|
|
25
|
+
windowMs: {
|
|
26
|
+
type: Number,
|
|
27
|
+
required: [true, "windowMs is required for a specific route rule"],
|
|
28
|
+
min: [1000, "windowMs must be at least 1000 (1 second)"],
|
|
29
|
+
},
|
|
30
|
+
}, { _id: false });
|
|
23
31
|
const CustomRateLimitSchema = new mongoose.Schema({
|
|
24
32
|
createdAt: {
|
|
25
33
|
type: mongoose.Schema.Types.Date,
|
|
26
|
-
default: Date.now,
|
|
34
|
+
default: Date.now,
|
|
27
35
|
},
|
|
28
36
|
ipAddress: {
|
|
29
37
|
type: String,
|
|
30
38
|
required: true,
|
|
39
|
+
// Add index for faster IP lookups
|
|
40
|
+
index: true,
|
|
31
41
|
},
|
|
32
|
-
limit
|
|
33
|
-
type: Number,
|
|
34
|
-
required: true,
|
|
35
|
-
},
|
|
36
|
-
windowMs: {
|
|
37
|
-
type: Number,
|
|
38
|
-
required: true,
|
|
39
|
-
},
|
|
42
|
+
// Removed limit and windowMs from top level schema
|
|
40
43
|
routes: {
|
|
41
44
|
type: [RouteRuleSchema],
|
|
42
45
|
required: true,
|
|
43
|
-
// Default to empty array
|
|
46
|
+
// Default to empty array. If empty, default limiter settings apply.
|
|
44
47
|
default: [],
|
|
45
48
|
},
|
|
46
49
|
}, {
|
|
@@ -54,7 +57,6 @@ const buildCustomRateLimit = (mongoose) => {
|
|
|
54
57
|
});
|
|
55
58
|
CustomRateLimitSchema.statics.build = (attrs) => {
|
|
56
59
|
var _a;
|
|
57
|
-
// Ensure default createdAt is handled if not provided
|
|
58
60
|
return new CustomRateLimit(Object.assign(Object.assign({}, attrs), { createdAt: (_a = attrs.createdAt) !== null && _a !== void 0 ? _a : new Date() }));
|
|
59
61
|
};
|
|
60
62
|
const CustomRateLimit = mongoose.model("CustomRateLimit", CustomRateLimitSchema);
|