Skip to content

Commit e73619b

Browse files
authored
Merge pull request #103 from chachew/fix-user-requests-after-ban
Fix issue where a banned user can make unlimited requests Thank you for the PR! I will fix some formatting issues and add it to the next release
2 parents 59c904b + cd5e19c commit e73619b

File tree

1 file changed

+54
-56
lines changed

1 file changed

+54
-56
lines changed
Lines changed: 54 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import type { RateLimit } from '../types/RateLimit'
2-
import shieldLog from '../utils/shieldLog'
1+
import shieldLog from '../utils/shieldLog.js'
32
import {
43
createError,
54
defineEventHandler,
@@ -12,87 +11,86 @@ import {
1211
export default defineEventHandler(async (event) => {
1312
const config = useRuntimeConfig().public.nuxtApiShield
1413
const url = getRequestURL(event)
15-
16-
if (!url?.pathname?.startsWith('/api/') || (config.routes?.length && !config.routes.some(route => url.pathname?.startsWith(route)))) {
14+
if (
15+
!url?.pathname?.startsWith('/api/')
16+
|| (config.routes?.length
17+
&& !config.routes.some(route => url.pathname?.startsWith(route)))
18+
) {
1719
return
1820
}
1921

20-
// console.log(
21-
// `👉 Handling request for URL: ${url} from IP: ${getRequestIP(event, { xForwardedFor: true }) || "unKnownIP"
22-
// }`
23-
// );
24-
2522
const shieldStorage = useStorage('shield')
2623
const requestIP = getRequestIP(event, { xForwardedFor: true }) || 'unKnownIP'
27-
2824
const banKey = `ban:${requestIP}`
2925
const bannedUntilRaw = await shieldStorage.getItem(banKey)
30-
const bannedUntil = typeof bannedUntilRaw === 'number' ? bannedUntilRaw : Number(bannedUntilRaw)
31-
if (bannedUntilRaw && !Number.isNaN(bannedUntil) && Date.now() < bannedUntil) {
26+
const bannedUntil
27+
= typeof bannedUntilRaw === 'number'
28+
? bannedUntilRaw
29+
: Number(bannedUntilRaw)
30+
31+
// Check if the user is currently banned
32+
if (
33+
bannedUntilRaw
34+
&& !Number.isNaN(bannedUntil)
35+
&& Date.now() < bannedUntil
36+
) {
3237
if (config.retryAfterHeader) {
33-
const retryAfter = Math.ceil((bannedUntil - Date.now()) / 1000)
38+
const retryAfter = Math.ceil((bannedUntil - Date.now()) / 1e3)
3439
event.node.res.setHeader('Retry-After', retryAfter)
3540
}
3641
throw createError({
3742
statusCode: 429,
3843
message: config.errorMessage,
3944
})
4045
}
41-
else if (bannedUntilRaw && !Number.isNaN(bannedUntil) && Date.now() >= bannedUntil) {
46+
// Unban the user if the ban has expired
47+
else if (
48+
bannedUntilRaw
49+
&& !Number.isNaN(bannedUntil)
50+
&& Date.now() >= bannedUntil
51+
) {
4252
await shieldStorage.removeItem(banKey)
43-
await shieldStorage.setItem(`ip:${requestIP}`, {
44-
count: 1,
45-
time: Date.now(),
46-
})
4753
}
4854

49-
if (!(await shieldStorage.hasItem(`ip:${requestIP}`))) {
50-
// console.log(" IP not found in storage, setting initial count.", requestIP);
51-
return await shieldStorage.setItem(`ip:${requestIP}`, {
55+
const ipKey = `ip:${requestIP}`
56+
const req = await shieldStorage.getItem(ipKey)
57+
const now = Date.now()
58+
59+
// Check if a new request is outside the duration window
60+
if (!req || (now - req.time) / 1e3 >= config.limit.duration) {
61+
// If no record exists, or the duration has expired, reset the counter and timestamp
62+
await shieldStorage.setItem(ipKey, {
5263
count: 1,
53-
time: Date.now(),
64+
time: now,
5465
})
66+
return
5567
}
5668

57-
const req = (await shieldStorage.getItem(`ip:${requestIP}`)) as RateLimit
69+
// Increment the counter for a request within the duration
5870
req.count++
59-
// console.log(` Set count for IP ${requestIP}: ${req.count}`);
60-
6171
shieldLog(req, requestIP, url.toString())
6272

63-
if (!isRateLimited(req)) {
64-
// console.log(" Request not rate-limited, updating storage.");
65-
return await shieldStorage.setItem(`ip:${requestIP}`, {
73+
// Check if the new count triggers a rate limit
74+
if (req.count > config.limit.max) {
75+
const banUntil = now + config.limit.ban * 1e3
76+
await shieldStorage.setItem(banKey, banUntil)
77+
await shieldStorage.setItem(ipKey, {
78+
count: 1,
79+
time: now,
80+
})
81+
if (config.retryAfterHeader) {
82+
event.node.res.setHeader('Retry-After', config.limit.ban)
83+
}
84+
throw createError({
85+
statusCode: 429,
86+
message: config.errorMessage,
87+
})
88+
}
89+
else {
90+
// Update the count for the current duration
91+
await shieldStorage.setItem(ipKey, {
6692
count: req.count,
6793
time: req.time,
6894
})
6995
}
70-
71-
const banUntil = Date.now() + config.limit.ban * 1000
72-
await shieldStorage.setItem(banKey, banUntil)
73-
await shieldStorage.setItem(`ip:${requestIP}`, {
74-
count: 1,
75-
time: Date.now(),
76-
})
77-
78-
if (config.retryAfterHeader) {
79-
event.node.res.setHeader('Retry-After', config.limit.ban)
80-
}
81-
82-
// console.error("Throwing 429 error");
83-
throw createError({
84-
statusCode: 429,
85-
message: config.errorMessage,
86-
})
8796
})
88-
89-
const isRateLimited = (req: RateLimit) => {
90-
const options = useRuntimeConfig().public.nuxtApiShield
91-
92-
// console.log(` count: ${req.count} <= limit: ${options.limit.max}`);
93-
if (req.count <= options.limit.max) {
94-
return false
95-
}
96-
// console.log(" ", (Date.now() - req.time) / 1000, "<", options.limit.duration);
97-
return (Date.now() - req.time) / 1000 < options.limit.duration
98-
}

0 commit comments

Comments
 (0)