Compare commits

..

5 Commits

Author SHA1 Message Date
Juergen Kunz
d1ab85cbb3 feat(port-management): add findFreePort method for automatic port discovery within a range
Some checks failed
Default (tags) / security (push) Successful in 46s
Default (tags) / test (push) Failing after 6m10s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-07-31 22:04:20 +00:00
9cf4e433bf 4.0.2
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Failing after 15m18s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-05-19 09:15:42 +00:00
7c88ecd82a fix(tests): Update dev dependencies and refactor test assertions for improved clarity 2025-05-19 09:15:42 +00:00
771bfe94e7 4.0.1
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 1m34s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-03 18:56:00 +00:00
def467a27b fix(formatting): Fix minor formatting issues and newline consistency across project files 2025-05-03 18:56:00 +00:00
13 changed files with 1034 additions and 1784 deletions

View File

@@ -1,5 +1,29 @@
# Changelog # 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
- Bumped @git.zone/tsbuild version to ^2.5.1
- Bumped @git.zone/tstest version to ^1.9.0
- Updated npm test script to include the verbose flag
- Replaced expectAsync assertions with resolves based assertions in test files
## 2025-05-03 - 4.0.1 - fix(formatting)
Fix minor formatting issues and newline consistency across project files
- Ensure newline at end of package.json, errors.ts, logging.ts, and test files
- Refine code block formatting in readme.md
- Adjust whitespace and code style in smartnetwork classes and cloudflarespeed module
- Minor commitinfo data format update
## 2025-04-28 - 4.0.0 - BREAKING CHANGE(smartnetwork) ## 2025-04-28 - 4.0.0 - BREAKING CHANGE(smartnetwork)
Enhance documentation and add configurable speed test options with plugin architecture improvements Enhance documentation and add configurable speed test options with plugin architecture improvements

View File

