@liberstudio/cloudflare-list 2.0.14 → 2.0.16

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/README.md CHANGED
@@ -7,9 +7,9 @@
7
7
  3. Manage Account > Configuration > List > Create List
8
8
  4. Account API tokens > Create Token > Create Custom Token (Permission: Account > Account WAF > Edit, Account > Account Filter List > Edit)
9
9
  5. Salvare il token API senza scadenza
10
- 6. Domains > Scegliere il dominio da utilizzare
10
+ 6. Domains > Scegliere il dominio da utilizzare
11
11
  7. Andare nella categoria Security > Security Rules > Create rule > Custom Rules
12
- 8. Nome: <Name>, Field: <IP Source Address , is in, scegli la lista creata prima>, Action: Block
12
+ 8. Nome: <Name>, Field: <IP Source Address , is in, scegli la lista creata prima>, Action: Block
13
13
  9. Salvare la regola
14
14
 
15
15
  ## Installazione
@@ -34,6 +34,7 @@ import { CloudflareAttacksModule } from "@liberstudio/cloudflare-list";
34
34
  listId: "<list_id>",
35
35
  apiToken: "<api_token>",
36
36
  comment: "<comment>",
37
+ logPath: "<logPath>",
37
38
  }),
38
39
  ],
39
40
  })
@@ -54,7 +55,8 @@ import { CloudflareAttacksModule } from "@liberstudio/cloudflare-list";
54
55
  apiToken: config.getOrThrow<string>('CLOUDFLARE_API_TOKEN'),
55
56
  accountId: config.getOrThrow<string>('CLOUDFLARE_ACCOUNT_ID'),
56
57
  listId: config.getOrThrow<string>('CLOUDFLARE_LIST_ID'),
57
- comment: config.get<string>('CLOUDFLARE_LIST_COMMENT') || 'Blocked'
58
+ comment: config.get<string>('CLOUDFLARE_LIST_COMMENT') || 'Blocked',
59
+ logPath: config.get<string>('CLOUDFLARE_LIST_LOG_PATH') || '/var/log/nestjs-attacks.log',
58
60
  })
59
61
  }),
60
62
  ],
