@push.rocks/smartnetwork 4.8.0 → 4.10.0
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_rust/rustnetwork_linux_amd64 +0 -0
- package/dist_rust/rustnetwork_linux_arm64 +0 -0
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/helpers/ip.d.ts +21 -0
- package/dist_ts/helpers/ip.js +80 -1
- package/dist_ts/index.d.ts +1 -1
- package/dist_ts/index.js +2 -2
- package/dist_ts/smartnetwork.classes.domainintelligence.d.ts +13 -58
- package/dist_ts/smartnetwork.classes.domainintelligence.js +45 -319
- package/dist_ts/smartnetwork.classes.ipintelligence.d.ts +3 -34
- package/dist_ts/smartnetwork.classes.ipintelligence.js +18 -281
- package/dist_ts/smartnetwork.plugins.d.ts +2 -1
- package/dist_ts/smartnetwork.plugins.js +3 -2
- package/package.json +5 -4
- package/readme.md +3 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/helpers/ip.ts +79 -0
- package/ts/index.ts +1 -1
- package/ts/smartnetwork.classes.domainintelligence.ts +48 -341
- package/ts/smartnetwork.classes.ipintelligence.ts +19 -310
- package/ts/smartnetwork.plugins.ts +2 -1
|
@@ -64,30 +64,12 @@ export interface IIpIntelligenceOptions {
|
|
|
64
64
|
const CITY_MMDB_URL = 'https://cdn.jsdelivr.net/npm/@ip-location-db/geolite2-city-mmdb/geolite2-city-ipv4.mmdb';
|
|
65
65
|
const ASN_MMDB_URL = 'https://cdn.jsdelivr.net/npm/@ip-location-db/geolite2-asn-mmdb/geolite2-asn-ipv4.mmdb';
|
|
66
66
|
|
|
67
|
-
// IANA bootstrap for RDAP
|
|
68
|
-
const IANA_BOOTSTRAP_IPV4_URL = 'https://data.iana.org/rdap/ipv4.json';
|
|
69
|
-
|
|
70
67
|
const DEFAULT_DB_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
71
68
|
const DEFAULT_TIMEOUT = 5000;
|
|
72
69
|
|
|
73
|
-
/**
|
|
74
|
-
* Parsed IANA bootstrap entry: a CIDR prefix mapped to an RDAP base URL
|
|
75
|
-
*/
|
|
76
|
-
interface IBootstrapEntry {
|
|
77
|
-
prefix: string;
|
|
78
|
-
prefixNum: number; // numeric representation of the network address
|
|
79
|
-
maskBits: number;
|
|
80
|
-
baseUrl: string;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
interface IRdapNetworkInfo {
|
|
84
|
-
networkRange: string | null;
|
|
85
|
-
networkCidrs: string[] | null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
70
|
/**
|
|
89
71
|
* IpIntelligence provides IP address intelligence by combining three data sources:
|
|
90
|
-
* - RDAP (direct to RIRs) for registration/org data
|
|
72
|
+
* - RDAP (direct to RIRs, via @push.rocks/smartwhois) for registration/org data
|
|
91
73
|
* - Team Cymru DNS for ASN
|
|
92
74
|
* - MaxMind GeoLite2 (in-memory MMDB) for geolocation
|
|
93
75
|
*/
|
|
@@ -102,9 +84,8 @@ export class IpIntelligence {
|
|
|
102
84
|
private lastFetchTime = 0;
|
|
103
85
|
private refreshPromise: Promise<void> | null = null;
|
|
104
86
|
|
|
105
|
-
// RDAP bootstrap
|
|
106
|
-
private
|
|
107
|
-
private bootstrapPromise: Promise<void> | null = null;
|
|
87
|
+
// RDAP client from smartwhois (owns bootstrap caching and RIR matching)
|
|
88
|
+
private readonly rdapClient: plugins.smartwhois.RdapClient;
|
|
108
89
|
|
|
109
90
|
// Optional injected smartdns client (shared by SmartNetwork)
|
|
110
91
|
private readonly sharedDnsClient: InstanceType<typeof plugins.smartdns.dnsClientMod.Smartdns> | null;
|
|
@@ -113,6 +94,11 @@ export class IpIntelligence {
|
|
|
113
94
|
this.dbMaxAge = options?.dbMaxAge ?? DEFAULT_DB_MAX_AGE;
|
|
114
95
|
this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
|
|
115
96
|
this.sharedDnsClient = options?.dnsClient ?? null;
|
|
97
|
+
this.rdapClient = new plugins.smartwhois.RdapClient({
|
|
98
|
+
requestTimeoutMs: this.timeout,
|
|
99
|
+
totalTimeoutMs: this.timeout,
|
|
100
|
+
userAgent: '@push.rocks/smartnetwork',
|
|
101
|
+
});
|
|
116
102
|
}
|
|
117
103
|
|
|
118
104
|
/**
|
|
@@ -186,80 +172,10 @@ export class IpIntelligence {
|
|
|
186
172
|
return result;
|
|
187
173
|
}
|
|
188
174
|
|
|
189
|
-
// ─── RDAP Subsystem
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Load and cache the IANA RDAP bootstrap file
|
|
193
|
-
*/
|
|
194
|
-
private async ensureBootstrap(): Promise<void> {
|
|
195
|
-
if (this.bootstrapEntries) return;
|
|
196
|
-
if (this.bootstrapPromise) {
|
|
197
|
-
await this.bootstrapPromise;
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
this.bootstrapPromise = (async () => {
|
|
202
|
-
try {
|
|
203
|
-
const controller = new AbortController();
|
|
204
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
205
|
-
try {
|
|
206
|
-
const response = await fetch(IANA_BOOTSTRAP_IPV4_URL, {
|
|
207
|
-
signal: controller.signal,
|
|
208
|
-
headers: { 'User-Agent': '@push.rocks/smartnetwork' },
|
|
209
|
-
});
|
|
210
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
211
|
-
const data = await response.json() as { services: [string[], string[]][] };
|
|
212
|
-
|
|
213
|
-
const entries: IBootstrapEntry[] = [];
|
|
214
|
-
for (const [prefixes, urls] of data.services) {
|
|
215
|
-
const baseUrl = urls[0]; // first URL is preferred
|
|
216
|
-
for (const prefix of prefixes) {
|
|
217
|
-
const [network, bits] = prefix.split('/');
|
|
218
|
-
entries.push({
|
|
219
|
-
prefix,
|
|
220
|
-
prefixNum: this.ipToNumber(network),
|
|
221
|
-
maskBits: parseInt(bits, 10),
|
|
222
|
-
baseUrl: baseUrl.replace(/\/$/, ''), // strip trailing slash
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Sort by mask bits descending for longest-prefix match
|
|
228
|
-
entries.sort((a, b) => b.maskBits - a.maskBits);
|
|
229
|
-
this.bootstrapEntries = entries;
|
|
230
|
-
} finally {
|
|
231
|
-
clearTimeout(timeoutId);
|
|
232
|
-
}
|
|
233
|
-
} catch (err: any) {
|
|
234
|
-
this.logger.debug?.(`Failed to load RDAP bootstrap: ${err.message}`);
|
|
235
|
-
this.bootstrapEntries = []; // empty = all RDAP lookups will skip
|
|
236
|
-
}
|
|
237
|
-
})();
|
|
238
|
-
|
|
239
|
-
await this.bootstrapPromise;
|
|
240
|
-
this.bootstrapPromise = null;
|
|
241
|
-
}
|
|
175
|
+
// ─── RDAP Subsystem (delegated to @push.rocks/smartwhois) ───────────
|
|
242
176
|
|
|
243
177
|
/**
|
|
244
|
-
*
|
|
245
|
-
*/
|
|
246
|
-
private matchRir(ip: string): string | null {
|
|
247
|
-
if (!this.bootstrapEntries || this.bootstrapEntries.length === 0) return null;
|
|
248
|
-
|
|
249
|
-
const ipNum = this.ipToNumber(ip);
|
|
250
|
-
|
|
251
|
-
for (const entry of this.bootstrapEntries) {
|
|
252
|
-
const mask = (0xFFFFFFFF << (32 - entry.maskBits)) >>> 0;
|
|
253
|
-
if ((ipNum & mask) === (entry.prefixNum & mask)) {
|
|
254
|
-
return entry.baseUrl;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Query RDAP for registration data
|
|
178
|
+
* Query RDAP for registration data via the smartwhois RdapClient (IPv4 and IPv6).
|
|
263
179
|
*/
|
|
264
180
|
private async queryRdap(ip: string): Promise<{
|
|
265
181
|
registrantOrg: string | null;
|
|
@@ -268,160 +184,20 @@ export class IpIntelligence {
|
|
|
268
184
|
networkCidrs: string[] | null;
|
|
269
185
|
abuseContact: string | null;
|
|
270
186
|
} | null> {
|
|
271
|
-
await this.ensureBootstrap();
|
|
272
|
-
const baseUrl = this.matchRir(ip);
|
|
273
|
-
if (!baseUrl) return null;
|
|
274
|
-
|
|
275
|
-
const controller = new AbortController();
|
|
276
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
277
187
|
try {
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
let registrantOrg: string | null = null;
|
|
289
|
-
let registrantCountry: string | null = data.country || null;
|
|
290
|
-
let abuseContact: string | null = null;
|
|
291
|
-
|
|
292
|
-
const { networkRange, networkCidrs } = this.parseRdapNetworkInfo(data);
|
|
293
|
-
|
|
294
|
-
// Parse entities
|
|
295
|
-
if (data.entities && Array.isArray(data.entities)) {
|
|
296
|
-
for (const entity of data.entities) {
|
|
297
|
-
const roles: string[] = entity.roles || [];
|
|
298
|
-
|
|
299
|
-
if (roles.includes('registrant') || roles.includes('administrative')) {
|
|
300
|
-
const orgName = this.extractVcardFn(entity);
|
|
301
|
-
if (orgName) registrantOrg = orgName;
|
|
302
|
-
|
|
303
|
-
// Try to get country from registrant address if not at top level
|
|
304
|
-
if (!registrantCountry) {
|
|
305
|
-
registrantCountry = this.extractVcardCountry(entity);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (roles.includes('abuse')) {
|
|
310
|
-
abuseContact = this.extractVcardEmail(entity);
|
|
311
|
-
// Check nested entities for abuse contact
|
|
312
|
-
if (!abuseContact && entity.entities) {
|
|
313
|
-
for (const subEntity of entity.entities) {
|
|
314
|
-
const subRoles: string[] = subEntity.roles || [];
|
|
315
|
-
if (subRoles.includes('abuse')) {
|
|
316
|
-
abuseContact = this.extractVcardEmail(subEntity);
|
|
317
|
-
if (abuseContact) break;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
return { registrantOrg, registrantCountry, networkRange, networkCidrs, abuseContact };
|
|
188
|
+
const rdapRecord = await this.rdapClient.lookupIp(ip);
|
|
189
|
+
if (!rdapRecord) return null;
|
|
190
|
+
return {
|
|
191
|
+
registrantOrg: rdapRecord.registrantOrg ?? null,
|
|
192
|
+
registrantCountry: rdapRecord.registrantCountry ?? null,
|
|
193
|
+
networkRange: rdapRecord.networkRange ?? null,
|
|
194
|
+
networkCidrs: rdapRecord.networkCidrs ?? null,
|
|
195
|
+
abuseContact: rdapRecord.abuseEmail ?? null,
|
|
196
|
+
};
|
|
326
197
|
} catch (err: any) {
|
|
327
198
|
this.logger.debug?.(`RDAP query failed for ${ip}: ${err.message}`);
|
|
328
199
|
return null;
|
|
329
|
-
} finally {
|
|
330
|
-
clearTimeout(timeoutId);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
private parseRdapNetworkInfo(data: any): IRdapNetworkInfo {
|
|
335
|
-
const cidrs = this.extractRdapCidrs(data);
|
|
336
|
-
if (cidrs.length > 0) {
|
|
337
|
-
return {
|
|
338
|
-
networkRange: cidrs[0],
|
|
339
|
-
networkCidrs: cidrs,
|
|
340
|
-
};
|
|
341
200
|
}
|
|
342
|
-
|
|
343
|
-
if (typeof data.startAddress === 'string' && typeof data.endAddress === 'string') {
|
|
344
|
-
const rangeCidrs = this.ipv4RangeToCidrs(data.startAddress, data.endAddress);
|
|
345
|
-
return {
|
|
346
|
-
networkRange: rangeCidrs.length === 1
|
|
347
|
-
? rangeCidrs[0]
|
|
348
|
-
: `${data.startAddress} - ${data.endAddress}`,
|
|
349
|
-
networkCidrs: rangeCidrs.length > 0 ? rangeCidrs : null,
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
return { networkRange: null, networkCidrs: null };
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
private extractRdapCidrs(data: any): string[] {
|
|
357
|
-
if (!Array.isArray(data.cidr0_cidrs)) return [];
|
|
358
|
-
return data.cidr0_cidrs
|
|
359
|
-
.map((cidr: any) => {
|
|
360
|
-
const prefix = cidr?.v4prefix || cidr?.v6prefix;
|
|
361
|
-
const length = Number(cidr?.length);
|
|
362
|
-
if (typeof prefix !== 'string' || !Number.isInteger(length)) return null;
|
|
363
|
-
return `${prefix}/${length}`;
|
|
364
|
-
})
|
|
365
|
-
.filter(Boolean) as string[];
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Extract the 'fn' (formatted name) from an entity's vcardArray
|
|
370
|
-
*/
|
|
371
|
-
private extractVcardFn(entity: any): string | null {
|
|
372
|
-
if (!entity.vcardArray || !Array.isArray(entity.vcardArray)) return null;
|
|
373
|
-
const properties = entity.vcardArray[1];
|
|
374
|
-
if (!Array.isArray(properties)) return null;
|
|
375
|
-
|
|
376
|
-
for (const prop of properties) {
|
|
377
|
-
if (Array.isArray(prop) && prop[0] === 'fn') {
|
|
378
|
-
return prop[3] || null;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Extract email from an entity's vcardArray
|
|
386
|
-
*/
|
|
387
|
-
private extractVcardEmail(entity: any): string | null {
|
|
388
|
-
if (!entity.vcardArray || !Array.isArray(entity.vcardArray)) return null;
|
|
389
|
-
const properties = entity.vcardArray[1];
|
|
390
|
-
if (!Array.isArray(properties)) return null;
|
|
391
|
-
|
|
392
|
-
for (const prop of properties) {
|
|
393
|
-
if (Array.isArray(prop) && prop[0] === 'email') {
|
|
394
|
-
return prop[3] || null;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
return null;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Extract country from an entity's vcardArray address field
|
|
402
|
-
*/
|
|
403
|
-
private extractVcardCountry(entity: any): string | null {
|
|
404
|
-
if (!entity.vcardArray || !Array.isArray(entity.vcardArray)) return null;
|
|
405
|
-
const properties = entity.vcardArray[1];
|
|
406
|
-
if (!Array.isArray(properties)) return null;
|
|
407
|
-
|
|
408
|
-
for (const prop of properties) {
|
|
409
|
-
if (Array.isArray(prop) && prop[0] === 'adr') {
|
|
410
|
-
// The label parameter often contains the full address with country at the end
|
|
411
|
-
const label = prop[1]?.label;
|
|
412
|
-
if (typeof label === 'string') {
|
|
413
|
-
const lines = label.split('\n');
|
|
414
|
-
const lastLine = lines[lines.length - 1]?.trim();
|
|
415
|
-
if (lastLine && lastLine.length > 1) return lastLine;
|
|
416
|
-
}
|
|
417
|
-
// Also check the structured value (7-element array, last element is country)
|
|
418
|
-
const value = prop[3];
|
|
419
|
-
if (Array.isArray(value) && value.length >= 7 && value[6]) {
|
|
420
|
-
return value[6];
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
return null;
|
|
425
201
|
}
|
|
426
202
|
|
|
427
203
|
// ─── Team Cymru DNS Subsystem ───────────────────────────────────────
|
|
@@ -601,71 +377,4 @@ export class IpIntelligence {
|
|
|
601
377
|
|
|
602
378
|
return { country, countryCode, city, latitude, longitude, accuracyRadius, timezone, asn, asnOrg };
|
|
603
379
|
}
|
|
604
|
-
|
|
605
|
-
// ─── Utilities ──────────────────────────────────────────────────────
|
|
606
|
-
|
|
607
|
-
/**
|
|
608
|
-
* Convert an IPv4 address string to a 32-bit unsigned number
|
|
609
|
-
*/
|
|
610
|
-
private ipToNumber(ip: string): number {
|
|
611
|
-
const parts = ip.split('.');
|
|
612
|
-
return (
|
|
613
|
-
((parseInt(parts[0], 10) << 24) |
|
|
614
|
-
(parseInt(parts[1], 10) << 16) |
|
|
615
|
-
(parseInt(parts[2], 10) << 8) |
|
|
616
|
-
parseInt(parts[3], 10)) >>> 0
|
|
617
|
-
);
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
private ipv4RangeToCidrs(startIp: string, endIp: string): string[] {
|
|
621
|
-
const start = this.ipv4ToBigInt(startIp);
|
|
622
|
-
const end = this.ipv4ToBigInt(endIp);
|
|
623
|
-
if (start === undefined || end === undefined || start > end) return [];
|
|
624
|
-
|
|
625
|
-
const cidrs: string[] = [];
|
|
626
|
-
let current = start;
|
|
627
|
-
while (current <= end) {
|
|
628
|
-
let maxBlockSize = current === 0n ? 1n << 32n : current & -current;
|
|
629
|
-
const remaining = end - current + 1n;
|
|
630
|
-
while (maxBlockSize > remaining) {
|
|
631
|
-
maxBlockSize = maxBlockSize / 2n;
|
|
632
|
-
}
|
|
633
|
-
const prefixLength = 32 - this.powerOfTwoExponent(maxBlockSize);
|
|
634
|
-
cidrs.push(`${this.numberToIpv4(current)}/${prefixLength}`);
|
|
635
|
-
current += maxBlockSize;
|
|
636
|
-
}
|
|
637
|
-
return cidrs;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
private ipv4ToBigInt(ip: string): bigint | undefined {
|
|
641
|
-
const parts = ip.trim().split('.');
|
|
642
|
-
if (parts.length !== 4) return undefined;
|
|
643
|
-
let result = 0n;
|
|
644
|
-
for (const part of parts) {
|
|
645
|
-
if (!/^\d+$/.test(part)) return undefined;
|
|
646
|
-
const number = Number(part);
|
|
647
|
-
if (!Number.isInteger(number) || number < 0 || number > 255) return undefined;
|
|
648
|
-
result = (result * 256n) + BigInt(number);
|
|
649
|
-
}
|
|
650
|
-
return result;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
private numberToIpv4(value: bigint): string {
|
|
654
|
-
return [
|
|
655
|
-
Number((value >> 24n) & 255n),
|
|
656
|
-
Number((value >> 16n) & 255n),
|
|
657
|
-
Number((value >> 8n) & 255n),
|
|
658
|
-
Number(value & 255n),
|
|
659
|
-
].join('.');
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
private powerOfTwoExponent(value: bigint): number {
|
|
663
|
-
let exponent = 0;
|
|
664
|
-
let remaining = value;
|
|
665
|
-
while (remaining > 1n) {
|
|
666
|
-
remaining >>= 1n;
|
|
667
|
-
exponent++;
|
|
668
|
-
}
|
|
669
|
-
return exponent;
|
|
670
|
-
}
|
|
671
380
|
}
|
|
@@ -8,8 +8,9 @@ export { os, https, perfHooks };
|
|
|
8
8
|
// @pushrocks scope
|
|
9
9
|
import * as smartdns from '@push.rocks/smartdns';
|
|
10
10
|
import * as smartrust from '@push.rocks/smartrust';
|
|
11
|
+
import * as smartwhois from '@push.rocks/smartwhois';
|
|
11
12
|
|
|
12
|
-
export { smartdns, smartrust };
|
|
13
|
+
export { smartdns, smartrust, smartwhois };
|
|
13
14
|
|
|
14
15
|
// third party
|
|
15
16
|
import * as maxmind from 'maxmind';
|