@@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartnetwork", "name": "@push.rocks/smartnetwork",
"version": "4.0.0", "version": "4.1.0",
"private": false, "private": false,
"description": "A toolkit for network diagnostics including speed tests, port availability checks, and more.", "description": "A toolkit for network diagnostics including speed tests, port availability checks, and more.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
@@ -9,17 +9,16 @@
"author": "Lossless GmbH", "author": "Lossless GmbH",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"test": "(tstest test/)", "test": "(tstest test/ --verbose)",
"build": "(tsbuild --web --allowimplicitany)", "build": "(tsbuild --web --allowimplicitany)",
"buildDocs": "tsdoc" "buildDocs": "tsdoc"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.1.61", "@git.zone/tsbuild": "^2.5.1",
"@git.zone/tsrun": "^1.2.39", "@git.zone/tsrun": "^1.2.39",
"@git.zone/tstest": "^1.0.69", "@git.zone/tstest": "^1.9.0",
"@push.rocks/smartenv": "^5.0.0", "@push.rocks/smartenv": "^5.0.0",
"@push.rocks/tapbundle": "^5.0.3", "@types/node": "^22.15.19"
"@types/node": "^22.15.3"
}, },
"dependencies": { "dependencies": {
"@push.rocks/smartping": "^1.0.7", "@push.rocks/smartping": "^1.0.7",

2197
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1,73 @@
# Project Analysis
## Architecture Overview
This is a comprehensive network diagnostics toolkit that provides various network-related utilities. The main entry point is the `SmartNetwork` class which orchestrates all functionality.
Key features:
- Speed testing via Cloudflare (parallelizable with duration support)
- Ping operations with statistics
- Port availability checks (local and remote)
- Network gateway discovery
- Public IP retrieval
- DNS resolution
- HTTP endpoint health checks
- Traceroute functionality (with fallback stub)
## Key Components
### SmartNetwork Class
- Central orchestrator for all network operations
- Supports caching via `cacheTtl` option for gateway and public IP lookups
- Plugin architecture for extensibility
### CloudflareSpeed Class
- Handles internet speed testing using Cloudflare's infrastructure
- Supports parallel streams and customizable test duration
- Measures both download and upload speeds using progressive chunk sizes
- Includes latency measurements (jitter, median, average)
### Error Handling
- Custom `NetworkError` and `TimeoutError` classes for better error context
- Error codes follow Node.js conventions (ENOTSUP, EINVAL, ETIMEOUT)
### Logging
- Global logger interface for consistent logging across the codebase
- Replaceable logger implementation (defaults to console)
- Used primarily for error reporting in speed tests
### Statistics Helpers
- Utility functions for statistical calculations (average, median, quartile, jitter)
- Used extensively by speed testing and ping operations
## Recent Changes (v4.0.0 - v4.0.1)
- Added configurable speed test options (parallelStreams, duration)
- Introduced plugin architecture for runtime extensibility
- Enhanced error handling with custom error classes
- Added global logging interface
- Improved connection management by disabling HTTP connection pooling
- Fixed memory leaks from listener accumulation
- Minor formatting fixes for consistency
## Testing
- Comprehensive test suite covering all major features
- Tests run on both browser and node environments
- Uses @push.rocks/tapbundle for testing with expectAsync
- Performance tests for speed testing functionality
- Edge case handling for network errors and timeouts
## Technical Details
- ESM-only package (module type)
- TypeScript with strict typing
- Depends on external modules for specific functionality:
- @push.rocks/smartping for ICMP operations
- public-ip for external IP discovery
- systeminformation for network interface details
- isopen for remote port checking
- Uses native Node.js modules for DNS, HTTP/HTTPS, and network operations
## Design Patterns
- Factory pattern for plugin registration
- Caching pattern with TTL for expensive operations
- Promise-based async/await throughout
- Deferred promises for complex async coordination
- Error propagation with custom error types

312
readme.md
View File

@@ -1,6 +1,6 @@
# @push.rocks/smartnetwork # @push.rocks/smartnetwork
network diagnostics Comprehensive network diagnostics and utilities for Node.js applications
## Install ## Install
@@ -10,22 +10,9 @@ To install `@push.rocks/smartnetwork`, run the following command in your termina
npm install @push.rocks/smartnetwork --save 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 ## 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 ### Basic Setup
@@ -39,133 +26,294 @@ Then, create an instance of `SmartNetwork`:
```typescript ```typescript
const myNetwork = new SmartNetwork(); 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: Measure network download and upload speeds using Cloudflare's speed test infrastructure:
- `parallelStreams`: number of concurrent streams (default: 1)
- `duration`: test duration in seconds (default: fixed segments)
```typescript ```typescript
const speedTest = async () => { const speedTest = async () => {
// Default fixed-segment test // Basic speed test
let r = await myNetwork.getSpeed(); const result = await myNetwork.getSpeed();
console.log(`Download: ${r.downloadSpeed} Mbps, Upload: ${r.uploadSpeed} Mbps`); console.log(`Download: ${result.downloadSpeed} Mbps`);
console.log(`Upload: ${result.uploadSpeed} Mbps`);
// Parallel + duration-based test // Advanced speed test with options
r = await myNetwork.getSpeed({ parallelStreams: 3, duration: 5 }); const advancedResult = await myNetwork.getSpeed({
console.log(`Download: ${r.downloadSpeed} Mbps, Upload: ${r.uploadSpeed} Mbps`); 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 ```typescript
const checkLocalPort = async (port: number) => { const checkLocalPort = async (port: number) => {
const isUnused = await myNetwork.isLocalPortUnused(port); const isUnused = await myNetwork.isLocalPortUnused(port);
if (isUnused) { if (isUnused) {
console.log(`Port ${port} is available.`); console.log(`Port ${port} is available`);
} else { } 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 ```typescript
// Using "host:port" const findFreePort = async () => {
await myNetwork.isRemotePortAvailable('example.com:443'); // 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 #### Check Remote Port Availability
await myNetwork.isRemotePortAvailable('example.com', 443);
// 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 { try {
await myNetwork.isRemotePortAvailable('example.com', { port: 53, protocol: 'udp' }); await myNetwork.isRemotePortAvailable('example.com', {
port: 53,
protocol: 'udp'
});
} catch (e) { } 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 ```typescript
// Single ping // Simple ping
const p1 = await myNetwork.ping('google.com'); const pingResult = await myNetwork.ping('google.com');
console.log(`Alive: ${p1.alive}, RTT: ${p1.time} ms`); console.log(`Host alive: ${pingResult.alive}`);
console.log(`RTT: ${pingResult.time} ms`);
// Multiple pings with statistics // Ping with statistics (multiple pings)
const stats = await myNetwork.ping('google.com', { count: 5 }); const pingStats = await myNetwork.ping('google.com', {
console.log( count: 5, // Number of pings
`min=${stats.min} ms, max=${stats.max} ms, avg=${stats.avg.toFixed(2)} ms, loss=${stats.packetLoss}%`, 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 ```typescript
// Create with cache TTL of 60 seconds const hops = await myNetwork.traceroute('google.com', {
const netCached = new SmartNetwork({ cacheTtl: 60000 }); maxHops: 10, // Maximum number of hops
timeout: 5000 // Timeout in ms
});
// List all interfaces hops.forEach(hop => {
const gateways = await netCached.getGateways(); const rtt = hop.rtt === null ? '*' : `${hop.rtt} ms`;
console.log(gateways); console.log(`${hop.ttl}\t${hop.ip}\t${rtt}`);
});
// Get default gateway
const defaultGw = await netCached.getDefaultGateway();
console.log(defaultGw);
``` ```
### 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 ```typescript
const publicIps = await netCached.getPublicIps(); const dnsRecords = await myNetwork.resolveDns('example.com');
console.log(`Public IPv4: ${publicIps.v4}`);
console.log(`Public IPv6: ${publicIps.v6}`); 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
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
```
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.
### Plugin Architecture ### Plugin Architecture
You can extend `SmartNetwork` with custom plugins by registering them at runtime: Extend SmartNetwork's functionality with custom plugins:
```typescript ```typescript
import { SmartNetwork } from '@push.rocks/smartnetwork'; // Define your plugin
class CustomNetworkPlugin {
// Define your plugin class or constructor constructor(private smartNetwork: SmartNetwork) {}
class MyCustomPlugin {
// plugin implementation goes here async customMethod() {
// Your custom network functionality
}
} }
// Register and unregister your plugin by name // Register the plugin
SmartNetwork.registerPlugin('myPlugin', MyCustomPlugin); SmartNetwork.registerPlugin('customPlugin', CustomNetworkPlugin);
// Later, remove it if no longer needed
SmartNetwork.unregisterPlugin('myPlugin'); // 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 ## 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. **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. 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.

View File

@@ -1,4 +1,4 @@
import { tap, expect, expectAsync } from '@push.rocks/tapbundle'; import { tap, expect } from '@git.zone/tstest/tapbundle';
import { SmartNetwork, NetworkError } from '../ts/index.js'; import { SmartNetwork, NetworkError } from '../ts/index.js';
import * as net from 'net'; import * as net from 'net';
import type { AddressInfo } from 'net'; import type { AddressInfo } from 'net';
@@ -61,7 +61,7 @@ tap.test('traceroute fallback stub returns a single-hop stub', async () => {
const sn = new SmartNetwork(); const sn = new SmartNetwork();
const hops = await sn.traceroute('example.com', { maxHops: 5 }); const hops = await sn.traceroute('example.com', { maxHops: 5 });
expect(Array.isArray(hops)).toBeTrue(); expect(Array.isArray(hops)).toBeTrue();
expect(hops).toHaveLength(1); expect(hops).array.toHaveLength(1);
expect(hops[0]).toEqual({ ttl: 1, ip: 'example.com', rtt: null }); expect(hops[0]).toEqual({ ttl: 1, ip: 'example.com', rtt: null });
}); });
@@ -166,8 +166,72 @@ tap.test('isLocalPortUnused should detect used local port', async () => {
// port is now in use // port is now in use
const inUse = await sn.isLocalPortUnused(addr.port); const inUse = await sn.isLocalPortUnused(addr.port);
expect(inUse).toBeFalse(); expect(inUse).toBeFalse();
await new Promise<void>((res) => server.close(res)); await new Promise<void>((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<void>((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<void>((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) // Real traceroute integration test (skipped if `traceroute` binary is unavailable)
tap.test('traceroute real integration against google.com', async () => { tap.test('traceroute real integration against google.com', async () => {
const sn = new SmartNetwork(); const sn = new SmartNetwork();
@@ -188,4 +252,4 @@ tap.test('traceroute real integration against google.com', async () => {
} }
}); });
tap.start(); tap.start();

View File

@@ -1,4 +1,4 @@
import { tap, expect, expectAsync } from '@push.rocks/tapbundle'; import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as smartnetwork from '../ts/index.js'; import * as smartnetwork from '../ts/index.js';
@@ -20,11 +20,11 @@ tap.test('should send a ping to Google', async () => {
}); });
tap.test('should state when a ping is not alive ', async () => { tap.test('should state when a ping is not alive ', async () => {
await expectAsync(testSmartnetwork.ping('notthere.lossless.com')).property('alive').toBeFalse(); await expect(testSmartnetwork.ping('notthere.lossless.com')).resolves.property('alive').toBeFalse();
}); });
tap.test('should send a ping to an IP', async () => { tap.test('should send a ping to an IP', async () => {
await expectAsync(testSmartnetwork.ping('192.168.186.999')).property('alive').toBeFalse(); await expect(testSmartnetwork.ping('192.168.186.999')).resolves.property('alive').toBeFalse();
}); });
tap.start(); tap.start();

View File

@@ -1,4 +1,4 @@
import { expect, expectAsync, tap } from '@push.rocks/tapbundle'; import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartnetwork from '../ts/index.js'; import * as smartnetwork from '../ts/index.js';
let testSmartNetwork: smartnetwork.SmartNetwork; let testSmartNetwork: smartnetwork.SmartNetwork;
@@ -20,13 +20,13 @@ tap.test('should perform a speedtest', async () => {
}); });
tap.test('should determine wether a port is free', async () => { tap.test('should determine wether a port is free', async () => {
await expectAsync(testSmartNetwork.isLocalPortUnused(8080)).toBeTrue(); await expect(testSmartNetwork.isLocalPortUnused(8080)).resolves.toBeTrue();
}); });
tap.test('should scan a port', async () => { tap.test('should scan a port', async () => {
await expectAsync(testSmartNetwork.isRemotePortAvailable('lossless.com:443')).toBeTrue(); await expect(testSmartNetwork.isRemotePortAvailable('lossless.com:443')).resolves.toBeTrue();
await expectAsync(testSmartNetwork.isRemotePortAvailable('lossless.com', 443)).toBeTrue(); await expect(testSmartNetwork.isRemotePortAvailable('lossless.com', 443)).resolves.toBeTrue();
await expectAsync(testSmartNetwork.isRemotePortAvailable('lossless.com:444')).toBeFalse(); await expect(testSmartNetwork.isRemotePortAvailable('lossless.com:444')).resolves.toBeFalse();
}); });
tap.test('should get gateways', async () => { tap.test('should get gateways', async () => {

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartnetwork', name: '@push.rocks/smartnetwork',
version: '4.0.0', version: '4.0.2',
description: 'A toolkit for network diagnostics including speed tests, port availability checks, and more.' description: 'A toolkit for network diagnostics including speed tests, port availability checks, and more.'
} }

View File

@@ -17,4 +17,4 @@ export class TimeoutError extends NetworkError {
this.name = 'TimeoutError'; this.name = 'TimeoutError';
Object.setPrototypeOf(this, new.target.prototype); Object.setPrototypeOf(this, new.target.prototype);
} }
} }

View File

@@ -27,4 +27,4 @@ export function setLogger(logger: Logger): void {
*/ */
export function getLogger(): Logger { export function getLogger(): Logger {
return globalLogger; return globalLogger;
} }

View File

@@ -164,9 +164,10 @@ export class CloudflareSpeed {
} }
public async fetchServerLocations(): Promise<{ [key: string]: string }> { public async fetchServerLocations(): Promise<{ [key: string]: string }> {
const res = JSON.parse( const res = JSON.parse(await this.get('speed.cloudflare.com', '/locations')) as Array<{
await this.get('speed.cloudflare.com', '/locations'), iata: string;
) as Array<{ iata: string; city: string }>; city: string;
}>;
return res.reduce( return res.reduce(
(data: Record<string, string>, optionsArg) => { (data: Record<string, string>, optionsArg) => {
data[optionsArg.iata] = optionsArg.city; data[optionsArg.iata] = optionsArg.city;
@@ -198,9 +199,9 @@ export class CloudflareSpeed {
reject(e); reject(e);
} }
}); });
req.on('error', (err: Error & { code?: string }) => { req.on('error', (err: Error & { code?: string }) => {
reject(new NetworkError(err.message, err.code)); reject(new NetworkError(err.message, err.code));
}); });
}, },
); );
@@ -251,15 +252,15 @@ export class CloudflareSpeed {
res.on('data', () => {}); res.on('data', () => {});
res.on('end', () => { res.on('end', () => {
ended = plugins.perfHooks.performance.now(); ended = plugins.perfHooks.performance.now();
resolve([ resolve([
started, started,
dnsLookup, dnsLookup,
tcpHandshake, tcpHandshake,
sslHandshake, sslHandshake,
ttfb, ttfb,
ended, ended,
parseFloat((res.headers['server-timing'] as string).slice(22)), parseFloat((res.headers['server-timing'] as string).slice(22)),
]); ]);
}); });
}); });
@@ -296,11 +297,14 @@ export class CloudflareSpeed {
const parts = i.split('='); const parts = i.split('=');
return [parts[0], parts[1]]; return [parts[0], parts[1]];
}) })
.reduce((data: Record<string, string>, [k, v]) => { .reduce(
if (v === undefined) return data; (data: Record<string, string>, [k, v]) => {
data[k] = v; if (v === undefined) return data;
return data; data[k] = v;
}, {} as Record<string, string>); return data;
},
{} as Record<string, string>,
);
return this.get('speed.cloudflare.com', '/cdn-cgi/trace').then(parseCfCdnCgiTrace); return this.get('speed.cloudflare.com', '/cdn-cgi/trace').then(parseCfCdnCgiTrace);
} }

View File

@@ -44,9 +44,7 @@ export class SmartNetwork {
* get network speed * get network speed
* @param opts optional speed test parameters * @param opts optional speed test parameters
*/ */
public async getSpeed( public async getSpeed(opts?: { parallelStreams?: number; duration?: number }) {
opts?: { parallelStreams?: number; duration?: number },
) {
const cloudflareSpeedInstance = new CloudflareSpeed(opts); const cloudflareSpeedInstance = new CloudflareSpeed(opts);
return cloudflareSpeedInstance.speedTest(); return cloudflareSpeedInstance.speedTest();
} }
@@ -54,10 +52,7 @@ export class SmartNetwork {
/** /**
* Send ICMP pings to a host. Optionally specify count for multiple pings. * Send ICMP pings to a host. Optionally specify count for multiple pings.
*/ */
public async ping( public async ping(host: string, opts?: { timeout?: number; count?: number }): Promise<any> {
host: string,
opts?: { timeout?: number; count?: number },
): Promise<any> {
const timeout = opts?.timeout ?? 500; const timeout = opts?.timeout ?? 500;
const count = opts?.count && opts.count > 1 ? opts.count : 1; const count = opts?.count && opts.count > 1 ? opts.count : 1;
const pinger = new plugins.smartping.Smartping(); const pinger = new plugins.smartping.Smartping();
@@ -85,11 +80,7 @@ export class SmartNetwork {
const min = valid.length ? Math.min(...valid) : NaN; const min = valid.length ? Math.min(...valid) : NaN;
const max = valid.length ? Math.max(...valid) : NaN; const max = valid.length ? Math.max(...valid) : NaN;
const avg = valid.length ? stats.average(valid) : NaN; const avg = valid.length ? stats.average(valid) : NaN;
const stddev = valid.length const stddev = valid.length ? Math.sqrt(stats.average(valid.map((v) => (v - avg) ** 2))) : NaN;
? Math.sqrt(
stats.average(valid.map((v) => (v - avg) ** 2)),
)
: NaN;
const packetLoss = ((count - aliveCount) / count) * 100; const packetLoss = ((count - aliveCount) / count) * 100;
return { return {
host, host,
@@ -152,6 +143,33 @@ export class SmartNetwork {
return result; 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<number | null> {
// 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 * checks wether a remote port is available
* @param domainArg * @param domainArg
@@ -168,7 +186,9 @@ export class SmartNetwork {
*/ */
public async isRemotePortAvailable( public async isRemotePortAvailable(
target: string, target: string,
portOrOpts?: number | { port?: number; protocol?: 'tcp' | 'udp'; timeout?: number; retries?: number }, portOrOpts?:
| number
| { port?: number; protocol?: 'tcp' | 'udp'; timeout?: number; retries?: number },
): Promise<boolean> { ): Promise<boolean> {
let hostPart: string; let hostPart: string;
let port: number | undefined; let port: number | undefined;
@@ -252,7 +272,9 @@ export class SmartNetwork {
/** /**
* Resolve DNS records (A, AAAA, MX) * Resolve DNS records (A, AAAA, MX)
*/ */
public async resolveDns(host: string): Promise<{ A: string[]; AAAA: string[]; MX: { exchange: string; priority: number }[] }> { public async resolveDns(
host: string,
): Promise<{ A: string[]; AAAA: string[]; MX: { exchange: string; priority: number }[] }> {
try { try {
const dns = await import('dns'); const dns = await import('dns');
const { resolve4, resolve6, resolveMx } = dns.promises; const { resolve4, resolve6, resolveMx } = dns.promises;
@@ -316,14 +338,10 @@ export class SmartNetwork {
const { exec } = await import('child_process'); const { exec } = await import('child_process');
const cmd = `traceroute -n -m ${maxHops} ${host}`; const cmd = `traceroute -n -m ${maxHops} ${host}`;
const stdout: string = await new Promise((resolve, reject) => { const stdout: string = await new Promise((resolve, reject) => {
exec( exec(cmd, { encoding: 'utf8', timeout }, (err, stdout) => {
cmd, if (err) return reject(err);
{ encoding: 'utf8', timeout }, resolve(stdout);
(err, stdout) => { });
if (err) return reject(err);
resolve(stdout);
},
);
}); });
const hops: Hop[] = []; const hops: Hop[] = [];
for (const raw of stdout.split('\n')) { for (const raw of stdout.split('\n')) {