@@ -63,4 +65,4 @@ export class AppModule {}
63
65
  ```
64
66
 
65
67
  ## License
66
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
68
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -14,17 +14,19 @@ const core_1 = require("@nestjs/core");
14
14
  const filters_1 = require("./filters");
15
15
  const attacks_service_1 = require("./attacks.service");
16
16
  const middleware_1 = require("./middleware");
17
+ const utils_1 = require("./utils");
17
18
  let CloudflareAttacksModule = CloudflareAttacksModule_1 = class CloudflareAttacksModule {
18
19
  configure(consumer) {
19
20
  consumer.apply(middleware_1.AttackLoggerMiddleware).forRoutes("*");
20
21
  }
21
22
  static forRoot(options) {
23
+ (0, utils_1.validateOptions)(options);
22
24
  return {
23
25
  module: CloudflareAttacksModule_1,
24
26
  imports: [axios_1.HttpModule],
25
27
  providers: [
26
28
  {
27
- provide: "CLOUDFLARE_OPTIONS",
29
+ provide: utils_1.CLOUDFLARE_OPTIONS,
28
30
  useValue: options,
29
31
  },
30
32
  ],
@@ -36,8 +38,12 @@ let CloudflareAttacksModule = CloudflareAttacksModule_1 = class CloudflareAttack
36
38
  imports: [axios_1.HttpModule, ...(options.imports || [])],
37
39
  providers: [
38
40
  {
39
- provide: "CLOUDFLARE_OPTIONS",
40
- useFactory: options.useFactory,
41
+ provide: utils_1.CLOUDFLARE_OPTIONS,
42
+ useFactory: async (...args) => {
43
+ const opts = await options.useFactory(...args);
44
+ (0, utils_1.validateOptions)(opts);
45
+ return opts;
46
+ },
41
47
  inject: options.inject || [],
42
48
  },
43
49
  ],
@@ -21,6 +21,7 @@ const common_1 = require("@nestjs/common");
21
21
  const axios_1 = require("@nestjs/axios");
22
22
  const axios_2 = __importDefault(require("axios"));
23
23
  const rxjs_1 = require("rxjs");
24
+ const utils_1 = require("./utils");
24
25
  let AttacksService = class AttacksService {
25
26
  constructor(httpService, options) {
26
27
  this.httpService = httpService;
@@ -57,6 +58,6 @@ let AttacksService = class AttacksService {
57
58
  exports.AttacksService = AttacksService;
58
59
  exports.AttacksService = AttacksService = __decorate([
59
60
  (0, common_1.Injectable)(),
60
- __param(1, (0, common_1.Inject)("CLOUDFLARE_OPTIONS")),
61
+ __param(1, (0, common_1.Inject)(utils_1.CLOUDFLARE_OPTIONS)),
61
62
  __metadata("design:paramtypes", [axios_1.HttpService, Object])
62
63
  ], AttacksService);
@@ -33,13 +33,13 @@ let AllExceptionsFilter = AllExceptionsFilter_1 = class AllExceptionsFilter {
33
33
  message,
34
34
  stack: exception instanceof Error ? exception.stack : undefined,
35
35
  };
36
- const isMissingToken = message === "Invalid or missing token";
37
- if (status >= 500) {
38
- this.logger.error(`[${ip}] [${request.method}] ${request.url} → ${status}`, isMissingToken ? null : JSON.stringify(errorLog, null, 2));
39
- }
40
- else {
41
- this.logger.warn(`[${ip}] [${request.method}] ${request.url} → ${status}`, isMissingToken ? null : JSON.stringify(errorLog, null, 2));
42
- }
36
+ /* const isMissingToken = message === "Invalid or missing token"; */
37
+ this.logger.error(`[${ip}] [${request.method}] ${request.url} → ${status}`);
38
+ /* if (status >= 500) {
39
+ this.logger.error(`[${ip}] [${request.method}] ${request.url} → ${status}`, isMissingToken ? null : JSON.stringify(errorLog, null, 2));
40
+ } else {
41
+ this.logger.warn(`[${ip}] [${request.method}] ${request.url} → ${status}`, isMissingToken ? null : JSON.stringify(errorLog, null, 2));
42
+ } */
43
43
  response.status(status).json({
44
44
  statusCode: status,
45
45
  timestamp: errorLog.timestamp,
@@ -3,6 +3,7 @@ export interface CloudflareAttacksOptions {
3
3
  accountId: string;
4
4
  listId: string;
5
5
  comment: string;
6
+ logPath: string;
6
7
  }
7
8
  export interface CloudflareAttacksAsyncOptions {
8
9
  imports?: any[];
@@ -1,17 +1,20 @@
1
- import { NestMiddleware } from "@nestjs/common";
2
- import { NextFunction, Request, Response } from "express";
1
+ import { NestMiddleware, OnModuleDestroy } from "@nestjs/common";
3
2
  import { AttacksService } from "../attacks.service";
4
- export declare class AttackLoggerMiddleware implements NestMiddleware {
3
+ import type { NextFunction, Request, Response } from "express";
4
+ import type { CloudflareAttacksOptions } from "../interfaces";
5
+ export declare class AttackLoggerMiddleware implements NestMiddleware, OnModuleDestroy {
5
6
  private readonly attSrv;
7
+ private readonly options;
6
8
  private readonly logger;
7
- private readonly logPath;
8
9
  private readonly recentIps;
9
- private readonly THROTTLE_MS;
10
- private writeQueue;
11
- constructor(attSrv: AttacksService);
10
+ private readonly throttleMs;
11
+ private readonly stream;
12
+ constructor(attSrv: AttacksService, options: CloudflareAttacksOptions);
12
13
  use(req: Request, res: Response, next: NextFunction): void;
13
- private enqueue;
14
- private processSuspiciousRequest;
14
+ private handleSuspicious;
15
15
  private isThrottled;
16
16
  private getClientIp;
17
+ private sanitize;
18
+ private ensureDirectoryExists;
19
+ onModuleDestroy(): void;
17
20
  }
@@ -1,107 +1,83 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
2
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
3
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
4
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
5
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
22
6
  return c > 3 && r && Object.defineProperty(target, key, r), r;
23
7
  };
24
- var __importStar = (this && this.__importStar) || (function () {
25
- var ownKeys = function(o) {
26
- ownKeys = Object.getOwnPropertyNames || function (o) {
27
- var ar = [];
28
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
- return ar;
30
- };
31
- return ownKeys(o);
32
- };
33
- return function (mod) {
34
- if (mod && mod.__esModule) return mod;
35
- var result = {};
36
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
- __setModuleDefault(result, mod);
38
- return result;
39
- };
40
- })();
41
8
  var __metadata = (this && this.__metadata) || function (k, v) {
42
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
43
10
  };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var AttackLoggerMiddleware_1;
44
15
  Object.defineProperty(exports, "__esModule", { value: true });
45
16
  exports.AttackLoggerMiddleware = void 0;
46
- const fs = __importStar(require("fs/promises"));
47
17
  const common_1 = require("@nestjs/common");
18
+ const fs_1 = require("fs");
19
+ const path_1 = require("path");
48
20
  const attacks_service_1 = require("../attacks.service");
49
- let AttackLoggerMiddleware = class AttackLoggerMiddleware {
50
- constructor(attSrv) {
21
+ const utils_1 = require("../utils");
22
+ let AttackLoggerMiddleware = AttackLoggerMiddleware_1 = class AttackLoggerMiddleware {
23
+ constructor(attSrv, options) {
51
24
  this.attSrv = attSrv;
52
- this.logger = new common_1.Logger("AttackLogger");
53
- this.logPath = "/var/log/nestjs-attacks.log";
54
- // Throttle per IP: evita flood verso Cloudflare
25
+ this.options = options;
26
+ this.logger = new common_1.Logger(AttackLoggerMiddleware_1.name);
55
27
  this.recentIps = new Map();
56
- this.THROTTLE_MS = 5_000;
57
- // Coda serializzata (fire-and-forget, non awaited dall'esterno)
58
- this.writeQueue = Promise.resolve();
28
+ this.throttleMs = 5_000;
29
+ if (!options.logPath || typeof options.logPath !== "string") {
30
+ throw new Error("CloudflareAttacksOptions.logPath deve essere una stringa valida");
31
+ }
32
+ const absolutePath = (0, path_1.resolve)(options.logPath);
33
+ this.ensureDirectoryExists(absolutePath);
34
+ this.stream = (0, fs_1.createWriteStream)(absolutePath, {
35
+ flags: "a",
36
+ encoding: "utf8",
37
+ highWaterMark: 64 * 1024,
38
+ });
39
+ this.stream.on("error", (err) => {
40
+ this.logger.error(`Errore nello stream del log (${absolutePath}): ${err.message}`);
41
+ });
59
42
  }
60
43
  use(req, res, next) {
61
44
  res.on("finish", () => {
62
45
  if (res.statusCode === 404) {
63
- this.enqueue(req);
46
+ this.handleSuspicious(req);
64
47
  }
65
48
  });
66
49
  next();
67
50
  }
68
- enqueue(req) {
69
- this.writeQueue = this.writeQueue
70
- .then(() => this.processSuspiciousRequest(req))
71
- .catch((err) => {
72
- const msg = err instanceof Error ? err.message : "Unknown error";
73
- this.logger.error(`Errore processing richiesta: ${msg}`);
74
- });
75
- }
76
- async processSuspiciousRequest(req) {
51
+ handleSuspicious(req) {
77
52
  const ip = this.getClientIp(req);
78
- if (ip === "unknown")
79
- return;
80
- if (this.isThrottled(ip))
53
+ if (!ip || this.isThrottled(ip))
81
54
  return;
82
- const safeUrl = req.url.replace(/[\r\n]/g, "_");
83
- const logEntry = `${new Date().toISOString()} [ATTACK] IP=${ip} METHOD=${req.method} URL=${safeUrl}\n`;
84
- this.logger.debug(logEntry.trimEnd());
85
- // Esegue in parallelo: log su file + update Cloudflare
86
- await Promise.allSettled([
87
- fs.appendFile(this.logPath, logEntry).catch((error) => {
88
- const msg = error instanceof Error ? error.message : "FS Error";
89
- this.logger.error(`Impossibile scrivere log su ${this.logPath}: ${msg}`);
90
- }),
91
- this.attSrv.updateIpList(ip).catch((error) => {
92
- const msg = error instanceof Error ? error.message : "Service Error";
93
- this.logger.error(`Impossibile aggiornare IP list: ${msg}`);
94
- }),
95
- ]);
55
+ const entry = {
56
+ timestamp: new Date().toISOString(),
57
+ type: "ATTACK",
58
+ ip,
59
+ method: req.method,
60
+ url: this.sanitize(req.url),
61
+ };
62
+ const line = JSON.stringify(entry) + "\n";
63
+ if (!this.stream.write(line)) {
64
+ this.stream.once("drain", () => {
65
+ this.logger.debug("Stream del log degli attacchi svuotato");
66
+ });
67
+ }
68
+ this.attSrv.updateIpList(ip).catch((err) => {
69
+ const msg = err instanceof Error ? err.message : "Errore sconosciuto";
70
+ ;
71
+ this.logger.error(`Aggiornamento Cloudflare fallito: ${msg}`);
72
+ });
96
73
  }
97
74
  isThrottled(ip) {
98
- const now = Date.now();
99
- const last = this.recentIps.get(ip);
100
- if (last && now - last < this.THROTTLE_MS)
75
+ if (this.recentIps.has(ip))
101
76
  return true;
102
- if (this.recentIps.size > 10_000)
103
- this.recentIps.clear();
104
- this.recentIps.set(ip, now);
77
+ const timeout = setTimeout(() => {
78
+ this.recentIps.delete(ip);
79
+ }, this.throttleMs);
80
+ this.recentIps.set(ip, timeout);
105
81
  return false;
106
82
  }
107
83
  getClientIp(req) {
@@ -115,11 +91,29 @@ let AttackLoggerMiddleware = class AttackLoggerMiddleware {
115
91
  if (typeof xForwardedFor === "string") {
116
92
  return xForwardedFor.split(",")[0].trim();
117
93
  }
118
- return req.socket.remoteAddress ?? "unknown";
94
+ return req.socket.remoteAddress ?? null;
95
+ }
96
+ sanitize(value) {
97
+ return value.replace(/[\r\n]/g, "_");
98
+ }
99
+ ensureDirectoryExists(filePath) {
100
+ const dir = (0, path_1.dirname)(filePath);
101
+ if (!(0, fs_1.existsSync)(dir)) {
102
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
103
+ this.logger.log(`Cartella dei log creata: ${dir}`);
104
+ }
105
+ }
106
+ onModuleDestroy() {
107
+ this.stream.end();
108
+ for (const timeout of this.recentIps.values()) {
109
+ clearTimeout(timeout);
110
+ }
111
+ this.recentIps.clear();
119
112
  }
120
113
  };
121
114
  exports.AttackLoggerMiddleware = AttackLoggerMiddleware;
122
- exports.AttackLoggerMiddleware = AttackLoggerMiddleware = __decorate([
115
+ exports.AttackLoggerMiddleware = AttackLoggerMiddleware = AttackLoggerMiddleware_1 = __decorate([
123
116
  (0, common_1.Injectable)(),
124
- __metadata("design:paramtypes", [attacks_service_1.AttacksService])
117
+ __param(1, (0, common_1.Inject)(utils_1.CLOUDFLARE_OPTIONS)),
118
+ __metadata("design:paramtypes", [attacks_service_1.AttacksService, Object])
125
119
  ], AttackLoggerMiddleware);
@@ -0,0 +1 @@
1
+ export declare const CLOUDFLARE_OPTIONS: unique symbol;
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CLOUDFLARE_OPTIONS = void 0;
4
+ exports.CLOUDFLARE_OPTIONS = Symbol('CLOUDFLARE_OPTIONS');
@@ -1 +1,3 @@
1
+ export * from './cloudflare.constants';
1
2
  export * from './get-client-ip.util';
3
+ export * from './options.validator';
@@ -14,4 +14,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./cloudflare.constants"), exports);
17
18
  __exportStar(require("./get-client-ip.util"), exports);
19
+ __exportStar(require("./options.validator"), exports);
@@ -0,0 +1,2 @@
1
+ import type { CloudflareAttacksOptions } from "../interfaces";
2
+ export declare function validateOptions(opzioni: CloudflareAttacksOptions): void;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateOptions = validateOptions;
4
+ function validateOptions(opzioni) {
5
+ if (!opzioni)
6
+ throw new Error("CloudflareAttacksOptions è obbligatorio");
7
+ const campiObbligatori = ["apiToken", "accountId", "listId", "comment", "logPath"];
8
+ for (const campo of campiObbligatori) {
9
+ const valore = opzioni[campo];
10
+ if (typeof valore !== "string" || valore.trim().length === 0) {
11
+ throw new Error(`CloudflareAttacksOptions.${campo} deve essere una stringa non vuota`);
12
+ }
13
+ }
14
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liberstudio/cloudflare-list",
3
- "version": "2.0.14",
3
+ "version": "2.0.16",
4
4
  "description": "Modulo NestJS per gestione IP List Cloudflare",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",