/** * DNS over HTTPS (DOH) Utility Module * * Uses 'dns-over-http-resolver' package for robust DOH resolution. * Reads configuration from config/player.json. * Automatically detects system proxy (Env vars or Windows Registry) to bypass local DNS pollution. */ import DnsOverHttpResolver from 'dns-over-http-resolver'; import axios from 'axios'; import https from 'https'; import fs from 'fs'; import path from 'path'; import {fileURLToPath} from 'url'; import {HttpsProxyAgent} from 'https-proxy-agent'; import {exec} from 'child_process'; import util from 'util'; import {ENV} from './env.js'; // Import ENV utility const execAsync = util.promisify(exec); const __dirname = path.dirname(fileURLToPath(import.meta.url)); // DOH Configuration and Resolver Lazy Init let dohServers = null; let resolver = null; const configPath = path.resolve(__dirname, '../config/player.json'); // Initialize Resolver Lazy function getResolver() { // Check if DOH is enabled via ENV (default: 0/false) const enableDoh = ENV.get('enable_doh', '0') === '1' || ENV.get('enable_doh') === 'true'; if (!enableDoh) { // console.log('[DOH] DOH is disabled via ENV.'); return null; } if (resolver) return resolver; try { // Load config if not loaded if (!dohServers) { try { if (fs.existsSync(configPath)) { const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); if (config.doh && Array.isArray(config.doh) && config.doh.length > 0) { dohServers = config.doh.map(server => server.url); console.log(`[DOH] Loaded ${dohServers.length} DOH servers from config.`); } } } catch (e) { console.error('[DOH] Failed to load DOH config:', e.message); } } resolver = new DnsOverHttpResolver({ maxCache: 1000, request: customRequest }); if (dohServers && dohServers.length > 0) { resolver.setServers(dohServers); } } catch (e) { console.error('[DOH] Init failed:', e.message); return null; } return resolver; } // Proxy Detection Logic let cachedProxy = null; let lastCheckTime = 0; let checkPromise = null; const PROXY_CACHE_TTL = 60000; // 60 seconds cache export function getSystemProxy() { // Check if system proxy detection is enabled via ENV (default: 1/true) const enableProxy = ENV.get('enable_system_proxy', '1') === '1' || ENV.get('enable_system_proxy') === 'true'; if (!enableProxy) { // console.log('[DOH] System proxy detection is disabled via ENV.'); return Promise.resolve(null); } const now = Date.now(); // 1. If cache is valid (checked within 60s), return immediately if (lastCheckTime > 0 && (now - lastCheckTime < PROXY_CACHE_TTL)) { return Promise.resolve(cachedProxy); } // 2. If a check is already in progress, join it (prevent concurrent spawning) if (checkPromise) { return checkPromise; } // 3. Start a new check checkPromise = (async () => { // Timeout option for exec commands const execOpts = {timeout: 300}; let detectedProxy = null; try { // ... (Checks) ... // 1. Check Environment Variables const envProxy = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy; if (envProxy) { // console.log(`[DOH] Detected proxy from env: ${envProxy}`); detectedProxy = envProxy; } // 2. Check Windows Registry else if (process.platform === 'win32') { try { const {stdout: enableOut} = await execAsync('reg query "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ProxyEnable', execOpts); if (/0x1/.test(enableOut)) { const {stdout: serverOut} = await execAsync('reg query "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ProxyServer', execOpts); const match = serverOut.match(/ProxyServer\s+REG_SZ\s+(.*)/i); if (match && match[1]) { let proxyStr = match[1].trim(); if (proxyStr.includes('=')) { const parts = proxyStr.split(';'); for (const part of parts) { if (part.startsWith('https=')) { proxyStr = part.substring(6); break; } } } if (!proxyStr.startsWith('http')) { proxyStr = 'http://' + proxyStr; } // console.log(`[DOH] Detected system proxy: ${proxyStr}`); detectedProxy = proxyStr; } } } catch (e) { } } // 3. Check Android/Linux global http_proxy else if (process.platform === 'android' || process.platform === 'linux') { try { const {stdout} = await execAsync('settings get global http_proxy', execOpts); const proxyStr = stdout ? stdout.trim() : ''; if (proxyStr && proxyStr !== 'null' && proxyStr !== ':0') { const finalProxy = proxyStr.startsWith('http') ? proxyStr : `http://${proxyStr}`; // console.log(`[DOH] Detected Android/Linux system proxy: ${finalProxy}`); detectedProxy = finalProxy; } } catch (e) { } } // 4. Check Linux GNOME if (!detectedProxy && process.platform === 'linux') { try { const {stdout: mode} = await execAsync('gsettings get org.gnome.system.proxy mode', execOpts); if (mode && mode.trim().replace(/'/g, '') === 'manual') { const {stdout: host} = await execAsync('gsettings get org.gnome.system.proxy.http host', execOpts); const {stdout: port} = await execAsync('gsettings get org.gnome.system.proxy.http port', execOpts); const hostStr = host ? host.trim().replace(/'/g, '') : ''; const portStr = port ? port.trim() : ''; if (hostStr && portStr && portStr !== '0') { detectedProxy = `http://${hostStr}:${portStr}`; // console.log(`[DOH] Detected GNOME proxy: ${detectedProxy}`); } } } catch (e) { } } // 5. Check macOS if (!detectedProxy && process.platform === 'darwin') { try { const {stdout} = await execAsync('scutil --proxy', execOpts); if (/HTTPEnable\s*:\s*1/.test(stdout)) { const hostMatch = stdout.match(/HTTPProxy\s*:\s*([^\s]+)/); const portMatch = stdout.match(/HTTPPort\s*:\s*(\d+)/); if (hostMatch && hostMatch[1]) { detectedProxy = `http://${hostMatch[1]}:${portMatch && portMatch[1] ? portMatch[1] : '80'}`; // console.log(`[DOH] Detected macOS proxy: ${detectedProxy}`); } } } catch (e) { } } } catch (e) { console.error('[DOH] Error detecting proxy:', e.message); } finally { // Update cache if (detectedProxy !== cachedProxy) { if (detectedProxy) console.log(`[DOH] System proxy updated: ${detectedProxy}`); else if (cachedProxy) console.log(`[DOH] System proxy cleared`); cachedProxy = detectedProxy; } lastCheckTime = Date.now(); checkPromise = null; } return cachedProxy; })(); return checkPromise; } // Custom request function using axios const customRequest = async (resource, signal) => { try { const proxy = await getSystemProxy(); const config = { headers: { 'Accept': 'application/dns-json' }, signal: signal, timeout: 5000 }; if (proxy) { config.httpsAgent = new HttpsProxyAgent(proxy); config.proxy = false; // Disable axios internal proxy handling to use agent } else { config.httpsAgent = new https.Agent({rejectUnauthorized: false}); } const response = await axios.get(resource, config); return response.data; } catch (error) { throw error; } }; // Initialize Resolver - REMOVED top-level init // const resolver = new DnsOverHttpResolver({ ... }); // if (dohServers.length > 0) { resolver.setServers(dohServers); } /** * Resolve domain using DOH * @param {string} domain * @returns {Promise} Resolved IP or null */ export async function resolveDoh(domain) { // Return immediately if it's already an IP if (!domain || /^(\d{1,3}\.){3}\d{1,3}$|^\[[\da-fA-F:]+\]$/.test(domain)) return domain; // Skip localhost if (domain === 'localhost' || domain === '127.0.0.1') return domain; try { const resolver = getResolver(); if (!resolver) return null; const ips = await resolver.resolve(domain, 'A'); if (ips && ips.length > 0) { return ips[0]; } } catch (e) { // console.error(`[DOH] Failed to resolve ${domain}:`, e.message); } return null; }