Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d1ab85cbb3 | ||
9cf4e433bf | |||
7c88ecd82a | |||
771bfe94e7 | |||
def467a27b |
24
changelog.md
24
changelog.md
@@ -1,5 +1,29 @@
|
||||
# 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)
|
||||
Enhance documentation and add configurable speed test options with plugin architecture improvements
|
||||
|
||||
|
11
package.json
11
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartnetwork",
|
||||
"version": "4.0.0",
|
||||
"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",
|
||||
@@ -9,17 +9,16 @@
|
||||
"author": "Lossless GmbH",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "(tstest test/)",
|
||||
"test": "(tstest test/ --verbose)",
|
||||
"build": "(tsbuild --web --allowimplicitany)",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.1.61",
|
||||
"@git.zone/tsbuild": "^2.5.1",
|
||||
"@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/tapbundle": "^5.0.3",
|
||||
"@types/node": "^22.15.3"
|
||||
"@types/node": "^22.15.19"
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/smartping": "^1.0.7",
|
||||
|
2197
pnpm-lock.yaml
generated
2197
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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
|
306
readme.md
306
readme.md
@@ -1,6 +1,6 @@
|
||||
# @push.rocks/smartnetwork
|
||||
|
||||
network diagnostics
|
||||
Comprehensive network diagnostics and utilities for Node.js applications
|
||||
|
||||
## Install
|
||||
|
||||
@@ -10,22 +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
|
||||
|
||||
@@ -39,129 +26,290 @@ 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);
|
||||
|
||||
// Using host + port
|
||||
await myNetwork.isRemotePortAvailable('example.com', 443);
|
||||
if (freePort) {
|
||||
console.log(`Found free port: ${freePort}`);
|
||||
} else {
|
||||
console.log('No free ports available in the specified range');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
// UDP is not supported:
|
||||
#### Check Remote Port Availability
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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 CustomNetworkPlugin {
|
||||
constructor(private smartNetwork: SmartNetwork) {}
|
||||
|
||||
// Define your plugin class or constructor
|
||||
class MyCustomPlugin {
|
||||
// plugin implementation goes here
|
||||
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
|
||||
|
||||
|
@@ -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 * as net 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 hops = await sn.traceroute('example.com', { maxHops: 5 });
|
||||
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 });
|
||||
});
|
||||
|
||||
@@ -166,8 +166,72 @@ tap.test('isLocalPortUnused should detect used local port', async () => {
|
||||
// port is now in use
|
||||
const inUse = await sn.isLocalPortUnused(addr.port);
|
||||
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)
|
||||
tap.test('traceroute real integration against google.com', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
@@ -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';
|
||||
|
||||
@@ -20,11 +20,11 @@ tap.test('should send a ping to Google', 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 () => {
|
||||
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();
|
||||
|
10
test/test.ts
10
test/test.ts
@@ -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';
|
||||
|
||||
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 () => {
|
||||
await expectAsync(testSmartNetwork.isLocalPortUnused(8080)).toBeTrue();
|
||||
await expect(testSmartNetwork.isLocalPortUnused(8080)).resolves.toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should scan a port', async () => {
|
||||
await expectAsync(testSmartNetwork.isRemotePortAvailable('lossless.com:443')).toBeTrue();
|
||||
await expectAsync(testSmartNetwork.isRemotePortAvailable('lossless.com', 443)).toBeTrue();
|
||||
await expectAsync(testSmartNetwork.isRemotePortAvailable('lossless.com:444')).toBeFalse();
|
||||
await expect(testSmartNetwork.isRemotePortAvailable('lossless.com:443')).resolves.toBeTrue();
|
||||
await expect(testSmartNetwork.isRemotePortAvailable('lossless.com', 443)).resolves.toBeTrue();
|
||||
await expect(testSmartNetwork.isRemotePortAvailable('lossless.com:444')).resolves.toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('should get gateways', async () => {
|
||||
|
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
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.'
|
||||
}
|
||||
|
@@ -164,9 +164,10 @@ export class CloudflareSpeed {
|
||||
}
|
||||
|
||||
public async fetchServerLocations(): Promise<{ [key: string]: string }> {
|
||||
const res = JSON.parse(
|
||||
await this.get('speed.cloudflare.com', '/locations'),
|
||||
) as Array<{ iata: string; city: string }>;
|
||||
const res = JSON.parse(await this.get('speed.cloudflare.com', '/locations')) as Array<{
|
||||
iata: string;
|
||||
city: string;
|
||||
}>;
|
||||
return res.reduce(
|
||||
(data: Record<string, string>, optionsArg) => {
|
||||
data[optionsArg.iata] = optionsArg.city;
|
||||
@@ -198,9 +199,9 @@ export class CloudflareSpeed {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
req.on('error', (err: Error & { code?: string }) => {
|
||||
reject(new NetworkError(err.message, err.code));
|
||||
});
|
||||
req.on('error', (err: Error & { code?: string }) => {
|
||||
reject(new NetworkError(err.message, err.code));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -251,15 +252,15 @@ export class CloudflareSpeed {
|
||||
res.on('data', () => {});
|
||||
res.on('end', () => {
|
||||
ended = plugins.perfHooks.performance.now();
|
||||
resolve([
|
||||
started,
|
||||
dnsLookup,
|
||||
tcpHandshake,
|
||||
sslHandshake,
|
||||
ttfb,
|
||||
ended,
|
||||
parseFloat((res.headers['server-timing'] as string).slice(22)),
|
||||
]);
|
||||
resolve([
|
||||
started,
|
||||
dnsLookup,
|
||||
tcpHandshake,
|
||||
sslHandshake,
|
||||
ttfb,
|
||||
ended,
|
||||
parseFloat((res.headers['server-timing'] as string).slice(22)),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -296,11 +297,14 @@ export class CloudflareSpeed {
|
||||
const parts = i.split('=');
|
||||
return [parts[0], parts[1]];
|
||||
})
|
||||
.reduce((data: Record<string, string>, [k, v]) => {
|
||||
if (v === undefined) return data;
|
||||
data[k] = v;
|
||||
return data;
|
||||
}, {} as Record<string, string>);
|
||||
.reduce(
|
||||
(data: Record<string, string>, [k, v]) => {
|
||||
if (v === undefined) return data;
|
||||
data[k] = v;
|
||||
return data;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
return this.get('speed.cloudflare.com', '/cdn-cgi/trace').then(parseCfCdnCgiTrace);
|
||||
}
|
||||
|
@@ -44,9 +44,7 @@ export class SmartNetwork {
|
||||
* get network speed
|
||||
* @param opts optional speed test parameters
|
||||
*/
|
||||
public async getSpeed(
|
||||
opts?: { parallelStreams?: number; duration?: number },
|
||||
) {
|
||||
public async getSpeed(opts?: { parallelStreams?: number; duration?: number }) {
|
||||
const cloudflareSpeedInstance = new CloudflareSpeed(opts);
|
||||
return cloudflareSpeedInstance.speedTest();
|
||||
}
|
||||
@@ -54,10 +52,7 @@ export class SmartNetwork {
|
||||
/**
|
||||
* Send ICMP pings to a host. Optionally specify count for multiple pings.
|
||||
*/
|
||||
public async ping(
|
||||
host: string,
|
||||
opts?: { timeout?: number; count?: number },
|
||||
): Promise<any> {
|
||||
public async ping(host: string, opts?: { timeout?: number; count?: number }): Promise<any> {
|
||||
const timeout = opts?.timeout ?? 500;
|
||||
const count = opts?.count && opts.count > 1 ? opts.count : 1;
|
||||
const pinger = new plugins.smartping.Smartping();
|
||||
@@ -85,11 +80,7 @@ export class SmartNetwork {
|
||||
const min = valid.length ? Math.min(...valid) : NaN;
|
||||
const max = valid.length ? Math.max(...valid) : NaN;
|
||||
const avg = valid.length ? stats.average(valid) : NaN;
|
||||
const stddev = valid.length
|
||||
? Math.sqrt(
|
||||
stats.average(valid.map((v) => (v - avg) ** 2)),
|
||||
)
|
||||
: NaN;
|
||||
const stddev = valid.length ? Math.sqrt(stats.average(valid.map((v) => (v - avg) ** 2))) : NaN;
|
||||
const packetLoss = ((count - aliveCount) / count) * 100;
|
||||
return {
|
||||
host,
|
||||
@@ -152,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<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
|
||||
* @param domainArg
|
||||
@@ -168,7 +186,9 @@ export class SmartNetwork {
|
||||
*/
|
||||
public async isRemotePortAvailable(
|
||||
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> {
|
||||
let hostPart: string;
|
||||
let port: number | undefined;
|
||||
@@ -252,7 +272,9 @@ export class SmartNetwork {
|
||||
/**
|
||||
* 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 {
|
||||
const dns = await import('dns');
|
||||
const { resolve4, resolve6, resolveMx } = dns.promises;
|
||||
@@ -316,14 +338,10 @@ export class SmartNetwork {
|
||||
const { exec } = await import('child_process');
|
||||
const cmd = `traceroute -n -m ${maxHops} ${host}`;
|
||||
const stdout: string = await new Promise((resolve, reject) => {
|
||||
exec(
|
||||
cmd,
|
||||
{ encoding: 'utf8', timeout },
|
||||
(err, stdout) => {
|
||||
if (err) return reject(err);
|
||||
resolve(stdout);
|
||||
},
|
||||
);
|
||||
exec(cmd, { encoding: 'utf8', timeout }, (err, stdout) => {
|
||||
if (err) return reject(err);
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
const hops: Hop[] = [];
|
||||
for (const raw of stdout.split('\n')) {
|
||||
|
Reference in New Issue
Block a user