Files
p2ns/includes/infrastructure/utils.js
T
2025-12-17 20:05:50 -05:00

387 lines
13 KiB
JavaScript

const dgram = require('dgram');
const dnsPacket = require('dns-packet');
const net = require('net');
const { logDebug, logWarn, logError, logInfo } = require('./logger');
const fs = require('fs').promises;
const path = require('path');
const state = require('./state');
// Check port responsive
function checkPortResponsive(ip, port) {
return new Promise((resolve) => {
const client = new net.Socket();
client.setTimeout(parseSecondsToMs(process.env.PORT_CHECK_TIMEOUT || '2'));
client.connect(port, ip,() => {
client.destroy();
resolve(true);
});
client.on('error', () => resolve(false));
client.on('timeout', () => {
client.destroy();
resolve(false);
});
});
}
// Check public DNS with failover support
function checkPublicDNS(domain) {
return new Promise(async (resolve) => {
// Parse comma-separated DNS servers
const publicDnsServerEnv = process.env.PUBLIC_DNS_SERVER || '1.1.1.1';
const dnsServers = publicDnsServerEnv.split(',').map(s => s.trim()).filter(s => s.length > 0);
// Try each DNS server in order (failover strategy)
for (let serverIndex = 0; serverIndex < dnsServers.length; serverIndex++) {
const publicDNSServer = dnsServers[serverIndex];
const isLastServer = serverIndex === dnsServers.length - 1;
const result = await new Promise((innerResolve) => {
const resolver = dgram.createSocket('udp4');
let resolved = false;
const cleanup = () => {
if (!resolved) {
resolved = true;
try {
resolver.removeAllListeners();
resolver.close();
} catch (err) {
// Ignore cleanup errors
}
}
};
const timeout = setTimeout(() => {
logWarn('Utils', `DNS query timeout for ${domain} on server ${publicDNSServer}`);
cleanup();
innerResolve(null);
}, 5000);
resolver.on('message', (res) => {
if (resolved) return;
clearTimeout(timeout);
try {
const response = dnsPacket.decode(res);
if (response.answers.length > 0) {
logInfo('Utils', `Public DNS returned records for ${domain} from ${publicDNSServer}`);
cleanup();
innerResolve(response.answers);
} else {
cleanup();
innerResolve(null);
}
} catch (err) {
logError('Utils', `Error decoding DNS response from ${publicDNSServer}: ${err.message}`);
cleanup();
innerResolve(null);
}
});
resolver.on('error', (err) => {
if (resolved) return;
clearTimeout(timeout);
logWarn('Utils', `Error receiving DNS response from ${publicDNSServer}: ${err.message}`);
cleanup();
innerResolve(null);
});
const query = dnsPacket.encode({
type: 'query',
id: Math.floor(Math.random() * 65535),
questions: [{ type: 'A', name: domain }]
});
resolver.send(query, 53, publicDNSServer, (err) => {
if (err) {
clearTimeout(timeout);
logWarn('Utils', `Error forwarding DNS query to ${publicDNSServer}: ${err.message}`);
cleanup();
innerResolve(null);
}
});
});
// If we got a successful response, return it
if (result) {
return resolve(result);
}
// If this was the last server, return null
if (isLastServer) {
logWarn('Utils', `All DNS servers failed for ${domain}`);
return resolve(null);
}
// Otherwise, try next server
logDebug('Utils', `DNS server ${publicDNSServer} failed, trying next server...`);
}
resolve(null);
});
}
async function setupCache() {
const cacheDir = './cache';
const cacheFiles = [
'domains.json',
'holesail_clients.json',
'holesail_servers.json',
'local_dns.json',
'selector_cache.json'
];
try {
// Check if cache directory exists, create if it doesn't
await fs.access(cacheDir).catch(async () => {
await fs.mkdir(cacheDir);
logInfo('Utils', 'Created cache directory');
});
// Create empty JSON files if they don't exist
for (const file of cacheFiles) {
const filePath = path.join(cacheDir, file);
try {
await fs.access(filePath);
} catch {
await fs.writeFile(filePath, '{}');
logInfo('Utils', `Created ${file}`);
}
}
logInfo('Utils', `Cache setup complete`);
} catch (error) {
logError('Utils', `Error setting up cache`);
}
}
/**
* Parse a time value that can be in seconds or minutes
* Accepts formats: "60", "60s", "1m", "1.5m", etc.
* Returns milliseconds
* @param {string|number} value - Time value to parse
* @param {string} defaultUnit - Default unit if no suffix ('s' for seconds, 'm' for minutes)
* @returns {number} - Time in milliseconds
*/
function parseTimeToMs(value, defaultUnit = 's') {
if (typeof value === 'number') {
// If it's a number, assume it's already in the default unit
return defaultUnit === 'm' ? value * 60 * 1000 : value * 1000;
}
if (typeof value !== 'string') {
throw new Error(`Invalid time value: ${value}`);
}
const trimmed = value.trim().toLowerCase();
// Check for explicit unit suffix
if (trimmed.endsWith('s')) {
const num = parseFloat(trimmed.slice(0, -1));
if (isNaN(num)) throw new Error(`Invalid time value: ${value}`);
return num * 1000; // seconds to ms
}
if (trimmed.endsWith('m')) {
const num = parseFloat(trimmed.slice(0, -1));
if (isNaN(num)) throw new Error(`Invalid time value: ${value}`);
return num * 60 * 1000; // minutes to ms
}
// No suffix, use default unit
const num = parseFloat(trimmed);
if (isNaN(num)) throw new Error(`Invalid time value: ${value}`);
return defaultUnit === 'm' ? num * 60 * 1000 : num * 1000;
}
/**
* Parse time as seconds (converts to milliseconds)
* @param {string|number} value - Time in seconds
* @returns {number} - Time in milliseconds
*/
function parseSecondsToMs(value) {
return parseTimeToMs(value, 's');
}
/**
* Parse time as minutes (converts to milliseconds)
* @param {string|number} value - Time in minutes
* @returns {number} - Time in milliseconds
*/
function parseMinutesToMs(value) {
return parseTimeToMs(value, 'm');
}
/**
* Convert seconds to milliseconds
* @param {number} seconds - Time in seconds
* @returns {number} - Time in milliseconds
*/
function secondsToMs(seconds) {
return seconds * 1000;
}
/**
* Convert minutes to milliseconds
* @param {number} minutes - Time in minutes
* @returns {number} - Time in milliseconds
*/
function minutesToMs(minutes) {
return minutes * 60 * 1000;
}
/**
* Get the persistent hyperswarm public key as a hex string
* This is the key that should be used for claim records and voting
* @returns {string|null} - The persistent public key in hex format, or null if not available
*/
function getPersistentPublicKey() {
// First check if already loaded in state (fast path)
if (state.keypair && state.keypair.publicKey) {
return state.keypair.publicKey.toString('hex');
}
// Read directly from file - this is the reliable source
// Use absolute path from process.cwd() to ensure we find the file regardless of where the code is called from
try {
const fsSync = require('fs');
// Try multiple possible paths to handle different execution contexts
const possiblePaths = [
path.join(process.cwd(), 'cache', 'keypair.json'),
path.join(__dirname, '..', '..', 'cache', 'keypair.json'),
'./cache/keypair.json'
];
let keypairData = null;
let keypairPath = null;
for (const tryPath of possiblePaths) {
try {
const resolvedPath = path.resolve(tryPath);
if (fsSync.existsSync(resolvedPath)) {
keypairData = fsSync.readFileSync(resolvedPath, 'utf8');
keypairPath = resolvedPath;
break;
}
} catch (err) {
// Try next path
continue;
}
}
if (!keypairData) {
logError('Utils', 'Could not find keypair.json file in any expected location');
return null;
}
const parsed = JSON.parse(keypairData);
if (!parsed || !parsed.publicKey || typeof parsed.publicKey !== 'string') {
logError('Utils', 'Invalid keypair file format');
return null;
}
// Store in state for future use
if (!state.keypair && parsed.secretKey) {
try {
state.keypair = {
publicKey: Buffer.from(parsed.publicKey, 'hex'),
secretKey: Buffer.from(parsed.secretKey, 'hex')
};
} catch (err) {
logWarn('Utils', `Could not parse keypair buffers: ${err.message}`);
}
}
logDebug('Utils', `Loaded persistent public key from file: ${keypairPath}`);
return parsed.publicKey; // Already in hex format from JSON
} catch (err) {
logError('Utils', `Failed to read persistent public key: ${err.message}`);
return null;
}
}
/**
* Load or create a Hyperswarm keypair, persisting it to cache/keypair.json
* @returns {Promise<{publicKey: Buffer, secretKey: Buffer}>} - The keypair object
*/
async function loadOrCreateKeypair() {
const keypairPath = path.join('./cache', 'keypair.json');
try {
// Try to load existing keypair
const keypairData = await fs.readFile(keypairPath, 'utf8');
const parsed = JSON.parse(keypairData);
// Validate the keypair structure
if (!parsed.publicKey || !parsed.secretKey) {
throw new Error('Invalid keypair format in cache file');
}
const keypair = {
publicKey: Buffer.from(parsed.publicKey, 'hex'),
secretKey: Buffer.from(parsed.secretKey, 'hex')
};
// Validate buffer sizes (Ed25519: 32 bytes public, 64 bytes secret)
if (keypair.publicKey.length !== 32 || keypair.secretKey.length !== 64) {
throw new Error('Invalid keypair buffer sizes');
}
logInfo('Utils', 'Loaded existing Hyperswarm keypair from cache');
return keypair;
} catch (err) {
// File doesn't exist or is invalid, generate a new keypair
if (err.code === 'ENOENT' || err.message.includes('Invalid')) {
logInfo('Utils', 'Generating new Hyperswarm keypair');
// Generate Ed25519 keypair directly using sodium-native to avoid creating
// any Hyperswarm instances that might interfere with network initialization
const sodium = require('sodium-native');
const publicKey = Buffer.allocUnsafe(32);
const secretKey = Buffer.allocUnsafe(64);
// Generate keypair (secretKey contains both secret and public key)
sodium.crypto_sign_keypair(publicKey, secretKey);
const keypair = {
publicKey: publicKey,
secretKey: secretKey
};
// Ensure cache directory exists
const cacheDir = './cache';
try {
await fs.access(cacheDir);
} catch {
await fs.mkdir(cacheDir);
logInfo('Utils', 'Created cache directory for keypair');
}
// Save the keypair to file
const keypairData = {
publicKey: keypair.publicKey.toString('hex'),
secretKey: keypair.secretKey.toString('hex')
};
await fs.writeFile(keypairPath, JSON.stringify(keypairData, null, 2));
logInfo('Utils', 'Saved new Hyperswarm keypair to cache');
return keypair;
} else {
// Unexpected error, rethrow
logError('Utils', `Error loading keypair: ${err.message}`);
throw err;
}
}
}
module.exports = {
checkPortResponsive,
checkPublicDNS,
setupCache,
parseTimeToMs,
parseSecondsToMs,
parseMinutesToMs,
secondsToMs,
minutesToMs,
loadOrCreateKeypair,
getPersistentPublicKey
};