@riocrypto/common-server 1.0.2277 → 1.0.2278
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.
|
@@ -72,18 +72,12 @@ function refreshCustomIpRulesCache(CustomRateLimit, filePathForLog) {
|
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
74
|
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
|
-
;
|
|
75
|
+
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
76
|
if (!mongooseConnection) {
|
|
84
77
|
throw new Error("[DynamicRateLimiter] Mongoose connection is required.");
|
|
85
78
|
}
|
|
86
79
|
const CustomRateLimit = (0, custom_rate_limit_1.buildCustomRateLimit)(mongooseConnection);
|
|
80
|
+
// Initial cache load (regex compilation happens here too)
|
|
87
81
|
refreshCustomIpRulesCache(CustomRateLimit, "initialization");
|
|
88
82
|
return (req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
89
83
|
try {
|
|
@@ -92,49 +86,63 @@ function createDynamicRateLimiter(options) {
|
|
|
92
86
|
(customIpRulesCache.size === 0 && lastCacheRefreshTime === 0)) {
|
|
93
87
|
yield refreshCustomIpRulesCache(CustomRateLimit, req.path);
|
|
94
88
|
}
|
|
95
|
-
|
|
96
|
-
let
|
|
97
|
-
let currentWindowMs = defaultWindowMs; // Use destructured default
|
|
98
|
-
let ruleIdentifier = "default";
|
|
99
|
-
let customRule;
|
|
89
|
+
let clientIp = req.ip;
|
|
90
|
+
let normalizedIp;
|
|
100
91
|
if (clientIp) {
|
|
101
|
-
|
|
92
|
+
// Normalize IPv4-mapped IPv6 addresses (::ffff:x.x.x.x)
|
|
93
|
+
if (clientIp.startsWith("::ffff:")) {
|
|
94
|
+
normalizedIp = clientIp.substring(7);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
normalizedIp = clientIp;
|
|
98
|
+
}
|
|
102
99
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
100
|
+
let currentLimit = defaultMax;
|
|
101
|
+
let currentWindowMs = defaultWindowMs;
|
|
102
|
+
let ruleIdentifier = "default"; // Start with default identifier
|
|
103
|
+
let customRuleDoc;
|
|
104
|
+
let matchingRule = undefined;
|
|
105
|
+
if (normalizedIp) {
|
|
106
|
+
customRuleDoc = customIpRulesCache.get(normalizedIp);
|
|
107
|
+
}
|
|
108
|
+
if (customRuleDoc &&
|
|
109
|
+
customRuleDoc.routes &&
|
|
110
|
+
customRuleDoc.routes.length > 0) {
|
|
111
|
+
// Find the first specific rule that matches the request path and method
|
|
112
|
+
matchingRule = customRuleDoc.routes.find((rule) => {
|
|
113
|
+
const methodMatch = rule.action === "*" || rule.action === req.method.toUpperCase();
|
|
114
|
+
if (!methodMatch)
|
|
115
|
+
return false;
|
|
116
|
+
const regex = regexCache.get(rule.path);
|
|
117
|
+
if (!regex)
|
|
118
|
+
return false; // Should have been compiled on refresh
|
|
119
|
+
return regex.test(req.path);
|
|
120
|
+
});
|
|
121
|
+
if (matchingRule) {
|
|
122
|
+
// Apply limit and window from the *specific matching rule*
|
|
123
|
+
currentLimit = matchingRule.limit;
|
|
124
|
+
currentWindowMs = matchingRule.windowMs;
|
|
125
|
+
// Create a more specific identifier including the matched rule path/action
|
|
126
|
+
ruleIdentifier = `custom_ip:${normalizedIp}_rule:${matchingRule.action}:${matchingRule.path}`;
|
|
127
127
|
}
|
|
128
|
+
// If no specific rule in the array matches, we just fall through
|
|
129
|
+
// and the defaults (currentLimit = defaultMax etc.) will be used.
|
|
128
130
|
}
|
|
129
|
-
//
|
|
131
|
+
// If customRuleDoc is undefined or routes array is empty, defaults apply.
|
|
132
|
+
// Generate cache key for the limiter instance. This will be unique for:
|
|
133
|
+
// - Default limit (if no rule found/matched)
|
|
134
|
+
// - Each specific matching rule (due to unique ruleIdentifier)
|
|
130
135
|
const limiterCacheKey = `${currentLimit}-${currentWindowMs}-${ruleIdentifier}`;
|
|
131
136
|
let selectedLimiter = limiterInstanceCache.get(limiterCacheKey);
|
|
132
137
|
if (!selectedLimiter) {
|
|
138
|
+
// Create a new limiter instance with the determined limit/window
|
|
133
139
|
selectedLimiter = (0, express_rate_limit_1.default)(Object.assign({ windowMs: currentWindowMs, max: currentLimit, keyGenerator: (request) => {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
let ipForKey = request.ip;
|
|
141
|
+
if (ipForKey === null || ipForKey === void 0 ? void 0 : ipForKey.startsWith("::ffff:")) {
|
|
142
|
+
ipForKey = ipForKey.substring(7);
|
|
143
|
+
}
|
|
144
|
+
return ipForKey || "unknown_ip_for_rate_limit";
|
|
145
|
+
}, message: message, standardHeaders: standardHeaders, legacyHeaders: legacyHeaders, skip: skip, requestPropertyName: requestPropertyName }, otherRateLimitOptions));
|
|
138
146
|
limiterInstanceCache.set(limiterCacheKey, selectedLimiter);
|
|
139
147
|
}
|
|
140
148
|
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);
|