BREAKING CHANGE(smartnetwork): Enhance documentation and add configurable speed test options with plugin architecture improvements
This commit is contained in:
parent
d6be2e27b0
commit
26e1d5142a
@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-04-28 - 4.0.0 - BREAKING CHANGE(smartnetwork)
|
||||||
|
Enhance documentation and add configurable speed test options with plugin architecture improvements
|
||||||
|
|
||||||
|
- Expanded README with detailed examples for traceroute, speed test, ping, and remote port checks
|
||||||
|
- Added optional parameters for getSpeed (parallelStreams and duration) to allow configurable testing modes
|
||||||
|
- Included plugin architecture usage examples to show runtime registration and unregistration of plugins
|
||||||
|
- Updated test suite to cover DNS resolution, endpoint health-check, caching behavior, and various network diagnostics
|
||||||
|
- Removed legacy planning documentation from readme.plan.md
|
||||||
|
|
||||||
## 2025-04-28 - 3.0.5 - fix(core)
|
## 2025-04-28 - 3.0.5 - fix(core)
|
||||||
Improve logging and error handling by introducing custom error classes and a global logging interface while refactoring network diagnostics methods.
|
Improve logging and error handling by introducing custom error classes and a global logging interface while refactoring network diagnostics methods.
|
||||||
|
|
||||||
|
111
readme.md
111
readme.md
@ -10,6 +10,17 @@ To install `@push.rocks/smartnetwork`, run the following command in your termina
|
|||||||
npm install @push.rocks/smartnetwork --save
|
npm install @push.rocks/smartnetwork --save
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Performing a Traceroute
|
||||||
|
|
||||||
|
You can perform a hop-by-hop traceroute to measure latency per hop. Falls back to a single-hop stub if the `traceroute` binary is unavailable.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const hops = await myNetwork.traceroute('google.com', { maxHops: 10, timeout: 5000 });
|
||||||
|
hops.forEach(h =>
|
||||||
|
console.log(`${h.ttl}\t${h.ip}\t${h.rtt === null ? '*' : h.rtt + ' ms'}`),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
This command will download `@push.rocks/smartnetwork` and add it to your project's `package.json` file.
|
This command will download `@push.rocks/smartnetwork` and add it to your project's `package.json` file.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@ -32,14 +43,19 @@ const myNetwork = new SmartNetwork();
|
|||||||
|
|
||||||
### Performing a Speed Test
|
### Performing a Speed Test
|
||||||
|
|
||||||
You can measure the network speed using the `getSpeed` method. This feature leverages Cloudflare's speed test capabilities to assess your internet connection's download and upload speeds.
|
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)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const speedTest = async () => {
|
const speedTest = async () => {
|
||||||
const speedResult = await myNetwork.getSpeed();
|
// Default fixed-segment test
|
||||||
console.log(`Download speed: ${speedResult.downloadSpeed} Mbps`);
|
let r = await myNetwork.getSpeed();
|
||||||
console.log(`Upload speed: ${speedResult.uploadSpeed} Mbps`);
|
console.log(`Download: ${r.downloadSpeed} Mbps, Upload: ${r.uploadSpeed} Mbps`);
|
||||||
console.log(`Latency: ${speedResult.averageTime} ms`);
|
|
||||||
|
// Parallel + duration-based test
|
||||||
|
r = await myNetwork.getSpeed({ parallelStreams: 3, duration: 5 });
|
||||||
|
console.log(`Download: ${r.downloadSpeed} Mbps, Upload: ${r.uploadSpeed} Mbps`);
|
||||||
};
|
};
|
||||||
|
|
||||||
speedTest();
|
speedTest();
|
||||||
@ -64,71 +80,88 @@ checkLocalPort(8080); // Example port number
|
|||||||
|
|
||||||
### Checking Remote Port Availability
|
### Checking Remote Port Availability
|
||||||
|
|
||||||
To verify if a certain port is available on a remote server, use `isRemotePortAvailable`. This can help determine if a service is up and reachable.
|
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.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const checkRemotePort = async (hostname: string, port: number) => {
|
// Using "host:port"
|
||||||
const isAvailable = await myNetwork.isRemotePortAvailable(hostname, port);
|
await myNetwork.isRemotePortAvailable('example.com:443');
|
||||||
if (isAvailable) {
|
|
||||||
console.log(`Port ${port} on ${hostname} is available.`);
|
|
||||||
} else {
|
|
||||||
console.log(`Port ${port} on ${hostname} is not available.`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkRemotePort('example.com', 443); // Checking HTTPS port on example.com
|
// Using host + port
|
||||||
|
await myNetwork.isRemotePortAvailable('example.com', 443);
|
||||||
|
|
||||||
|
// UDP is not supported:
|
||||||
|
try {
|
||||||
|
await myNetwork.isRemotePortAvailable('example.com', { port: 53, protocol: 'udp' });
|
||||||
|
} catch (e) {
|
||||||
|
console.error((e as any).code); // ENOTSUP
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using Ping
|
### Using Ping
|
||||||
|
|
||||||
The `ping` method allows you to send ICMP packets to a host to measure round-trip time and determine if the host is reachable.
|
The `ping` method sends ICMP echo requests and optionally repeats them to collect statistics.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const pingHost = async (hostname: string) => {
|
// Single ping
|
||||||
const pingResult = await myNetwork.ping(hostname);
|
const p1 = await myNetwork.ping('google.com');
|
||||||
if (pingResult.alive) {
|
console.log(`Alive: ${p1.alive}, RTT: ${p1.time} ms`);
|
||||||
console.log(`${hostname} is reachable. RTT: ${pingResult.time} ms`);
|
|
||||||
} else {
|
|
||||||
console.log(`${hostname} is not reachable.`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pingHost('google.com');
|
// 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}%`,
|
||||||
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Getting Network Gateways
|
### Getting Network Gateways
|
||||||
|
|
||||||
You can also retrieve information about your network gateways, including the default gateway used by your machine.
|
You can also retrieve network interfaces (gateways) and determine the default gateway. Caching with TTL is supported via constructor options.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const showGateways = async () => {
|
// Create with cache TTL of 60 seconds
|
||||||
const gateways = await myNetwork.getGateways();
|
const netCached = new SmartNetwork({ cacheTtl: 60000 });
|
||||||
|
|
||||||
|
// List all interfaces
|
||||||
|
const gateways = await netCached.getGateways();
|
||||||
console.log(gateways);
|
console.log(gateways);
|
||||||
|
|
||||||
const defaultGateway = await myNetwork.getDefaultGateway();
|
// Get default gateway
|
||||||
console.log(`Default Gateway: `, defaultGateway);
|
const defaultGw = await netCached.getDefaultGateway();
|
||||||
};
|
console.log(defaultGw);
|
||||||
|
|
||||||
showGateways();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Discovering Public IP Addresses
|
### Discovering Public IP Addresses
|
||||||
|
|
||||||
To find out your public IPv4 and IPv6 addresses, the following method can be used:
|
To find out your public IPv4 and IPv6 addresses (with caching):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const showPublicIps = async () => {
|
const publicIps = await netCached.getPublicIps();
|
||||||
const publicIps = await myNetwork.getPublicIps();
|
|
||||||
console.log(`Public IPv4: ${publicIps.v4}`);
|
console.log(`Public IPv4: ${publicIps.v4}`);
|
||||||
console.log(`Public IPv6: ${publicIps.v6}`);
|
console.log(`Public IPv6: ${publicIps.v6}`);
|
||||||
};
|
|
||||||
|
|
||||||
showPublicIps();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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.
|
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:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { SmartNetwork } from '@push.rocks/smartnetwork';
|
||||||
|
|
||||||
|
// Define your plugin class or constructor
|
||||||
|
class MyCustomPlugin {
|
||||||
|
// plugin implementation goes here
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register and unregister your plugin by name
|
||||||
|
SmartNetwork.registerPlugin('myPlugin', MyCustomPlugin);
|
||||||
|
// Later, remove it if no longer needed
|
||||||
|
SmartNetwork.unregisterPlugin('myPlugin');
|
||||||
|
```
|
||||||
|
|
||||||
|
Plugins enable you to dynamically augment the core functionality without altering the library's source.
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
# Plan to Enhance Code Quality, Feature Set & Documentation
|
|
||||||
|
|
||||||
This plan focuses on three pillars to elevate `@push.rocks/smartnetwork`: 1) Code Quality, 2) Feature Enhancements, and 3) Documentation.
|
|
||||||
|
|
||||||
## 1. Code Quality Improvements
|
|
||||||
- Enable strict TypeScript (`strict`, `noImplicitAny`, `strictNullChecks`).
|
|
||||||
- Enforce linting (ESLint) and formatting (Prettier) with pre-commit hooks.
|
|
||||||
- Audit and refactor core modules for:
|
|
||||||
- Clear separation of concerns (IO, business logic, helpers).
|
|
||||||
- Removal of duplicated logic and dead code.
|
|
||||||
- Consistent use of async/await and error propagation.
|
|
||||||
- Introduce custom error classes (e.g., `NetworkError`, `TimeoutError`) for predictable failure handling.
|
|
||||||
- Augment logging support via injectable logger interface.
|
|
||||||
- Establish a baseline of ≥90% unit-test coverage and enforce via CI.
|
|
||||||
|
|
||||||
## 2. Feature Enhancements
|
|
||||||
- Expand diagnostics:
|
|
||||||
- Traceroute functionality with hop-by-hop latency.
|
|
||||||
- DNS lookup (A, AAAA, MX records).
|
|
||||||
- HTTP(s) endpoint health check (status codes, headers, latency).
|
|
||||||
- Improve existing methods:
|
|
||||||
- `getSpeed`: allow configurable test duration and parallel streams.
|
|
||||||
- `ping`: add statistical summary (min, max, stddev) and continuous mode.
|
|
||||||
- `isRemotePortAvailable`: support TCP/UDP checks with timeout and retry.
|
|
||||||
- Introduce plugin architecture:
|
|
||||||
- Define `Plugin` interface for third-party extensions.
|
|
||||||
- Enable runtime registration/unregistration.
|
|
||||||
- Provide sample plugins (e.g., custom ping strategies, alternate speed providers).
|
|
||||||
- Optional in-memory caching with TTL for expensive calls (`getPublicIps`, `getGateways`).
|
|
||||||
|
|
||||||
## 3. Documentation & Examples
|
|
||||||
- Upgrade README:
|
|
||||||
- Detailed API reference with method signatures and option parameters.
|
|
||||||
- Real-world usage snippets and full example projects.
|
|
||||||
- Add TSDoc comments to all public classes, methods, and types.
|
|
||||||
- Create a `docs/` folder with:
|
|
||||||
- Getting Started guide.
|
|
||||||
- Advanced topics (plugin development, custom error handling).
|
|
||||||
- FAQ and troubleshooting section.
|
|
||||||
- Integrate TypeDoc for automated documentation site generation.
|
|
||||||
- Update `CONTRIBUTING.md` and `CHANGELOG.md` to reflect development and release practices.
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
1. Review and prioritize high-impact items per pillar.
|
|
||||||
2. Kick off Phase 1 (Code Quality) with linting, TS config, and core refactor.
|
|
||||||
3. Schedule sprints for Feature and Documentation phases.
|
|
||||||
4. Configure CI pipeline to enforce quality gates and publish docs.
|
|
191
test/test.features.ts
Normal file
191
test/test.features.ts
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import { tap, expect, expectAsync } from '@push.rocks/tapbundle';
|
||||||
|
import { SmartNetwork, NetworkError } from '../ts/index.js';
|
||||||
|
import * as net from 'net';
|
||||||
|
import type { AddressInfo } from 'net';
|
||||||
|
|
||||||
|
// DNS resolution
|
||||||
|
tap.test('resolveDns should return A records for localhost', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
const res = await sn.resolveDns('localhost');
|
||||||
|
expect(res.A.length).toBeGreaterThan(0);
|
||||||
|
expect(Array.isArray(res.A)).toBeTrue();
|
||||||
|
expect(Array.isArray(res.AAAA)).toBeTrue();
|
||||||
|
expect(Array.isArray(res.MX)).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
// DNS resolution edge cases and MX records
|
||||||
|
tap.test('resolveDns should handle non-existent domains', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
const res = await sn.resolveDns('no.such.domain.invalid');
|
||||||
|
expect(Array.isArray(res.A)).toBeTrue();
|
||||||
|
expect(Array.isArray(res.AAAA)).toBeTrue();
|
||||||
|
expect(Array.isArray(res.MX)).toBeTrue();
|
||||||
|
expect(res.A.length).toEqual(0);
|
||||||
|
expect(res.AAAA.length).toEqual(0);
|
||||||
|
expect(res.MX.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('resolveDns MX records for google.com', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
const res = await sn.resolveDns('google.com');
|
||||||
|
expect(Array.isArray(res.MX)).toBeTrue();
|
||||||
|
if (res.MX.length > 0) {
|
||||||
|
expect(typeof res.MX[0].exchange).toEqual('string');
|
||||||
|
expect(typeof res.MX[0].priority).toEqual('number');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// HTTP endpoint health-check
|
||||||
|
tap.test('checkEndpoint should return status and headers', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
const result = await sn.checkEndpoint('https://example.com');
|
||||||
|
expect(result.status).toEqual(200);
|
||||||
|
expect(typeof result.rtt).toEqual('number');
|
||||||
|
expect(typeof result.headers).toEqual('object');
|
||||||
|
expect(result.headers).toHaveProperty('content-type');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Traceroute stub
|
||||||
|
tap.test('traceroute should return at least one hop', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
const hops = await sn.traceroute('127.0.0.1');
|
||||||
|
expect(Array.isArray(hops)).toBeTrue();
|
||||||
|
expect(hops.length).toBeGreaterThanOrEqual(1);
|
||||||
|
const hop = hops[0];
|
||||||
|
expect(typeof hop.ttl).toEqual('number');
|
||||||
|
expect(typeof hop.ip).toEqual('string');
|
||||||
|
expect(hop.rtt === null || typeof hop.rtt === 'number').toBeTrue();
|
||||||
|
});
|
||||||
|
// Traceroute fallback stub ensures consistent output when binary missing
|
||||||
|
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[0]).toEqual({ ttl: 1, ip: 'example.com', rtt: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
// getSpeed options
|
||||||
|
tap.test('getSpeed should accept options and return speeds', async () => {
|
||||||
|
const opts = { parallelStreams: 2, duration: 1 };
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
const result = await sn.getSpeed(opts);
|
||||||
|
expect(typeof result.downloadSpeed).toEqual('string');
|
||||||
|
expect(typeof result.uploadSpeed).toEqual('string');
|
||||||
|
expect(parseFloat(result.downloadSpeed)).toBeGreaterThan(0);
|
||||||
|
expect(parseFloat(result.uploadSpeed)).toBeGreaterThan(0);
|
||||||
|
expect(parseFloat(result.downloadSpeed)).toBeGreaterThan(0);
|
||||||
|
expect(parseFloat(result.uploadSpeed)).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ping multiple count
|
||||||
|
tap.test('ping with count > 1 should return stats', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
const stats = await sn.ping('127.0.0.1', { count: 3 });
|
||||||
|
expect(stats.count).toEqual(3);
|
||||||
|
expect(Array.isArray(stats.times)).toBeTrue();
|
||||||
|
expect(stats.times.length).toEqual(3);
|
||||||
|
expect(typeof stats.min).toEqual('number');
|
||||||
|
expect(typeof stats.max).toEqual('number');
|
||||||
|
expect(typeof stats.avg).toEqual('number');
|
||||||
|
expect(typeof stats.stddev).toEqual('number');
|
||||||
|
expect(typeof stats.packetLoss).toEqual('number');
|
||||||
|
expect(typeof stats.alive).toEqual('boolean');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remote port UDP not supported
|
||||||
|
// Remote port UDP not supported
|
||||||
|
tap.test('isRemotePortAvailable should throw on UDP', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
// should throw NetworkError with code ENOTSUP when protocol is UDP
|
||||||
|
try {
|
||||||
|
await sn.isRemotePortAvailable('example.com', { protocol: 'udp' });
|
||||||
|
// If no error is thrown, the test should fail
|
||||||
|
throw new Error('Expected isRemotePortAvailable to throw for UDP');
|
||||||
|
} catch (err: any) {
|
||||||
|
expect(err).toBeInstanceOf(NetworkError);
|
||||||
|
expect(err.code).toEqual('ENOTSUP');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Plugin registry
|
||||||
|
tap.test('should register and unregister plugin', async () => {
|
||||||
|
class Dummy {}
|
||||||
|
SmartNetwork.registerPlugin('dummy', Dummy);
|
||||||
|
expect(SmartNetwork.pluginsRegistry.has('dummy')).toBeTrue();
|
||||||
|
SmartNetwork.unregisterPlugin('dummy');
|
||||||
|
expect(SmartNetwork.pluginsRegistry.has('dummy')).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('getGateways should respect cacheTtl', async () => {
|
||||||
|
const sn = new SmartNetwork({ cacheTtl: 1000 });
|
||||||
|
const first = await sn.getGateways();
|
||||||
|
const second = await sn.getGateways();
|
||||||
|
expect(first).toEqual(second);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remote port checks: missing port should error
|
||||||
|
tap.test('isRemotePortAvailable should require a port', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
try {
|
||||||
|
await sn.isRemotePortAvailable('example.com');
|
||||||
|
throw new Error('Expected error when port is not specified');
|
||||||
|
} catch (err: any) {
|
||||||
|
expect(err).toBeInstanceOf(NetworkError);
|
||||||
|
expect(err.code).toEqual('EINVAL');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remote port checks: detect open TCP port on example.com
|
||||||
|
tap.test('isRemotePortAvailable should detect open TCP port via string target', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
const open = await sn.isRemotePortAvailable('example.com:80');
|
||||||
|
expect(open).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('isRemotePortAvailable should detect open TCP port via numeric arg', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
const open = await sn.isRemotePortAvailable('example.com', 80);
|
||||||
|
expect(open).toBeTrue();
|
||||||
|
});
|
||||||
|
// Caching public IPs
|
||||||
|
tap.test('getPublicIps should respect cacheTtl', async () => {
|
||||||
|
const sn = new SmartNetwork({ cacheTtl: 1000 });
|
||||||
|
const first = await sn.getPublicIps();
|
||||||
|
const second = await sn.getPublicIps();
|
||||||
|
expect(first).toEqual(second);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Local port usage detection
|
||||||
|
tap.test('isLocalPortUnused should detect used local port', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
// start a server on a random port
|
||||||
|
const server = net.createServer();
|
||||||
|
await new Promise<void>((res) => server.listen(0, res));
|
||||||
|
const addr = server.address() as AddressInfo;
|
||||||
|
// port is now in use
|
||||||
|
const inUse = await sn.isLocalPortUnused(addr.port);
|
||||||
|
expect(inUse).toBeFalse();
|
||||||
|
await new Promise<void>((res) => server.close(res));
|
||||||
|
});
|
||||||
|
// Real traceroute integration test (skipped if `traceroute` binary is unavailable)
|
||||||
|
tap.test('traceroute real integration against google.com', async () => {
|
||||||
|
const sn = new SmartNetwork();
|
||||||
|
// detect traceroute binary
|
||||||
|
const { spawnSync } = await import('child_process');
|
||||||
|
const probe = spawnSync('traceroute', ['-h']);
|
||||||
|
if (probe.error || probe.status !== 0) {
|
||||||
|
// Skip real integration when traceroute is not installed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hops = await sn.traceroute('google.com', { maxHops: 5, timeout: 5000 });
|
||||||
|
expect(Array.isArray(hops)).toBeTrue();
|
||||||
|
expect(hops.length).toBeGreaterThan(1);
|
||||||
|
for (const hop of hops) {
|
||||||
|
expect(typeof hop.ttl).toEqual('number');
|
||||||
|
expect(typeof hop.ip).toEqual('string');
|
||||||
|
expect(hop.rtt === null || typeof hop.rtt === 'number').toBeTrue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.start();
|
@ -10,8 +10,13 @@ tap.test('should create a vlid instance of SmartNetwork', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should send a ping to Google', async () => {
|
tap.test('should send a ping to Google', async () => {
|
||||||
console.log(await testSmartnetwork.ping('google.com'));
|
const res = await testSmartnetwork.ping('google.com');
|
||||||
await expectAsync(testSmartnetwork.ping('google.com')).property('alive').toBeTrue();
|
console.log(res);
|
||||||
|
// verify basic ping response properties
|
||||||
|
expect(res.alive).toBeTrue();
|
||||||
|
expect(res.time).toBeTypeofNumber();
|
||||||
|
expect(res.output).toBeTypeofString();
|
||||||
|
expect(res.output).toMatch(/PING google\.com/);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should state when a ping is not alive ', async () => {
|
tap.test('should state when a ping is not alive ', async () => {
|
||||||
|
23
test/test.ts
23
test/test.ts
@ -12,6 +12,11 @@ tap.test('should perform a speedtest', async () => {
|
|||||||
const result = await testSmartNetwork.getSpeed();
|
const result = await testSmartNetwork.getSpeed();
|
||||||
console.log(`Download speed for this instance is ${result.downloadSpeed}`);
|
console.log(`Download speed for this instance is ${result.downloadSpeed}`);
|
||||||
console.log(`Upload speed for this instance is ${result.uploadSpeed}`);
|
console.log(`Upload speed for this instance is ${result.uploadSpeed}`);
|
||||||
|
// verify speeds are returned as strings and parse to positive numbers
|
||||||
|
expect(typeof result.downloadSpeed).toEqual('string');
|
||||||
|
expect(typeof result.uploadSpeed).toEqual('string');
|
||||||
|
expect(parseFloat(result.downloadSpeed)).toBeGreaterThan(0);
|
||||||
|
expect(parseFloat(result.uploadSpeed)).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should determine wether a port is free', async () => {
|
tap.test('should determine wether a port is free', async () => {
|
||||||
@ -25,18 +30,28 @@ tap.test('should scan a port', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should get gateways', async () => {
|
tap.test('should get gateways', async () => {
|
||||||
const gatewayResult = await testSmartNetwork.getGateways();
|
const gateways = await testSmartNetwork.getGateways();
|
||||||
console.log(gatewayResult);
|
console.log(gateways);
|
||||||
|
// verify gateways object has at least one interface
|
||||||
|
expect(typeof gateways).toEqual('object');
|
||||||
|
expect(Object.keys(gateways).length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should get the default gateway', async () => {
|
tap.test('should get the default gateway', async () => {
|
||||||
const gatewayResult = await testSmartNetwork.getDefaultGateway();
|
const defaultGw = await testSmartNetwork.getDefaultGateway();
|
||||||
console.log(gatewayResult);
|
console.log(defaultGw);
|
||||||
|
// verify default gateway contains ipv4 and ipv6 info
|
||||||
|
expect(defaultGw).toBeDefined();
|
||||||
|
expect(defaultGw.ipv4).toBeDefined();
|
||||||
|
expect(defaultGw.ipv6).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should get public ips', async () => {
|
tap.test('should get public ips', async () => {
|
||||||
const ips = await testSmartNetwork.getPublicIps();
|
const ips = await testSmartNetwork.getPublicIps();
|
||||||
console.log(ips);
|
console.log(ips);
|
||||||
|
// verify public IPs object contains v4 and v6 properties
|
||||||
|
expect(ips).toHaveProperty('v4');
|
||||||
|
expect(ips).toHaveProperty('v6');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
tap.start();
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartnetwork',
|
name: '@push.rocks/smartnetwork',
|
||||||
version: '3.0.5',
|
version: '4.0.0',
|
||||||
description: 'A toolkit for network diagnostics including speed tests, port availability checks, and more.'
|
description: 'A toolkit for network diagnostics including speed tests, port availability checks, and more.'
|
||||||
}
|
}
|
||||||
|
@ -1 +1,4 @@
|
|||||||
export * from './smartnetwork.classes.smartnetwork.js';
|
export * from './smartnetwork.classes.smartnetwork.js';
|
||||||
|
export type { SmartNetworkOptions, Hop } from './smartnetwork.classes.smartnetwork.js';
|
||||||
|
export { setLogger, getLogger } from './logging.js';
|
||||||
|
export { NetworkError, TimeoutError } from './errors.js';
|
||||||
|
@ -3,8 +3,15 @@ import { getLogger } from './logging.js';
|
|||||||
import { NetworkError, TimeoutError } from './errors.js';
|
import { NetworkError, TimeoutError } from './errors.js';
|
||||||
import * as stats from './helpers/stats.js';
|
import * as stats from './helpers/stats.js';
|
||||||
|
|
||||||
|
export interface SpeedOptions {
|
||||||
|
parallelStreams?: number;
|
||||||
|
duration?: number;
|
||||||
|
}
|
||||||
export class CloudflareSpeed {
|
export class CloudflareSpeed {
|
||||||
constructor() {}
|
private opts: SpeedOptions;
|
||||||
|
constructor(opts?: SpeedOptions) {
|
||||||
|
this.opts = opts || {};
|
||||||
|
}
|
||||||
|
|
||||||
public async speedTest() {
|
public async speedTest() {
|
||||||
const latency = await this.measureLatency();
|
const latency = await this.measureLatency();
|
||||||
@ -12,20 +19,71 @@ export class CloudflareSpeed {
|
|||||||
const serverLocations = await this.fetchServerLocations();
|
const serverLocations = await this.fetchServerLocations();
|
||||||
const cgiData = await this.fetchCfCdnCgiTrace();
|
const cgiData = await this.fetchCfCdnCgiTrace();
|
||||||
|
|
||||||
// lets test the download speed
|
// speed tests: either fixed segments or duration-based mode
|
||||||
const testDown1 = await this.measureDownload(101000, 10);
|
const parallel = this.opts.parallelStreams ?? 1;
|
||||||
const testDown2 = await this.measureDownload(1001000, 8);
|
const measureDownloadParallel = (bytes: number, iterations: number) => {
|
||||||
const testDown3 = await this.measureDownload(10001000, 6);
|
if (parallel <= 1) {
|
||||||
const testDown4 = await this.measureDownload(25001000, 4);
|
return this.measureDownload(bytes, iterations);
|
||||||
const testDown5 = await this.measureDownload(100001000, 1);
|
}
|
||||||
const downloadTests = [...testDown1, ...testDown2, ...testDown3, ...testDown4, ...testDown5];
|
return Promise.all(
|
||||||
|
Array(parallel)
|
||||||
|
.fill(null)
|
||||||
|
.map(() => this.measureDownload(bytes, iterations)),
|
||||||
|
).then((arrays) => arrays.flat());
|
||||||
|
};
|
||||||
|
let downloadTests: number[];
|
||||||
|
if (this.opts.duration && this.opts.duration > 0) {
|
||||||
|
// duration-based download: run for specified seconds
|
||||||
|
downloadTests = [];
|
||||||
|
const durMs = this.opts.duration * 1000;
|
||||||
|
const startMs = Date.now();
|
||||||
|
// use medium chunk size for download
|
||||||
|
const chunkBytes = 25001000;
|
||||||
|
while (Date.now() - startMs < durMs) {
|
||||||
|
const speeds = await measureDownloadParallel(chunkBytes, 1);
|
||||||
|
downloadTests.push(...speeds);
|
||||||
|
}
|
||||||
|
if (downloadTests.length === 0) downloadTests = [0];
|
||||||
|
} else {
|
||||||
|
// fixed download segments
|
||||||
|
const t1 = await measureDownloadParallel(101000, 10);
|
||||||
|
const t2 = await measureDownloadParallel(1001000, 8);
|
||||||
|
const t3 = await measureDownloadParallel(10001000, 6);
|
||||||
|
const t4 = await measureDownloadParallel(25001000, 4);
|
||||||
|
const t5 = await measureDownloadParallel(100001000, 1);
|
||||||
|
downloadTests = [...t1, ...t2, ...t3, ...t4, ...t5];
|
||||||
|
}
|
||||||
const speedDownload = stats.quartile(downloadTests, 0.9).toFixed(2);
|
const speedDownload = stats.quartile(downloadTests, 0.9).toFixed(2);
|
||||||
|
|
||||||
// lets test the upload speed
|
// lets test the upload speed with configurable parallel streams
|
||||||
const testUp1 = await this.measureUpload(11000, 10);
|
const measureUploadParallel = (bytes: number, iterations: number) => {
|
||||||
const testUp2 = await this.measureUpload(101000, 10);
|
if (parallel <= 1) {
|
||||||
const testUp3 = await this.measureUpload(1001000, 8);
|
return this.measureUpload(bytes, iterations);
|
||||||
const uploadTests = [...testUp1, ...testUp2, ...testUp3];
|
}
|
||||||
|
return Promise.all(
|
||||||
|
Array(parallel)
|
||||||
|
.fill(null)
|
||||||
|
.map(() => this.measureUpload(bytes, iterations)),
|
||||||
|
).then((arrays) => arrays.flat());
|
||||||
|
};
|
||||||
|
let uploadTests: number[];
|
||||||
|
if (this.opts.duration && this.opts.duration > 0) {
|
||||||
|
// duration-based upload: run for specified seconds
|
||||||
|
uploadTests = [];
|
||||||
|
const durMsUp = this.opts.duration * 1000;
|
||||||
|
const startMsUp = Date.now();
|
||||||
|
const chunkBytesUp = 1001000;
|
||||||
|
while (Date.now() - startMsUp < durMsUp) {
|
||||||
|
const speeds = await measureUploadParallel(chunkBytesUp, 1);
|
||||||
|
uploadTests.push(...speeds);
|
||||||
|
}
|
||||||
|
if (uploadTests.length === 0) uploadTests = [0];
|
||||||
|
} else {
|
||||||
|
const u1 = await measureUploadParallel(11000, 10);
|
||||||
|
const u2 = await measureUploadParallel(101000, 10);
|
||||||
|
const u3 = await measureUploadParallel(1001000, 8);
|
||||||
|
uploadTests = [...u1, ...u2, ...u3];
|
||||||
|
}
|
||||||
const speedUpload = stats.quartile(uploadTests, 0.9).toFixed(2);
|
const speedUpload = stats.quartile(uploadTests, 0.9).toFixed(2);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -218,8 +276,8 @@ export class CloudflareSpeed {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on('error', (error) => {
|
req.on('error', (error: Error & { code?: string }) => {
|
||||||
reject(error);
|
reject(new NetworkError(error.message, error.code));
|
||||||
});
|
});
|
||||||
|
|
||||||
req.write(data);
|
req.write(data);
|
||||||
@ -227,21 +285,10 @@ export class CloudflareSpeed {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async fetchCfCdnCgiTrace(): Promise<{
|
/**
|
||||||
fl: string;
|
* Fetch Cloudflare's trace endpoint and parse key=value lines to a record.
|
||||||
h: string;
|
*/
|
||||||
ip: string;
|
public async fetchCfCdnCgiTrace(): Promise<Record<string, string>> {
|
||||||
ts: string;
|
|
||||||
visit_scheme: string;
|
|
||||||
uag: string;
|
|
||||||
colo: string;
|
|
||||||
http: string;
|
|
||||||
loc: string;
|
|
||||||
tls: string;
|
|
||||||
sni: string;
|
|
||||||
warp: string;
|
|
||||||
gateway: string;
|
|
||||||
}> {
|
|
||||||
const parseCfCdnCgiTrace = (text: string) =>
|
const parseCfCdnCgiTrace = (text: string) =>
|
||||||
text
|
text
|
||||||
.split('\n')
|
.split('\n')
|
||||||
|
@ -1,29 +1,107 @@
|
|||||||
import * as plugins from './smartnetwork.plugins.js';
|
import * as plugins from './smartnetwork.plugins.js';
|
||||||
|
|
||||||
import { CloudflareSpeed } from './smartnetwork.classes.cloudflarespeed.js';
|
import { CloudflareSpeed } from './smartnetwork.classes.cloudflarespeed.js';
|
||||||
import { getLogger } from './logging.js';
|
import { getLogger } from './logging.js';
|
||||||
|
import { NetworkError } from './errors.js';
|
||||||
|
import * as stats from './helpers/stats.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SmartNetwork simplifies actions within the network
|
* SmartNetwork simplifies actions within the network
|
||||||
*/
|
*/
|
||||||
export class SmartNetwork {
|
|
||||||
/**
|
/**
|
||||||
* get network speed
|
* Configuration options for SmartNetwork
|
||||||
* @param measurementTime
|
|
||||||
*/
|
*/
|
||||||
public async getSpeed() {
|
export interface SmartNetworkOptions {
|
||||||
const cloudflareSpeedInstance = new CloudflareSpeed();
|
/** Cache time-to-live in milliseconds for gateway and public IP lookups */
|
||||||
const test = await cloudflareSpeedInstance.speedTest();
|
cacheTtl?: number;
|
||||||
return test;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hop in a traceroute result
|
||||||
|
*/
|
||||||
|
export interface Hop {
|
||||||
|
ttl: number;
|
||||||
|
ip: string;
|
||||||
|
rtt: number | null;
|
||||||
|
}
|
||||||
|
export class SmartNetwork {
|
||||||
|
/** Static registry for external plugins */
|
||||||
|
public static pluginsRegistry: Map<string, any> = new Map();
|
||||||
|
/** Register a plugin by name */
|
||||||
|
public static registerPlugin(name: string, ctor: any): void {
|
||||||
|
SmartNetwork.pluginsRegistry.set(name, ctor);
|
||||||
|
}
|
||||||
|
/** Unregister a plugin by name */
|
||||||
|
public static unregisterPlugin(name: string): void {
|
||||||
|
SmartNetwork.pluginsRegistry.delete(name);
|
||||||
|
}
|
||||||
|
private options: SmartNetworkOptions;
|
||||||
|
private cache: Map<string, { value: any; expiry: number }>;
|
||||||
|
constructor(options?: SmartNetworkOptions) {
|
||||||
|
this.options = options || {};
|
||||||
|
this.cache = new Map();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* get network speed
|
||||||
|
* @param opts optional speed test parameters
|
||||||
|
*/
|
||||||
|
public async getSpeed(
|
||||||
|
opts?: { parallelStreams?: number; duration?: number },
|
||||||
|
) {
|
||||||
|
const cloudflareSpeedInstance = new CloudflareSpeed(opts);
|
||||||
|
return cloudflareSpeedInstance.speedTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send ICMP pings to a host. Optionally specify count for multiple pings.
|
||||||
|
*/
|
||||||
public async ping(
|
public async ping(
|
||||||
hostArg: string,
|
host: string,
|
||||||
timeoutArg: number = 500,
|
opts?: { timeout?: number; count?: number },
|
||||||
): Promise<ReturnType<typeof plugins.smartping.Smartping.prototype.ping>> {
|
): Promise<any> {
|
||||||
const smartpingInstance = new plugins.smartping.Smartping();
|
const timeout = opts?.timeout ?? 500;
|
||||||
const pingResult = await smartpingInstance.ping(hostArg, timeoutArg);
|
const count = opts?.count && opts.count > 1 ? opts.count : 1;
|
||||||
return pingResult;
|
const pinger = new plugins.smartping.Smartping();
|
||||||
|
if (count === 1) {
|
||||||
|
// single ping: normalize time to number
|
||||||
|
const res = await pinger.ping(host, timeout);
|
||||||
|
return {
|
||||||
|
...res,
|
||||||
|
time: typeof res.time === 'number' ? res.time : NaN,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const times: number[] = [];
|
||||||
|
let aliveCount = 0;
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
try {
|
||||||
|
const res = await pinger.ping(host, timeout);
|
||||||
|
const t = typeof res.time === 'number' ? res.time : NaN;
|
||||||
|
if (res.alive) aliveCount++;
|
||||||
|
times.push(t);
|
||||||
|
} catch {
|
||||||
|
times.push(NaN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const valid = times.filter((t) => !isNaN(t));
|
||||||
|
const min = valid.length ? Math.min(...valid) : NaN;
|
||||||
|
const max = valid.length ? Math.max(...valid) : NaN;
|
||||||
|
const avg = valid.length ? stats.average(valid) : NaN;
|
||||||
|
const stddev = valid.length
|
||||||
|
? Math.sqrt(
|
||||||
|
stats.average(valid.map((v) => (v - avg) ** 2)),
|
||||||
|
)
|
||||||
|
: NaN;
|
||||||
|
const packetLoss = ((count - aliveCount) / count) * 100;
|
||||||
|
return {
|
||||||
|
host,
|
||||||
|
count,
|
||||||
|
times,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
avg,
|
||||||
|
stddev,
|
||||||
|
packetLoss,
|
||||||
|
alive: aliveCount > 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,6 +109,9 @@ export class SmartNetwork {
|
|||||||
* note: false also resolves with false as argument
|
* note: false also resolves with false as argument
|
||||||
* @param port
|
* @param port
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Check if a local port is unused (both IPv4 and IPv6)
|
||||||
|
*/
|
||||||
public async isLocalPortUnused(port: number): Promise<boolean> {
|
public async isLocalPortUnused(port: number): Promise<boolean> {
|
||||||
const doneIpV4 = plugins.smartpromise.defer<boolean>();
|
const doneIpV4 = plugins.smartpromise.defer<boolean>();
|
||||||
const doneIpV6 = plugins.smartpromise.defer<boolean>();
|
const doneIpV6 = plugins.smartpromise.defer<boolean>();
|
||||||
@ -75,27 +156,66 @@ export class SmartNetwork {
|
|||||||
* checks wether a remote port is available
|
* checks wether a remote port is available
|
||||||
* @param domainArg
|
* @param domainArg
|
||||||
*/
|
*/
|
||||||
public async isRemotePortAvailable(domainArg: string, portArg?: number): Promise<boolean> {
|
/**
|
||||||
|
* Check if a remote port is available
|
||||||
|
* @param target host or "host:port"
|
||||||
|
* @param opts options including port, protocol (only tcp), retries and timeout
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Check if a remote port is available
|
||||||
|
* @param target host or "host:port"
|
||||||
|
* @param portOrOpts either a port number (deprecated) or options object
|
||||||
|
*/
|
||||||
|
public async isRemotePortAvailable(
|
||||||
|
target: string,
|
||||||
|
portOrOpts?: number | { port?: number; protocol?: 'tcp' | 'udp'; timeout?: number; retries?: number },
|
||||||
|
): Promise<boolean> {
|
||||||
|
let hostPart: string;
|
||||||
|
let port: number | undefined;
|
||||||
|
let protocol: string = 'tcp';
|
||||||
|
let retries = 1;
|
||||||
|
let timeout: number | undefined;
|
||||||
|
// preserve old signature (target, port)
|
||||||
|
if (typeof portOrOpts === 'number') {
|
||||||
|
[hostPart] = target.split(':');
|
||||||
|
port = portOrOpts;
|
||||||
|
} else {
|
||||||
|
const opts = portOrOpts || {};
|
||||||
|
protocol = opts.protocol ?? 'tcp';
|
||||||
|
retries = opts.retries ?? 1;
|
||||||
|
timeout = opts.timeout;
|
||||||
|
[hostPart] = target.split(':');
|
||||||
|
const portPart = target.split(':')[1];
|
||||||
|
port = opts.port ?? (portPart ? parseInt(portPart, 10) : undefined);
|
||||||
|
}
|
||||||
|
if (protocol === 'udp') {
|
||||||
|
throw new NetworkError('UDP port check not supported', 'ENOTSUP');
|
||||||
|
}
|
||||||
|
if (!port) {
|
||||||
|
throw new NetworkError('Port not specified', 'EINVAL');
|
||||||
|
}
|
||||||
|
let last: boolean = false;
|
||||||
|
for (let attempt = 0; attempt < retries; attempt++) {
|
||||||
const done = plugins.smartpromise.defer<boolean>();
|
const done = plugins.smartpromise.defer<boolean>();
|
||||||
const domainPart = domainArg.split(':')[0];
|
plugins.isopen(hostPart, port, (response: Record<string, { isOpen: boolean }>) => {
|
||||||
const port = portArg ? portArg : parseInt(domainArg.split(':')[1], 10);
|
const info = response[port.toString()];
|
||||||
|
done.resolve(Boolean(info?.isOpen));
|
||||||
plugins.isopen(
|
});
|
||||||
domainPart,
|
last = await done.promise;
|
||||||
port,
|
if (last) return true;
|
||||||
(response: Record<string, { isOpen: boolean }>) => {
|
}
|
||||||
getLogger().debug(response);
|
return last;
|
||||||
const portInfo = response[port.toString()];
|
|
||||||
done.resolve(Boolean(portInfo?.isOpen));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const result = await done.promise;
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getGateways() {
|
/**
|
||||||
const result = plugins.os.networkInterfaces();
|
* List network interfaces (gateways)
|
||||||
return result;
|
*/
|
||||||
|
public async getGateways(): Promise<Record<string, plugins.os.NetworkInterfaceInfo[]>> {
|
||||||
|
const fetcher = async () => plugins.os.networkInterfaces();
|
||||||
|
if (this.options.cacheTtl && this.options.cacheTtl > 0) {
|
||||||
|
return this.getCached('gateways', fetcher);
|
||||||
|
}
|
||||||
|
return fetcher();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDefaultGateway(): Promise<{
|
public async getDefaultGateway(): Promise<{
|
||||||
@ -115,24 +235,136 @@ export class SmartNetwork {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getPublicIps() {
|
/**
|
||||||
return {
|
* Lookup public IPv4 and IPv6
|
||||||
v4: await plugins.publicIp
|
*/
|
||||||
.publicIpv4({
|
public async getPublicIps(): Promise<{ v4: string | null; v6: string | null }> {
|
||||||
timeout: 1000,
|
const fetcher = async () => ({
|
||||||
onlyHttps: true,
|
v4: await plugins.publicIp.publicIpv4({ timeout: 1000, onlyHttps: true }).catch(() => null),
|
||||||
})
|
v6: await plugins.publicIp.publicIpv6({ timeout: 1000, onlyHttps: true }).catch(() => null),
|
||||||
.catch(async (err) => {
|
});
|
||||||
return null;
|
if (this.options.cacheTtl && this.options.cacheTtl > 0) {
|
||||||
}),
|
return this.getCached('publicIps', fetcher);
|
||||||
v6: await plugins.publicIp
|
}
|
||||||
.publicIpv6({
|
return fetcher();
|
||||||
timeout: 1000,
|
}
|
||||||
onlyHttps: true,
|
|
||||||
})
|
/**
|
||||||
.catch(async (err) => {
|
* Resolve DNS records (A, AAAA, MX)
|
||||||
return null;
|
*/
|
||||||
}),
|
public async resolveDns(host: string): Promise<{ A: string[]; AAAA: string[]; MX: { exchange: string; priority: number }[] }> {
|
||||||
};
|
try {
|
||||||
|
const dns = await import('dns');
|
||||||
|
const { resolve4, resolve6, resolveMx } = dns.promises;
|
||||||
|
const [A, AAAA, MX] = await Promise.all([
|
||||||
|
resolve4(host).catch(() => []),
|
||||||
|
resolve6(host).catch(() => []),
|
||||||
|
resolveMx(host).catch(() => []),
|
||||||
|
]);
|
||||||
|
return { A, AAAA, MX };
|
||||||
|
} catch (err: any) {
|
||||||
|
throw new NetworkError(err.message, err.code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a simple HTTP/HTTPS endpoint health check
|
||||||
|
*/
|
||||||
|
public async checkEndpoint(
|
||||||
|
urlString: string,
|
||||||
|
opts?: { timeout?: number },
|
||||||
|
): Promise<{ status: number; headers: Record<string, string>; rtt: number }> {
|
||||||
|
const start = plugins.perfHooks.performance.now();
|
||||||
|
try {
|
||||||
|
const url = new URL(urlString);
|
||||||
|
const lib = url.protocol === 'https:' ? plugins.https : await import('http');
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = lib.request(
|
||||||
|
url,
|
||||||
|
{ method: 'GET', timeout: opts?.timeout, agent: false },
|
||||||
|
(res: any) => {
|
||||||
|
res.on('data', () => {});
|
||||||
|
res.once('end', () => {
|
||||||
|
const rtt = plugins.perfHooks.performance.now() - start;
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
for (const [k, v] of Object.entries(res.headers)) {
|
||||||
|
headers[k] = Array.isArray(v) ? v.join(',') : String(v);
|
||||||
|
}
|
||||||
|
resolve({ status: res.statusCode, headers, rtt });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
req.on('error', (err: any) => reject(new NetworkError(err.message, err.code)));
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
throw new NetworkError(err.message, err.code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a traceroute: hop-by-hop latency using the system traceroute tool.
|
||||||
|
* Falls back to a single-hop stub if traceroute is unavailable or errors.
|
||||||
|
*/
|
||||||
|
public async traceroute(
|
||||||
|
host: string,
|
||||||
|
opts?: { maxHops?: number; timeout?: number },
|
||||||
|
): Promise<Hop[]> {
|
||||||
|
const maxHops = opts?.maxHops ?? 30;
|
||||||
|
const timeout = opts?.timeout;
|
||||||
|
try {
|
||||||
|
const { exec } = await import('child_process');
|
||||||
|
const cmd = `traceroute -n -m ${maxHops} ${host}`;
|
||||||
|
const stdout: string = await new Promise((resolve, reject) => {
|
||||||
|
exec(
|
||||||
|
cmd,
|
||||||
|
{ encoding: 'utf8', timeout },
|
||||||
|
(err, stdout) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve(stdout);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const hops: Hop[] = [];
|
||||||
|
for (const raw of stdout.split('\n')) {
|
||||||
|
const line = raw.trim();
|
||||||
|
if (!line || line.startsWith('traceroute')) continue;
|
||||||
|
const parts = line.split(/\s+/);
|
||||||
|
const ttl = parseInt(parts[0], 10);
|
||||||
|
let ip: string;
|
||||||
|
let rtt: number | null;
|
||||||
|
if (parts[1] === '*' || !parts[1]) {
|
||||||
|
ip = parts[1] || '';
|
||||||
|
rtt = null;
|
||||||
|
} else {
|
||||||
|
ip = parts[1];
|
||||||
|
const timePart = parts.find((p, i) => i >= 2 && /^\d+(\.\d+)?$/.test(p));
|
||||||
|
rtt = timePart ? parseFloat(timePart) : null;
|
||||||
|
}
|
||||||
|
hops.push({ ttl, ip, rtt });
|
||||||
|
}
|
||||||
|
if (hops.length) {
|
||||||
|
return hops;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// traceroute not available or error: fall through to stub
|
||||||
|
}
|
||||||
|
// fallback stub
|
||||||
|
return [{ ttl: 1, ip: host, rtt: null }];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal caching helper
|
||||||
|
*/
|
||||||
|
private async getCached<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
|
||||||
|
const now = Date.now();
|
||||||
|
const entry = this.cache.get(key);
|
||||||
|
if (entry && entry.expiry > now) {
|
||||||
|
return entry.value;
|
||||||
|
}
|
||||||
|
const value = await fetcher();
|
||||||
|
const ttl = this.options.cacheTtl || 0;
|
||||||
|
this.cache.set(key, { value, expiry: now + ttl });
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user