@liberstudio/cloudflare-list 2.4.0 → 2.4.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/attacks.service.js +4 -0
- package/dist/utils/cidr.util.d.ts +10 -0
- package/dist/utils/cidr.util.js +56 -0
- package/dist/utils/cloudflare-ips.util.js +8 -49
- package/dist/utils/googlebot.util.d.ts +11 -0
- package/dist/utils/googlebot.util.js +95 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/package.json +10 -7
package/dist/attacks.service.js
CHANGED
|
@@ -39,6 +39,10 @@ let AttacksService = class AttacksService {
|
|
|
39
39
|
this.logger.log(`IP ${rawIp} appartiene al range Cloudflare — ban saltato`);
|
|
40
40
|
return null;
|
|
41
41
|
}
|
|
42
|
+
if (await (0, utils_1.isGoogleBotIp)(rawIp)) {
|
|
43
|
+
this.logger.log(`IP ${rawIp} è un crawler Google — ban saltato`);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
42
46
|
const asnRange = await this.lookupAsnRange(rawIp);
|
|
43
47
|
if (asnRange) {
|
|
44
48
|
this.logger.error(`ASN range trovato per ${rawIp}: ${asnRange} — blocco intera subnet`);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Rimuove il suffisso CIDR, fa trim e srotola gli IPv4-mapped IPv6 (`::ffff:`). */
|
|
2
|
+
export declare function purifyIp(ip: string): string;
|
|
3
|
+
export declare function ipv4ToInt(ip: string): number;
|
|
4
|
+
export declare function ipv6ToBigInt(ip: string): bigint;
|
|
5
|
+
export declare function isInCidrV4(ip: string, cidr: string): boolean;
|
|
6
|
+
export declare function isInCidrV6(ip: string, cidr: string): boolean;
|
|
7
|
+
/** Verifica se un IP "puro" appartiene a uno qualsiasi dei CIDR della sua versione. */
|
|
8
|
+
export declare function isIpInAnyCidr(pureIp: string, cidrs: string[], version: 4 | 6): boolean;
|
|
9
|
+
/** Ritorna la versione IP (4 | 6) di un valore già "purificato", oppure null. */
|
|
10
|
+
export declare function ipVersion(pureIp: string): 4 | 6 | null;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.purifyIp = purifyIp;
|
|
4
|
+
exports.ipv4ToInt = ipv4ToInt;
|
|
5
|
+
exports.ipv6ToBigInt = ipv6ToBigInt;
|
|
6
|
+
exports.isInCidrV4 = isInCidrV4;
|
|
7
|
+
exports.isInCidrV6 = isInCidrV6;
|
|
8
|
+
exports.isIpInAnyCidr = isIpInAnyCidr;
|
|
9
|
+
exports.ipVersion = ipVersion;
|
|
10
|
+
const net_1 = require("net");
|
|
11
|
+
/** Rimuove il suffisso CIDR, fa trim e srotola gli IPv4-mapped IPv6 (`::ffff:`). */
|
|
12
|
+
function purifyIp(ip) {
|
|
13
|
+
let pure = ip.includes("/") ? ip.split("/")[0] : ip;
|
|
14
|
+
pure = pure.trim();
|
|
15
|
+
if (pure.startsWith("::ffff:"))
|
|
16
|
+
pure = pure.slice("::ffff:".length);
|
|
17
|
+
return pure;
|
|
18
|
+
}
|
|
19
|
+
function ipv4ToInt(ip) {
|
|
20
|
+
return ip.split(".").reduce((acc, oct) => (acc << 8) + Number(oct), 0) >>> 0;
|
|
21
|
+
}
|
|
22
|
+
function ipv6ToBigInt(ip) {
|
|
23
|
+
const [headStr, tailStr] = ip.includes("::") ? ip.split("::") : [ip, ""];
|
|
24
|
+
const head = headStr ? headStr.split(":") : [];
|
|
25
|
+
const tail = tailStr ? tailStr.split(":") : [];
|
|
26
|
+
const missing = 8 - head.length - tail.length;
|
|
27
|
+
const groups = [...head, ...Array(missing).fill("0"), ...tail];
|
|
28
|
+
return groups.reduce((acc, g) => (acc << 16n) + BigInt(parseInt(g || "0", 16)), 0n);
|
|
29
|
+
}
|
|
30
|
+
function isInCidrV4(ip, cidr) {
|
|
31
|
+
const [range, bitsStr] = cidr.split("/");
|
|
32
|
+
const bits = bitsStr === undefined ? 32 : Number(bitsStr);
|
|
33
|
+
if (bits === 0)
|
|
34
|
+
return true;
|
|
35
|
+
const mask = (0xffffffff << (32 - bits)) >>> 0;
|
|
36
|
+
return (ipv4ToInt(ip) & mask) === (ipv4ToInt(range) & mask);
|
|
37
|
+
}
|
|
38
|
+
function isInCidrV6(ip, cidr) {
|
|
39
|
+
const [range, bitsStr] = cidr.split("/");
|
|
40
|
+
const prefix = bitsStr === undefined ? 128 : Number(bitsStr);
|
|
41
|
+
if (prefix === 0)
|
|
42
|
+
return true;
|
|
43
|
+
const mask = ((1n << 128n) - 1n) ^ ((1n << BigInt(128 - prefix)) - 1n);
|
|
44
|
+
return (ipv6ToBigInt(ip) & mask) === (ipv6ToBigInt(range) & mask);
|
|
45
|
+
}
|
|
46
|
+
/** Verifica se un IP "puro" appartiene a uno qualsiasi dei CIDR della sua versione. */
|
|
47
|
+
function isIpInAnyCidr(pureIp, cidrs, version) {
|
|
48
|
+
return version === 4
|
|
49
|
+
? cidrs.some((cidr) => isInCidrV4(pureIp, cidr))
|
|
50
|
+
: cidrs.some((cidr) => isInCidrV6(pureIp, cidr));
|
|
51
|
+
}
|
|
52
|
+
/** Ritorna la versione IP (4 | 6) di un valore già "purificato", oppure null. */
|
|
53
|
+
function ipVersion(pureIp) {
|
|
54
|
+
const version = (0, net_1.isIP)(pureIp);
|
|
55
|
+
return version === 0 ? null : version;
|
|
56
|
+
}
|
|
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.isAllowedIp = isAllowedIp;
|
|
7
7
|
exports.isCloudflareIp = isCloudflareIp;
|
|
8
8
|
const axios_1 = __importDefault(require("axios"));
|
|
9
|
-
const
|
|
9
|
+
const cidr_util_1 = require("./cidr.util");
|
|
10
10
|
const CF_IPS_V4_URL = "https://www.cloudflare.com/ips-v4/";
|
|
11
11
|
const CF_IPS_V6_URL = "https://www.cloudflare.com/ips-v6/";
|
|
12
12
|
const TTL_MS = 24 * 60 * 60 * 1000;
|
|
@@ -34,71 +34,30 @@ async function loadCloudflareIps() {
|
|
|
34
34
|
});
|
|
35
35
|
return inFlight;
|
|
36
36
|
}
|
|
37
|
-
function ipv4ToInt(ip) {
|
|
38
|
-
return ip.split(".").reduce((acc, oct) => (acc << 8) + Number(oct), 0) >>> 0;
|
|
39
|
-
}
|
|
40
|
-
function ipv6ToBigInt(ip) {
|
|
41
|
-
const [headStr, tailStr] = ip.includes("::") ? ip.split("::") : [ip, ""];
|
|
42
|
-
const head = headStr ? headStr.split(":") : [];
|
|
43
|
-
const tail = tailStr ? tailStr.split(":") : [];
|
|
44
|
-
const missing = 8 - head.length - tail.length;
|
|
45
|
-
const groups = [...head, ...Array(missing).fill("0"), ...tail];
|
|
46
|
-
return groups.reduce((acc, g) => (acc << 16n) + BigInt(parseInt(g || "0", 16)), 0n);
|
|
47
|
-
}
|
|
48
|
-
function isInCidrV4(ip, cidr) {
|
|
49
|
-
const [range, bitsStr] = cidr.split("/");
|
|
50
|
-
const bits = bitsStr === undefined ? 32 : Number(bitsStr);
|
|
51
|
-
if (bits === 0)
|
|
52
|
-
return true;
|
|
53
|
-
const mask = (0xffffffff << (32 - bits)) >>> 0;
|
|
54
|
-
return (ipv4ToInt(ip) & mask) === (ipv4ToInt(range) & mask);
|
|
55
|
-
}
|
|
56
|
-
function isInCidrV6(ip, cidr) {
|
|
57
|
-
const [range, bitsStr] = cidr.split("/");
|
|
58
|
-
const prefix = bitsStr === undefined ? 128 : Number(bitsStr);
|
|
59
|
-
if (prefix === 0)
|
|
60
|
-
return true;
|
|
61
|
-
const mask = ((1n << 128n) - 1n) ^ ((1n << BigInt(128 - prefix)) - 1n);
|
|
62
|
-
return (ipv6ToBigInt(ip) & mask) === (ipv6ToBigInt(range) & mask);
|
|
63
|
-
}
|
|
64
37
|
function isAllowedIp(ip, allowedIps) {
|
|
65
38
|
if (!ip || !allowedIps.length)
|
|
66
39
|
return false;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (pure.startsWith("::ffff:"))
|
|
70
|
-
pure = pure.slice("::ffff:".length);
|
|
71
|
-
const version = (0, net_1.isIP)(pure);
|
|
40
|
+
const pure = (0, cidr_util_1.purifyIp)(ip);
|
|
41
|
+
const version = (0, cidr_util_1.ipVersion)(pure);
|
|
72
42
|
if (!version)
|
|
73
43
|
return false;
|
|
74
44
|
return allowedIps.some((entry) => {
|
|
75
|
-
|
|
76
|
-
entryPure = entryPure.trim();
|
|
77
|
-
if (entryPure.startsWith("::ffff:"))
|
|
78
|
-
entryPure = entryPure.slice("::ffff:".length);
|
|
79
|
-
const entryVersion = (0, net_1.isIP)(entryPure);
|
|
45
|
+
const entryVersion = (0, cidr_util_1.ipVersion)((0, cidr_util_1.purifyIp)(entry));
|
|
80
46
|
if (!entryVersion || entryVersion !== version)
|
|
81
47
|
return false;
|
|
82
|
-
return
|
|
83
|
-
? isInCidrV4(pure, entry)
|
|
84
|
-
: isInCidrV6(pure, entry);
|
|
48
|
+
return (0, cidr_util_1.isIpInAnyCidr)(pure, [entry], version);
|
|
85
49
|
});
|
|
86
50
|
}
|
|
87
51
|
async function isCloudflareIp(ip) {
|
|
88
52
|
if (!ip)
|
|
89
53
|
return false;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (pure.startsWith("::ffff:"))
|
|
93
|
-
pure = pure.slice("::ffff:".length);
|
|
94
|
-
const version = (0, net_1.isIP)(pure);
|
|
54
|
+
const pure = (0, cidr_util_1.purifyIp)(ip);
|
|
55
|
+
const version = (0, cidr_util_1.ipVersion)(pure);
|
|
95
56
|
if (!version)
|
|
96
57
|
return false;
|
|
97
58
|
try {
|
|
98
59
|
const { v4, v6 } = await loadCloudflareIps();
|
|
99
|
-
return version === 4
|
|
100
|
-
? v4.some((cidr) => isInCidrV4(pure, cidr))
|
|
101
|
-
: v6.some((cidr) => isInCidrV6(pure, cidr));
|
|
60
|
+
return (0, cidr_util_1.isIpInAnyCidr)(pure, version === 4 ? v4 : v6, version);
|
|
102
61
|
}
|
|
103
62
|
catch {
|
|
104
63
|
return false;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True se l'IP appartiene ai crawler Google (Googlebot, special crawlers, fetcher).
|
|
3
|
+
*
|
|
4
|
+
* Strategia hybrid:
|
|
5
|
+
* 1. match veloce sulle liste IP ufficiali (cache 24h, auto-aggiornate);
|
|
6
|
+
* 2. fallback reverse DNS anti-spoofing se non presente nelle liste.
|
|
7
|
+
*
|
|
8
|
+
* Fail-open: se né le liste né il DNS riescono a verificare, ritorna `true`
|
|
9
|
+
* (non bannare) per non rischiare di bloccare Googlebot durante un disservizio.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isGoogleBotIp(ip: string): Promise<boolean>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isGoogleBotIp = isGoogleBotIp;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const dns_1 = require("dns");
|
|
9
|
+
const cidr_util_1 = require("./cidr.util");
|
|
10
|
+
// Liste IP ufficiali pubblicate (e aggiornate) da Google.
|
|
11
|
+
const GOOGLE_IP_LIST_URLS = [
|
|
12
|
+
"https://developers.google.com/static/search/apis/ipranges/googlebot.json",
|
|
13
|
+
"https://developers.google.com/static/search/apis/ipranges/special-crawlers.json",
|
|
14
|
+
"https://developers.google.com/static/search/apis/ipranges/user-triggered-fetchers.json",
|
|
15
|
+
];
|
|
16
|
+
// Domini dei crawler verificati da Google (per il fallback reverse DNS).
|
|
17
|
+
const GOOGLE_HOSTS = [".googlebot.com", ".google.com", ".googleusercontent.com"];
|
|
18
|
+
const TTL_MS = 24 * 60 * 60 * 1000;
|
|
19
|
+
let cache = null;
|
|
20
|
+
let inFlight = null;
|
|
21
|
+
async function loadGoogleIps() {
|
|
22
|
+
if (cache && Date.now() < cache.expiresAt)
|
|
23
|
+
return cache;
|
|
24
|
+
if (inFlight)
|
|
25
|
+
return inFlight;
|
|
26
|
+
inFlight = (async () => {
|
|
27
|
+
const responses = await Promise.all(GOOGLE_IP_LIST_URLS.map((url) => axios_1.default.get(url)));
|
|
28
|
+
const v4 = [];
|
|
29
|
+
const v6 = [];
|
|
30
|
+
for (const res of responses) {
|
|
31
|
+
for (const prefix of res.data?.prefixes ?? []) {
|
|
32
|
+
if (prefix.ipv4Prefix)
|
|
33
|
+
v4.push(prefix.ipv4Prefix);
|
|
34
|
+
if (prefix.ipv6Prefix)
|
|
35
|
+
v6.push(prefix.ipv6Prefix);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
cache = { v4, v6, expiresAt: Date.now() + TTL_MS };
|
|
39
|
+
return cache;
|
|
40
|
+
})().finally(() => {
|
|
41
|
+
inFlight = null;
|
|
42
|
+
});
|
|
43
|
+
return inFlight;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Verifica anti-spoofing via reverse + forward DNS (metodo raccomandato da Google).
|
|
47
|
+
* Ritorna `true`/`false` se determinabile, `null` se la risoluzione DNS fallisce.
|
|
48
|
+
*/
|
|
49
|
+
async function verifyByReverseDns(pureIp, version) {
|
|
50
|
+
try {
|
|
51
|
+
const hostnames = await dns_1.promises.reverse(pureIp);
|
|
52
|
+
const host = hostnames.find((h) => GOOGLE_HOSTS.some((d) => h.endsWith(d)));
|
|
53
|
+
if (!host)
|
|
54
|
+
return false; // reverse DNS risolto ma non è un dominio Google
|
|
55
|
+
const resolved = version === 4 ? await dns_1.promises.resolve4(host) : await dns_1.promises.resolve6(host);
|
|
56
|
+
return resolved.some((addr) => (0, cidr_util_1.purifyIp)(addr) === pureIp);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null; // nessun PTR / errore DNS: non determinabile
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* True se l'IP appartiene ai crawler Google (Googlebot, special crawlers, fetcher).
|
|
64
|
+
*
|
|
65
|
+
* Strategia hybrid:
|
|
66
|
+
* 1. match veloce sulle liste IP ufficiali (cache 24h, auto-aggiornate);
|
|
67
|
+
* 2. fallback reverse DNS anti-spoofing se non presente nelle liste.
|
|
68
|
+
*
|
|
69
|
+
* Fail-open: se né le liste né il DNS riescono a verificare, ritorna `true`
|
|
70
|
+
* (non bannare) per non rischiare di bloccare Googlebot durante un disservizio.
|
|
71
|
+
*/
|
|
72
|
+
async function isGoogleBotIp(ip) {
|
|
73
|
+
if (!ip)
|
|
74
|
+
return false;
|
|
75
|
+
const pure = (0, cidr_util_1.purifyIp)(ip);
|
|
76
|
+
const version = (0, cidr_util_1.ipVersion)(pure);
|
|
77
|
+
if (!version)
|
|
78
|
+
return false;
|
|
79
|
+
let listAvailable = false;
|
|
80
|
+
try {
|
|
81
|
+
const { v4, v6 } = await loadGoogleIps();
|
|
82
|
+
listAvailable = true;
|
|
83
|
+
if ((0, cidr_util_1.isIpInAnyCidr)(pure, version === 4 ? v4 : v6, version))
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
listAvailable = false;
|
|
88
|
+
}
|
|
89
|
+
const dnsResult = await verifyByReverseDns(pure, version);
|
|
90
|
+
if (dnsResult !== null)
|
|
91
|
+
return dnsResult;
|
|
92
|
+
// DNS non determinabile: se la lista era disponibile e l'IP non c'era → non è Google;
|
|
93
|
+
// se nemmeno la lista era raggiungibile → fail-open.
|
|
94
|
+
return !listAvailable;
|
|
95
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -14,7 +14,9 @@ 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("./cidr.util"), exports);
|
|
17
18
|
__exportStar(require("./cloudflare.constants"), exports);
|
|
18
19
|
__exportStar(require("./cloudflare-ips.util"), exports);
|
|
19
20
|
__exportStar(require("./get-client-ip.util"), exports);
|
|
21
|
+
__exportStar(require("./googlebot.util"), exports);
|
|
20
22
|
__exportStar(require("./options.validator"), exports);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liberstudio/cloudflare-list",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "Modulo NestJS per gestione IP List Cloudflare",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -32,14 +32,17 @@
|
|
|
32
32
|
"rxjs": ">=7.0.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@nestjs/axios": "^4.0.
|
|
36
|
-
"@nestjs/common": "^11.
|
|
37
|
-
"@nestjs/core": "^11.1.
|
|
35
|
+
"@nestjs/axios": "^4.0.1",
|
|
36
|
+
"@nestjs/common": "^11.1.26",
|
|
37
|
+
"@nestjs/core": "^11.1.26",
|
|
38
38
|
"@types/express": "^5.0.6",
|
|
39
|
-
"@types/node": "^20.
|
|
40
|
-
"axios": "^1.
|
|
41
|
-
"rxjs": "^7.8.
|
|
39
|
+
"@types/node": "^20.19.43",
|
|
40
|
+
"axios": "^1.18.0",
|
|
41
|
+
"rxjs": "^7.8.2",
|
|
42
42
|
"simple-git-hooks": "^2.13.1",
|
|
43
43
|
"typescript": "^5.0.0"
|
|
44
|
+
},
|
|
45
|
+
"allowScripts": {
|
|
46
|
+
"simple-git-hooks@2.13.1": true
|
|
44
47
|
}
|
|
45
48
|
}
|