Files
smartnetwork/ts/smartnetwork.classes.smartnetwork.ts

431 lines
13 KiB
TypeScript
Raw Normal View History

2022-03-24 23:11:53 +01:00
import * as plugins from './smartnetwork.plugins.js';
import { CloudflareSpeed } from './smartnetwork.classes.cloudflarespeed.js';
import { PublicIp } from './smartnetwork.classes.publicip.js';
import { IpIntelligence, type IIpIntelligenceResult } from './smartnetwork.classes.ipintelligence.js';
import { getLogger } from './logging.js';
import { NetworkError } from './errors.js';
import { RustNetworkBridge } from './smartnetwork.classes.rustbridge.js';
2019-04-16 10:21:11 +02:00
/**
* Configuration options for SmartNetwork
*/
export interface SmartNetworkOptions {
/** Cache time-to-live in milliseconds for gateway and public IP lookups */
cacheTtl?: number;
}
/**
* A hop in a traceroute result
*/
export interface Hop {
ttl: number;
ip: string;
rtt: number | null;
}
/**
* Options for the findFreePort method
*/
export interface IFindFreePortOptions {
/** If true, selects a random available port within the range instead of the first one */
randomize?: boolean;
/** Array of port numbers to exclude from the search */
exclude?: number[];
}
/**
* SmartNetwork simplifies actions within the network.
* Uses a Rust binary for system-dependent operations (ping, traceroute, port scanning, gateway detection).
*/
2019-04-16 10:21:11 +02:00
export class SmartNetwork {
/** Static registry for external plugins */
public static pluginsRegistry: Map<string, any> = new Map();
/** Register a plugin by name */
public static registerPlugin(name: string, ctor: any): void {
SmartNetwork.pluginsRegistry.set(name, ctor);
}
/** Unregister a plugin by name */
public static unregisterPlugin(name: string): void {
SmartNetwork.pluginsRegistry.delete(name);
}
private options: SmartNetworkOptions;
private cache: Map<string, { value: any; expiry: number }>;
private rustBridge: RustNetworkBridge;
private bridgeStarted = false;
private ipIntelligence: IpIntelligence | null = null;
constructor(options?: SmartNetworkOptions) {
this.options = options || {};
this.cache = new Map();
this.rustBridge = RustNetworkBridge.getInstance();
}
/**
* Start the Rust binary bridge. Must be called before using ping, traceroute,
* port scanning, or gateway operations. Safe to call multiple times.
*/
public async start(): Promise<void> {
if (!this.bridgeStarted) {
await this.rustBridge.start();
this.bridgeStarted = true;
}
}
/**
* Stop the Rust binary bridge.
*/
public async stop(): Promise<void> {
if (this.bridgeStarted) {
await this.rustBridge.stop();
this.bridgeStarted = false;
}
}
2019-09-06 18:26:32 +02:00
/**
* Ensure the Rust bridge is running before sending commands.
*/
private async ensureBridge(): Promise<void> {
if (!this.bridgeStarted) {
await this.start();
}
}
/**
* Get network speed via Cloudflare speed test (pure TS, no Rust needed).
2019-09-06 18:26:32 +02:00
*/
public async getSpeed(opts?: { parallelStreams?: number; duration?: number }) {
const cloudflareSpeedInstance = new CloudflareSpeed(opts);
return cloudflareSpeedInstance.speedTest();
2019-04-16 10:21:11 +02:00
}
/**
* Send ICMP pings to a host. Optionally specify count for multiple pings.
*/
public async ping(host: string, opts?: { timeout?: number; count?: number }): Promise<any> {
await this.ensureBridge();
const timeoutMs = opts?.timeout ?? 5000;
const count = opts?.count && opts.count > 1 ? opts.count : 1;
let result: Awaited<ReturnType<typeof this.rustBridge.ping>>;
try {
result = await this.rustBridge.ping(host, count, timeoutMs);
} catch {
// DNS resolution failure or other error — return dead ping
if (count === 1) {
return { alive: false, time: NaN };
}
return {
host,
count,
times: Array(count).fill(NaN),
min: NaN,
max: NaN,
avg: NaN,
stddev: NaN,
packetLoss: 100,
alive: false,
};
}
// Map times: replace null with NaN for backward compatibility
const times = result.times.map((t) => (t === null ? NaN : t));
const min = result.min === null ? NaN : result.min;
const max = result.max === null ? NaN : result.max;
const avg = result.avg === null ? NaN : result.avg;
const stddev = result.stddev === null ? NaN : result.stddev;
if (count === 1) {
return {
alive: result.alive,
time: times[0] ?? NaN,
};
}
return {
host,
count,
times,
min,
max,
avg,
stddev,
packetLoss: result.packetLoss,
alive: result.alive,
};
2022-02-17 00:03:13 +01:00
}
/**
* Check if a local port is unused (both IPv4 and IPv6)
*/
2021-04-13 10:09:39 +00:00
public async isLocalPortUnused(port: number): Promise<boolean> {
await this.ensureBridge();
const result = await this.rustBridge.isLocalPortFree(port);
return result.free;
2019-04-16 10:21:11 +02:00
}
/**
* Find the first available port within a given range
*/
public async findFreePort(startPort: number, endPort: number, options?: IFindFreePortOptions): Promise<number | null> {
// Validate port range
if (startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535) {
throw new NetworkError('Port numbers must be between 1 and 65535', 'EINVAL');
}
if (startPort > endPort) {
throw new NetworkError('Start port must be less than or equal to end port', 'EINVAL');
}
const excludedPorts = new Set(options?.exclude || []);
if (options?.randomize) {
const availablePorts: number[] = [];
for (let port = startPort; port <= endPort; port++) {
if (excludedPorts.has(port)) continue;
const isUnused = await this.isLocalPortUnused(port);
if (isUnused) {
availablePorts.push(port);
}
}
if (availablePorts.length > 0) {
const randomIndex = Math.floor(Math.random() * availablePorts.length);
return availablePorts[randomIndex];
}
return null;
} else {
for (let port = startPort; port <= endPort; port++) {
if (excludedPorts.has(port)) continue;
const isUnused = await this.isLocalPortUnused(port);
if (isUnused) {
return port;
}
}
return null;
}
}
/**
* Check if a remote port is available
*/
public async isRemotePortAvailable(
target: string,
portOrOpts?:
| number
| { port?: number; protocol?: 'tcp' | 'udp'; timeout?: number; retries?: number },
): Promise<boolean> {
let hostPart: string;
let port: number | undefined;
let protocol: string = 'tcp';
let retries = 1;
let timeout: number | undefined;
if (typeof portOrOpts === 'number') {
[hostPart] = target.split(':');
port = portOrOpts;
} else {
const opts = portOrOpts || {};
protocol = opts.protocol ?? 'tcp';
retries = opts.retries ?? 1;
timeout = opts.timeout;
[hostPart] = target.split(':');
const portPart = target.split(':')[1];
port = opts.port ?? (portPart ? parseInt(portPart, 10) : undefined);
}
if (protocol === 'udp') {
throw new NetworkError('UDP port check not supported', 'ENOTSUP');
}
if (!port) {
throw new NetworkError('Port not specified', 'EINVAL');
}
await this.ensureBridge();
let last = false;
for (let attempt = 0; attempt < retries; attempt++) {
try {
const result = await this.rustBridge.tcpPortCheck(hostPart, port, timeout);
last = result.isOpen;
if (last) return true;
} catch {
// DNS resolution failure or connection error — treat as not available
last = false;
}
}
return last;
2019-04-16 10:21:11 +02:00
}
2019-09-06 18:26:32 +02:00
/**
* List network interfaces (gateways) pure TS, no Rust needed.
*/
public async getGateways(): Promise<Record<string, plugins.os.NetworkInterfaceInfo[]>> {
const fetcher = async () => plugins.os.networkInterfaces() as Record<string, plugins.os.NetworkInterfaceInfo[]>;
if (this.options.cacheTtl && this.options.cacheTtl > 0) {
return this.getCached('gateways', fetcher);
}
return fetcher();
2019-09-06 18:26:32 +02:00
}
2019-09-08 16:15:10 +02:00
/**
* Get the default gateway interface and its addresses.
*/
2019-11-19 23:00:37 +00:00
public async getDefaultGateway(): Promise<{
ipv4: plugins.os.NetworkInterfaceInfo;
ipv6: plugins.os.NetworkInterfaceInfo;
} | null> {
await this.ensureBridge();
const result = await this.rustBridge.defaultGateway();
const interfaceName = result.interfaceName;
if (!interfaceName) {
getLogger().warn?.('Cannot determine default gateway');
2019-09-08 16:24:59 +02:00
return null;
}
// Use os.networkInterfaces() to get rich interface info
2019-09-08 16:15:10 +02:00
const gateways = await this.getGateways();
const defaultGateway = gateways[interfaceName];
if (!defaultGateway) {
getLogger().warn?.(`Interface ${interfaceName} not found in os.networkInterfaces()`);
return null;
}
2019-09-08 16:15:10 +02:00
return {
ipv4: defaultGateway[0],
2020-08-12 16:30:17 +00:00
ipv6: defaultGateway[1],
2019-09-08 16:15:10 +02:00
};
}
2019-11-23 16:07:04 +00:00
/**
* Lookup public IPv4 and IPv6 pure TS, no Rust needed.
*/
public async getPublicIps(): Promise<{ v4: string | null; v6: string | null }> {
const fetcher = async () => {
const publicIp = new PublicIp({ timeout: 1000 });
return publicIp.getPublicIps();
};
if (this.options.cacheTtl && this.options.cacheTtl > 0) {
return this.getCached('publicIps', fetcher);
}
return fetcher();
}
/**
* Resolve DNS records (A, AAAA, MX) uses smartdns, no Rust needed.
*/
public async resolveDns(
host: string,
): Promise<{ A: string[]; AAAA: string[]; MX: { exchange: string; priority: number }[] }> {
try {
const dnsClient = new plugins.smartdns.dnsClientMod.Smartdns({
strategy: 'prefer-system',
allowDohFallback: true,
});
const [aRecords, aaaaRecords, mxRecords] = await Promise.all([
dnsClient.getRecordsA(host).catch((): any[] => []),
dnsClient.getRecordsAAAA(host).catch((): any[] => []),
dnsClient.getRecords(host, 'MX').catch((): any[] => []),
]);
const A = aRecords.map((record: any) => record.value);
const AAAA = aaaaRecords.map((record: any) => record.value);
const MX = mxRecords.map((record: any) => {
const parts = record.value.split(' ');
return {
priority: parseInt(parts[0], 10),
exchange: parts[1] || '',
};
});
return { A, AAAA, MX };
} catch (err: any) {
throw new NetworkError(err.message, err.code);
}
}
/**
* Perform a simple HTTP/HTTPS endpoint health check pure TS.
*/
public async checkEndpoint(
urlString: string,
opts?: { timeout?: number; rejectUnauthorized?: boolean },
): Promise<{ status: number; headers: Record<string, string>; rtt: number }> {
const start = plugins.perfHooks.performance.now();
try {
const url = new URL(urlString);
const lib = url.protocol === 'https:' ? plugins.https : await import('node:http');
return new Promise((resolve, reject) => {
const req = lib.request(
url,
{ method: 'GET', timeout: opts?.timeout, agent: false, rejectUnauthorized: opts?.rejectUnauthorized ?? true },
(res: any) => {
res.on('data', () => {});
res.once('end', () => {
const rtt = plugins.perfHooks.performance.now() - start;
const headers: Record<string, string> = {};
for (const [k, v] of Object.entries(res.headers)) {
headers[k] = Array.isArray(v) ? v.join(',') : String(v);
}
resolve({ status: res.statusCode, headers, rtt });
});
},
);
req.on('error', (err: any) => reject(new NetworkError(err.message, err.code)));
req.end();
});
} catch (err: any) {
throw new NetworkError(err.message, err.code);
}
}
/**
* Perform a traceroute: hop-by-hop latency using the Rust binary.
*/
public async traceroute(
host: string,
opts?: { maxHops?: number; timeout?: number },
): Promise<Hop[]> {
await this.ensureBridge();
const maxHops = opts?.maxHops ?? 30;
const timeoutMs = opts?.timeout ?? 5000;
const result = await this.rustBridge.traceroute(host, maxHops, timeoutMs);
return result.hops.map((h) => ({
ttl: h.ttl,
ip: h.ip || '*',
rtt: h.rtt,
}));
}
/**
* Get IP intelligence: ASN, organization, geolocation, and RDAP registration data.
* Combines RDAP (RIRs), Team Cymru DNS, and MaxMind GeoLite2 all run in parallel.
* Pure TS, no Rust needed.
*/
public async getIpIntelligence(ip: string): Promise<IIpIntelligenceResult> {
if (!this.ipIntelligence) {
this.ipIntelligence = new IpIntelligence();
}
const fetcher = () => this.ipIntelligence!.getIntelligence(ip);
if (this.options.cacheTtl && this.options.cacheTtl > 0) {
return this.getCached(`ipIntelligence:${ip}`, fetcher);
}
return fetcher();
}
/**
* Internal caching helper
*/
private async getCached<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
const now = Date.now();
const entry = this.cache.get(key);
if (entry && entry.expiry > now) {
return entry.value;
}
const value = await fetcher();
const ttl = this.options.cacheTtl || 0;
this.cache.set(key, { value, expiry: now + ttl });
return value;
2019-11-23 16:07:04 +00:00
}
2019-04-16 10:21:11 +02:00
}