UNPKG

@nasriya/atomix

Version:

Composable helper functions for building reliable systems

316 lines (315 loc) 12.1 kB
import os from 'os'; import { execSync } from 'child_process'; import networks from '../network-utils.js'; import numbersGuard from '../../data-types/number/numbers-guard.js'; import networkInspector from './inspect.js'; class LocalNetwork { /** * Get the local IP address of the server. * * Compatibility (tested): * - Node.js: ✅ Supported (v10+) * - Bun: ✅ Supported (v1.2.15+) * - Deno: ❓ Untested (compatibility unknown) * * @returns {string[]} An array of local IPs. */ getLocalIPs() { const nets = os.networkInterfaces(); const interfaces = {}; for (const name of Object.keys(nets)) { for (const net of nets[name]) { // Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses // 'IPv4' is in Node <= 17, from 18 it's a number 4 or 6 const familyV4Value = typeof net.family === 'string' ? 'IPv4' : 4; if (net.family === familyV4Value && !net.internal) { if (!interfaces[name]) { interfaces[name] = []; } interfaces[name].push(net.address); } } } const interfacesArr = Object.entries(interfaces).map(entry => { return { name: entry[0], ips: entry[1] }; }); interfacesArr.sort((int1, int2) => { if (int1.name === 'Ethernet' && int2.name === 'Ethernet') { return 0; } if (int1.name === 'Ethernet') { return -1; } if (int2.name === 'Ethernet') { return 1; } if (int1.name === 'vEthernet' && int2.name === 'vEthernet') { return 0; } if (int1.name === 'vEthernet') { return -1; } if (int2.name === 'vEthernet') { return 1; } return 0; }); const local_ips = interfacesArr.map(i => i.ips).flat(3); return [...new Set(local_ips)]; } /** * Get the hostname of the server. * * Compatibility (tested): * - Node.js: Supported (v10+) * - Bun: Supported (v1.2.15+) * - Deno: Untested (compatibility unknown) * * @returns {string} The hostname of the server. * @since v1.0.0 */ getHostname() { return os.hostname(); } /** * Retrieves the MAC addresses of all non-internal network interfaces on the server. * * Compatibility (tested): * - Node.js: Supported (v10+) * - Bun: Supported (v1.2.15+) * - Deno: Untested (compatibility unknown) * * @returns {string[]} An array of MAC addresses. * @since v1.0.0 */ getMACAddresses() { const interfaces = os.networkInterfaces(); const macAddresses = []; for (const name of Object.keys(interfaces)) { for (const net of interfaces[name]) { if (net.mac && !net.internal) { macAddresses.push(net.mac); } } } return macAddresses; } /** * Gets the default gateway IP address. * * Compatibility: * - Node.js: ✅ * - Bun: ✅ (requires `bun:ffi` or shell access) * - Deno: ❓ Untested * * @returns {string} The default gateway IP address, or empty string if not found. * @since v1.0.0 */ getDefaultGateway() { let mainGateway = ''; try { const platform = os.platform(); if (platform === 'win32') { // Windows: use `route print` and parse the default route const output = execSync('route print 0.0.0.0').toString(); const lines = output.split('\n'); for (const line of lines) { if (line.includes('0.0.0.0')) { const parts = line.trim().split(/\s+/); const gateway = parts[2]; if (gateway && gateway !== 'On-link') { mainGateway = gateway; break; } } } } else if (platform === 'darwin' || platform === 'linux') { // Unix (macOS, Linux): use `ip route` or `netstat` try { const output = execSync('ip route get 1.1.1.1').toString(); const match = output.match(/via (\d+\.\d+\.\d+\.\d+)/); if (match) { mainGateway = match[1]; } } catch { const fallback = execSync('netstat -rn').toString(); const lines = fallback.split('\n'); for (const line of lines) { if (line.startsWith('default') || line.startsWith('0.0.0.0')) { const parts = line.trim().split(/\s+/); const gateway = parts[1]; if (gateway) { mainGateway = gateway; break; } } } } } } catch (error) { if (error instanceof Error) { error.message = `Unable to detect default gateway: ${error.message}`; } throw error; } return mainGateway; } /** * Retrieves the network CIDRs of all non-internal network interfaces on the server. * * Compatibility (tested): * - Node.js: Supported (v10+) * - Bun: Supported (v1.2.15+) * - Deno: Untested (compatibility unknown) * * @returns {Promise<string[]>} An array of CIDRs of the network interfaces. * @since v1.0.0 */ getNetworkCIDRs() { const interfaces = os.networkInterfaces(); const networkCIDRs = []; for (const name of Object.keys(interfaces)) { for (const net of interfaces[name]) { if (net.family === 'IPv4' && !net.internal) { networkCIDRs.push(net.cidr); } } } return networkCIDRs; } /** * Retrieves a map of local network interfaces and their corresponding IPs. * * @returns {Map<string, string[]>} A map where the keys are the interface names and the values are arrays of IPv4 addresses. * @since v1.0.0 */ getLocalNetworkMap() { const interfaces = os.networkInterfaces(); const localNetworkMap = new Map(); for (const name of Object.keys(interfaces)) { const ips = interfaces[name].filter(net => net.family === 'IPv4' && !net.internal).map(net => net.address); localNetworkMap.set(name, ips); } return localNetworkMap; } /** * Scans the local network subnets for hosts that have the specified port open. * * This method enumerates all local network CIDRs, calculates all IP addresses * within those subnets, and checks if the given port is open on each IP using TCP. * * You can customize both the overall scan timeout and the timeout used for each * individual host check. * * **Note:** This scan only targets the local network. It is not designed * to scan public or external IP addresses. * * @param port - The TCP port number to check on each local IP. Must be between 0 and 65535. * @param options - Optional object to configure timeouts: * - `scanTimeout`: Maximum total time (in ms) to allow for the full scan. If exceeded, the scan aborts with an error. * - `hostTimeout`: Timeout (in ms) to wait for each individual IP check before considering it unreachable. * * @returns A promise that resolves to an array of IP addresses on the local network * where the specified port is open. * * @throws Will throw if: * - The port is invalid. * - A timeout option is invalid. * - The scan times out (`SCAN_TIMEOUT` error). * * @since v1.0.0 * * @example * ```ts * const openHosts = await networks.local.discoverServiceHosts(80, { * scanTimeout: 10000, * hostTimeout: 200 * }); * console.log('Hosts with port 80 open:', openHosts); * ``` */ async discoverServiceHosts(port, options) { if (!networks.isValidPort(port)) { throw new Error(`Invalid port: ${port}. Must be between 0 and 65535`); } const { scanTimeout, hostTimeout } = options ?? {}; if (scanTimeout !== undefined) { if (!numbersGuard.isNumber(scanTimeout)) { throw new TypeError(`Invalid scanTimeout: Expected number but got ${typeof scanTimeout}`); } if (!numbersGuard.isInteger(scanTimeout) || !numbersGuard.isPositive(scanTimeout)) { throw new TypeError(`Invalid scanTimeout: Must be a positive integer`); } } if (hostTimeout !== undefined) { if (!numbersGuard.isNumber(hostTimeout)) { throw new TypeError(`Invalid hostTimeout: Expected number but got ${typeof hostTimeout}`); } if (!numbersGuard.isInteger(hostTimeout) || !numbersGuard.isPositive(hostTimeout)) { throw new TypeError(`Invalid hostTimeout: Must be a positive integer`); } } let aborted = false; let timeoutTimer; const { promise, resolve, reject } = Promise.withResolvers(); const openHosts = []; try { const scanTask = async () => { try { const cidrs = localNetwork.getNetworkCIDRs().map(i => { const [ip, subnet] = i.split('/'); return { ip, subnet: parseInt(subnet, 10) }; }); for (const { ip, subnet } of cidrs) { if (aborted) { break; } const ips = networks.getSubnetIPs(ip, subnet); const checks = ips.map(ip => { return (async () => { if (aborted) { return; } const open = await networkInspector.isPortOpen(port, { hostname: ip, timeout: hostTimeout }); if (open) { openHosts.push(ip); } })(); }); await Promise.all(checks); } resolve(openHosts); } catch (error) { reject(error); } finally { if (timeoutTimer) { clearTimeout(timeoutTimer); } } }; if (scanTimeout !== undefined) { timeoutTimer = setTimeout(() => { aborted = true; const error = new Error(`Unable to scan local network for port ${port}: Scanning has timed out after ${scanTimeout}ms`); error.cause = 'SCAN_TIMEOUT'; reject(error); }, scanTimeout); } void scanTask(); return promise; } catch (error) { if (error instanceof Error) { error.message = `Unable to scan local network for port ${port}: ${error.message}`; } throw error; } } } const localNetwork = new LocalNetwork; export default localNetwork;