@push.rocks/smartnetwork 4.4.0 → 4.5.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.
@@ -1,13 +1,11 @@
1
1
  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
+ import { IpIntelligence, type IIpIntelligenceResult } from './smartnetwork.classes.ipintelligence.js';
4
5
  import { getLogger } from './logging.js';
5
6
  import { NetworkError } from './errors.js';
6
- import * as stats from './helpers/stats.js';
7
+ import { RustNetworkBridge } from './smartnetwork.classes.rustbridge.js';
7
8
 
8
- /**
9
- * SmartNetwork simplifies actions within the network
10
- */
11
9
  /**
12
10
  * Configuration options for SmartNetwork
13
11
  */
@@ -35,6 +33,10 @@ export interface IFindFreePortOptions {
35
33
  exclude?: number[];
36
34
  }
37
35
 
36
+ /**
37
+ * SmartNetwork simplifies actions within the network.
38
+ * Uses a Rust binary for system-dependent operations (ping, traceroute, port scanning, gateway detection).
39
+ */
38
40
  export class SmartNetwork {
39
41
  /** Static registry for external plugins */
40
42
  public static pluginsRegistry: Map<string, any> = new Map();
@@ -46,15 +48,51 @@ export class SmartNetwork {
46
48
  public static unregisterPlugin(name: string): void {
47
49
  SmartNetwork.pluginsRegistry.delete(name);
48
50
  }
51
+
49
52
  private options: SmartNetworkOptions;
50
53
  private cache: Map<string, { value: any; expiry: number }>;
54
+ private rustBridge: RustNetworkBridge;
55
+ private bridgeStarted = false;
56
+ private ipIntelligence: IpIntelligence | null = null;
57
+
51
58
  constructor(options?: SmartNetworkOptions) {
52
59
  this.options = options || {};
53
60
  this.cache = new Map();
61
+ this.rustBridge = RustNetworkBridge.getInstance();
62
+ }
63
+
64
+ /**
65
+ * Start the Rust binary bridge. Must be called before using ping, traceroute,
66
+ * port scanning, or gateway operations. Safe to call multiple times.
67
+ */
68
+ public async start(): Promise<void> {
69
+ if (!this.bridgeStarted) {
70
+ await this.rustBridge.start();
71
+ this.bridgeStarted = true;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Stop the Rust binary bridge.
77
+ */
78
+ public async stop(): Promise<void> {
79
+ if (this.bridgeStarted) {
80
+ await this.rustBridge.stop();
81
+ this.bridgeStarted = false;
82
+ }
54
83
  }
84
+
55
85
  /**
56
- * get network speed
57
- * @param opts optional speed test parameters
86
+ * Ensure the Rust bridge is running before sending commands.
87
+ */
88
+ private async ensureBridge(): Promise<void> {
89
+ if (!this.bridgeStarted) {
90
+ await this.start();
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Get network speed via Cloudflare speed test (pure TS, no Rust needed).
58
96
  */
59
97
  public async getSpeed(opts?: { parallelStreams?: number; duration?: number }) {
60
98
  const cloudflareSpeedInstance = new CloudflareSpeed(opts);
@@ -65,35 +103,45 @@ export class SmartNetwork {
65
103
  * Send ICMP pings to a host. Optionally specify count for multiple pings.
66
104
  */
67
105
  public async ping(host: string, opts?: { timeout?: number; count?: number }): Promise<any> {
68
- const timeout = opts?.timeout ?? 500;
106
+ await this.ensureBridge();
107
+ const timeoutMs = opts?.timeout ?? 5000;
69
108
  const count = opts?.count && opts.count > 1 ? opts.count : 1;
70
- const pinger = new plugins.smartping.Smartping();
71
- if (count === 1) {
72
- // single ping: normalize time to number
73
- const res = await pinger.ping(host, timeout);
109
+
110
+ let result: Awaited<ReturnType<typeof this.rustBridge.ping>>;
111
+ try {
112
+ result = await this.rustBridge.ping(host, count, timeoutMs);
113
+ } catch {
114
+ // DNS resolution failure or other error — return dead ping
115
+ if (count === 1) {
116
+ return { alive: false, time: NaN };
117
+ }
74
118
  return {
75
- ...res,
76
- time: typeof res.time === 'number' ? res.time : NaN,
119
+ host,
120
+ count,
121
+ times: Array(count).fill(NaN),
122
+ min: NaN,
123
+ max: NaN,
124
+ avg: NaN,
125
+ stddev: NaN,
126
+ packetLoss: 100,
127
+ alive: false,
77
128
  };
78
129
  }
79
- const times: number[] = [];
80
- let aliveCount = 0;
81
- for (let i = 0; i < count; i++) {
82
- try {
83
- const res = await pinger.ping(host, timeout);
84
- const t = typeof res.time === 'number' ? res.time : NaN;
85
- if (res.alive) aliveCount++;
86
- times.push(t);
87
- } catch {
88
- times.push(NaN);
89
- }
130
+
131
+ // Map times: replace null with NaN for backward compatibility
132
+ const times = result.times.map((t) => (t === null ? NaN : t));
133
+ const min = result.min === null ? NaN : result.min;
134
+ const max = result.max === null ? NaN : result.max;
135
+ const avg = result.avg === null ? NaN : result.avg;
136
+ const stddev = result.stddev === null ? NaN : result.stddev;
137
+
138
+ if (count === 1) {
139
+ return {
140
+ alive: result.alive,
141
+ time: times[0] ?? NaN,
142
+ };
90
143
  }
91
- const valid = times.filter((t) => !isNaN(t));
92
- const min = valid.length ? Math.min(...valid) : NaN;
93
- const max = valid.length ? Math.max(...valid) : NaN;
94
- const avg = valid.length ? stats.average(valid) : NaN;
95
- const stddev = valid.length ? Math.sqrt(stats.average(valid.map((v) => (v - avg) ** 2))) : NaN;
96
- const packetLoss = ((count - aliveCount) / count) * 100;
144
+
97
145
  return {
98
146
  host,
99
147
  count,
@@ -102,65 +150,22 @@ export class SmartNetwork {
102
150
  max,
103
151
  avg,
104
152
  stddev,
105
- packetLoss,
106
- alive: aliveCount > 0,
153
+ packetLoss: result.packetLoss,
154
+ alive: result.alive,
107
155
  };
108
156
  }
109
157
 
110
- /**
111
- * returns a promise with a boolean answer
112
- * note: false also resolves with false as argument
113
- * @param port
114
- */
115
158
  /**
116
159
  * Check if a local port is unused (both IPv4 and IPv6)
117
160
  */
118
161
  public async isLocalPortUnused(port: number): Promise<boolean> {
119
- const doneIpV4 = plugins.smartpromise.defer<boolean>();
120
- const doneIpV6 = plugins.smartpromise.defer<boolean>();
121
- const net = await import('net'); // creates only one instance of net ;) even on multiple calls
122
-
123
- // test IPv4 space
124
- const ipv4Test = net.createServer();
125
- ipv4Test.once('error', () => {
126
- doneIpV4.resolve(false);
127
- });
128
- ipv4Test.once('listening', () => {
129
- ipv4Test.once('close', () => {
130
- doneIpV4.resolve(true);
131
- });
132
- ipv4Test.close();
133
- });
134
- ipv4Test.listen(port, '0.0.0.0');
135
-
136
- await doneIpV4.promise;
137
-
138
- // test IPv6 space
139
- const ipv6Test = net.createServer();
140
- ipv6Test.once('error', () => {
141
- doneIpV6.resolve(false);
142
- });
143
- ipv6Test.once('listening', () => {
144
- ipv6Test.once('close', () => {
145
- doneIpV6.resolve(true);
146
- });
147
- ipv6Test.close();
148
- });
149
- ipv6Test.listen(port, '::');
150
-
151
- // lets wait for the result
152
- const resultIpV4 = await doneIpV4.promise;
153
- const resultIpV6 = await doneIpV6.promise;
154
- const result = resultIpV4 === true && resultIpV6 === true;
155
- return result;
162
+ await this.ensureBridge();
163
+ const result = await this.rustBridge.isLocalPortFree(port);
164
+ return result.free;
156
165
  }
157
166
 
158
167
  /**
159
168
  * Find the first available port within a given range
160
- * @param startPort The start of the port range (inclusive)
161
- * @param endPort The end of the port range (inclusive)
162
- * @param options Optional configuration for port selection behavior
163
- * @returns The first available port number (or random if options.randomize is true), or null if no ports are available
164
169
  */
165
170
  public async findFreePort(startPort: number, endPort: number, options?: IFindFreePortOptions): Promise<number | null> {
166
171
  // Validate port range
@@ -171,66 +176,36 @@ export class SmartNetwork {
171
176
  throw new NetworkError('Start port must be less than or equal to end port', 'EINVAL');
172
177
  }
173
178
 
174
- // Create a set of excluded ports for efficient lookup
175
179
  const excludedPorts = new Set(options?.exclude || []);
176
180
 
177
- // If randomize option is true, collect all available ports and select randomly
178
181
  if (options?.randomize) {
179
182
  const availablePorts: number[] = [];
180
-
181
- // Scan the range to find all available ports
182
183
  for (let port = startPort; port <= endPort; port++) {
183
- // Skip excluded ports
184
- if (excludedPorts.has(port)) {
185
- continue;
186
- }
187
-
184
+ if (excludedPorts.has(port)) continue;
188
185
  const isUnused = await this.isLocalPortUnused(port);
189
186
  if (isUnused) {
190
187
  availablePorts.push(port);
191
188
  }
192
189
  }
193
-
194
- // If there are available ports, select one randomly
195
190
  if (availablePorts.length > 0) {
196
191
  const randomIndex = Math.floor(Math.random() * availablePorts.length);
197
192
  return availablePorts[randomIndex];
198
193
  }
199
-
200
- // No free port found in the range
201
194
  return null;
202
195
  } else {
203
- // Default behavior: return the first available port (sequential search)
204
196
  for (let port = startPort; port <= endPort; port++) {
205
- // Skip excluded ports
206
- if (excludedPorts.has(port)) {
207
- continue;
208
- }
209
-
197
+ if (excludedPorts.has(port)) continue;
210
198
  const isUnused = await this.isLocalPortUnused(port);
211
199
  if (isUnused) {
212
200
  return port;
213
201
  }
214
202
  }
215
-
216
- // No free port found in the range
217
203
  return null;
218
204
  }
219
205
  }
220
206
 
221
- /**
222
- * checks wether a remote port is available
223
- * @param domainArg
224
- */
225
207
  /**
226
208
  * Check if a remote port is available
227
- * @param target host or "host:port"
228
- * @param opts options including port, protocol (only tcp), retries and timeout
229
- */
230
- /**
231
- * Check if a remote port is available
232
- * @param target host or "host:port"
233
- * @param portOrOpts either a port number (deprecated) or options object
234
209
  */
235
210
  public async isRemotePortAvailable(
236
211
  target: string,
@@ -243,7 +218,7 @@ export class SmartNetwork {
243
218
  let protocol: string = 'tcp';
244
219
  let retries = 1;
245
220
  let timeout: number | undefined;
246
- // preserve old signature (target, port)
221
+
247
222
  if (typeof portOrOpts === 'number') {
248
223
  [hostPart] = target.split(':');
249
224
  port = portOrOpts;
@@ -256,47 +231,64 @@ export class SmartNetwork {
256
231
  const portPart = target.split(':')[1];
257
232
  port = opts.port ?? (portPart ? parseInt(portPart, 10) : undefined);
258
233
  }
234
+
259
235
  if (protocol === 'udp') {
260
236
  throw new NetworkError('UDP port check not supported', 'ENOTSUP');
261
237
  }
262
238
  if (!port) {
263
239
  throw new NetworkError('Port not specified', 'EINVAL');
264
240
  }
265
- let last: boolean = false;
241
+
242
+ await this.ensureBridge();
243
+ let last = false;
266
244
  for (let attempt = 0; attempt < retries; attempt++) {
267
- const done = plugins.smartpromise.defer<boolean>();
268
- plugins.isopen(hostPart, port, (response: Record<string, { isOpen: boolean }>) => {
269
- const info = response[port.toString()];
270
- done.resolve(Boolean(info?.isOpen));
271
- });
272
- last = await done.promise;
273
- if (last) return true;
245
+ try {
246
+ const result = await this.rustBridge.tcpPortCheck(hostPart, port, timeout);
247
+ last = result.isOpen;
248
+ if (last) return true;
249
+ } catch {
250
+ // DNS resolution failure or connection error — treat as not available
251
+ last = false;
252
+ }
274
253
  }
275
254
  return last;
276
255
  }
277
256
 
278
257
  /**
279
- * List network interfaces (gateways)
258
+ * List network interfaces (gateways) — pure TS, no Rust needed.
280
259
  */
281
260
  public async getGateways(): Promise<Record<string, plugins.os.NetworkInterfaceInfo[]>> {
282
- const fetcher = async () => plugins.os.networkInterfaces();
261
+ const fetcher = async () => plugins.os.networkInterfaces() as Record<string, plugins.os.NetworkInterfaceInfo[]>;
283
262
  if (this.options.cacheTtl && this.options.cacheTtl > 0) {
284
263
  return this.getCached('gateways', fetcher);
285
264
  }
286
265
  return fetcher();
287
266
  }
288
267
 
268
+ /**
269
+ * Get the default gateway interface and its addresses.
270
+ */
289
271
  public async getDefaultGateway(): Promise<{
290
272
  ipv4: plugins.os.NetworkInterfaceInfo;
291
273
  ipv6: plugins.os.NetworkInterfaceInfo;
292
- }> {
293
- const defaultGatewayName = await plugins.systeminformation.networkInterfaceDefault();
294
- if (!defaultGatewayName) {
274
+ } | null> {
275
+ await this.ensureBridge();
276
+ const result = await this.rustBridge.defaultGateway();
277
+ const interfaceName = result.interfaceName;
278
+
279
+ if (!interfaceName) {
295
280
  getLogger().warn?.('Cannot determine default gateway');
296
281
  return null;
297
282
  }
283
+
284
+ // Use os.networkInterfaces() to get rich interface info
298
285
  const gateways = await this.getGateways();
299
- const defaultGateway = gateways[defaultGatewayName];
286
+ const defaultGateway = gateways[interfaceName];
287
+ if (!defaultGateway) {
288
+ getLogger().warn?.(`Interface ${interfaceName} not found in os.networkInterfaces()`);
289
+ return null;
290
+ }
291
+
300
292
  return {
301
293
  ipv4: defaultGateway[0],
302
294
  ipv6: defaultGateway[1],
@@ -304,7 +296,7 @@ export class SmartNetwork {
304
296
  }
305
297
 
306
298
  /**
307
- * Lookup public IPv4 and IPv6
299
+ * Lookup public IPv4 and IPv6 — pure TS, no Rust needed.
308
300
  */
309
301
  public async getPublicIps(): Promise<{ v4: string | null; v6: string | null }> {
310
302
  const fetcher = async () => {
@@ -318,28 +310,25 @@ export class SmartNetwork {
318
310
  }
319
311
 
320
312
  /**
321
- * Resolve DNS records (A, AAAA, MX)
313
+ * Resolve DNS records (A, AAAA, MX) — uses smartdns, no Rust needed.
322
314
  */
323
315
  public async resolveDns(
324
316
  host: string,
325
317
  ): Promise<{ A: string[]; AAAA: string[]; MX: { exchange: string; priority: number }[] }> {
326
318
  try {
327
319
  const dnsClient = new plugins.smartdns.dnsClientMod.Smartdns({
328
- strategy: 'prefer-system', // Try system resolver first (handles localhost), fallback to DoH
320
+ strategy: 'prefer-system',
329
321
  allowDohFallback: true,
330
322
  });
331
-
323
+
332
324
  const [aRecords, aaaaRecords, mxRecords] = await Promise.all([
333
325
  dnsClient.getRecordsA(host).catch((): any[] => []),
334
326
  dnsClient.getRecordsAAAA(host).catch((): any[] => []),
335
327
  dnsClient.getRecords(host, 'MX').catch((): any[] => []),
336
328
  ]);
337
-
338
- // Extract values from the record objects
329
+
339
330
  const A = aRecords.map((record: any) => record.value);
340
331
  const AAAA = aaaaRecords.map((record: any) => record.value);
341
-
342
- // Parse MX records - the value contains "priority exchange"
343
332
  const MX = mxRecords.map((record: any) => {
344
333
  const parts = record.value.split(' ');
345
334
  return {
@@ -347,7 +336,7 @@ export class SmartNetwork {
347
336
  exchange: parts[1] || '',
348
337
  };
349
338
  });
350
-
339
+
351
340
  return { A, AAAA, MX };
352
341
  } catch (err: any) {
353
342
  throw new NetworkError(err.message, err.code);
@@ -355,20 +344,20 @@ export class SmartNetwork {
355
344
  }
356
345
 
357
346
  /**
358
- * Perform a simple HTTP/HTTPS endpoint health check
347
+ * Perform a simple HTTP/HTTPS endpoint health check — pure TS.
359
348
  */
360
349
  public async checkEndpoint(
361
350
  urlString: string,
362
- opts?: { timeout?: number },
351
+ opts?: { timeout?: number; rejectUnauthorized?: boolean },
363
352
  ): Promise<{ status: number; headers: Record<string, string>; rtt: number }> {
364
353
  const start = plugins.perfHooks.performance.now();
365
354
  try {
366
355
  const url = new URL(urlString);
367
- const lib = url.protocol === 'https:' ? plugins.https : await import('http');
356
+ const lib = url.protocol === 'https:' ? plugins.https : await import('node:http');
368
357
  return new Promise((resolve, reject) => {
369
358
  const req = lib.request(
370
359
  url,
371
- { method: 'GET', timeout: opts?.timeout, agent: false },
360
+ { method: 'GET', timeout: opts?.timeout, agent: false, rejectUnauthorized: opts?.rejectUnauthorized ?? true },
372
361
  (res: any) => {
373
362
  res.on('data', () => {});
374
363
  res.once('end', () => {
@@ -390,50 +379,38 @@ export class SmartNetwork {
390
379
  }
391
380
 
392
381
  /**
393
- * Perform a traceroute: hop-by-hop latency using the system traceroute tool.
394
- * Falls back to a single-hop stub if traceroute is unavailable or errors.
382
+ * Perform a traceroute: hop-by-hop latency using the Rust binary.
395
383
  */
396
384
  public async traceroute(
397
385
  host: string,
398
386
  opts?: { maxHops?: number; timeout?: number },
399
387
  ): Promise<Hop[]> {
388
+ await this.ensureBridge();
400
389
  const maxHops = opts?.maxHops ?? 30;
401
- const timeout = opts?.timeout;
402
- try {
403
- const { exec } = await import('child_process');
404
- const cmd = `traceroute -n -m ${maxHops} ${host}`;
405
- const stdout: string = await new Promise((resolve, reject) => {
406
- exec(cmd, { encoding: 'utf8', timeout }, (err, stdout) => {
407
- if (err) return reject(err);
408
- resolve(stdout);
409
- });
410
- });
411
- const hops: Hop[] = [];
412
- for (const raw of stdout.split('\n')) {
413
- const line = raw.trim();
414
- if (!line || line.startsWith('traceroute')) continue;
415
- const parts = line.split(/\s+/);
416
- const ttl = parseInt(parts[0], 10);
417
- let ip: string;
418
- let rtt: number | null;
419
- if (parts[1] === '*' || !parts[1]) {
420
- ip = parts[1] || '';
421
- rtt = null;
422
- } else {
423
- ip = parts[1];
424
- const timePart = parts.find((p, i) => i >= 2 && /^\d+(\.\d+)?$/.test(p));
425
- rtt = timePart ? parseFloat(timePart) : null;
426
- }
427
- hops.push({ ttl, ip, rtt });
428
- }
429
- if (hops.length) {
430
- return hops;
431
- }
432
- } catch {
433
- // traceroute not available or error: fall through to stub
390
+ const timeoutMs = opts?.timeout ?? 5000;
391
+
392
+ const result = await this.rustBridge.traceroute(host, maxHops, timeoutMs);
393
+ return result.hops.map((h) => ({
394
+ ttl: h.ttl,
395
+ ip: h.ip || '*',
396
+ rtt: h.rtt,
397
+ }));
398
+ }
399
+
400
+ /**
401
+ * Get IP intelligence: ASN, organization, geolocation, and RDAP registration data.
402
+ * Combines RDAP (RIRs), Team Cymru DNS, and MaxMind GeoLite2 — all run in parallel.
403
+ * Pure TS, no Rust needed.
404
+ */
405
+ public async getIpIntelligence(ip: string): Promise<IIpIntelligenceResult> {
406
+ if (!this.ipIntelligence) {
407
+ this.ipIntelligence = new IpIntelligence();
408
+ }
409
+ const fetcher = () => this.ipIntelligence!.getIntelligence(ip);
410
+ if (this.options.cacheTtl && this.options.cacheTtl > 0) {
411
+ return this.getCached(`ipIntelligence:${ip}`, fetcher);
434
412
  }
435
- // fallback stub
436
- return [{ ttl: 1, ip: host, rtt: null }];
413
+ return fetcher();
437
414
  }
438
415
 
439
416
  /**
@@ -1,21 +1,17 @@
1
1
  // native scope
2
- import * as os from 'os';
3
- import * as https from 'https';
4
- import * as perfHooks from 'perf_hooks';
2
+ import * as os from 'node:os';
3
+ import * as https from 'node:https';
4
+ import * as perfHooks from 'node:perf_hooks';
5
5
 
6
6
  export { os, https, perfHooks };
7
7
 
8
8
  // @pushrocks scope
9
9
  import * as smartdns from '@push.rocks/smartdns';
10
- import * as smartping from '@push.rocks/smartping';
11
- import * as smartpromise from '@push.rocks/smartpromise';
12
- import * as smartstring from '@push.rocks/smartstring';
10
+ import * as smartrust from '@push.rocks/smartrust';
13
11
 
14
- export { smartdns, smartpromise, smartping, smartstring };
12
+ export { smartdns, smartrust };
15
13
 
16
- // @third party scope
17
- // @ts-ignore
18
- import isopen from 'isopen';
19
- import * as systeminformation from 'systeminformation';
14
+ // third party
15
+ import * as maxmind from 'maxmind';
20
16
 
21
- export { isopen, systeminformation };
17
+ export { maxmind };