@nasriya/atomix
Version:
Composable helper functions for building reliable systems
316 lines (315 loc) • 12.1 kB
JavaScript
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;