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