163 lines
4.2 KiB
TypeScript
163 lines
4.2 KiB
TypeScript
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);
|
|
}
|
|
} |