@push.rocks/smartnetwork 4.5.2 → 4.7.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_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +2 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/smartnetwork.classes.domainintelligence.d.ts +170 -0
- package/dist_ts/smartnetwork.classes.domainintelligence.js +542 -0
- package/dist_ts/smartnetwork.classes.ipintelligence.d.ts +16 -0
- package/dist_ts/smartnetwork.classes.ipintelligence.js +100 -18
- package/dist_ts/smartnetwork.classes.smartnetwork.d.ts +23 -2
- package/dist_ts/smartnetwork.classes.smartnetwork.js +52 -8
- package/package.json +1 -1
- package/readme.md +115 -8
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +2 -0
- package/ts/smartnetwork.classes.domainintelligence.ts +659 -0
- package/ts/smartnetwork.classes.ipintelligence.ts +119 -16
- package/ts/smartnetwork.classes.smartnetwork.ts +56 -7
|
@@ -30,6 +30,7 @@ export interface IIpIntelligenceResult {
|
|
|
30
30
|
registrantOrg: string | null;
|
|
31
31
|
registrantCountry: string | null;
|
|
32
32
|
networkRange: string | null;
|
|
33
|
+
networkCidrs: string[] | null;
|
|
33
34
|
abuseContact: string | null;
|
|
34
35
|
|
|
35
36
|
// Geolocation (MaxMind GeoLite2 City)
|
|
@@ -50,6 +51,13 @@ export interface IIpIntelligenceOptions {
|
|
|
50
51
|
dbMaxAge?: number;
|
|
51
52
|
/** Timeout (ms) for RDAP/DNS/CDN requests. Default: 5000 */
|
|
52
53
|
timeout?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Optional injected smartdns client. When provided, IpIntelligence will
|
|
56
|
+
* not create or destroy its own client (the owner — typically SmartNetwork —
|
|
57
|
+
* manages lifecycle). When omitted, a short-lived client is created per
|
|
58
|
+
* Team Cymru lookup and destroyed in finally.
|
|
59
|
+
*/
|
|
60
|
+
dnsClient?: InstanceType<typeof plugins.smartdns.dnsClientMod.Smartdns>;
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
// CDN URLs for GeoLite2 MMDB files (served via jsDelivr from npm packages)
|
|
@@ -72,6 +80,11 @@ interface IBootstrapEntry {
|
|
|
72
80
|
baseUrl: string;
|
|
73
81
|
}
|
|
74
82
|
|
|
83
|
+
interface IRdapNetworkInfo {
|
|
84
|
+
networkRange: string | null;
|
|
85
|
+
networkCidrs: string[] | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
/**
|
|
76
89
|
* IpIntelligence provides IP address intelligence by combining three data sources:
|
|
77
90
|
* - RDAP (direct to RIRs) for registration/org data
|
|
@@ -93,9 +106,13 @@ export class IpIntelligence {
|
|
|
93
106
|
private bootstrapEntries: IBootstrapEntry[] | null = null;
|
|
94
107
|
private bootstrapPromise: Promise<void> | null = null;
|
|
95
108
|
|
|
109
|
+
// Optional injected smartdns client (shared by SmartNetwork)
|
|
110
|
+
private readonly sharedDnsClient: InstanceType<typeof plugins.smartdns.dnsClientMod.Smartdns> | null;
|
|
111
|
+
|
|
96
112
|
constructor(options?: IIpIntelligenceOptions) {
|
|
97
113
|
this.dbMaxAge = options?.dbMaxAge ?? DEFAULT_DB_MAX_AGE;
|
|
98
114
|
this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
|
|
115
|
+
this.sharedDnsClient = options?.dnsClient ?? null;
|
|
99
116
|
}
|
|
100
117
|
|
|
101
118
|
/**
|
|
@@ -109,6 +126,7 @@ export class IpIntelligence {
|
|
|
109
126
|
registrantOrg: null,
|
|
110
127
|
registrantCountry: null,
|
|
111
128
|
networkRange: null,
|
|
129
|
+
networkCidrs: null,
|
|
112
130
|
abuseContact: null,
|
|
113
131
|
country: null,
|
|
114
132
|
countryCode: null,
|
|
@@ -132,6 +150,7 @@ export class IpIntelligence {
|
|
|
132
150
|
result.registrantOrg = rdap.registrantOrg;
|
|
133
151
|
result.registrantCountry = rdap.registrantCountry;
|
|
134
152
|
result.networkRange = rdap.networkRange;
|
|
153
|
+
result.networkCidrs = rdap.networkCidrs;
|
|
135
154
|
result.abuseContact = rdap.abuseContact;
|
|
136
155
|
}
|
|
137
156
|
|
|
@@ -246,6 +265,7 @@ export class IpIntelligence {
|
|
|
246
265
|
registrantOrg: string | null;
|
|
247
266
|
registrantCountry: string | null;
|
|
248
267
|
networkRange: string | null;
|
|
268
|
+
networkCidrs: string[] | null;
|
|
249
269
|
abuseContact: string | null;
|
|
250
270
|
} | null> {
|
|
251
271
|
await this.ensureBootstrap();
|
|
@@ -269,14 +289,7 @@ export class IpIntelligence {
|
|
|
269
289
|
let registrantCountry: string | null = data.country || null;
|
|
270
290
|
let abuseContact: string | null = null;
|
|
271
291
|
|
|
272
|
-
|
|
273
|
-
let networkRange: string | null = null;
|
|
274
|
-
if (data.cidr0_cidrs && data.cidr0_cidrs.length > 0) {
|
|
275
|
-
const cidr = data.cidr0_cidrs[0];
|
|
276
|
-
networkRange = `${cidr.v4prefix || cidr.v6prefix}/${cidr.length}`;
|
|
277
|
-
} else if (data.startAddress && data.endAddress) {
|
|
278
|
-
networkRange = `${data.startAddress} - ${data.endAddress}`;
|
|
279
|
-
}
|
|
292
|
+
const { networkRange, networkCidrs } = this.parseRdapNetworkInfo(data);
|
|
280
293
|
|
|
281
294
|
// Parse entities
|
|
282
295
|
if (data.entities && Array.isArray(data.entities)) {
|
|
@@ -309,7 +322,7 @@ export class IpIntelligence {
|
|
|
309
322
|
}
|
|
310
323
|
}
|
|
311
324
|
|
|
312
|
-
return { registrantOrg, registrantCountry, networkRange, abuseContact };
|
|
325
|
+
return { registrantOrg, registrantCountry, networkRange, networkCidrs, abuseContact };
|
|
313
326
|
} catch (err: any) {
|
|
314
327
|
this.logger.debug?.(`RDAP query failed for ${ip}: ${err.message}`);
|
|
315
328
|
return null;
|
|
@@ -318,6 +331,40 @@ export class IpIntelligence {
|
|
|
318
331
|
}
|
|
319
332
|
}
|
|
320
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
|
+
}
|
|
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
|
+
|
|
321
368
|
/**
|
|
322
369
|
* Extract the 'fn' (formatted name) from an entity's vcardArray
|
|
323
370
|
*/
|
|
@@ -385,16 +432,19 @@ export class IpIntelligence {
|
|
|
385
432
|
* Response: "ASN | prefix | CC | rir | date"
|
|
386
433
|
*/
|
|
387
434
|
private async queryTeamCymru(ip: string): Promise<{ asn: number; prefix: string; country: string } | null> {
|
|
388
|
-
|
|
435
|
+
const external = this.sharedDnsClient !== null;
|
|
436
|
+
let dnsClient = this.sharedDnsClient;
|
|
389
437
|
try {
|
|
390
438
|
const reversed = ip.split('.').reverse().join('.');
|
|
391
439
|
const queryName = `${reversed}.origin.asn.cymru.com`;
|
|
392
440
|
|
|
393
|
-
dnsClient
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
441
|
+
if (!dnsClient) {
|
|
442
|
+
dnsClient = new plugins.smartdns.dnsClientMod.Smartdns({
|
|
443
|
+
strategy: 'prefer-system',
|
|
444
|
+
allowDohFallback: true,
|
|
445
|
+
timeoutMs: this.timeout,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
398
448
|
|
|
399
449
|
const records = await dnsClient.getRecordsTxt(queryName);
|
|
400
450
|
if (!records || records.length === 0) return null;
|
|
@@ -418,7 +468,8 @@ export class IpIntelligence {
|
|
|
418
468
|
this.logger.debug?.(`Team Cymru DNS query failed for ${ip}: ${err.message}`);
|
|
419
469
|
return null;
|
|
420
470
|
} finally {
|
|
421
|
-
|
|
471
|
+
// Only destroy clients we created ourselves; leave injected ones alone.
|
|
472
|
+
if (!external && dnsClient) {
|
|
422
473
|
dnsClient.destroy();
|
|
423
474
|
}
|
|
424
475
|
}
|
|
@@ -558,4 +609,56 @@ export class IpIntelligence {
|
|
|
558
609
|
parseInt(parts[3], 10)) >>> 0
|
|
559
610
|
);
|
|
560
611
|
}
|
|
612
|
+
|
|
613
|
+
private ipv4RangeToCidrs(startIp: string, endIp: string): string[] {
|
|
614
|
+
const start = this.ipv4ToBigInt(startIp);
|
|
615
|
+
const end = this.ipv4ToBigInt(endIp);
|
|
616
|
+
if (start === undefined || end === undefined || start > end) return [];
|
|
617
|
+
|
|
618
|
+
const cidrs: string[] = [];
|
|
619
|
+
let current = start;
|
|
620
|
+
while (current <= end) {
|
|
621
|
+
let maxBlockSize = current === 0n ? 1n << 32n : current & -current;
|
|
622
|
+
const remaining = end - current + 1n;
|
|
623
|
+
while (maxBlockSize > remaining) {
|
|
624
|
+
maxBlockSize = maxBlockSize / 2n;
|
|
625
|
+
}
|
|
626
|
+
const prefixLength = 32 - this.powerOfTwoExponent(maxBlockSize);
|
|
627
|
+
cidrs.push(`${this.numberToIpv4(current)}/${prefixLength}`);
|
|
628
|
+
current += maxBlockSize;
|
|
629
|
+
}
|
|
630
|
+
return cidrs;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
private ipv4ToBigInt(ip: string): bigint | undefined {
|
|
634
|
+
const parts = ip.trim().split('.');
|
|
635
|
+
if (parts.length !== 4) return undefined;
|
|
636
|
+
let result = 0n;
|
|
637
|
+
for (const part of parts) {
|
|
638
|
+
if (!/^\d+$/.test(part)) return undefined;
|
|
639
|
+
const number = Number(part);
|
|
640
|
+
if (!Number.isInteger(number) || number < 0 || number > 255) return undefined;
|
|
641
|
+
result = (result * 256n) + BigInt(number);
|
|
642
|
+
}
|
|
643
|
+
return result;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
private numberToIpv4(value: bigint): string {
|
|
647
|
+
return [
|
|
648
|
+
Number((value >> 24n) & 255n),
|
|
649
|
+
Number((value >> 16n) & 255n),
|
|
650
|
+
Number((value >> 8n) & 255n),
|
|
651
|
+
Number(value & 255n),
|
|
652
|
+
].join('.');
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
private powerOfTwoExponent(value: bigint): number {
|
|
656
|
+
let exponent = 0;
|
|
657
|
+
let remaining = value;
|
|
658
|
+
while (remaining > 1n) {
|
|
659
|
+
remaining >>= 1n;
|
|
660
|
+
exponent++;
|
|
661
|
+
}
|
|
662
|
+
return exponent;
|
|
663
|
+
}
|
|
561
664
|
}
|
|
@@ -2,10 +2,14 @@ import * as plugins from './smartnetwork.plugins.js';
|
|
|
2
2
|
import { CloudflareSpeed } from './smartnetwork.classes.cloudflarespeed.js';
|
|
3
3
|
import { PublicIp } from './smartnetwork.classes.publicip.js';
|
|
4
4
|
import { IpIntelligence, type IIpIntelligenceResult } from './smartnetwork.classes.ipintelligence.js';
|
|
5
|
+
import { DomainIntelligence, type IDomainIntelligenceResult } from './smartnetwork.classes.domainintelligence.js';
|
|
5
6
|
import { getLogger } from './logging.js';
|
|
6
7
|
import { NetworkError } from './errors.js';
|
|
7
8
|
import { RustNetworkBridge } from './smartnetwork.classes.rustbridge.js';
|
|
8
9
|
|
|
10
|
+
/** Type alias for the shared Smartdns client instance */
|
|
11
|
+
type TSmartdnsClient = InstanceType<typeof plugins.smartdns.dnsClientMod.Smartdns>;
|
|
12
|
+
|
|
9
13
|
/**
|
|
10
14
|
* Configuration options for SmartNetwork
|
|
11
15
|
*/
|
|
@@ -54,6 +58,8 @@ export class SmartNetwork {
|
|
|
54
58
|
private rustBridge: RustNetworkBridge;
|
|
55
59
|
private bridgeStarted = false;
|
|
56
60
|
private ipIntelligence: IpIntelligence | null = null;
|
|
61
|
+
private domainIntelligence: DomainIntelligence | null = null;
|
|
62
|
+
private dnsClient: TSmartdnsClient | null = null;
|
|
57
63
|
|
|
58
64
|
constructor(options?: SmartNetworkOptions) {
|
|
59
65
|
this.options = options || {};
|
|
@@ -73,13 +79,24 @@ export class SmartNetwork {
|
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
/**
|
|
76
|
-
* Stop the Rust binary bridge.
|
|
82
|
+
* Stop the Rust binary bridge and tear down the shared Smartdns client.
|
|
83
|
+
* Call this before your Node process exits if you've used any DNS or
|
|
84
|
+
* Rust-backed features, otherwise the smartdns Rust backend may keep
|
|
85
|
+
* the event loop alive.
|
|
77
86
|
*/
|
|
78
87
|
public async stop(): Promise<void> {
|
|
79
88
|
if (this.bridgeStarted) {
|
|
80
89
|
await this.rustBridge.stop();
|
|
81
90
|
this.bridgeStarted = false;
|
|
82
91
|
}
|
|
92
|
+
if (this.dnsClient) {
|
|
93
|
+
this.dnsClient.destroy();
|
|
94
|
+
this.dnsClient = null;
|
|
95
|
+
// Intelligence instances hold a stale reference to the destroyed
|
|
96
|
+
// client; drop them so the next call rebuilds with a fresh one.
|
|
97
|
+
this.ipIntelligence = null;
|
|
98
|
+
this.domainIntelligence = null;
|
|
99
|
+
}
|
|
83
100
|
}
|
|
84
101
|
|
|
85
102
|
/**
|
|
@@ -91,6 +108,23 @@ export class SmartNetwork {
|
|
|
91
108
|
}
|
|
92
109
|
}
|
|
93
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Lazily create the shared Smartdns client. The Rust backend inside
|
|
113
|
+
* Smartdns is only spawned on first query that requires it (NS/MX/SOA
|
|
114
|
+
* with prefer-system strategy, or any query with doh/udp strategy).
|
|
115
|
+
* The client is destroyed by stop().
|
|
116
|
+
*/
|
|
117
|
+
private ensureDnsClient(): TSmartdnsClient {
|
|
118
|
+
if (!this.dnsClient) {
|
|
119
|
+
this.dnsClient = new plugins.smartdns.dnsClientMod.Smartdns({
|
|
120
|
+
strategy: 'prefer-system',
|
|
121
|
+
allowDohFallback: true,
|
|
122
|
+
timeoutMs: 5000,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return this.dnsClient;
|
|
126
|
+
}
|
|
127
|
+
|
|
94
128
|
/**
|
|
95
129
|
* Get network speed via Cloudflare speed test (pure TS, no Rust needed).
|
|
96
130
|
*/
|
|
@@ -310,16 +344,15 @@ export class SmartNetwork {
|
|
|
310
344
|
}
|
|
311
345
|
|
|
312
346
|
/**
|
|
313
|
-
* Resolve DNS records (A, AAAA, MX)
|
|
347
|
+
* Resolve DNS records (A, AAAA, MX) via the shared smartdns client.
|
|
348
|
+
* The client is lifecycle-managed by start()/stop() — MX queries spawn
|
|
349
|
+
* the smartdns Rust bridge, which is torn down by stop().
|
|
314
350
|
*/
|
|
315
351
|
public async resolveDns(
|
|
316
352
|
host: string,
|
|
317
353
|
): Promise<{ A: string[]; AAAA: string[]; MX: { exchange: string; priority: number }[] }> {
|
|
318
354
|
try {
|
|
319
|
-
const dnsClient =
|
|
320
|
-
strategy: 'prefer-system',
|
|
321
|
-
allowDohFallback: true,
|
|
322
|
-
});
|
|
355
|
+
const dnsClient = this.ensureDnsClient();
|
|
323
356
|
|
|
324
357
|
const [aRecords, aaaaRecords, mxRecords] = await Promise.all([
|
|
325
358
|
dnsClient.getRecordsA(host).catch((): any[] => []),
|
|
@@ -404,7 +437,7 @@ export class SmartNetwork {
|
|
|
404
437
|
*/
|
|
405
438
|
public async getIpIntelligence(ip: string): Promise<IIpIntelligenceResult> {
|
|
406
439
|
if (!this.ipIntelligence) {
|
|
407
|
-
this.ipIntelligence = new IpIntelligence();
|
|
440
|
+
this.ipIntelligence = new IpIntelligence({ dnsClient: this.ensureDnsClient() });
|
|
408
441
|
}
|
|
409
442
|
const fetcher = () => this.ipIntelligence!.getIntelligence(ip);
|
|
410
443
|
if (this.options.cacheTtl && this.options.cacheTtl > 0) {
|
|
@@ -413,6 +446,22 @@ export class SmartNetwork {
|
|
|
413
446
|
return fetcher();
|
|
414
447
|
}
|
|
415
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Get domain intelligence: registrar, registrant, nameservers, registration
|
|
451
|
+
* events, status flags, DNSSEC, and abuse contact via RDAP. Pure TS, no
|
|
452
|
+
* Rust needed.
|
|
453
|
+
*/
|
|
454
|
+
public async getDomainIntelligence(domain: string): Promise<IDomainIntelligenceResult> {
|
|
455
|
+
if (!this.domainIntelligence) {
|
|
456
|
+
this.domainIntelligence = new DomainIntelligence({ dnsClient: this.ensureDnsClient() });
|
|
457
|
+
}
|
|
458
|
+
const fetcher = () => this.domainIntelligence!.getIntelligence(domain);
|
|
459
|
+
if (this.options.cacheTtl && this.options.cacheTtl > 0) {
|
|
460
|
+
return this.getCached(`domainIntelligence:${domain}`, fetcher);
|
|
461
|
+
}
|
|
462
|
+
return fetcher();
|
|
463
|
+
}
|
|
464
|
+
|
|
416
465
|
/**
|
|
417
466
|
* Internal caching helper
|
|
418
467
|
*/
|