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:
@@ -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