This commit is contained in:
2026-01-09 07:14:39 +00:00
parent 95da37590c
commit 05e1f94c79
22 changed files with 6549 additions and 10 deletions

View File

@@ -0,0 +1,171 @@
/**
* IP Range utility functions for network scanning
*/
/**
* Validates an IPv4 address
*/
export function isValidIp(ip: string): boolean {
const parts = ip.split('.');
if (parts.length !== 4) return false;
return parts.every((part) => {
const num = parseInt(part, 10);
return !isNaN(num) && num >= 0 && num <= 255 && part === num.toString();
});
}
/**
* Converts an IPv4 address to a 32-bit number
*/
export function ipToNumber(ip: string): number {
const parts = ip.split('.').map((p) => parseInt(p, 10));
return ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0;
}
/**
* Converts a 32-bit number to an IPv4 address
*/
export function numberToIp(num: number): string {
return [
(num >>> 24) & 255,
(num >>> 16) & 255,
(num >>> 8) & 255,
num & 255,
].join('.');
}
/**
* Generates an array of IP addresses from a start to end range
* Excludes network address (.0) and broadcast address (.255) for /24 ranges
*/
export function ipRangeToIps(startIp: string, endIp: string): string[] {
if (!isValidIp(startIp) || !isValidIp(endIp)) {
throw new Error(`Invalid IP address: ${!isValidIp(startIp) ? startIp : endIp}`);
}
const start = ipToNumber(startIp);
const end = ipToNumber(endIp);
if (start > end) {
throw new Error(`Start IP (${startIp}) must be less than or equal to end IP (${endIp})`);
}
const ips: string[] = [];
for (let i = start; i <= end; i++) {
ips.push(numberToIp(i));
}
return ips;
}
/**
* Parses CIDR notation and returns an array of usable host IPs
* Excludes network address and broadcast address
* Example: "192.168.1.0/24" returns 192.168.1.1 through 192.168.1.254
*/
export function cidrToIps(cidr: string): string[] {
const match = cidr.match(/^(\d+\.\d+\.\d+\.\d+)\/(\d+)$/);
if (!match) {
throw new Error(`Invalid CIDR notation: ${cidr}`);
}
const [, networkIp, prefixStr] = match;
const prefix = parseInt(prefixStr, 10);
if (!isValidIp(networkIp)) {
throw new Error(`Invalid network address: ${networkIp}`);
}
if (prefix < 0 || prefix > 32) {
throw new Error(`Invalid prefix length: ${prefix}`);
}
// For /32, just return the single IP
if (prefix === 32) {
return [networkIp];
}
// For /31 (point-to-point), return both IPs
if (prefix === 31) {
const networkNum = ipToNumber(networkIp);
const mask = (0xffffffff << (32 - prefix)) >>> 0;
const network = (networkNum & mask) >>> 0;
return [numberToIp(network), numberToIp(network + 1)];
}
// Calculate network and broadcast addresses
const networkNum = ipToNumber(networkIp);
const mask = (0xffffffff << (32 - prefix)) >>> 0;
const network = (networkNum & mask) >>> 0;
const broadcast = (network | (~mask >>> 0)) >>> 0;
// Generate usable host IPs (exclude network and broadcast)
const ips: string[] = [];
for (let i = network + 1; i < broadcast; i++) {
ips.push(numberToIp(i));
}
return ips;
}
/**
* Gets the local network interfaces and returns the first non-loopback IPv4 subnet
* Returns CIDR notation (e.g., "192.168.1.0/24")
*/
export function getLocalSubnet(): string | null {
try {
const os = require('os');
const interfaces = os.networkInterfaces();
for (const name of Object.keys(interfaces)) {
const iface = interfaces[name];
if (!iface) continue;
for (const info of iface) {
// Skip loopback and non-IPv4
if (info.family !== 'IPv4' || info.internal) continue;
// Calculate the network address from IP and netmask
const ip = ipToNumber(info.address);
const mask = ipToNumber(info.netmask);
const network = (ip & mask) >>> 0;
// Calculate prefix length from netmask
const maskBits = info.netmask.split('.').reduce((acc: number, octet: string) => {
const byte = parseInt(octet, 10);
let bits = 0;
for (let i = 7; i >= 0; i--) {
if ((byte >> i) & 1) bits++;
else break;
}
return acc + bits;
}, 0);
return `${numberToIp(network)}/${maskBits}`;
}
}
} catch {
// os module might not be available
}
return null;
}
/**
* Counts the number of IPs in a CIDR range (excluding network and broadcast)
*/
export function countIpsInCidr(cidr: string): number {
const match = cidr.match(/^(\d+\.\d+\.\d+\.\d+)\/(\d+)$/);
if (!match) {
throw new Error(`Invalid CIDR notation: ${cidr}`);
}
const prefix = parseInt(match[2], 10);
if (prefix === 32) return 1;
if (prefix === 31) return 2;
// 2^(32-prefix) - 2 (minus network and broadcast)
return Math.pow(2, 32 - prefix) - 2;
}

100
ts/helpers/helpers.retry.ts Normal file
View File

@@ -0,0 +1,100 @@
import * as plugins from '../plugins.js';
import type { IRetryOptions } from '../interfaces/index.js';
const defaultRetryOptions: Required<IRetryOptions> = {
maxRetries: 5,
baseDelay: 1000,
maxDelay: 16000,
multiplier: 2,
jitter: true,
};
/**
* Calculates the delay for a retry attempt using exponential backoff
*/
function calculateDelay(
attempt: number,
options: Required<IRetryOptions>
): number {
const delay = Math.min(
options.baseDelay * Math.pow(options.multiplier, attempt),
options.maxDelay
);
if (options.jitter) {
// Add random jitter of +/- 25%
const jitterRange = delay * 0.25;
return delay + (Math.random() * jitterRange * 2 - jitterRange);
}
return delay;
}
/**
* Executes a function with retry logic using exponential backoff
*
* @param fn - The async function to execute
* @param options - Retry configuration options
* @returns The result of the function
* @throws The last error if all retries fail
*/
export async function withRetry<T>(
fn: () => Promise<T>,
options?: IRetryOptions
): Promise<T> {
const opts: Required<IRetryOptions> = { ...defaultRetryOptions, ...options };
let lastError: Error | undefined;
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt === opts.maxRetries) {
break;
}
const delay = calculateDelay(attempt, opts);
await plugins.smartdelay.delayFor(delay);
}
}
throw lastError ?? new Error('Retry failed with unknown error');
}
/**
* Creates a retryable version of an async function
*
* @param fn - The async function to wrap
* @param options - Retry configuration options
* @returns A wrapped function that retries on failure
*/
export function createRetryable<TArgs extends unknown[], TResult>(
fn: (...args: TArgs) => Promise<TResult>,
options?: IRetryOptions
): (...args: TArgs) => Promise<TResult> {
return (...args: TArgs) => withRetry(() => fn(...args), options);
}
/**
* Retry decorator for class methods
* Note: Use as a wrapper function since TC39 decorators have different semantics
*/
export function retryMethod<T extends Record<string, unknown>>(
target: T,
methodName: keyof T & string,
options?: IRetryOptions
): void {
const originalMethod = target[methodName];
if (typeof originalMethod !== 'function') {
throw new Error(`${methodName} is not a function`);
}
target[methodName] = createRetryable(
originalMethod.bind(target) as (...args: unknown[]) => Promise<unknown>,
options
) as T[typeof methodName];
}
export { defaultRetryOptions };