Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
e70e5ac15c | |||
806606c9b9 | |||
ac3b501adf | |||
da02e04edf | |||
1a81adaabd | |||
5ae4187065 | |||
b7d7e405eb | |||
d1ab85cbb3 | |||
9cf4e433bf | |||
7c88ecd82a |
27
changelog.md
27
changelog.md
@@ -1,5 +1,32 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-09-12 - 4.2.0 - feat(PublicIp)
|
||||
Add PublicIp service and refactor SmartNetwork to use it; remove public-ip dependency; update exports, docs and dependencies
|
||||
|
||||
- Add PublicIp class (ts/smartnetwork.classes.publicip.ts) implementing public IPv4/IPv6 lookup with multiple fallback services, timeouts and validation
|
||||
- Refactor SmartNetwork.getPublicIps to use the new PublicIp class and preserve caching behavior
|
||||
- Export PublicIp from package entry (ts/index.ts)
|
||||
- Remove public-ip from plugins/exports and stop using the public-ip package
|
||||
- Bump devDependencies and runtime dependency versions in package.json (@git.zone/tsbuild, @git.zone/tstest, @push.rocks/smartenv, systeminformation)
|
||||
- Improve README: expanded usage, examples, formatting and added emojis for clarity
|
||||
- Add project local settings file (.claude/settings.local.json) for CI/permissions configuration
|
||||
|
||||
## 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
|
||||
|
||||
|
24
package.json
24
package.json
@@ -1,34 +1,32 @@
|
||||
{
|
||||
"name": "@push.rocks/smartnetwork",
|
||||
"version": "4.0.1",
|
||||
"version": "4.2.0",
|
||||
"private": false,
|
||||
"description": "A toolkit for network diagnostics including speed tests, port availability checks, and more.",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"exports": {
|
||||
".": "./dist_ts/index.js"
|
||||
},
|
||||
"type": "module",
|
||||
"author": "Lossless GmbH",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "(tstest test/)",
|
||||
"build": "(tsbuild --web --allowimplicitany)",
|
||||
"test": "(tstest test/ --verbose)",
|
||||
"build": "(tsbuild tsfolders --allowimplicitany)",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.1.61",
|
||||
"@git.zone/tsbuild": "^2.6.8",
|
||||
"@git.zone/tsrun": "^1.2.39",
|
||||
"@git.zone/tstest": "^1.0.69",
|
||||
"@push.rocks/smartenv": "^5.0.0",
|
||||
"@push.rocks/tapbundle": "^5.0.3",
|
||||
"@types/node": "^22.15.3"
|
||||
"@git.zone/tstest": "^2.3.6",
|
||||
"@push.rocks/smartenv": "^5.0.13",
|
||||
"@types/node": "^22.15.19"
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/smartping": "^1.0.7",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartstring": "^4.0.2",
|
||||
"@types/default-gateway": "^7.2.2",
|
||||
"isopen": "^1.3.0",
|
||||
"public-ip": "^7.0.1",
|
||||
"systeminformation": "^5.11.9"
|
||||
"systeminformation": "^5.27.8"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
|
6115
pnpm-lock.yaml
generated
6115
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
|
402
readme.md
402
readme.md
@@ -1,171 +1,389 @@
|
||||
# @push.rocks/smartnetwork
|
||||
# @push.rocks/smartnetwork 🌐
|
||||
|
||||
network diagnostics
|
||||
Comprehensive network diagnostics and utilities for Node.js applications
|
||||
|
||||
## Install
|
||||
## 🚀 Install
|
||||
|
||||
To install `@push.rocks/smartnetwork`, run the following command in your terminal:
|
||||
|
||||
```bash
|
||||
npm install @push.rocks/smartnetwork --save
|
||||
pnpm install @push.rocks/smartnetwork --save
|
||||
```
|
||||
|
||||
### Performing a Traceroute
|
||||
## 🎯 Overview
|
||||
|
||||
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.
|
||||
**@push.rocks/smartnetwork** is your Swiss Army knife for network diagnostics in Node.js. Whether you're building network monitoring tools, implementing health checks, or just need to debug connectivity issues, this library has you covered with a clean, promise-based API.
|
||||
|
||||
```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'}`));
|
||||
```
|
||||
### ✨ Key Features
|
||||
|
||||
This command will download `@push.rocks/smartnetwork` and add it to your project's `package.json` file.
|
||||
- **🏎️ Speed Testing** - Measure download/upload speeds using Cloudflare's infrastructure
|
||||
- **🔌 Port Management** - Check local/remote port availability, find free ports
|
||||
- **📡 Connectivity Testing** - Ping hosts, trace routes, check endpoints
|
||||
- **🌍 DNS Operations** - Resolve A, AAAA, and MX records
|
||||
- **🔍 Network Discovery** - Get network interfaces, gateways, public IPs
|
||||
- **⚡ Performance Caching** - Built-in caching for expensive operations
|
||||
- **🔧 Plugin Architecture** - Extend functionality with custom plugins
|
||||
- **📝 Full TypeScript Support** - Complete type definitions included
|
||||
|
||||
## 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.
|
||||
## 💻 Usage
|
||||
|
||||
### Basic Setup
|
||||
|
||||
First, import the package into your project:
|
||||
First, import and initialize SmartNetwork:
|
||||
|
||||
```typescript
|
||||
import { SmartNetwork } from '@push.rocks/smartnetwork';
|
||||
|
||||
// Basic instance
|
||||
const network = new SmartNetwork();
|
||||
|
||||
// With caching enabled (60 seconds TTL)
|
||||
const cachedNetwork = new SmartNetwork({ cacheTtl: 60000 });
|
||||
```
|
||||
|
||||
Then, create an instance of `SmartNetwork`:
|
||||
### 🏎️ Network Speed Testing
|
||||
|
||||
```typescript
|
||||
const myNetwork = new SmartNetwork();
|
||||
```
|
||||
|
||||
### Performing a Speed Test
|
||||
|
||||
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 your network performance using Cloudflare's global infrastructure:
|
||||
|
||||
```typescript
|
||||
const speedTest = async () => {
|
||||
// Default fixed-segment test
|
||||
let r = await myNetwork.getSpeed();
|
||||
console.log(`Download: ${r.downloadSpeed} Mbps, Upload: ${r.uploadSpeed} Mbps`);
|
||||
// Quick speed test
|
||||
const result = await network.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 configuration
|
||||
const advancedResult = await network.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 port is available on your local machine (checks both IPv4 and IPv6):
|
||||
|
||||
```typescript
|
||||
const checkLocalPort = async (port: number) => {
|
||||
const isUnused = await myNetwork.isLocalPortUnused(port);
|
||||
const isUnused = await network.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 discover available ports:
|
||||
|
||||
```typescript
|
||||
// Using "host:port"
|
||||
await myNetwork.isRemotePortAvailable('example.com:443');
|
||||
const findFreePort = async () => {
|
||||
// Find a free port between 3000 and 3100
|
||||
const freePort = await network.findFreePort(3000, 3100);
|
||||
|
||||
if (freePort) {
|
||||
console.log(`🎉 Found free port: ${freePort}`);
|
||||
} else {
|
||||
console.log('😢 No free ports available in range');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
// Using host + port
|
||||
await myNetwork.isRemotePortAvailable('example.com', 443);
|
||||
#### Check Remote Port Availability
|
||||
|
||||
// UDP is not supported:
|
||||
Test if services are accessible on remote servers:
|
||||
|
||||
```typescript
|
||||
// Method 1: Using "host:port" syntax
|
||||
const isOpen1 = await network.isRemotePortAvailable('example.com:443');
|
||||
|
||||
// Method 2: Using separate host and port
|
||||
const isOpen2 = await network.isRemotePortAvailable('example.com', 443);
|
||||
|
||||
// Method 3: With advanced options
|
||||
const isOpen3 = await network.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
|
||||
try {
|
||||
await myNetwork.isRemotePortAvailable('example.com', { port: 53, protocol: 'udp' });
|
||||
await network.isRemotePortAvailable('example.com', {
|
||||
port: 53,
|
||||
protocol: 'udp'
|
||||
});
|
||||
} catch (e) {
|
||||
console.error((e as any).code); // ENOTSUP
|
||||
console.error('UDP not supported:', e.code); // ENOTSUP
|
||||
}
|
||||
```
|
||||
|
||||
### Using Ping
|
||||
### 📡 Network Connectivity
|
||||
|
||||
The `ping` method sends ICMP echo requests and optionally repeats them to collect statistics.
|
||||
#### Ping Operations
|
||||
|
||||
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 network.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}%`,
|
||||
);
|
||||
// Detailed ping statistics
|
||||
const pingStats = await network.ping('google.com', {
|
||||
count: 5, // Number of pings
|
||||
timeout: 1000 // Timeout per ping in ms
|
||||
});
|
||||
|
||||
console.log(`📊 Ping Statistics:`);
|
||||
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.
|
||||
Analyze network paths hop-by-hop:
|
||||
|
||||
```typescript
|
||||
// Create with cache TTL of 60 seconds
|
||||
const netCached = new SmartNetwork({ cacheTtl: 60000 });
|
||||
const hops = await network.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);
|
||||
console.log('🛤️ Route to destination:');
|
||||
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.*
|
||||
|
||||
To find out your public IPv4 and IPv6 addresses (with caching):
|
||||
### 🌍 DNS Operations
|
||||
|
||||
Resolve various DNS record types:
|
||||
|
||||
```typescript
|
||||
const publicIps = await netCached.getPublicIps();
|
||||
console.log(`Public IPv4: ${publicIps.v4}`);
|
||||
console.log(`Public IPv6: ${publicIps.v6}`);
|
||||
const dnsRecords = await network.resolveDns('example.com');
|
||||
|
||||
console.log('🔍 DNS Records:');
|
||||
console.log(' A records:', dnsRecords.A); // IPv4 addresses
|
||||
console.log(' AAAA records:', dnsRecords.AAAA); // IPv6 addresses
|
||||
|
||||
// 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 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.
|
||||
|
||||
### Plugin Architecture
|
||||
|
||||
You can extend `SmartNetwork` with custom plugins by registering them at runtime:
|
||||
Monitor endpoint availability and response times:
|
||||
|
||||
```typescript
|
||||
import { SmartNetwork } from '@push.rocks/smartnetwork';
|
||||
const health = await network.checkEndpoint('https://api.example.com/health', {
|
||||
timeout: 5000 // Request timeout in ms
|
||||
});
|
||||
|
||||
// Define your plugin class or constructor
|
||||
class MyCustomPlugin {
|
||||
// plugin implementation goes here
|
||||
console.log(`🩺 Endpoint Health:`);
|
||||
console.log(` Status: ${health.status}`);
|
||||
console.log(` RTT: ${health.rtt} ms`);
|
||||
console.log(` Headers:`, health.headers);
|
||||
```
|
||||
|
||||
### 🖥️ Network Interface Information
|
||||
|
||||
#### Get All Network Interfaces
|
||||
|
||||
List all network adapters on the system:
|
||||
|
||||
```typescript
|
||||
const gateways = await network.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 primary network interface:
|
||||
|
||||
```typescript
|
||||
const defaultGateway = await network.getDefaultGateway();
|
||||
|
||||
if (defaultGateway) {
|
||||
console.log('🌐 Default Gateway:');
|
||||
console.log(' IPv4:', defaultGateway.ipv4.address);
|
||||
console.log(' IPv6:', defaultGateway.ipv6.address);
|
||||
}
|
||||
```
|
||||
|
||||
### 🌎 Public IP Discovery
|
||||
|
||||
Discover your public-facing IP addresses:
|
||||
|
||||
```typescript
|
||||
const publicIps = await network.getPublicIps();
|
||||
|
||||
console.log(`🌍 Public IPs:`);
|
||||
console.log(` IPv4: ${publicIps.v4 || 'Not available'}`);
|
||||
console.log(` IPv6: ${publicIps.v6 || 'Not available'}`);
|
||||
```
|
||||
|
||||
### ⚡ Performance Caching
|
||||
|
||||
Reduce network calls with built-in caching:
|
||||
|
||||
```typescript
|
||||
// Create instance with 60-second cache TTL
|
||||
const cachedNetwork = new SmartNetwork({ cacheTtl: 60000 });
|
||||
|
||||
// First call fetches from network
|
||||
const gateways1 = await cachedNetwork.getGateways();
|
||||
const publicIps1 = await cachedNetwork.getPublicIps();
|
||||
|
||||
// Subsequent calls within 60 seconds use cache
|
||||
const gateways2 = await cachedNetwork.getGateways(); // From cache ⚡
|
||||
const publicIps2 = await cachedNetwork.getPublicIps(); // From cache ⚡
|
||||
```
|
||||
|
||||
### 🔧 Plugin Architecture
|
||||
|
||||
Extend SmartNetwork with custom functionality:
|
||||
|
||||
```typescript
|
||||
// Define your plugin
|
||||
class CustomNetworkPlugin {
|
||||
constructor(private smartNetwork: SmartNetwork) {}
|
||||
|
||||
async customMethod() {
|
||||
// Your custom network logic here
|
||||
return 'Custom result';
|
||||
}
|
||||
}
|
||||
|
||||
// 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 PluginClass = SmartNetwork.pluginsRegistry.get('customPlugin');
|
||||
const plugin = new PluginClass(network);
|
||||
await plugin.customMethod();
|
||||
|
||||
// Clean up when done
|
||||
SmartNetwork.unregisterPlugin('customPlugin');
|
||||
```
|
||||
|
||||
Plugins enable you to dynamically augment the core functionality without altering the library's source.
|
||||
### 🚨 Error Handling
|
||||
|
||||
Handle network errors gracefully with custom error types:
|
||||
|
||||
```typescript
|
||||
import { NetworkError } from '@push.rocks/smartnetwork';
|
||||
|
||||
try {
|
||||
await network.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 comprehensive type definitions:
|
||||
|
||||
```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
|
||||
}
|
||||
|
||||
// ... and many more types for complete type safety
|
||||
```
|
||||
|
||||
## 🛠️ Advanced Examples
|
||||
|
||||
### Building a Network Monitor
|
||||
|
||||
```typescript
|
||||
const monitorNetwork = async () => {
|
||||
const network = new SmartNetwork({ cacheTtl: 30000 });
|
||||
|
||||
// Check critical services
|
||||
const services = [
|
||||
{ name: 'Web Server', host: 'example.com', port: 443 },
|
||||
{ name: 'Database', host: 'db.internal', port: 5432 },
|
||||
{ name: 'Cache', host: 'redis.internal', port: 6379 }
|
||||
];
|
||||
|
||||
for (const service of services) {
|
||||
const isUp = await network.isRemotePortAvailable(service.host, service.port);
|
||||
console.log(`${service.name}: ${isUp ? '✅ UP' : '❌ DOWN'}`);
|
||||
}
|
||||
|
||||
// Check internet connectivity
|
||||
const ping = await network.ping('8.8.8.8');
|
||||
console.log(`Internet: ${ping.alive ? '✅ Connected' : '❌ Disconnected'}`);
|
||||
|
||||
// Measure network performance
|
||||
const speed = await network.getSpeed();
|
||||
console.log(`Speed: ⬇️ ${speed.downloadSpeed} Mbps / ⬆️ ${speed.uploadSpeed} Mbps`);
|
||||
};
|
||||
|
||||
// Run monitor every minute
|
||||
setInterval(monitorNetwork, 60000);
|
||||
```
|
||||
|
||||
### Service Discovery
|
||||
|
||||
```typescript
|
||||
const discoverServices = async () => {
|
||||
const network = new SmartNetwork();
|
||||
const commonPorts = [22, 80, 443, 3000, 3306, 5432, 6379, 8080, 9200];
|
||||
|
||||
console.log('🔍 Scanning local services...');
|
||||
|
||||
for (const port of commonPorts) {
|
||||
const isUsed = !(await network.isLocalPortUnused(port));
|
||||
if (isUsed) {
|
||||
console.log(` Port ${port}: In use (possible service running)`);
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 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 +398,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.
|
@@ -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();
|
||||
|
392
test/test.ports.ts
Normal file
392
test/test.ports.ts
Normal file
@@ -0,0 +1,392 @@
|
||||
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';
|
||||
|
||||
// Helper to create a server on a specific port
|
||||
const createServerOnPort = async (port: number): Promise<net.Server> => {
|
||||
const server = net.createServer();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(port, () => {
|
||||
server.removeListener('error', reject);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return server;
|
||||
};
|
||||
|
||||
// Helper to clean up servers
|
||||
const cleanupServers = async (servers: net.Server[]): Promise<void> => {
|
||||
await Promise.all(servers.map(s => new Promise<void>((res) => s.close(() => res()))));
|
||||
};
|
||||
|
||||
// ========= isLocalPortUnused Tests =========
|
||||
|
||||
tap.test('isLocalPortUnused - should detect free port correctly', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
// Port 0 lets the OS assign a free port, we'll use a high range instead
|
||||
const result = await sn.isLocalPortUnused(54321);
|
||||
expect(typeof result).toEqual('boolean');
|
||||
// Most likely this high port is free, but we can't guarantee it
|
||||
});
|
||||
|
||||
tap.test('isLocalPortUnused - should detect occupied port', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const server = net.createServer();
|
||||
await new Promise<void>((res) => server.listen(0, res));
|
||||
const addr = server.address() as AddressInfo;
|
||||
|
||||
const isUnused = await sn.isLocalPortUnused(addr.port);
|
||||
expect(isUnused).toBeFalse();
|
||||
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
});
|
||||
|
||||
tap.test('isLocalPortUnused - should handle multiple simultaneous checks', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const ports = [55001, 55002, 55003, 55004, 55005];
|
||||
|
||||
// Check all ports simultaneously
|
||||
const results = await Promise.all(
|
||||
ports.map(port => sn.isLocalPortUnused(port))
|
||||
);
|
||||
|
||||
// All should likely be free
|
||||
results.forEach(result => {
|
||||
expect(typeof result).toEqual('boolean');
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('isLocalPortUnused - should work with IPv6 loopback', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const server = net.createServer();
|
||||
|
||||
// Explicitly bind to IPv6
|
||||
await new Promise<void>((res) => server.listen(55100, '::', res));
|
||||
const addr = server.address() as AddressInfo;
|
||||
|
||||
const isUnused = await sn.isLocalPortUnused(addr.port);
|
||||
expect(isUnused).toBeFalse();
|
||||
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
});
|
||||
|
||||
tap.test('isLocalPortUnused - boundary port numbers', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Test port 1 (usually requires root)
|
||||
const port1Result = await sn.isLocalPortUnused(1);
|
||||
expect(typeof port1Result).toEqual('boolean');
|
||||
|
||||
// Test port 65535
|
||||
const port65535Result = await sn.isLocalPortUnused(65535);
|
||||
expect(typeof port65535Result).toEqual('boolean');
|
||||
});
|
||||
|
||||
// ========= findFreePort Tests =========
|
||||
|
||||
tap.test('findFreePort - should find free port in small range', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const freePort = await sn.findFreePort(50000, 50010);
|
||||
|
||||
expect(freePort).not.toBeNull();
|
||||
expect(freePort).toBeGreaterThanOrEqual(50000);
|
||||
expect(freePort).toBeLessThanOrEqual(50010);
|
||||
|
||||
// Verify the port is actually free
|
||||
if (freePort !== null) {
|
||||
const isUnused = await sn.isLocalPortUnused(freePort);
|
||||
expect(isUnused).toBeTrue();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('findFreePort - should find first available port', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const servers = [];
|
||||
|
||||
// Occupy ports 50100 and 50101
|
||||
servers.push(await createServerOnPort(50100));
|
||||
servers.push(await createServerOnPort(50101));
|
||||
|
||||
// Port 50102 should be free
|
||||
const freePort = await sn.findFreePort(50100, 50105);
|
||||
expect(freePort).toEqual(50102);
|
||||
|
||||
await cleanupServers(servers);
|
||||
});
|
||||
|
||||
tap.test('findFreePort - should handle fully occupied range', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const servers = [];
|
||||
const startPort = 50200;
|
||||
const endPort = 50202;
|
||||
|
||||
// Occupy all ports in range
|
||||
for (let port = startPort; port <= endPort; port++) {
|
||||
servers.push(await createServerOnPort(port));
|
||||
}
|
||||
|
||||
const freePort = await sn.findFreePort(startPort, endPort);
|
||||
expect(freePort).toBeNull();
|
||||
|
||||
await cleanupServers(servers);
|
||||
});
|
||||
|
||||
tap.test('findFreePort - should validate port boundaries', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Test port < 1
|
||||
try {
|
||||
await sn.findFreePort(0, 100);
|
||||
throw new Error('Should have thrown for port < 1');
|
||||
} catch (err: any) {
|
||||
expect(err).toBeInstanceOf(NetworkError);
|
||||
expect(err.code).toEqual('EINVAL');
|
||||
expect(err.message).toContain('between 1 and 65535');
|
||||
}
|
||||
|
||||
// Test port > 65535
|
||||
try {
|
||||
await sn.findFreePort(100, 70000);
|
||||
throw new Error('Should have thrown for port > 65535');
|
||||
} catch (err: any) {
|
||||
expect(err).toBeInstanceOf(NetworkError);
|
||||
expect(err.code).toEqual('EINVAL');
|
||||
}
|
||||
|
||||
// Test negative ports
|
||||
try {
|
||||
await sn.findFreePort(-100, 100);
|
||||
throw new Error('Should have thrown for negative port');
|
||||
} catch (err: any) {
|
||||
expect(err).toBeInstanceOf(NetworkError);
|
||||
expect(err.code).toEqual('EINVAL');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('findFreePort - should validate range order', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
try {
|
||||
await sn.findFreePort(200, 100);
|
||||
throw new Error('Should have thrown for startPort > endPort');
|
||||
} catch (err: any) {
|
||||
expect(err).toBeInstanceOf(NetworkError);
|
||||
expect(err.code).toEqual('EINVAL');
|
||||
expect(err.message).toContain('less than or equal to end port');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('findFreePort - should handle single port range', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Test when start and end are the same
|
||||
const freePort = await sn.findFreePort(50300, 50300);
|
||||
// Should either be 50300 or null
|
||||
expect(freePort === 50300 || freePort === null).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('findFreePort - should work with large ranges', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Test with a large range
|
||||
const freePort = await sn.findFreePort(40000, 50000);
|
||||
expect(freePort).not.toBeNull();
|
||||
expect(freePort).toBeGreaterThanOrEqual(40000);
|
||||
expect(freePort).toBeLessThanOrEqual(50000);
|
||||
});
|
||||
|
||||
tap.test('findFreePort - should handle intermittent occupied ports', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const servers = [];
|
||||
|
||||
// Occupy every other port
|
||||
servers.push(await createServerOnPort(50400));
|
||||
servers.push(await createServerOnPort(50402));
|
||||
servers.push(await createServerOnPort(50404));
|
||||
|
||||
// Should find 50401, 50403, or 50405
|
||||
const freePort = await sn.findFreePort(50400, 50405);
|
||||
expect([50401, 50403, 50405]).toContain(freePort);
|
||||
|
||||
await cleanupServers(servers);
|
||||
});
|
||||
|
||||
// ========= isRemotePortAvailable Tests =========
|
||||
|
||||
tap.test('isRemotePortAvailable - should detect open HTTP port', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Test with string format
|
||||
const open1 = await sn.isRemotePortAvailable('example.com:80');
|
||||
expect(open1).toBeTrue();
|
||||
|
||||
// Test with separate parameters
|
||||
const open2 = await sn.isRemotePortAvailable('example.com', 80);
|
||||
expect(open2).toBeTrue();
|
||||
|
||||
// Test with options object
|
||||
const open3 = await sn.isRemotePortAvailable('example.com', { port: 80 });
|
||||
expect(open3).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('isRemotePortAvailable - should detect closed port', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Port 12345 is likely closed on example.com
|
||||
const closed = await sn.isRemotePortAvailable('example.com', 12345);
|
||||
expect(closed).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('isRemotePortAvailable - should handle retries', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Test with retries
|
||||
const result = await sn.isRemotePortAvailable('example.com', {
|
||||
port: 80,
|
||||
retries: 3,
|
||||
timeout: 1000
|
||||
});
|
||||
expect(result).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('isRemotePortAvailable - should reject UDP protocol', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
try {
|
||||
await sn.isRemotePortAvailable('example.com', {
|
||||
port: 53,
|
||||
protocol: 'udp'
|
||||
});
|
||||
throw new Error('Should have thrown for UDP protocol');
|
||||
} catch (err: any) {
|
||||
expect(err).toBeInstanceOf(NetworkError);
|
||||
expect(err.code).toEqual('ENOTSUP');
|
||||
expect(err.message).toContain('UDP port check not supported');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('isRemotePortAvailable - should require port specification', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
try {
|
||||
await sn.isRemotePortAvailable('example.com');
|
||||
throw new Error('Should have thrown for missing port');
|
||||
} catch (err: any) {
|
||||
expect(err).toBeInstanceOf(NetworkError);
|
||||
expect(err.code).toEqual('EINVAL');
|
||||
expect(err.message).toContain('Port not specified');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('isRemotePortAvailable - should parse port from host:port string', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Valid formats
|
||||
const result1 = await sn.isRemotePortAvailable('example.com:443');
|
||||
expect(result1).toBeTrue();
|
||||
|
||||
// With options overriding the string port
|
||||
const result2 = await sn.isRemotePortAvailable('example.com:8080', { port: 80 });
|
||||
expect(result2).toBeTrue(); // Should use port 80 from options, not 8080
|
||||
});
|
||||
|
||||
tap.test('isRemotePortAvailable - should handle localhost', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const server = net.createServer();
|
||||
|
||||
// Start a local server
|
||||
await new Promise<void>((res) => server.listen(51000, 'localhost', res));
|
||||
|
||||
// Should detect it as open
|
||||
const isOpen = await sn.isRemotePortAvailable('localhost', 51000);
|
||||
expect(isOpen).toBeTrue();
|
||||
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
|
||||
// After closing, might still show as open due to TIME_WAIT, or closed
|
||||
// We won't assert on this as it's OS-dependent
|
||||
});
|
||||
|
||||
tap.test('isRemotePortAvailable - should handle invalid hosts gracefully', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Non-existent domain
|
||||
const result = await sn.isRemotePortAvailable('this-domain-definitely-does-not-exist-12345.com', 80);
|
||||
expect(result).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('isRemotePortAvailable - edge case ports', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Test HTTPS port
|
||||
const https = await sn.isRemotePortAvailable('example.com', 443);
|
||||
expect(https).toBeTrue();
|
||||
|
||||
// Test SSH port (likely closed on example.com)
|
||||
const ssh = await sn.isRemotePortAvailable('example.com', 22);
|
||||
expect(ssh).toBeFalse();
|
||||
});
|
||||
|
||||
// ========= Integration Tests =========
|
||||
|
||||
tap.test('Integration - findFreePort and isLocalPortUnused consistency', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Find a free port
|
||||
const freePort = await sn.findFreePort(52000, 52100);
|
||||
expect(freePort).not.toBeNull();
|
||||
|
||||
if (freePort !== null) {
|
||||
// Verify it's actually free
|
||||
const isUnused1 = await sn.isLocalPortUnused(freePort);
|
||||
expect(isUnused1).toBeTrue();
|
||||
|
||||
// Start a server on it
|
||||
const server = await createServerOnPort(freePort);
|
||||
|
||||
// Now it should be in use
|
||||
const isUnused2 = await sn.isLocalPortUnused(freePort);
|
||||
expect(isUnused2).toBeFalse();
|
||||
|
||||
// findFreePort should skip it
|
||||
const nextFreePort = await sn.findFreePort(freePort, freePort + 10);
|
||||
expect(nextFreePort).not.toEqual(freePort);
|
||||
|
||||
await cleanupServers([server]);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Integration - stress test with many concurrent port checks', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const portRange = Array.from({ length: 20 }, (_, i) => 53000 + i);
|
||||
|
||||
// Check all ports concurrently
|
||||
const results = await Promise.all(
|
||||
portRange.map(async port => ({
|
||||
port,
|
||||
isUnused: await sn.isLocalPortUnused(port)
|
||||
}))
|
||||
);
|
||||
|
||||
// All operations should complete without error
|
||||
results.forEach(result => {
|
||||
expect(typeof result.isUnused).toEqual('boolean');
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('Performance - findFreePort with large range', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const startTime = Date.now();
|
||||
|
||||
// This should be fast even with a large range
|
||||
const freePort = await sn.findFreePort(30000, 60000);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
expect(freePort).not.toBeNull();
|
||||
// Should complete quickly (within 100ms) as it should find a port early
|
||||
expect(duration).toBeLessThan(100);
|
||||
});
|
||||
|
||||
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.1',
|
||||
version: '4.2.0',
|
||||
description: 'A toolkit for network diagnostics including speed tests, port availability checks, and more.'
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
export * from './smartnetwork.classes.smartnetwork.js';
|
||||
export type { SmartNetworkOptions, Hop } from './smartnetwork.classes.smartnetwork.js';
|
||||
export { PublicIp } from './smartnetwork.classes.publicip.js';
|
||||
export { setLogger, getLogger } from './logging.js';
|
||||
export { NetworkError, TimeoutError } from './errors.js';
|
||||
|
163
ts/smartnetwork.classes.publicip.ts
Normal file
163
ts/smartnetwork.classes.publicip.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { getLogger } from './logging.js';
|
||||
|
||||
/**
|
||||
* Service configuration for IP detection
|
||||
*/
|
||||
interface IpService {
|
||||
name: string;
|
||||
v4Url?: string;
|
||||
v6Url?: string;
|
||||
parseResponse?: (text: string) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* PublicIp class for detecting public IPv4 and IPv6 addresses
|
||||
* Uses multiple fallback services for reliability
|
||||
*/
|
||||
export class PublicIp {
|
||||
private readonly services: IpService[] = [
|
||||
{
|
||||
name: 'ipify',
|
||||
v4Url: 'https://api.ipify.org?format=text',
|
||||
v6Url: 'https://api6.ipify.org?format=text',
|
||||
},
|
||||
{
|
||||
name: 'ident.me',
|
||||
v4Url: 'https://v4.ident.me',
|
||||
v6Url: 'https://v6.ident.me',
|
||||
},
|
||||
{
|
||||
name: 'seeip',
|
||||
v4Url: 'https://ipv4.seeip.org',
|
||||
v6Url: 'https://ipv6.seeip.org',
|
||||
},
|
||||
{
|
||||
name: 'icanhazip',
|
||||
v4Url: 'https://ipv4.icanhazip.com',
|
||||
v6Url: 'https://ipv6.icanhazip.com',
|
||||
},
|
||||
];
|
||||
|
||||
private readonly timeout: number;
|
||||
private readonly logger = getLogger();
|
||||
|
||||
constructor(options?: { timeout?: number }) {
|
||||
this.timeout = options?.timeout ?? 2000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public IPv4 address
|
||||
*/
|
||||
public async getPublicIpv4(): Promise<string | null> {
|
||||
for (const service of this.services) {
|
||||
if (!service.v4Url) continue;
|
||||
|
||||
try {
|
||||
const ip = await this.fetchIpFromService(service.v4Url, service.parseResponse);
|
||||
if (this.isValidIpv4(ip)) {
|
||||
this.logger.info?.(`Got IPv4 from ${service.name}: ${ip}`);
|
||||
return ip;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.debug?.(`Failed to get IPv4 from ${service.name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.warn?.('Could not determine public IPv4 address from any service');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public IPv6 address
|
||||
*/
|
||||
public async getPublicIpv6(): Promise<string | null> {
|
||||
for (const service of this.services) {
|
||||
if (!service.v6Url) continue;
|
||||
|
||||
try {
|
||||
const ip = await this.fetchIpFromService(service.v6Url, service.parseResponse);
|
||||
if (this.isValidIpv6(ip)) {
|
||||
this.logger.info?.(`Got IPv6 from ${service.name}: ${ip}`);
|
||||
return ip;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.debug?.(`Failed to get IPv6 from ${service.name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.warn?.('Could not determine public IPv6 address from any service');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get both IPv4 and IPv6 addresses
|
||||
*/
|
||||
public async getPublicIps(): Promise<{ v4: string | null; v6: string | null }> {
|
||||
const [v4, v6] = await Promise.all([
|
||||
this.getPublicIpv4(),
|
||||
this.getPublicIpv6(),
|
||||
]);
|
||||
|
||||
return { v4, v6 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch IP from a service URL
|
||||
*/
|
||||
private async fetchIpFromService(
|
||||
url: string,
|
||||
parseResponse?: (text: string) => string
|
||||
): Promise<string> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'User-Agent': '@push.rocks/smartnetwork',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
const ip = parseResponse ? parseResponse(text) : text.trim();
|
||||
|
||||
return ip;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate IPv4 address format
|
||||
*/
|
||||
private isValidIpv4(ip: string): boolean {
|
||||
if (!ip) return false;
|
||||
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
if (!ipv4Regex.test(ip)) return false;
|
||||
|
||||
const parts = ip.split('.');
|
||||
return parts.every(part => {
|
||||
const num = parseInt(part, 10);
|
||||
return num >= 0 && num <= 255;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate IPv6 address format
|
||||
*/
|
||||
private isValidIpv6(ip: string): boolean {
|
||||
if (!ip) return false;
|
||||
|
||||
// Simplified IPv6 validation - checks for colon-separated hex values
|
||||
const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
|
||||
const ipv6CompressedRegex = /^::([0-9a-fA-F]{0,4}:){0,6}[0-9a-fA-F]{0,4}$|^([0-9a-fA-F]{0,4}:){1,7}:$/;
|
||||
|
||||
return ipv6Regex.test(ip) || ipv6CompressedRegex.test(ip);
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
import * as plugins from './smartnetwork.plugins.js';
|
||||
import { CloudflareSpeed } from './smartnetwork.classes.cloudflarespeed.js';
|
||||
import { PublicIp } from './smartnetwork.classes.publicip.js';
|
||||
import { getLogger } from './logging.js';
|
||||
import { NetworkError } from './errors.js';
|
||||
import * as stats from './helpers/stats.js';
|
||||
@@ -143,6 +144,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
|
||||
@@ -232,10 +260,10 @@ export class SmartNetwork {
|
||||
* Lookup public IPv4 and IPv6
|
||||
*/
|
||||
public async getPublicIps(): Promise<{ v4: string | null; v6: string | null }> {
|
||||
const fetcher = async () => ({
|
||||
v4: await plugins.publicIp.publicIpv4({ timeout: 1000, onlyHttps: true }).catch(() => null),
|
||||
v6: await plugins.publicIp.publicIpv6({ timeout: 1000, onlyHttps: true }).catch(() => null),
|
||||
});
|
||||
const fetcher = async () => {
|
||||
const publicIp = new PublicIp({ timeout: 1000 });
|
||||
return publicIp.getPublicIps();
|
||||
};
|
||||
if (this.options.cacheTtl && this.options.cacheTtl > 0) {
|
||||
return this.getCached('publicIps', fetcher);
|
||||
}
|
||||
|
@@ -15,8 +15,6 @@ export { smartpromise, smartping, smartstring };
|
||||
// @third party scope
|
||||
// @ts-ignore
|
||||
import isopen from 'isopen';
|
||||
// @ts-ignore
|
||||
import * as publicIp from 'public-ip';
|
||||
import * as systeminformation from 'systeminformation';
|
||||
|
||||
export { isopen, publicIp, systeminformation };
|
||||
export { isopen, systeminformation };
|
||||
|
Reference in New Issue
Block a user