diff --git a/changelog.md b/changelog.md index f94ea8e..3118572 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2025-07-31 - 4.1.0 - feat(port-management) +Add findFreePort method for automatic port discovery within a range + +- Added new `findFreePort` method to SmartNetwork class that finds the first available port in a specified range +- Added comprehensive tests for the new port finding functionality +- Updated README documentation with usage examples for the new feature +- Improved port management capabilities for dynamic port allocation scenarios + ## 2025-05-19 - 4.0.2 - fix(tests) Update dev dependencies and refactor test assertions for improved clarity diff --git a/package.json b/package.json index 6a80bfa..0b5d412 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@push.rocks/smartnetwork", - "version": "4.0.2", + "version": "4.1.0", "private": false, "description": "A toolkit for network diagnostics including speed tests, port availability checks, and more.", "main": "dist_ts/index.js", diff --git a/readme.md b/readme.md index 1dacef4..019c6fd 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # @push.rocks/smartnetwork -network diagnostics +Comprehensive network diagnostics and utilities for Node.js applications ## Install @@ -10,20 +10,9 @@ To install `@push.rocks/smartnetwork`, run the following command in your termina npm install @push.rocks/smartnetwork --save ``` -### Performing a Traceroute - -You can perform a hop-by-hop traceroute to measure latency per hop. Falls back to a single-hop stub if the `traceroute` binary is unavailable. - -```typescript -const hops = await myNetwork.traceroute('google.com', { maxHops: 10, timeout: 5000 }); -hops.forEach((h) => console.log(`${h.ttl}\t${h.ip}\t${h.rtt === null ? '*' : h.rtt + ' ms'}`)); -``` - -This command will download `@push.rocks/smartnetwork` and add it to your project's `package.json` file. - ## Usage -In this section, we will dive deep into the capabilities of the `@push.rocks/smartnetwork` package, exploring its various features through TypeScript examples. The package is designed to simplify network diagnostics tasks, including speed tests, port availability checks, ping operations, and more. +The `@push.rocks/smartnetwork` package provides a comprehensive suite of network diagnostic tools including speed tests, port availability checks, ping operations, DNS resolution, HTTP endpoint health checks, and more. ### Basic Setup @@ -37,135 +26,294 @@ Then, create an instance of `SmartNetwork`: ```typescript const myNetwork = new SmartNetwork(); + +// Or with caching enabled (60 seconds TTL) +const myNetworkCached = new SmartNetwork({ cacheTtl: 60000 }); ``` -### Performing a Speed Test +### Network Speed Testing -You can measure the network speed using the `getSpeed` method. It supports optional parameters: - -- `parallelStreams`: number of concurrent streams (default: 1) -- `duration`: test duration in seconds (default: fixed segments) +Measure network download and upload speeds using Cloudflare's speed test infrastructure: ```typescript const speedTest = async () => { - // Default fixed-segment test - let r = await myNetwork.getSpeed(); - console.log(`Download: ${r.downloadSpeed} Mbps, Upload: ${r.uploadSpeed} Mbps`); + // Basic speed test + const result = await myNetwork.getSpeed(); + console.log(`Download: ${result.downloadSpeed} Mbps`); + console.log(`Upload: ${result.uploadSpeed} Mbps`); - // Parallel + duration-based test - r = await myNetwork.getSpeed({ parallelStreams: 3, duration: 5 }); - console.log(`Download: ${r.downloadSpeed} Mbps, Upload: ${r.uploadSpeed} Mbps`); + // Advanced speed test with options + const advancedResult = await myNetwork.getSpeed({ + parallelStreams: 3, // Number of concurrent connections + duration: 5 // Test duration in seconds + }); + console.log(`Download: ${advancedResult.downloadSpeed} Mbps`); + console.log(`Upload: ${advancedResult.uploadSpeed} Mbps`); }; - -speedTest(); ``` -### Checking Port Availability Locally +### Port Management -The `isLocalPortUnused` method allows you to check if a specific port on your local machine is available for use. +#### Check Local Port Availability + +Verify if a specific port is available on your local machine (checks both IPv4 and IPv6): ```typescript const checkLocalPort = async (port: number) => { const isUnused = await myNetwork.isLocalPortUnused(port); if (isUnused) { - console.log(`Port ${port} is available.`); + console.log(`Port ${port} is available`); } else { - console.log(`Port ${port} is in use.`); + console.log(`Port ${port} is in use`); } }; -checkLocalPort(8080); // Example port number +await checkLocalPort(8080); ``` -### Checking Remote Port Availability +#### Find Free Port in Range -To verify if a port is available on a remote server, use `isRemotePortAvailable`. You can specify target as `"host:port"` or host plus a numeric port. +Automatically find the first available port within a specified range: ```typescript -// Using "host:port" -await myNetwork.isRemotePortAvailable('example.com:443'); +const findFreePort = async () => { + // Find a free port between 3000 and 3100 + const freePort = await myNetwork.findFreePort(3000, 3100); + + if (freePort) { + console.log(`Found free port: ${freePort}`); + } else { + console.log('No free ports available in the specified range'); + } +}; +``` -// Using host + port -await myNetwork.isRemotePortAvailable('example.com', 443); +#### Check Remote Port Availability -// UDP is not supported: +Verify if a port is open on a remote server: + +```typescript +// Method 1: Using "host:port" syntax +const isOpen1 = await myNetwork.isRemotePortAvailable('example.com:443'); + +// Method 2: Using separate host and port +const isOpen2 = await myNetwork.isRemotePortAvailable('example.com', 443); + +// Method 3: With options (retries, timeout) +const isOpen3 = await myNetwork.isRemotePortAvailable('example.com', { + port: 443, + protocol: 'tcp', // Only TCP is supported + retries: 3, // Number of connection attempts + timeout: 5000 // Timeout per attempt in ms +}); + +// Note: UDP is not supported and will throw an error try { - await myNetwork.isRemotePortAvailable('example.com', { port: 53, protocol: 'udp' }); + await myNetwork.isRemotePortAvailable('example.com', { + port: 53, + protocol: 'udp' + }); } catch (e) { - console.error((e as any).code); // ENOTSUP + console.error(e.code); // ENOTSUP } ``` -### Using Ping +### Network Connectivity -The `ping` method sends ICMP echo requests and optionally repeats them to collect statistics. +#### Ping Operations + +Send ICMP echo requests to test connectivity and measure latency: ```typescript -// Single ping -const p1 = await myNetwork.ping('google.com'); -console.log(`Alive: ${p1.alive}, RTT: ${p1.time} ms`); +// Simple ping +const pingResult = await myNetwork.ping('google.com'); +console.log(`Host alive: ${pingResult.alive}`); +console.log(`RTT: ${pingResult.time} ms`); -// Multiple pings with statistics -const stats = await myNetwork.ping('google.com', { count: 5 }); -console.log( - `min=${stats.min} ms, max=${stats.max} ms, avg=${stats.avg.toFixed(2)} ms, loss=${stats.packetLoss}%`, -); +// Ping with statistics (multiple pings) +const pingStats = await myNetwork.ping('google.com', { + count: 5, // Number of pings + timeout: 1000 // Timeout per ping in ms +}); + +console.log(`Packet loss: ${pingStats.packetLoss}%`); +console.log(`Min: ${pingStats.min} ms`); +console.log(`Max: ${pingStats.max} ms`); +console.log(`Avg: ${pingStats.avg.toFixed(2)} ms`); +console.log(`Stddev: ${pingStats.stddev.toFixed(2)} ms`); ``` -### Getting Network Gateways +#### Traceroute -You can also retrieve network interfaces (gateways) and determine the default gateway. Caching with TTL is supported via constructor options. +Perform hop-by-hop network path analysis: ```typescript -// Create with cache TTL of 60 seconds -const netCached = new SmartNetwork({ cacheTtl: 60000 }); +const hops = await myNetwork.traceroute('google.com', { + maxHops: 10, // Maximum number of hops + timeout: 5000 // Timeout in ms +}); -// List all interfaces -const gateways = await netCached.getGateways(); -console.log(gateways); - -// Get default gateway -const defaultGw = await netCached.getDefaultGateway(); -console.log(defaultGw); +hops.forEach(hop => { + const rtt = hop.rtt === null ? '*' : `${hop.rtt} ms`; + console.log(`${hop.ttl}\t${hop.ip}\t${rtt}`); +}); ``` -### Discovering Public IP Addresses +Note: Falls back to a single-hop stub if the `traceroute` binary is unavailable on the system. -To find out your public IPv4 and IPv6 addresses (with caching): +### DNS Operations + +Resolve DNS records for a hostname: ```typescript -const publicIps = await netCached.getPublicIps(); -console.log(`Public IPv4: ${publicIps.v4}`); -console.log(`Public IPv6: ${publicIps.v6}`); +const dnsRecords = await myNetwork.resolveDns('example.com'); + +console.log('A records:', dnsRecords.A); // IPv4 addresses +console.log('AAAA records:', dnsRecords.AAAA); // IPv6 addresses +console.log('MX records:', dnsRecords.MX); // Mail servers + +// MX records include priority +dnsRecords.MX.forEach(mx => { + console.log(`Mail server: ${mx.exchange} (priority: ${mx.priority})`); +}); ``` -The `@push.rocks/smartnetwork` package provides an easy-to-use, comprehensive suite of tools for network diagnostics and monitoring, encapsulating complex network operations into simple asynchronous methods. By leveraging TypeScript, developers can benefit from type checking, ensuring that they can work with clear structures and expectations. +### HTTP/HTTPS Endpoint Health Checks -These examples offer a glimpse into the module's utility in real-world scenarios, demonstrating its versatility in handling common network tasks. Whether you're developing a network-sensitive application, diagnosing connectivity issues, or simply curious about your network performance, `@push.rocks/smartnetwork` equips you with the tools you need. +Check the health and response time of HTTP/HTTPS endpoints: + +```typescript +const health = await myNetwork.checkEndpoint('https://example.com', { + timeout: 5000 // Request timeout in ms +}); + +console.log(`Status: ${health.status}`); +console.log(`RTT: ${health.rtt} ms`); +console.log('Headers:', health.headers); +``` + +### Network Interface Information + +#### Get Network Gateways + +List all network interfaces on the system: + +```typescript +const gateways = await myNetwork.getGateways(); + +Object.entries(gateways).forEach(([name, interfaces]) => { + console.log(`Interface: ${name}`); + interfaces.forEach(iface => { + console.log(` ${iface.family}: ${iface.address}`); + console.log(` Netmask: ${iface.netmask}`); + console.log(` MAC: ${iface.mac}`); + }); +}); +``` + +#### Get Default Gateway + +Retrieve the system's default network gateway: + +```typescript +const defaultGateway = await myNetwork.getDefaultGateway(); + +if (defaultGateway) { + console.log('IPv4 Gateway:', defaultGateway.ipv4.address); + console.log('IPv6 Gateway:', defaultGateway.ipv6.address); +} +``` + +### Public IP Discovery + +Discover your public IPv4 and IPv6 addresses: + +```typescript +const publicIps = await myNetwork.getPublicIps(); + +console.log(`Public IPv4: ${publicIps.v4 || 'Not available'}`); +console.log(`Public IPv6: ${publicIps.v6 || 'Not available'}`); +``` + +### Caching + +SmartNetwork supports caching for gateway and public IP lookups to reduce repeated network calls: + +```typescript +// Create instance with 60-second cache TTL +const cachedNetwork = new SmartNetwork({ cacheTtl: 60000 }); + +// These calls will use cached results if called within 60 seconds +const gateways1 = await cachedNetwork.getGateways(); +const publicIps1 = await cachedNetwork.getPublicIps(); + +// Subsequent calls within TTL return cached results +const gateways2 = await cachedNetwork.getGateways(); // From cache +const publicIps2 = await cachedNetwork.getPublicIps(); // From cache +``` ### Plugin Architecture -You can extend `SmartNetwork` with custom plugins by registering them at runtime: +Extend SmartNetwork's functionality with custom plugins: ```typescript -import { SmartNetwork } from '@push.rocks/smartnetwork'; - -// Define your plugin class or constructor -class MyCustomPlugin { - // plugin implementation goes here +// Define your plugin +class CustomNetworkPlugin { + constructor(private smartNetwork: SmartNetwork) {} + + async customMethod() { + // Your custom network functionality + } } -// Register and unregister your plugin by name -SmartNetwork.registerPlugin('myPlugin', MyCustomPlugin); -// Later, remove it if no longer needed -SmartNetwork.unregisterPlugin('myPlugin'); +// Register the plugin +SmartNetwork.registerPlugin('customPlugin', CustomNetworkPlugin); + +// Use the plugin +const network = new SmartNetwork(); +const plugin = new (SmartNetwork.pluginsRegistry.get('customPlugin'))(network); +await plugin.customMethod(); + +// Unregister when no longer needed +SmartNetwork.unregisterPlugin('customPlugin'); ``` -Plugins enable you to dynamically augment the core functionality without altering the library's source. +### Error Handling + +The package uses custom `NetworkError` class for network-related errors: + +```typescript +import { NetworkError } from '@push.rocks/smartnetwork'; + +try { + await myNetwork.isRemotePortAvailable('example.com', { protocol: 'udp' }); +} catch (error) { + if (error instanceof NetworkError) { + console.error(`Network error: ${error.message}`); + console.error(`Error code: ${error.code}`); + } +} +``` + +### TypeScript Support + +This package is written in TypeScript and provides full type definitions. Key interfaces include: + +```typescript +interface SmartNetworkOptions { + cacheTtl?: number; // Cache TTL in milliseconds +} + +interface Hop { + ttl: number; // Time to live + ip: string; // IP address of the hop + rtt: number | null; // Round trip time in ms +} +``` ## License and Legal Information -This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. +This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file. @@ -180,4 +328,4 @@ Registered at District court Bremen HRB 35230 HB, Germany For any legal inquiries or if you require further information, please contact us via email at hello@task.vc. -By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works. +By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works. \ No newline at end of file diff --git a/test/test.features.ts b/test/test.features.ts index 9dcee27..1c6e4a9 100644 --- a/test/test.features.ts +++ b/test/test.features.ts @@ -168,6 +168,70 @@ tap.test('isLocalPortUnused should detect used local port', async () => { expect(inUse).toBeFalse(); await new Promise((resolve) => server.close(() => resolve())); }); + +// findFreePort tests +tap.test('findFreePort should find an available port in range', async () => { + const sn = new SmartNetwork(); + const freePort = await sn.findFreePort(49152, 49200); + expect(freePort).toBeGreaterThanOrEqual(49152); + expect(freePort).toBeLessThanOrEqual(49200); + + // Verify the port is actually free + const isUnused = await sn.isLocalPortUnused(freePort); + expect(isUnused).toBeTrue(); +}); + +tap.test('findFreePort should return null when all ports are occupied', async () => { + const sn = new SmartNetwork(); + // Create servers to occupy a small range + const servers = []; + const startPort = 49300; + const endPort = 49302; + + for (let port = startPort; port <= endPort; port++) { + const server = net.createServer(); + await new Promise((res) => server.listen(port, res)); + servers.push(server); + } + + // Now all ports in range should be occupied + const freePort = await sn.findFreePort(startPort, endPort); + expect(freePort).toBeNull(); + + // Clean up servers + await Promise.all(servers.map(s => new Promise((res) => s.close(() => res())))); +}); + +tap.test('findFreePort should validate port range', async () => { + const sn = new SmartNetwork(); + + // Test invalid port numbers + try { + await sn.findFreePort(0, 100); + throw new Error('Expected error for port < 1'); + } catch (err: any) { + expect(err).toBeInstanceOf(NetworkError); + expect(err.code).toEqual('EINVAL'); + } + + try { + await sn.findFreePort(100, 70000); + throw new Error('Expected error for port > 65535'); + } catch (err: any) { + expect(err).toBeInstanceOf(NetworkError); + expect(err.code).toEqual('EINVAL'); + } + + // Test startPort > endPort + try { + await sn.findFreePort(200, 100); + throw new Error('Expected error for startPort > endPort'); + } catch (err: any) { + expect(err).toBeInstanceOf(NetworkError); + expect(err.code).toEqual('EINVAL'); + } +}); + // Real traceroute integration test (skipped if `traceroute` binary is unavailable) tap.test('traceroute real integration against google.com', async () => { const sn = new SmartNetwork(); diff --git a/ts/smartnetwork.classes.smartnetwork.ts b/ts/smartnetwork.classes.smartnetwork.ts index 9a16cd6..d892a46 100644 --- a/ts/smartnetwork.classes.smartnetwork.ts +++ b/ts/smartnetwork.classes.smartnetwork.ts @@ -143,6 +143,33 @@ export class SmartNetwork { return result; } + /** + * Find the first available port within a given range + * @param startPort The start of the port range (inclusive) + * @param endPort The end of the port range (inclusive) + * @returns The first available port number, or null if no ports are available + */ + public async findFreePort(startPort: number, endPort: number): Promise { + // 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'); + } + + // Check each port in the range + for (let port = startPort; port <= endPort; port++) { + const isUnused = await this.isLocalPortUnused(port); + if (isUnused) { + return port; + } + } + + // No free port found in the range + return null; + } + /** * checks wether a remote port is available * @param domainArg