@nasriya/atomix
Version:
Composable helper functions for building reliable systems
321 lines (320 loc) • 12.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const os_1 = __importDefault(require("os"));
const child_process_1 = require("child_process");
const network_utils_1 = __importDefault(require("../network-utils"));
const numbers_guard_1 = __importDefault(require("../../data-types/number/numbers-guard"));
const inspect_1 = __importDefault(require("./inspect"));
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_1.default.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_1.default.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_1.default.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_1.default.platform();
if (platform === 'win32') {
// Windows: use `route print` and parse the default route
const output = (0, child_process_1.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 = (0, child_process_1.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 = (0, child_process_1.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_1.default.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_1.default.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 (!network_utils_1.default.isValidPort(port)) {
throw new Error(`Invalid port: ${port}. Must be between 0 and 65535`);
}
const { scanTimeout, hostTimeout } = options ?? {};
if (scanTimeout !== undefined) {
if (!numbers_guard_1.default.isNumber(scanTimeout)) {
throw new TypeError(`Invalid scanTimeout: Expected number but got ${typeof scanTimeout}`);
}
if (!numbers_guard_1.default.isInteger(scanTimeout) || !numbers_guard_1.default.isPositive(scanTimeout)) {
throw new TypeError(`Invalid scanTimeout: Must be a positive integer`);
}
}
if (hostTimeout !== undefined) {
if (!numbers_guard_1.default.isNumber(hostTimeout)) {
throw new TypeError(`Invalid hostTimeout: Expected number but got ${typeof hostTimeout}`);
}
if (!numbers_guard_1.default.isInteger(hostTimeout) || !numbers_guard_1.default.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 = network_utils_1.default.getSubnetIPs(ip, subnet);
const checks = ips.map(ip => {
return (async () => {
if (aborted) {
return;
}
const open = await inspect_1.default.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;
exports.default = localNetwork;