@creator.co/wapi 1.10.0 → 1.10.1
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.
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/dist/src/Server/lib/container/Proxy.d.ts +15 -0
- package/dist/src/Server/lib/container/Proxy.js +80 -1
- package/dist/src/Server/lib/container/Proxy.js.map +1 -1
- package/package.json +1 -1
- package/src/Server/lib/container/Proxy.ts +89 -2
- package/tests/Server/lib/container/RateLimit.test.ts +142 -1
package/dist/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@creator.co/wapi",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@creator.co/wapi",
|
|
9
|
-
"version": "1.10.
|
|
9
|
+
"version": "1.10.1",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@aws-sdk/client-dynamodb": "^3.730.0",
|
package/dist/package.json
CHANGED
|
@@ -30,6 +30,12 @@ export default class Proxy {
|
|
|
30
30
|
* @returns None
|
|
31
31
|
*/
|
|
32
32
|
private readonly serverlessHandler;
|
|
33
|
+
/**
|
|
34
|
+
* Route resolver used to identify per-route rate limit configuration.
|
|
35
|
+
* @private
|
|
36
|
+
* @readonly
|
|
37
|
+
*/
|
|
38
|
+
private readonly routeResolver;
|
|
33
39
|
/**
|
|
34
40
|
* Represents a listener for an HTTP server.
|
|
35
41
|
* @private
|
|
@@ -70,6 +76,15 @@ export default class Proxy {
|
|
|
70
76
|
* @returns None
|
|
71
77
|
*/
|
|
72
78
|
private installRoutes;
|
|
79
|
+
/**
|
|
80
|
+
* Creates rate limiting middleware from a per-route {@link RateLimitConfig},
|
|
81
|
+
* inheriting the global Redis store configuration when available so all
|
|
82
|
+
* rate-limit counters share the same Redis connection.
|
|
83
|
+
* @param {RateLimitConfig} config - The per-route rate limit configuration
|
|
84
|
+
* @returns {express.RequestHandler} Express middleware for rate limiting
|
|
85
|
+
* @private
|
|
86
|
+
*/
|
|
87
|
+
private createRouteRateLimitMiddleware;
|
|
73
88
|
/**
|
|
74
89
|
* Creates rate limiting middleware based on the provided configuration.
|
|
75
90
|
* @param {GlobalRateLimitConfig} config - The rate limit configuration
|
|
@@ -18,6 +18,7 @@ import HealthHandler from './HealthHandler.js';
|
|
|
18
18
|
import Globals from '../../../Globals.js';
|
|
19
19
|
import Logger from '../../../Logger/Logger.js';
|
|
20
20
|
import Utils from '../../../Util/Utils.js';
|
|
21
|
+
import RouteResolver from '../../RouteResolver.js';
|
|
21
22
|
/* Get package.json version from Wapi on ESM */
|
|
22
23
|
const { version: appVersion } = JSON.parse(fs.readFileSync('package.json').toString());
|
|
23
24
|
/**
|
|
@@ -34,6 +35,7 @@ export default class Proxy {
|
|
|
34
35
|
this.stopping = false;
|
|
35
36
|
this.config = config;
|
|
36
37
|
this.serverlessHandler = serverlessHandler;
|
|
38
|
+
this.routeResolver = new RouteResolver(this.config);
|
|
37
39
|
this.logger = new Logger({ logLevel: 'INFO' }, 'proxy-container');
|
|
38
40
|
this.app = express();
|
|
39
41
|
// Trust the first proxy hop so req.ip resolves to the real client IP
|
|
@@ -57,7 +59,16 @@ export default class Proxy {
|
|
|
57
59
|
// Apply global rate limiting if configured
|
|
58
60
|
if (this.config.rateLimit && this.config.rateLimit.enabled !== false) {
|
|
59
61
|
this.logger.info('[Proxy] - [RATE-LIMIT] - Global rate limiting enabled');
|
|
60
|
-
|
|
62
|
+
// Augment the skip function to bypass the global limit for routes that
|
|
63
|
+
// have their own rateLimit config (false = disable, or a RateLimitConfig).
|
|
64
|
+
const globalConfig = Object.assign(Object.assign({}, this.config.rateLimit), { skip: (req) => {
|
|
65
|
+
var _a, _b, _c;
|
|
66
|
+
const route = this.routeResolver.resolveRoute(req.method, req.path);
|
|
67
|
+
if (route && route.rateLimit !== undefined)
|
|
68
|
+
return true;
|
|
69
|
+
return (_c = (_b = (_a = this.config.rateLimit).skip) === null || _b === void 0 ? void 0 : _b.call(_a, req)) !== null && _c !== void 0 ? _c : false;
|
|
70
|
+
} });
|
|
71
|
+
const rateLimitMiddleware = this.createRateLimitMiddleware(globalConfig);
|
|
61
72
|
this.app.use(rateLimitMiddleware);
|
|
62
73
|
}
|
|
63
74
|
// //This supposedly fix some 502 codes where nodejs socket would hang during
|
|
@@ -151,11 +162,79 @@ export default class Proxy {
|
|
|
151
162
|
this.app
|
|
152
163
|
.route(this.config.healthCheckRoute || Globals.Listener_HTTP_DefaultHealthCheckRoute)
|
|
153
164
|
.get(HealthHandler);
|
|
165
|
+
// Register individual routes that declare their own rateLimit config.
|
|
166
|
+
// These are installed BEFORE the wildcard so Express matches them first,
|
|
167
|
+
// giving each route an independent rate limit bucket.
|
|
168
|
+
for (const route of this.config.routes) {
|
|
169
|
+
if (!route.rateLimit)
|
|
170
|
+
continue;
|
|
171
|
+
const rlMiddleware = this.createRouteRateLimitMiddleware(route.rateLimit);
|
|
172
|
+
const paths = Array.isArray(route.path) ? route.path : [route.path];
|
|
173
|
+
for (const path of paths) {
|
|
174
|
+
;
|
|
175
|
+
this.app.route(path)[route.method.toLowerCase()](rlMiddleware, GenericHandler(this.serverlessHandler));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
154
178
|
//Main route -- We use a wildcard route because is not the job of the runtime and neither
|
|
155
179
|
//the task to deny/constrain routes that invoked this task; all the job is done by the
|
|
156
180
|
//load balancer and we just foward everything we have to the function.
|
|
157
181
|
this.app.route(Globals.Listener_HTTP_ProxyRoute).all(GenericHandler(this.serverlessHandler));
|
|
158
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Creates rate limiting middleware from a per-route {@link RateLimitConfig},
|
|
185
|
+
* inheriting the global Redis store configuration when available so all
|
|
186
|
+
* rate-limit counters share the same Redis connection.
|
|
187
|
+
* @param {RateLimitConfig} config - The per-route rate limit configuration
|
|
188
|
+
* @returns {express.RequestHandler} Express middleware for rate limiting
|
|
189
|
+
* @private
|
|
190
|
+
*/
|
|
191
|
+
createRouteRateLimitMiddleware(config) {
|
|
192
|
+
// Resolve keyGenerator string shorthands to concrete functions.
|
|
193
|
+
let keyGenerator;
|
|
194
|
+
if (config.keyGenerator === 'ip') {
|
|
195
|
+
keyGenerator = (req) => req.ip || req.socket.remoteAddress || 'unknown';
|
|
196
|
+
}
|
|
197
|
+
else if (config.keyGenerator === 'userId') {
|
|
198
|
+
keyGenerator = (req) => {
|
|
199
|
+
const authHeader = req.headers['authorization'];
|
|
200
|
+
if (authHeader === null || authHeader === void 0 ? void 0 : authHeader.startsWith('Bearer ')) {
|
|
201
|
+
const parts = authHeader.slice(7).split('.');
|
|
202
|
+
if (parts.length === 3) {
|
|
203
|
+
try {
|
|
204
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
205
|
+
const userId = payload.sub || payload.id || payload.userId;
|
|
206
|
+
if (userId)
|
|
207
|
+
return String(userId);
|
|
208
|
+
}
|
|
209
|
+
catch (_a) {
|
|
210
|
+
// malformed token — fall through to IP
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return req.ip || req.socket.remoteAddress || 'unknown';
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
keyGenerator = config.keyGenerator;
|
|
219
|
+
}
|
|
220
|
+
// Inherit the global Redis store (if configured) so per-route limiters
|
|
221
|
+
// share the same connection; distinguish them with a unique key prefix.
|
|
222
|
+
const globalRl = this.config.rateLimit;
|
|
223
|
+
const globalConfig = {
|
|
224
|
+
windowMs: config.windowMs,
|
|
225
|
+
limit: config.limit,
|
|
226
|
+
keyGenerator,
|
|
227
|
+
skip: config.skip,
|
|
228
|
+
store: globalRl === null || globalRl === void 0 ? void 0 : globalRl.store,
|
|
229
|
+
redis: (globalRl === null || globalRl === void 0 ? void 0 : globalRl.redis)
|
|
230
|
+
? {
|
|
231
|
+
client: globalRl.redis.client,
|
|
232
|
+
prefix: `${globalRl.redis.prefix || 'wapi:rl:'}route:`,
|
|
233
|
+
}
|
|
234
|
+
: undefined,
|
|
235
|
+
};
|
|
236
|
+
return this.createRateLimitMiddleware(globalConfig);
|
|
237
|
+
}
|
|
159
238
|
/**
|
|
160
239
|
* Creates rate limiting middleware based on the provided configuration.
|
|
161
240
|
* @param {GlobalRateLimitConfig} config - The rate limit configuration
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Proxy.js","sourceRoot":"","sources":["../../../../../src/Server/lib/container/Proxy.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,EAAwB,YAAY,EAAE,MAAM,MAAM,CAAA;AAEzD,OAAO,IAAI,MAAM,MAAM,CAAA;AACvB,OAAO,OAAO,MAAM,SAAS,CAAA;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAG7C,OAAO,cAAc,MAAM,qBAAqB,CAAA;AAChD,OAAO,aAAa,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"Proxy.js","sourceRoot":"","sources":["../../../../../src/Server/lib/container/Proxy.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -9,10 +9,12 @@ import { RedisStore } from 'rate-limit-redis'
|
|
|
9
9
|
import Server from './../Server.js'
|
|
10
10
|
import GenericHandler from './GenericHandler.js'
|
|
11
11
|
import HealthHandler from './HealthHandler.js'
|
|
12
|
+
import { HttpMethod } from '../../../API/Request.js'
|
|
12
13
|
import Globals from '../../../Globals.js'
|
|
13
14
|
import Logger from '../../../Logger/Logger.js'
|
|
14
15
|
import Utils from '../../../Util/Utils.js'
|
|
15
|
-
import { GlobalRateLimitConfig, RouterConfig } from '../../Router.js'
|
|
16
|
+
import { GlobalRateLimitConfig, RateLimitConfig, RouterConfig } from '../../Router.js'
|
|
17
|
+
import RouteResolver from '../../RouteResolver.js'
|
|
16
18
|
|
|
17
19
|
/* Get package.json version from Wapi on ESM */
|
|
18
20
|
const { version: appVersion } = JSON.parse(fs.readFileSync('package.json').toString())
|
|
@@ -47,6 +49,12 @@ export default class Proxy {
|
|
|
47
49
|
* @returns None
|
|
48
50
|
*/
|
|
49
51
|
private readonly serverlessHandler: Server['handleServerlessEvent']
|
|
52
|
+
/**
|
|
53
|
+
* Route resolver used to identify per-route rate limit configuration.
|
|
54
|
+
* @private
|
|
55
|
+
* @readonly
|
|
56
|
+
*/
|
|
57
|
+
private readonly routeResolver: RouteResolver
|
|
50
58
|
/**
|
|
51
59
|
* Represents a listener for an HTTP server.
|
|
52
60
|
* @private
|
|
@@ -64,6 +72,7 @@ export default class Proxy {
|
|
|
64
72
|
this.stopping = false
|
|
65
73
|
this.config = config
|
|
66
74
|
this.serverlessHandler = serverlessHandler
|
|
75
|
+
this.routeResolver = new RouteResolver(this.config)
|
|
67
76
|
this.logger = new Logger({ logLevel: 'INFO' }, 'proxy-container')
|
|
68
77
|
this.app = express()
|
|
69
78
|
// Trust the first proxy hop so req.ip resolves to the real client IP
|
|
@@ -94,7 +103,17 @@ export default class Proxy {
|
|
|
94
103
|
// Apply global rate limiting if configured
|
|
95
104
|
if (this.config.rateLimit && this.config.rateLimit.enabled !== false) {
|
|
96
105
|
this.logger.info('[Proxy] - [RATE-LIMIT] - Global rate limiting enabled')
|
|
97
|
-
|
|
106
|
+
// Augment the skip function to bypass the global limit for routes that
|
|
107
|
+
// have their own rateLimit config (false = disable, or a RateLimitConfig).
|
|
108
|
+
const globalConfig: GlobalRateLimitConfig = {
|
|
109
|
+
...this.config.rateLimit,
|
|
110
|
+
skip: (req: express.Request) => {
|
|
111
|
+
const route = this.routeResolver.resolveRoute(req.method as HttpMethod, req.path)
|
|
112
|
+
if (route && route.rateLimit !== undefined) return true
|
|
113
|
+
return this.config.rateLimit!.skip?.(req) ?? false
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
const rateLimitMiddleware = this.createRateLimitMiddleware(globalConfig)
|
|
98
117
|
this.app.use(rateLimitMiddleware)
|
|
99
118
|
}
|
|
100
119
|
|
|
@@ -189,12 +208,80 @@ export default class Proxy {
|
|
|
189
208
|
this.app
|
|
190
209
|
.route(this.config.healthCheckRoute || Globals.Listener_HTTP_DefaultHealthCheckRoute)
|
|
191
210
|
.get(HealthHandler)
|
|
211
|
+
// Register individual routes that declare their own rateLimit config.
|
|
212
|
+
// These are installed BEFORE the wildcard so Express matches them first,
|
|
213
|
+
// giving each route an independent rate limit bucket.
|
|
214
|
+
for (const route of this.config.routes) {
|
|
215
|
+
if (!route.rateLimit) continue
|
|
216
|
+
const rlMiddleware = this.createRouteRateLimitMiddleware(route.rateLimit)
|
|
217
|
+
const paths = Array.isArray(route.path) ? route.path : [route.path]
|
|
218
|
+
for (const path of paths) {
|
|
219
|
+
;(this.app.route(path) as any)[route.method.toLowerCase()](
|
|
220
|
+
rlMiddleware,
|
|
221
|
+
GenericHandler(this.serverlessHandler)
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
192
225
|
//Main route -- We use a wildcard route because is not the job of the runtime and neither
|
|
193
226
|
//the task to deny/constrain routes that invoked this task; all the job is done by the
|
|
194
227
|
//load balancer and we just foward everything we have to the function.
|
|
195
228
|
this.app.route(Globals.Listener_HTTP_ProxyRoute).all(GenericHandler(this.serverlessHandler))
|
|
196
229
|
}
|
|
197
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Creates rate limiting middleware from a per-route {@link RateLimitConfig},
|
|
233
|
+
* inheriting the global Redis store configuration when available so all
|
|
234
|
+
* rate-limit counters share the same Redis connection.
|
|
235
|
+
* @param {RateLimitConfig} config - The per-route rate limit configuration
|
|
236
|
+
* @returns {express.RequestHandler} Express middleware for rate limiting
|
|
237
|
+
* @private
|
|
238
|
+
*/
|
|
239
|
+
private createRouteRateLimitMiddleware(config: RateLimitConfig): express.RequestHandler {
|
|
240
|
+
// Resolve keyGenerator string shorthands to concrete functions.
|
|
241
|
+
let keyGenerator: GlobalRateLimitConfig['keyGenerator']
|
|
242
|
+
if (config.keyGenerator === 'ip') {
|
|
243
|
+
keyGenerator = (req: express.Request) => req.ip || req.socket.remoteAddress || 'unknown'
|
|
244
|
+
} else if (config.keyGenerator === 'userId') {
|
|
245
|
+
keyGenerator = (req: express.Request) => {
|
|
246
|
+
const authHeader = req.headers['authorization']
|
|
247
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
248
|
+
const parts = authHeader.slice(7).split('.')
|
|
249
|
+
if (parts.length === 3) {
|
|
250
|
+
try {
|
|
251
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'))
|
|
252
|
+
const userId = payload.sub || payload.id || payload.userId
|
|
253
|
+
if (userId) return String(userId)
|
|
254
|
+
} catch {
|
|
255
|
+
// malformed token — fall through to IP
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return req.ip || req.socket.remoteAddress || 'unknown'
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
keyGenerator = config.keyGenerator
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Inherit the global Redis store (if configured) so per-route limiters
|
|
266
|
+
// share the same connection; distinguish them with a unique key prefix.
|
|
267
|
+
const globalRl = this.config.rateLimit
|
|
268
|
+
const globalConfig: GlobalRateLimitConfig = {
|
|
269
|
+
windowMs: config.windowMs,
|
|
270
|
+
limit: config.limit,
|
|
271
|
+
keyGenerator,
|
|
272
|
+
skip: config.skip,
|
|
273
|
+
store: globalRl?.store,
|
|
274
|
+
redis: globalRl?.redis
|
|
275
|
+
? {
|
|
276
|
+
client: globalRl.redis.client,
|
|
277
|
+
prefix: `${globalRl.redis.prefix || 'wapi:rl:'}route:`,
|
|
278
|
+
}
|
|
279
|
+
: undefined,
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return this.createRateLimitMiddleware(globalConfig)
|
|
283
|
+
}
|
|
284
|
+
|
|
198
285
|
/**
|
|
199
286
|
* Creates rate limiting middleware based on the provided configuration.
|
|
200
287
|
* @param {GlobalRateLimitConfig} config - The rate limit configuration
|
|
@@ -3,9 +3,10 @@ import { APIGatewayProxyEvent, Context } from 'aws-lambda'
|
|
|
3
3
|
import { expect as c_expect } from 'chai'
|
|
4
4
|
import request from 'supertest'
|
|
5
5
|
|
|
6
|
+
import { HttpMethod } from '../../../../src/API/Request.js'
|
|
6
7
|
import Globals from '../../../../src/Globals.js'
|
|
7
8
|
import Proxy from '../../../../src/Server/lib/container/Proxy.js'
|
|
8
|
-
import { GlobalRateLimitConfig } from '../../../../src/Server/Router.js'
|
|
9
|
+
import { GlobalRateLimitConfig, RateLimitConfig } from '../../../../src/Server/Router.js'
|
|
9
10
|
import { defaultUrl } from '../../../Test.utils.js'
|
|
10
11
|
|
|
11
12
|
describe('Rate Limiting', () => {
|
|
@@ -829,4 +830,144 @@ describe('Rate Limiting', () => {
|
|
|
829
830
|
}, 10000)
|
|
830
831
|
})
|
|
831
832
|
|
|
833
|
+
describe('Per-route rate limiting', () => {
|
|
834
|
+
// @ts-ignore
|
|
835
|
+
let mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {})
|
|
836
|
+
let proxy: Proxy | null = null
|
|
837
|
+
|
|
838
|
+
beforeAll(() => {
|
|
839
|
+
// @ts-ignore
|
|
840
|
+
mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {})
|
|
841
|
+
})
|
|
842
|
+
|
|
843
|
+
afterAll(() => {
|
|
844
|
+
mockExit.mockRestore()
|
|
845
|
+
})
|
|
846
|
+
|
|
847
|
+
beforeEach(() => {
|
|
848
|
+
mockExit.mockReset()
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
afterEach(async () => {
|
|
852
|
+
if (proxy) {
|
|
853
|
+
await proxy.unload()
|
|
854
|
+
proxy = null
|
|
855
|
+
}
|
|
856
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
test('Per-route rate limit is applied independently of global', async () => {
|
|
860
|
+
const port = 57020
|
|
861
|
+
const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
|
|
862
|
+
|
|
863
|
+
const routeRateLimit: RateLimitConfig = { limit: 2, windowMs: 1000 }
|
|
864
|
+
|
|
865
|
+
proxy = new Proxy(
|
|
866
|
+
{
|
|
867
|
+
routes: [
|
|
868
|
+
{
|
|
869
|
+
path: '/api/limited',
|
|
870
|
+
method: HttpMethod.GET,
|
|
871
|
+
rateLimit: routeRateLimit,
|
|
872
|
+
handler: async () => ({}) as any,
|
|
873
|
+
},
|
|
874
|
+
],
|
|
875
|
+
port,
|
|
876
|
+
// No global rate limit — only per-route
|
|
877
|
+
},
|
|
878
|
+
async (event: APIGatewayProxyEvent, context: Context) => {
|
|
879
|
+
context.succeed({ body: JSON.stringify({ success: true }), statusCode: 200 })
|
|
880
|
+
}
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
await proxy.load()
|
|
884
|
+
|
|
885
|
+
// First 2 requests succeed
|
|
886
|
+
await request(url).get('/api/limited').expect(200)
|
|
887
|
+
await request(url).get('/api/limited').expect(200)
|
|
888
|
+
|
|
889
|
+
// 3rd request is blocked by the per-route rate limit
|
|
890
|
+
const res = await request(url).get('/api/limited').expect(429)
|
|
891
|
+
c_expect(res.body).to.have.property('error', 'rate_limit_exceeded')
|
|
892
|
+
|
|
893
|
+
// Wait for the window to reset and verify recovery
|
|
894
|
+
await new Promise(resolve => setTimeout(resolve, 1100))
|
|
895
|
+
await request(url).get('/api/limited').expect(200)
|
|
896
|
+
}, 10000)
|
|
897
|
+
|
|
898
|
+
test('rateLimit: false exempts a route from the global rate limit', async () => {
|
|
899
|
+
const port = 57021
|
|
900
|
+
const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
|
|
901
|
+
|
|
902
|
+
proxy = new Proxy(
|
|
903
|
+
{
|
|
904
|
+
routes: [
|
|
905
|
+
{
|
|
906
|
+
path: '/api/exempt',
|
|
907
|
+
method: HttpMethod.GET,
|
|
908
|
+
rateLimit: false,
|
|
909
|
+
handler: async () => ({}) as any,
|
|
910
|
+
},
|
|
911
|
+
],
|
|
912
|
+
port,
|
|
913
|
+
rateLimit: { enabled: true, limit: 2, windowMs: 5000 },
|
|
914
|
+
},
|
|
915
|
+
async (event: APIGatewayProxyEvent, context: Context) => {
|
|
916
|
+
context.succeed({ body: JSON.stringify({ success: true }), statusCode: 200 })
|
|
917
|
+
}
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
await proxy.load()
|
|
921
|
+
|
|
922
|
+
// Global limit is 2, but /api/exempt bypasses it — all 5 requests should succeed
|
|
923
|
+
for (let i = 0; i < 5; i++) {
|
|
924
|
+
const res = await request(url).get('/api/exempt').expect(200)
|
|
925
|
+
c_expect(res.body).to.deep.equal({ success: true })
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// A non-exempt path still hits the global limit
|
|
929
|
+
await request(url).get('/other-path').expect(200)
|
|
930
|
+
await request(url).get('/other-path').expect(200)
|
|
931
|
+
await request(url).get('/other-path').expect(429)
|
|
932
|
+
}, 10000)
|
|
933
|
+
|
|
934
|
+
test('Per-route and global limits operate independently', async () => {
|
|
935
|
+
const port = 57022
|
|
936
|
+
const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
|
|
937
|
+
|
|
938
|
+
proxy = new Proxy(
|
|
939
|
+
{
|
|
940
|
+
routes: [
|
|
941
|
+
{
|
|
942
|
+
path: '/api/strict',
|
|
943
|
+
method: HttpMethod.GET,
|
|
944
|
+
rateLimit: { limit: 2, windowMs: 5000 },
|
|
945
|
+
handler: async () => ({}) as any,
|
|
946
|
+
},
|
|
947
|
+
],
|
|
948
|
+
port,
|
|
949
|
+
rateLimit: { enabled: true, limit: 5, windowMs: 5000 },
|
|
950
|
+
},
|
|
951
|
+
async (event: APIGatewayProxyEvent, context: Context) => {
|
|
952
|
+
context.succeed({ body: JSON.stringify({ success: true }), statusCode: 200 })
|
|
953
|
+
}
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
await proxy.load()
|
|
957
|
+
|
|
958
|
+
// /api/strict has a tighter per-route limit (2); hits 429 on 3rd request
|
|
959
|
+
await request(url).get('/api/strict').expect(200)
|
|
960
|
+
await request(url).get('/api/strict').expect(200)
|
|
961
|
+
await request(url).get('/api/strict').expect(429)
|
|
962
|
+
|
|
963
|
+
// /api/strict requests do NOT consume global tokens — /other-path still has its full budget
|
|
964
|
+
for (let i = 0; i < 5; i++) {
|
|
965
|
+
const res = await request(url).get('/other-path').expect(200)
|
|
966
|
+
c_expect(res.body).to.deep.equal({ success: true })
|
|
967
|
+
}
|
|
968
|
+
// 6th request to /other-path exhausts the global limit
|
|
969
|
+
await request(url).get('/other-path').expect(429)
|
|
970
|
+
}, 15000)
|
|
971
|
+
})
|
|
972
|
+
|
|
832
973
|
export {}
|