Skip to content

Commit 73fce52

Browse files
committed
feat restrict shield to certain routes fix #26
1 parent 106a3e5 commit 73fce52

File tree

5 files changed

+410
-432
lines changed

5 files changed

+410
-432
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,14 @@
3333
"release:minor": "yarn lint && yarn test && yarn prepack && changelogen --release --minor && yarn publish && git push --follow-tags",
3434
"lint": "eslint .",
3535
"test": "vitest run --testTimeout 15000 --reporter=basic --disable-console-intercept",
36-
"test:watch": "vitest watch --testTimeout 15000 --reporter=basic --disable-console-intercept "
36+
"test:watch": "vitest watch --testTimeout 15000 --reporter=basic --disable-console-intercept"
3737
},
3838
"dependencies": {
3939
"@nuxt/kit": "^3.12.2",
4040
"defu": "^6.1.4"
4141
},
4242
"devDependencies": {
43-
"@nuxt/devtools": "latest",
43+
"@nuxt/devtools": "^1.4.2",
4444
"@nuxt/eslint-config": "^0.5.0",
4545
"@nuxt/module-builder": "^0.8.3",
4646
"@nuxt/schema": "^3.11.1",

src/runtime/server/middleware/shield.ts

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,15 @@ export default defineEventHandler(async (event) => {
1919

2020

2121
// console.log(
22-
// `👉 Handling request for URL: ${event.node.req.url} from IP: ${
23-
// getRequestIP(event, { xForwardedFor: true }) || "unKnownIP"
22+
// `👉 Handling request for URL: ${url} from IP: ${getRequestIP(event, { xForwardedFor: true }) || "unKnownIP"
2423
// }`
2524
// );
2625

2726
const shieldStorage = useStorage("shield");
2827
const requestIP = getRequestIP(event, { xForwardedFor: true }) || "unKnownIP";
2928

3029
if (!(await shieldStorage.hasItem(`ip:${requestIP}`))) {
31-
//console.log("IP not found in storage, setting initial count.", requestIP);
30+
// console.log(" IP not found in storage, setting initial count.", requestIP);
3231
return await shieldStorage.setItem(`ip:${requestIP}`, {
3332
count: 1,
3433
time: Date.now(),
@@ -37,34 +36,29 @@ export default defineEventHandler(async (event) => {
3736

3837
const req = (await shieldStorage.getItem(`ip:${requestIP}`)) as RateLimit;
3938
req.count++;
40-
//console.log(`Set count for IP ${requestIP}: ${req.count}`);
39+
// console.log(` Set count for IP ${requestIP}: ${req.count}`);
4140

42-
shieldLog(req, requestIP, event.node.req.url);
41+
shieldLog(req, requestIP, url);
4342

4443
if (!isRateLimited(req)) {
45-
//console.log("Request not rate-limited, updating storage.");
44+
// console.log(" Request not rate-limited, updating storage.");
4645
return await shieldStorage.setItem(`ip:${requestIP}`, {
4746
count: req.count,
4847
time: req.time,
4948
});
5049
}
5150

51+
// console.log(" Request is rate-limited.");
52+
5253
if (isBanExpired(req)) {
53-
//console.log("Ban expired, resetting count.");
54+
// console.log(" Ban expired, resetting count.");
5455
return await shieldStorage.setItem(`ip:${requestIP}`, {
5556
count: 1,
5657
time: Date.now(),
5758
});
5859
}
5960

60-
// console.log(
61-
// "setItem for IP:",
62-
// requestIP,
63-
// "with count:",
64-
// req.count,
65-
// "and time:",
66-
// req.time
67-
// );
61+
// console.log(" setItem for IP:", requestIP, "with count:", req.count, "and time:", req.time);
6862
shieldStorage.setItem(`ip:${requestIP}`, {
6963
count: req.count,
7064
time: req.time,
@@ -75,7 +69,7 @@ export default defineEventHandler(async (event) => {
7569
const options = useRuntimeConfig().public.nuxtApiShield;
7670

7771
if (options.retryAfterHeader) {
78-
//console.log("Setting Retry-After header", req.count + 1);
72+
// console.log(" Setting Retry-After header", req.count + 1);
7973
event.node.res.setHeader("Retry-After", req.count + 1); // and extra second is added
8074
}
8175

@@ -89,24 +83,24 @@ export default defineEventHandler(async (event) => {
8983
const isRateLimited = (req: RateLimit) => {
9084
const options = useRuntimeConfig().public.nuxtApiShield;
9185

92-
//console.log(`count: ${req.count} <= limit: ${options.limit.max}`);
86+
// console.log(` count: ${req.count} <= limit: ${options.limit.max}`);
9387
if (req.count <= options.limit.max) {
9488
return false;
9589
}
96-
//console.log((Date.now() - req.time) / 1000, "<", options.limit.duration);
90+
// console.log(" ", (Date.now() - req.time) / 1000, "<", options.limit.duration);
9791
return (Date.now() - req.time) / 1000 < options.limit.duration;
9892
};
9993

10094
const banDelay = async (req: RateLimit) => {
10195
const options = useRuntimeConfig().public.nuxtApiShield;
102-
//console.log("delayOnBan: " + options.delayOnBan);
96+
// console.log(" delayOnBan is: " + options.delayOnBan);
10397
if (options.delayOnBan && req.count > options.limit.max) {
10498
// INFO Nuxt Devtools will send a new request if the response is slow,
10599
// so we get the count incremented twice or more times, based on the ban delay time
106-
//console.log(`Applying ban delay for ${req.count - options.limit.max} sec`);
100+
// console.log(` Applying ban delay for ${(req.count - options.limit.max) * options.limit.ban} sec (${Date.now()})`);
107101
await new Promise((resolve) =>
108-
setTimeout(resolve, (req.count - options.limit.max) * 1000)
102+
setTimeout(resolve, (req.count - options.limit.max) * options.limit.ban * 1000)
109103
);
110-
//console.log(`Ban delay completed`);
104+
// console.log(` Ban delay completed (${Date.now()})`);
111105
}
112106
};

test/basic.test.ts

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,35 @@
1-
import { describe, it, expect } from "vitest";
1+
import { beforeEach, describe, it, expect } from "vitest";
22
import { fileURLToPath } from "node:url";
33
import { setup, $fetch } from "@nuxt/test-utils/e2e";
4-
import { beforeEach } from "vitest";
54
import { readFile, rm } from "node:fs/promises";
65

76
// TODO get these from the config
87
const nuxtConfigDuration = 3;
98
const nuxtConfigBan = 10;
109

11-
beforeEach(async () => {
12-
// await useStorage("shield").clear(); TODO waiting for https://github.com/nuxt/test-utils/issues/531
13-
// this is a workaround to clean the storage
14-
const storagePath = fileURLToPath(new URL("../_testBasciShield", import.meta.url));
15-
await rm(storagePath, { recursive: true, force: true });
16-
});
1710

1811
describe("shield", async () => {
1912
await setup({
2013
rootDir: fileURLToPath(new URL("./fixtures/basic", import.meta.url)),
2114
});
2215

16+
beforeEach(async () => {
17+
// await useStorage("shield").clear(); TODO waiting for https://github.com/nuxt/test-utils/issues/531
18+
// this is a workaround to clean the storage
19+
const storagePath = fileURLToPath(new URL("../_testBasicShield", import.meta.url));
20+
await rm(storagePath, { recursive: true, force: true });
21+
});
22+
2323
it("respond to api call 2 times (limit.max, limit.duration) and rejects the 3rd call", async () => {
2424
// req.count = 1
25-
let response = await $fetch("/api/example?c=1/1", {
25+
let response = await $fetch("/api/basicexample?c=1/1", {
2626
method: "GET",
2727
retryStatusCodes: [],
2828
});
2929
expect((response as any).name).toBe("Gauranga");
3030

3131
// req.count = 2
32-
response = await $fetch("/api/example?c=1/2", {
32+
response = await $fetch("/api/basicexample?c=1/2", {
3333
method: "GET",
3434
retryStatusCodes: [],
3535
});
@@ -39,7 +39,7 @@ describe("shield", async () => {
3939
// req.count = 3
4040
// as limit.max = 2, this should throw 429 and ban for 3 seconds (limit.ban)
4141
expect(async () =>
42-
$fetch("/api/example?c=1/3", { method: "GET", retryStatusCodes: [] })
42+
$fetch("/api/basicexample?c=1/3", { method: "GET", retryStatusCodes: [] })
4343
).rejects.toThrowError();
4444
} catch (err) {
4545
const typedErr = err as { statusCode: number; statusMessage: string };
@@ -49,18 +49,15 @@ describe("shield", async () => {
4949
});
5050

5151
it("respond to the 2nd api call when more then limit.duration time passes", async () => {
52-
// here we should wait for the 3 sec ban to expire
53-
await new Promise((resolve) => setTimeout(resolve, (nuxtConfigBan + 1) * 1000));
54-
5552
// see #13
5653
// req.count = 1
57-
let response = await $fetch("/api/example?c=2/1", {
54+
let response = await $fetch("/api/basicexample?c=2/1", {
5855
method: "GET",
5956
retryStatusCodes: [],
6057
});
6158

6259
// req.count = 2
63-
response = await $fetch("/api/example?c=2/2", {
60+
response = await $fetch("/api/basicexample?c=2/2", {
6461
method: "GET",
6562
retryStatusCodes: [],
6663
});
@@ -69,12 +66,12 @@ describe("shield", async () => {
6966

7067
it("respond to api call after limit.ban expires", async () => {
7168
// req.count reset here
72-
await $fetch("/api/example?c=3/1", { method: "GET", retryStatusCodes: [] }); // req.count = 1
73-
await $fetch("/api/example?c=3/2", { method: "GET", retryStatusCodes: [] }); // req.count = 2
69+
await $fetch("/api/basicexample?c=3/1", { method: "GET", retryStatusCodes: [] }); // req.count = 1
70+
await $fetch("/api/basicexample?c=3/2", { method: "GET", retryStatusCodes: [] }); // req.count = 2
7471
try {
7572
// req.count = 3
7673
expect(async () =>
77-
$fetch("/api/example?c=3/3", { method: "GET", retryStatusCodes: [] })
74+
$fetch("/api/basicexample?c=3/3", { method: "GET", retryStatusCodes: [] })
7875
).rejects.toThrowError();
7976
} catch (err) {
8077
const typedErr = err as {
@@ -90,7 +87,7 @@ describe("shield", async () => {
9087

9188
// here we should wait for the 3 sec ban to expire
9289
await new Promise((resolve) => setTimeout(resolve, nuxtConfigBan * 1000));
93-
const response = await $fetch("/api/example?c=3/4", {
90+
const response = await $fetch("/api/basicexample?c=3/4", {
9491
method: "GET",
9592
retryStatusCodes: [],
9693
});

0 commit comments

Comments
 (0)