feat(PublicIp): Add PublicIp service and refactor SmartNetwork to use it; remove public-ip dependency; update exports, docs and dependencies
This commit is contained in:
11
changelog.md
11
changelog.md
@@ -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
|
||||
|
||||
|
10
package.json
10
package.json
@@ -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
4476
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
264
readme.md
264
readme.md
@@ -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
|
||||
|
@@ -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.'
|
||||
}
|
||||
|
@@ -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';
|
||||
@@ -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);
|
||||
}
|
||||
|
@@ -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