diff options
| author | Gab <24553253+gabrix73@users.noreply.github.com> | 2025-10-17 03:43:17 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-17 03:43:17 +0200 |
| commit | 04add5fd5e35715b3c0f9d112e23d726f2751dd7 (patch) | |
| tree | 8c31f0c6b692012c9cd170c5a8a9cbe5e7a5721e | |
| parent | e07e196340ca8068147ced95102a69ebc17fc82f (diff) | |
| download | yamnweb-04add5fd5e35715b3c0f9d112e23d726f2751dd7.tar.gz yamnweb-04add5fd5e35715b3c0f9d112e23d726f2751dd7.tar.xz yamnweb-04add5fd5e35715b3c0f9d112e23d726f2751dd7.zip | |
Update download_remailers.php
| -rw-r--r-- | download_remailers.php | 584 |
1 files changed, 578 insertions, 6 deletions
diff --git a/download_remailers.php b/download_remailers.php index fc2221b..4612ea3 100644 --- a/download_remailers.php +++ b/download_remailers.php @@ -1,11 +1,583 @@ + <?php -function downloadRemailers() { - $url = 'https://echolot.virebent.art/mlist2.txt'; - $file = '/var/www/yamnweb/remailers.txt'; - if (!file_exists($file) || (time() - filemtime($file) > 86400)) { // Scarica solo se il file non esiste o è più vecchio di 24 ore - file_put_contents($file, file_get_contents($url)); +/** + * Secure Remailer List Downloader + * - Uses Tor SOCKS5 proxy + * - Multiple pinger sources with fallback + * - GPG signature verification + * - Randomized timing to prevent traffic analysis + * - Backup and rollback on failure + * - No metadata leakage + */ + +class SecureRemailerDownloader { + + // Multiple pinger sources for redundancy (verified active October 2025) + private $pingers = [ + 'https://echolot.virebent.art/mlist2.txt', // Victor Yamn Pinger (Oct 4, 2025) + 'http://echolot.theremailer.net/yamn/mlist.txt', // Frell Yamn Pinger (Oct 12, 2025) - Most recent! + 'https://www.mixmin.net/yamn/mlist.txt', // Mixmin Yamn Pinger (Oct 4, 2025) + 'https://www.haph.org/yamn/mlist.txt' // Haph Yamn Pinger (Apr 18, 2025) + ]; + + // Tor SOCKS5 proxy + private $torProxy = '127.0.0.1:9050'; + + // Secure storage paths (outside web root!) + private $storageDir = '/opt/yamn-data/cache'; + private $remailersFile = '/opt/yamn-data/cache/remailers.txt'; + private $pubringFile = '/opt/yamn-master/pubring.mix'; // Yamn expects it here + private $backupDir = '/opt/yamn-data/backups'; + private $logFile = '/var/log/yamn_download.log'; + + // Update interval with jitter (20-28 hours instead of exactly 24) + private $minUpdateInterval = 72000; // 20 hours + private $maxUpdateInterval = 100800; // 28 hours + + // GPG verification (if available) + private $verifyGPG = false; + private $gpgKeyId = null; + + public function __construct() { + // Create directories if they don't exist + if (!file_exists($this->storageDir)) { + mkdir($this->storageDir, 0700, true); + } + if (!file_exists($this->backupDir)) { + mkdir($this->backupDir, 0700, true); + } + } + + /** + * Main download function with all security checks + */ + public function downloadRemailers() { + // Download both stats and keyring + $statsResult = $this->downloadStats(); + $keyringResult = $this->downloadKeyring(); + + return $statsResult && $keyringResult; + } + + /** + * Download remailer stats (mlist.txt) + */ + private function downloadStats() { + // Check if update is needed (with randomized interval) + if (!$this->needsUpdate($this->remailersFile)) { + $this->log("Stats update not needed yet"); + return true; + } + + // Add random delay to prevent timing correlation + $this->randomDelay(5, 60); + + // Verify Tor is working + if (!$this->checkTorConnection()) { + $this->log("ERROR: Tor connection not available", 'ERROR'); + return false; + } + + // Backup current file + $this->backupCurrentFile($this->remailersFile, 'remailers'); + + // Try each pinger source + $success = false; + foreach ($this->pingers as $url) { + $this->log("Attempting stats download from: $url"); + + // Random delay between attempts + $this->randomDelay(3, 20); + + if ($this->downloadFromSource($url, $this->remailersFile, 'stats')) { + $success = true; + break; + } + } + + if (!$success) { + $this->log("ERROR: Failed to download stats from all sources", 'ERROR'); + $this->restoreBackup($this->remailersFile, 'remailers'); + return false; + } + + $this->log("Remailer stats updated successfully"); + $this->cleanOldBackups('remailers'); + + return true; + } + + /** + * Download public keyring (pubring.mix) + */ + private function downloadKeyring() { + // Check if update is needed (with randomized interval) + if (!$this->needsUpdate($this->pubringFile)) { + $this->log("Keyring update not needed yet"); + return true; + } + + // Add random delay to prevent timing correlation + $this->randomDelay(5, 60); + + // Verify Tor is working + if (!$this->checkTorConnection()) { + $this->log("ERROR: Tor connection not available", 'ERROR'); + return false; + } + + // Backup current file + $this->backupCurrentFile($this->pubringFile, 'pubring'); + + // Build pubring URLs + $pubringUrls = []; + foreach ($this->pingers as $statsUrl) { + // Replace mlist.txt or mlist2.txt with pubring.mix + $pubringUrl = preg_replace('/mlist2?\.txt$/', 'pubring.mix', $statsUrl); + $pubringUrls[] = $pubringUrl; + } + + // Try each pinger source + $success = false; + foreach ($pubringUrls as $url) { + $this->log("Attempting keyring download from: $url"); + + // Random delay between attempts + $this->randomDelay(3, 20); + + if ($this->downloadFromSource($url, $this->pubringFile, 'keyring')) { + $success = true; + break; + } + } + + if (!$success) { + $this->log("ERROR: Failed to download keyring from all sources", 'ERROR'); + $this->restoreBackup($this->pubringFile, 'pubring'); + return false; + } + + $this->log("Public keyring updated successfully"); + $this->cleanOldBackups('pubring'); + + return true; + } + + /** + * Check if update is needed with randomized interval + */ + private function needsUpdate($filepath) { + if (!file_exists($filepath)) { + return true; + } + + $fileAge = time() - filemtime($filepath); + + // Random threshold between 20-28 hours + $threshold = mt_rand($this->minUpdateInterval, $this->maxUpdateInterval); + + return $fileAge > $threshold; + } + + /** + * Random delay to prevent timing attacks + */ + private function randomDelay($minSeconds, $maxSeconds) { + $delay = mt_rand($minSeconds * 1000000, $maxSeconds * 1000000); + usleep($delay); + } + + /** + * Verify Tor is working + */ + private function checkTorConnection() { + $context = stream_context_create([ + 'http' => [ + 'proxy' => "socks5://{$this->torProxy}", + 'request_fulluri' => true, + 'timeout' => 30 + ] + ]); + + try { + $result = @file_get_contents( + 'https://check.torproject.org/api/ip', + false, + $context + ); + + if ($result) { + $data = json_decode($result, true); + if (isset($data['IsTor']) && $data['IsTor'] === true) { + $this->log("Tor connection verified"); + return true; + } + } + } catch (Exception $e) { + $this->log("Tor check failed: " . $e->getMessage(), 'ERROR'); + } + + return false; + } + + /** + * Download from a specific source via Tor + */ + private function downloadFromSource($url, $destinationFile, $type = 'stats') { + $tempFile = $this->storageDir . '/' . $type . '_' . bin2hex(random_bytes(8)) . '.tmp'; + + // Configure cURL for Tor SOCKS5 proxy + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_PROXY => $this->torProxy, + CURLOPT_PROXYTYPE => CURLPROXY_SOCKS5_HOSTNAME, + CURLOPT_TIMEOUT => 120, + CURLOPT_CONNECTTIMEOUT => 60, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_MAXREDIRS => 0, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_USERAGENT => $this->getRandomUserAgent(), + // Anti-fingerprinting headers + CURLOPT_HTTPHEADER => [ + 'Accept: text/plain,*/*', + 'Accept-Language: en-US,en;q=0.9', + 'Accept-Encoding: gzip, deflate', + 'DNT: 1', + 'Connection: close' + ] + ]); + + $content = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($content === false || $httpCode !== 200) { + $this->log("Download failed from $url: $error (HTTP $httpCode)", 'ERROR'); + return false; + } + + // Validate content based on type + if ($type === 'stats') { + if (!$this->validateRemailerList($content)) { + $this->log("Invalid stats content from $url", 'ERROR'); + return false; + } + } elseif ($type === 'keyring') { + if (!$this->validateKeyring($content)) { + $this->log("Invalid keyring content from $url", 'ERROR'); + return false; + } + } + + // Write to temp file with secure permissions + file_put_contents($tempFile, $content); + chmod($tempFile, 0600); + + // Verify GPG signature if available + if ($this->verifyGPG) { + if (!$this->verifySignature($tempFile, $url)) { + unlink($tempFile); + return false; + } + } + + // Move to final location + rename($tempFile, $destinationFile); + chmod($destinationFile, 0600); + + $this->log("Successfully downloaded $type from $url"); + return true; + } + + /** + * Validate remailer list content + */ + private function validateRemailerList($content) { + // Check minimum size + if (strlen($content) < 100) { + return false; + } + + // Check for typical remailer list patterns + $patterns = [ + '/mixmaster/i', + '/history/i', + '/latency/i', + '/uptime/i', + '/\*{3,}/' // Stars for reliability + ]; + + $matches = 0; + foreach ($patterns as $pattern) { + if (preg_match($pattern, $content)) { + $matches++; + } + } + + // At least 2 patterns should match + return $matches >= 2; + } + + /** + * Validate keyring content (pubring.mix) + */ + private function validateKeyring($content) { + // Check minimum size (keyring should be substantial) + if (strlen($content) < 1000) { + $this->log("Keyring too small: " . strlen($content) . " bytes", 'ERROR'); + return false; + } + + // Check for PGP/GPG key markers or binary key format + $validPatterns = [ + '/BEGIN PGP/i', // PGP format + '/-----BEGIN/i', // Generic armored format + '/remailer-key/i', // Remailer key marker + ]; + + // Also check if it's binary (Mixmaster format) + // Pubring.mix can be binary or ASCII armored + $isBinary = !mb_detect_encoding($content, 'ASCII', true); + + $hasPattern = false; + foreach ($validPatterns as $pattern) { + if (preg_match($pattern, $content)) { + $hasPattern = true; + break; + } + } + + // Valid if either has expected patterns OR is binary + if (!$hasPattern && !$isBinary) { + $this->log("Keyring validation failed: no key markers found", 'ERROR'); + return false; + } + + // Additional validation: check for multiple key entries + // A valid keyring should have multiple remailer keys + $keyCount = substr_count($content, 'remailer-key') + + substr_count($content, 'BEGIN PGP') + + substr_count($content, '-----BEGIN'); + + if ($keyCount > 0 && $keyCount < 3) { + $this->log("Keyring has too few keys: $keyCount", 'WARNING'); + // Don't fail, but log warning + } + + $this->log("Keyring validation passed: " . strlen($content) . " bytes, $keyCount keys"); + return true; + } + + /** + * Verify GPG signature (if enabled) + */ + private function verifySignature($file, $url) { + $sigUrl = $url . '.asc'; + $sigFile = $file . '.asc'; + + // Download signature + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $sigUrl, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_PROXY => $this->torProxy, + CURLOPT_PROXYTYPE => CURLPROXY_SOCKS5_HOSTNAME, + CURLOPT_TIMEOUT => 60 + ]); + + $signature = curl_exec($ch); + curl_close($ch); + + if (!$signature) { + $this->log("No signature available for $url", 'WARNING'); + return true; // Continue without signature + } + + file_put_contents($sigFile, $signature); + + // Verify with GPG + $output = []; + $returnVar = 0; + exec("gpg --verify " . escapeshellarg($sigFile) . " " . escapeshellarg($file) . " 2>&1", $output, $returnVar); + + unlink($sigFile); + + if ($returnVar === 0) { + $this->log("GPG signature verified"); + return true; + } else { + $this->log("GPG signature verification failed", 'ERROR'); + return false; + } + } + + /** + * Backup current file + */ + private function backupCurrentFile($filepath, $prefix = 'file') { + if (file_exists($filepath)) { + $timestamp = date('Ymd_His'); + $backupFile = $this->backupDir . "/{$prefix}_$timestamp.bak"; + copy($filepath, $backupFile); + chmod($backupFile, 0600); + $this->log("Backup created: $backupFile"); + } + } + + /** + * Restore from backup + */ + private function restoreBackup($filepath, $prefix = 'file') { + $backups = glob($this->backupDir . "/{$prefix}_*.bak"); + if (empty($backups)) { + $this->log("No backup available to restore for $prefix", 'ERROR'); + return false; + } + + // Sort by modification time (newest first) + usort($backups, function($a, $b) { + return filemtime($b) - filemtime($a); + }); + + $latestBackup = $backups[0]; + copy($latestBackup, $filepath); + $this->log("Restored backup from: $latestBackup"); + + return true; + } + + /** + * Clean old backups (keep last 10) + */ + private function cleanOldBackups($prefix = 'file') { + $backups = glob($this->backupDir . "/{$prefix}_*.bak"); + if (count($backups) <= 10) { + return; + } + + usort($backups, function($a, $b) { + return filemtime($b) - filemtime($a); + }); + + $toDelete = array_slice($backups, 10); + foreach ($toDelete as $file) { + unlink($file); + } + + $this->log("Cleaned " . count($toDelete) . " old $prefix backups"); + } + + /** + * Get random user agent to avoid fingerprinting + */ + private function getRandomUserAgent() { + $agents = [ + 'Mozilla/5.0 (Windows NT 10.0; rv:109.0) Gecko/20100101 Firefox/115.0', + 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/115.0' + ]; + + return $agents[array_rand($agents)]; + } + + /** + * Secure logging without metadata leakage + */ + private function log($message, $level = 'INFO') { + // Don't log to file if it could leak metadata + // For production: send to syslog or use in-memory logging + + $timestamp = date('Y-m-d H:i:s'); + $logEntry = "[$timestamp] [$level] $message\n"; + + // Only log to file if explicitly enabled + if (getenv('YAMN_ENABLE_LOGGING') === 'true') { + file_put_contents($this->logFile, $logEntry, FILE_APPEND | LOCK_EX); + } + + // For debugging (remove in production) + if ($level === 'ERROR') { + error_log($logEntry); + } + } + + /** + * Get current remailer list (public method) + */ + public function getRemailerList() { + if (!file_exists($this->remailersFile)) { + $this->downloadStats(); + } + + if (file_exists($this->remailersFile)) { + return file_get_contents($this->remailersFile); + } + + return false; + } + + /** + * Get current public keyring (public method) + */ + public function getPublicKeyring() { + if (!file_exists($this->pubringFile)) { + $this->downloadKeyring(); + } + + if (file_exists($this->pubringFile)) { + return file_get_contents($this->pubringFile); + } + + return false; + } + + /** + * Force update of both stats and keyring + */ + public function forceUpdate() { + $statsResult = $this->downloadStats(); + $keyringResult = $this->downloadKeyring(); + + return $statsResult && $keyringResult; + } + + /** + * Get status of downloaded files + */ + public function getStatus() { + $status = [ + 'stats' => [ + 'exists' => file_exists($this->remailersFile), + 'size' => file_exists($this->remailersFile) ? filesize($this->remailersFile) : 0, + 'age_hours' => file_exists($this->remailersFile) ? + round((time() - filemtime($this->remailersFile)) / 3600, 1) : null, + 'last_modified' => file_exists($this->remailersFile) ? + date('Y-m-d H:i:s', filemtime($this->remailersFile)) : null + ], + 'keyring' => [ + 'exists' => file_exists($this->pubringFile), + 'size' => file_exists($this->pubringFile) ? filesize($this->pubringFile) : 0, + 'age_hours' => file_exists($this->pubringFile) ? + round((time() - filemtime($this->pubringFile)) / 3600, 1) : null, + 'last_modified' => file_exists($this->pubringFile) ? + date('Y-m-d H:i:s', filemtime($this->pubringFile)) : null + ], + 'tor_available' => $this->checkTorConnection() + ]; + + return $status; } } -downloadRemailers(); +// Usage +try { + $downloader = new SecureRemailerDownloader(); + $downloader->downloadRemailers(); +} catch (Exception $e) { + error_log("Remailer download failed: " . $e->getMessage()); +} ?> |
