Compare commits

..

3 Commits

Author SHA1 Message Date
e70e5ac15c 4.2.0
Some checks failed
Default (tags) / security (push) Successful in 48s
Default (tags) / test (push) Failing after 1h14m13s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-09-12 13:30:09 +00:00
806606c9b9 feat(PublicIp): Add PublicIp service and refactor SmartNetwork to use it; remove public-ip dependency; update exports, docs and dependencies 2025-09-12 13:30:09 +00:00
ac3b501adf test(ports): add comprehensive test suite for port management functionality 2025-08-01 15:20:41 +00:00
10 changed files with 3189 additions and 2145 deletions

View File

@@ -1,5 +1,16 @@
# 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

View File

@@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartnetwork",
"version": "4.1.2",
"version": "4.2.0",
"private": false,
"description": "A toolkit for network diagnostics including speed tests, port availability checks, and more.",
"exports": {
@@ -15,20 +15,18 @@
"buildDocs": "tsdoc"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.5.1",
"@git.zone/tsbuild": "^2.6.8",
"@git.zone/tsrun": "^1.2.39",
"@git.zone/tstest": "^1.9.0",
"@push.rocks/smartenv": "^5.0.0",
"@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/**/*",

4476
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

264
readme.md
View File

@@ -1,49 +1,59 @@
# @push.rocks/smartnetwork
# @push.rocks/smartnetwork 🌐
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
```
## Usage
## 🎯 Overview
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.
**@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.
### ✨ Key Features
- **🏎️ 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
### 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();
// Or with caching enabled (60 seconds TTL)
const myNetworkCached = new SmartNetwork({ cacheTtl: 60000 });
```
### Network Speed Testing
Measure network download and upload speeds using Cloudflare's speed test infrastructure:
Measure your network performance using Cloudflare's global infrastructure:
```typescript
const speedTest = async () => {
// Basic speed test
const result = await myNetwork.getSpeed();
// Quick speed test
const result = await network.getSpeed();
console.log(`Download: ${result.downloadSpeed} Mbps`);
console.log(`Upload: ${result.uploadSpeed} Mbps`);
// Advanced speed test with options
const advancedResult = await myNetwork.getSpeed({
// Advanced configuration
const advancedResult = await network.getSpeed({
parallelStreams: 3, // Number of concurrent connections
duration: 5 // Test duration in seconds
});
@@ -52,19 +62,19 @@ const speedTest = async () => {
};
```
### Port Management
### 🔌 Port Management
#### Check Local Port Availability
Verify if a specific port is available on your local machine (checks both IPv4 and IPv6):
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`);
}
};
@@ -73,136 +83,139 @@ await checkLocalPort(8080);
#### Find Free Port in Range
Automatically find the first available port within a specified range:
Automatically discover available ports:
```typescript
const findFreePort = async () => {
// Find a free port between 3000 and 3100
const freePort = await myNetwork.findFreePort(3000, 3100);
const freePort = await network.findFreePort(3000, 3100);
if (freePort) {
console.log(`Found free port: ${freePort}`);
console.log(`🎉 Found free port: ${freePort}`);
} else {
console.log('No free ports available in the specified range');
console.log('😢 No free ports available in range');
}
};
```
#### Check Remote Port Availability
Verify if a port is open on a remote server:
Test if services are accessible on remote servers:
```typescript
// Method 1: Using "host:port" syntax
const isOpen1 = await myNetwork.isRemotePortAvailable('example.com:443');
const isOpen1 = await network.isRemotePortAvailable('example.com:443');
// Method 2: Using separate host and port
const isOpen2 = await myNetwork.isRemotePortAvailable('example.com', 443);
const isOpen2 = await network.isRemotePortAvailable('example.com', 443);
// Method 3: With options (retries, timeout)
const isOpen3 = await myNetwork.isRemotePortAvailable('example.com', {
// 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 and will throw an error
// Note: UDP is not supported
try {
await myNetwork.isRemotePortAvailable('example.com', {
await network.isRemotePortAvailable('example.com', {
port: 53,
protocol: 'udp'
});
} catch (e) {
console.error(e.code); // ENOTSUP
console.error('UDP not supported:', e.code); // ENOTSUP
}
```
### Network Connectivity
### 📡 Network Connectivity
#### Ping Operations
Send ICMP echo requests to test connectivity and measure latency:
Test connectivity and measure latency:
```typescript
// Simple ping
const pingResult = await myNetwork.ping('google.com');
const pingResult = await network.ping('google.com');
console.log(`Host alive: ${pingResult.alive}`);
console.log(`RTT: ${pingResult.time} ms`);
// Ping with statistics (multiple pings)
const pingStats = await myNetwork.ping('google.com', {
// Detailed ping statistics
const pingStats = await network.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`);
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`);
```
#### Traceroute
Perform hop-by-hop network path analysis:
Analyze network paths hop-by-hop:
```typescript
const hops = await myNetwork.traceroute('google.com', {
const hops = await network.traceroute('google.com', {
maxHops: 10, // Maximum number of hops
timeout: 5000 // Timeout in ms
});
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}`);
console.log(` ${hop.ttl}\t${hop.ip}\t${rtt}`);
});
```
Note: Falls back to a single-hop stub if the `traceroute` binary is unavailable on the system.
*Note: Falls back to a single-hop stub if the `traceroute` binary is unavailable.*
### DNS Operations
### 🌍 DNS Operations
Resolve DNS records for a hostname:
Resolve various DNS record types:
```typescript
const dnsRecords = await myNetwork.resolveDns('example.com');
const dnsRecords = await network.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
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})`);
console.log(` 📧 Mail server: ${mx.exchange} (priority: ${mx.priority})`);
});
```
### HTTP/HTTPS Endpoint Health Checks
### 🏥 HTTP/HTTPS Health Checks
Check the health and response time of HTTP/HTTPS endpoints:
Monitor endpoint availability and response times:
```typescript
const health = await myNetwork.checkEndpoint('https://example.com', {
const health = await network.checkEndpoint('https://api.example.com/health', {
timeout: 5000 // Request timeout in ms
});
console.log(`Status: ${health.status}`);
console.log(`RTT: ${health.rtt} ms`);
console.log('Headers:', health.headers);
console.log(`🩺 Endpoint Health:`);
console.log(` Status: ${health.status}`);
console.log(` RTT: ${health.rtt} ms`);
console.log(` Headers:`, health.headers);
```
### Network Interface Information
### 🖥️ Network Interface Information
#### Get Network Gateways
#### Get All Network Interfaces
List all network interfaces on the system:
List all network adapters on the system:
```typescript
const gateways = await myNetwork.getGateways();
const gateways = await network.getGateways();
Object.entries(gateways).forEach(([name, interfaces]) => {
console.log(`Interface: ${name}`);
console.log(`🔌 Interface: ${name}`);
interfaces.forEach(iface => {
console.log(` ${iface.family}: ${iface.address}`);
console.log(` Netmask: ${iface.netmask}`);
@@ -213,48 +226,50 @@ Object.entries(gateways).forEach(([name, interfaces]) => {
#### Get Default Gateway
Retrieve the system's default network gateway:
Retrieve the primary network interface:
```typescript
const defaultGateway = await myNetwork.getDefaultGateway();
const defaultGateway = await network.getDefaultGateway();
if (defaultGateway) {
console.log('IPv4 Gateway:', defaultGateway.ipv4.address);
console.log('IPv6 Gateway:', defaultGateway.ipv6.address);
console.log('🌐 Default Gateway:');
console.log(' IPv4:', defaultGateway.ipv4.address);
console.log(' IPv6:', defaultGateway.ipv6.address);
}
```
### Public IP Discovery
### 🌎 Public IP Discovery
Discover your public IPv4 and IPv6 addresses:
Discover your public-facing IP addresses:
```typescript
const publicIps = await myNetwork.getPublicIps();
const publicIps = await network.getPublicIps();
console.log(`Public IPv4: ${publicIps.v4 || 'Not available'}`);
console.log(`Public IPv6: ${publicIps.v6 || 'Not available'}`);
console.log(`🌍 Public IPs:`);
console.log(` IPv4: ${publicIps.v4 || 'Not available'}`);
console.log(` IPv6: ${publicIps.v6 || 'Not available'}`);
```
### Caching
### ⚡ Performance Caching
SmartNetwork supports caching for gateway and public IP lookups to reduce repeated network calls:
Reduce network calls with built-in caching:
```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
// First call fetches from network
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
// Subsequent calls within 60 seconds use cache
const gateways2 = await cachedNetwork.getGateways(); // From cache
const publicIps2 = await cachedNetwork.getPublicIps(); // From cache
```
### Plugin Architecture
### 🔧 Plugin Architecture
Extend SmartNetwork's functionality with custom plugins:
Extend SmartNetwork with custom functionality:
```typescript
// Define your plugin
@@ -262,7 +277,8 @@ class CustomNetworkPlugin {
constructor(private smartNetwork: SmartNetwork) {}
async customMethod() {
// Your custom network functionality
// Your custom network logic here
return 'Custom result';
}
}
@@ -271,33 +287,34 @@ SmartNetwork.registerPlugin('customPlugin', CustomNetworkPlugin);
// Use the plugin
const network = new SmartNetwork();
const plugin = new (SmartNetwork.pluginsRegistry.get('customPlugin'))(network);
const PluginClass = SmartNetwork.pluginsRegistry.get('customPlugin');
const plugin = new PluginClass(network);
await plugin.customMethod();
// Unregister when no longer needed
// Clean up when done
SmartNetwork.unregisterPlugin('customPlugin');
```
### Error Handling
### 🚨 Error Handling
The package uses custom `NetworkError` class for network-related errors:
Handle network errors gracefully with custom error types:
```typescript
import { NetworkError } from '@push.rocks/smartnetwork';
try {
await myNetwork.isRemotePortAvailable('example.com', { protocol: 'udp' });
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}`);
console.error(`Network error: ${error.message}`);
console.error(` Error code: ${error.code}`);
}
}
```
### TypeScript Support
### 📚 TypeScript Support
This package is written in TypeScript and provides full type definitions. Key interfaces include:
This package is written in TypeScript and provides comprehensive type definitions:
```typescript
interface SmartNetworkOptions {
@@ -309,6 +326,59 @@ interface Hop {
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

392
test/test.ports.ts Normal file
View 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();

View File

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

View File

@@ -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';

View 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);
}
}

View File

@@ -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';
@@ -259,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);
}

View File

@@ -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 };