Skip to content

Commit c3be39f

Browse files
committed
feat collect ban data to a log file fix #8
1 parent 656f5d6 commit c3be39f

File tree

7 files changed

+55
-0
lines changed

7 files changed

+55
-0
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ export default defineNuxtConfig({
6666
delayOnBan: true // delay every response with +1sec when the user is banned, default is true
6767
errorMessage: "Too Many Requests", // error message when the user is banned, default is "Too Many Requests"
6868
retryAfterHeader: false, // when the user is banned add the Retry-After header to the response, default is false
69+
log: false, // request logging into a file, it accepts a boolean `false`, or an object, default is false - no logging
70+
log: {
71+
path: "logs", // path to the log file, every day a new log file will be created
72+
attempts: 100, // if an IP reach 100 requests, all the requests will be logged, can be used for further analysis or blocking for example with fail2ban
73+
}
6974
*/
7075
},
7176
});

playground/nuxt.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ export default defineNuxtConfig({
88
},
99
delayOnBan: true,
1010
retryAfterHeader: true,
11+
log: {
12+
path: "logs",
13+
attempts: 5,
14+
},
1115
},
1216
nitro: {
1317
storage: {

src/module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface ModuleOptions {
1515
delayOnBan: boolean;
1616
errorMessage: string;
1717
retryAfterHeader: boolean;
18+
log: boolean | { path: string; attempts: number };
1819
}
1920

2021
export default defineNuxtModule<ModuleOptions>({
@@ -31,6 +32,7 @@ export default defineNuxtModule<ModuleOptions>({
3132
delayOnBan: true,
3233
errorMessage: "Too Many Requests",
3334
retryAfterHeader: false,
35+
log: false,
3436
},
3537
setup(options, nuxt) {
3638
const resolver = createResolver(import.meta.url);

src/runtime/server/middleware/shield.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createError, defineEventHandler, getRequestIP } from "h3";
22
import { useRuntimeConfig, useStorage } from "#imports";
33
import type { RateLimit } from "../types/RateLimit";
44
import isBanExpired from "../utils/isBanExpired";
5+
import shieldLog from "../utils/shieldLog";
56

67
export default defineEventHandler(async (event) => {
78
if (!event.node.req.url?.startsWith("/api/")) {
@@ -21,6 +22,8 @@ export default defineEventHandler(async (event) => {
2122
const req = await shieldStorage.getItem(`ip:${requestIP}`);
2223
req.count++;
2324

25+
shieldLog(req, requestIP, event.node.req.url);
26+
2427
if (isNotRateLimited(req)) {
2528
return await shieldStorage.setItem(`ip:${requestIP}`, {
2629
count: req.count,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { RateLimit } from "../types/RateLimit";
2+
import { useRuntimeConfig } from "#imports";
3+
import { access, appendFile, mkdir } from "node:fs/promises";
4+
5+
const shieldLog = async (req: RateLimit, requestIP: string, url: string) => {
6+
const options = useRuntimeConfig().public.nuxtApiShield;
7+
if (options.log?.attempts && req.count >= options.log.attempts) {
8+
const logLine = `${requestIP} - (${req.count}) - ${new Date(
9+
req.time
10+
).toISOString()} - ${url}\n`;
11+
12+
const date = new Date().toISOString().split("T")[0].replace(/-/g, "");
13+
14+
try {
15+
await access(options.log.path);
16+
await appendFile(`${options.log.path}/shield-${date}.log`, logLine);
17+
} catch (error) {
18+
if (error.code === "ENOENT") {
19+
await mkdir(options.log.path);
20+
await appendFile(`${options.log.path}/shield-${date}.log`, logLine);
21+
} else {
22+
console.error("Unexpected error:", error);
23+
// Handle other potential errors
24+
}
25+
}
26+
}
27+
};
28+
29+
export default shieldLog;

test/basic.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
22
import { fileURLToPath } from "node:url";
33
import { setup, $fetch } from "@nuxt/test-utils/e2e";
44
import { beforeEach } from "vitest";
5+
import { readFile } from "node:fs/promises";
56

67
beforeEach(async () => {
78
// TODO await useStorage().clear();
@@ -60,4 +61,11 @@ describe("shield", async () => {
6061
const response = await $fetch("/api/example", { method: "GET" });
6162
expect(response.name).toBe("Gauranga");
6263
});
64+
65+
it("should created a log file", async () => {
66+
const logDate = new Date().toISOString().split("T")[0].replace(/-/g, "");
67+
const logFile = `logs/shield-${logDate}.log`;
68+
const contents = await readFile(logFile, { encoding: "utf8" });
69+
expect(contents).toContain("127.0.0.1");
70+
});
6371
});

test/fixtures/basic/nuxt.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export default defineNuxtConfig({
1010
},
1111
errorMessage: "Leave me alone",
1212
retryAfterHeader: true,
13+
log: {
14+
path: "logs",
15+
attempts: 5,
16+
},
1317
},
1418
nitro: {
1519
storage: {

0 commit comments

Comments
 (0)