diff options
Diffstat (limited to 'send.php')
| -rw-r--r-- | send.php | 921 |
1 files changed, 821 insertions, 100 deletions
@@ -1,115 +1,836 @@ <?php -// /var/www/mail2usenet/send.php -// Hands the post to Postfix transport “mail2news”, enforcing Ed25519 signature. - -$required = [ - 'from', 'newsgroups', 'subject', 'xhashcash', 'x-ed25519-sig', 'message' -]; - -// Collect POST values -$data = []; -foreach ($_POST as $k => $v) $data[$k] = trim($v); - -// Check required fields -$missing = array_filter($required, fn($f) => empty($data[$f] ?? '')); -if ($missing) { - showError( - 'Missing required fields: ' . implode(', ', $missing) . - '. Remember: you must click <em>Sign Message</em> on the Digital Signature tab before sending.' +// /var/www/m2usenet/send.php +// m2usenet Gateway Handler v3.0.0 - PHP Native SOCKS5 Implementation +// Maximum security: no external dependencies, constant-time operations, anti-analysis +// Security-hardened implementation with all threat mitigations + +// PRODUCTION SECURITY +ini_set('display_errors', 0); +ini_set('display_startup_errors', 0); +ini_set('log_errors', 1); +error_reporting(0); + +// SMTP Relay Configuration +define('PRIMARY_SMTP_RELAY', 'dgayvmsxvvofpdxsas22fo7eu5tous6aavzjs6eun6jnouluwqflz7ad.onion'); +define('PRIMARY_SMTP_PORT', 2525); // Restored to original working port +define('PRIMARY_MAIL2NEWS', 'mail2news@xilb7y4kj6u6qfo45o3yk2kilfv54ffukzei3puonuqlncy7cn2afwyd.onion'); + +// Security Configuration +define('LOG_FILE', '/var/log/m2usenet/send.log'); +define('RATE_LIMIT_FILE', '/var/www/m2usenet/rate_limits.json'); +define('HASHCASH_CACHE', '/var/www/m2usenet/hashcash_cache.json'); +define('MAX_NEWSGROUPS', 3); +define('MAX_MESSAGE_SIZE', 65536); +define('MIN_MESSAGE_SIZE', 10); +define('PADDING_ENABLED', true); +define('PADDING_BOUNDARY', 1024); +define('RATE_LIMIT_REQUESTS', 10); +define('RATE_LIMIT_WINDOW', 3600); +define('HASHCASH_MIN_BITS', 20); +define('HASHCASH_CACHE_TTL', 172800); +define('QUIET_MODE', false); + +// Timing obfuscation - anti-analysis +define('BASE_TIMEOUT', 120); +define('MAX_JITTER_MS', 5000); +define('MIN_RANDOM_DELAY_MS', 100); +define('MAX_RANDOM_DELAY_MS', 3000); + +// SOCKS5 Configuration +define('SOCKS5_PROXY', '127.0.0.1'); +define('SOCKS5_PORT', 9050); +define('SOCKS5_TIMEOUT', 120); +define('SMTP_READ_TIMEOUT', 30); + +// Network security +define('MAX_SMTP_RESPONSE_SIZE', 8192); +define('MAX_RETRIES', 2); +define('RETRY_DELAY_BASE', 5); + +// Session security +if (session_status() === PHP_SESSION_NONE) { + ini_set('session.cookie_httponly', 1); + ini_set('session.cookie_secure', 1); + ini_set('session.use_strict_mode', 1); + ini_set('session.cookie_samesite', 'Strict'); + session_start(); +} + +function secureLog($message, $level = 'INFO') { + if (QUIET_MODE && $level !== 'ERROR' && $level !== 'CRITICAL') { + return; + } + + $timestamp = gmdate('Y-m-d H:i:s'); + // Sanitize: remove emails, message-ids, sensitive data + $sanitized = preg_replace('/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/', '[email]', $message); + $sanitized = preg_replace('/<[^>]+@[^>]+>/', '[message-id]', $sanitized); + $sanitized = preg_replace('/\b[a-z2-7]{56}\.onion\b/', '[onion]', $sanitized); + $logEntry = "[$timestamp UTC] [$level] $sanitized\n"; + + $logDir = dirname(LOG_FILE); + if (!is_dir($logDir)) { + @mkdir($logDir, 0700, true); + } + + @file_put_contents(LOG_FILE, $logEntry, FILE_APPEND | LOCK_EX); +} + +function cryptoRandBytes($length) { + try { + return random_bytes($length); + } catch (Exception $e) { + secureLog("CRITICAL: random_bytes failed", 'CRITICAL'); + http_response_code(500); + die("System error"); + } +} + +function cryptoRandInt($min, $max) { + try { + return random_int($min, $max); + } catch (Exception $e) { + secureLog("CRITICAL: random_int failed", 'CRITICAL'); + return $min; + } +} + +function constantTimeCompare($a, $b) { + // Constant-time string comparison to prevent timing attacks + if (function_exists('hash_equals')) { + return hash_equals($a, $b); + } + + if (strlen($a) !== strlen($b)) { + return false; + } + + $result = 0; + for ($i = 0; $i < strlen($a); $i++) { + $result |= ord($a[$i]) ^ ord($b[$i]); + } + + return $result === 0; +} + +function randomDelay($baseSeconds = 0) { + // Anti-timing analysis: randomized delays + $jitterMs = cryptoRandInt(0, MAX_JITTER_MS); + $totalUs = ($baseSeconds * 1000000) + ($jitterMs * 1000); + usleep($totalUs); +} + +function generateSecureMessageID() { + // Cryptographically secure, unpredictable Message-ID + $timestamp = time(); + $randomBytes = bin2hex(cryptoRandBytes(32)); // 256 bits of entropy + return sprintf("<%d.%s@m2usenet.local>", $timestamp, $randomBytes); +} + +function generateCSRFToken() { + if (!isset($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(cryptoRandBytes(32)); + } + return $_SESSION['csrf_token']; +} + +function verifyCSRFToken($token) { + if (!isset($_SESSION['csrf_token'])) { + return false; + } + return constantTimeCompare($_SESSION['csrf_token'], $token); +} + +function checkRateLimit() { + // IP-based rate limiting with time-window hashing + $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; + $ipHash = hash('sha256', $ip . date('Y-m-d-H')); + + $limits = []; + if (file_exists(RATE_LIMIT_FILE)) { + $data = @file_get_contents(RATE_LIMIT_FILE); + if ($data) { + $limits = json_decode($data, true) ?? []; + } + } + + $now = time(); + // Cleanup expired entries + foreach ($limits as $hash => $data) { + if ($now - $data['first_request'] > RATE_LIMIT_WINDOW) { + unset($limits[$hash]); + } + } + + if (!isset($limits[$ipHash])) { + $limits[$ipHash] = [ + 'count' => 1, + 'first_request' => $now + ]; + } else { + $limits[$ipHash]['count']++; + } + + @file_put_contents(RATE_LIMIT_FILE, json_encode($limits), LOCK_EX); + + if ($limits[$ipHash]['count'] > RATE_LIMIT_REQUESTS) { + secureLog("Rate limit exceeded for hashed IP", 'WARNING'); + randomDelay(cryptoRandInt(2, 5)); // Anti-timing: delay before rejection + return false; + } + + return true; +} + +function normalizeHashcashToken($token) { + // Normalize whitespace to prevent cache bypass + $normalized = preg_replace('/\s+/', '', $token); + return trim($normalized); +} + +function checkHashcashReplay($token) { + // Replay attack protection with expiring cache + if (!file_exists(HASHCASH_CACHE)) { + return false; + } + + $cache = json_decode(@file_get_contents(HASHCASH_CACHE), true) ?? []; + $now = time(); + + // Cleanup expired tokens (prevent cache growth) + $cleaned = false; + foreach ($cache as $hash => $timestamp) { + if ($now - $timestamp > HASHCASH_CACHE_TTL) { + unset($cache[$hash]); + $cleaned = true; + } + } + + if ($cleaned) { + @file_put_contents(HASHCASH_CACHE, json_encode($cache), LOCK_EX); + } + + $normalizedToken = normalizeHashcashToken($token); + $tokenHash = hash('sha256', $normalizedToken); + + if (isset($cache[$tokenHash])) { + secureLog("Replay attack detected - token hash: " . substr($tokenHash, 0, 16), 'WARNING'); + randomDelay(cryptoRandInt(1, 3)); // Anti-timing + return true; + } + + return false; +} + +function markHashcashUsed($token) { + // Atomically mark token as used + $cache = []; + if (file_exists(HASHCASH_CACHE)) { + $cache = json_decode(@file_get_contents(HASHCASH_CACHE), true) ?? []; + } + + $normalizedToken = normalizeHashcashToken($token); + $tokenHash = hash('sha256', $normalizedToken); + $cache[$tokenHash] = time(); + + @file_put_contents(HASHCASH_CACHE, json_encode($cache), LOCK_EX); +} + +function verifyHashcash($token, $fromEmail) { + // Constant-time hashcash verification + $token = normalizeHashcashToken($token); + + $parts = explode(':', $token); + if (count($parts) !== 7) { + randomDelay(0); // Constant-time: same delay on all paths + return false; + } + + list($version, $bits, $date, $resource, $ext, $rand, $counter) = $parts; + + $valid = true; + + // Constant-time checks + $valid = $valid && ($version === '1'); + $valid = $valid && ((int)$bits >= HASHCASH_MIN_BITS); + $valid = $valid && (strcasecmp(trim($resource), trim($fromEmail)) === 0); + + if (!$valid) { + randomDelay(0); // Constant-time + return false; + } + + // Verify proof-of-work + $hash = sha1($token); + $requiredZeros = str_repeat('0', (int)((int)$bits / 4)); + + if (strpos($hash, $requiredZeros) !== 0) { + randomDelay(0); // Constant-time + return false; + } + + return true; +} + +function validateInput($data) { + $errors = []; + + // Required fields check + $required = ['from', 'newsgroups', 'subject', 'xhashcash', 'message', 'csrf_token']; + foreach ($required as $field) { + if (!isset($data[$field]) || trim($data[$field]) === '') { + $errors[] = "Missing required field: $field"; + randomDelay(0); // Constant-time + return $errors; + } + } + + // CSRF token verification (constant-time) + if (!verifyCSRFToken($data['csrf_token'])) { + $errors[] = "Invalid request token"; + randomDelay(0); // Constant-time + return $errors; + } + + // From header validation + if (!preg_match('/^.+\s*<[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}>$/', $data['from'])) { + $errors[] = "Invalid sender format"; + randomDelay(0); + } + + preg_match('/<([^>]+)>/', $data['from'], $matches); + $fromEmail = $matches[1] ?? ''; + + // Newsgroups validation + $newsgroups = array_map('trim', explode(',', $data['newsgroups'])); + if (count($newsgroups) > MAX_NEWSGROUPS) { + $errors[] = "Too many newsgroups"; + } + + foreach ($newsgroups as $ng) { + if (!preg_match('/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/i', $ng)) { + $errors[] = "Invalid newsgroup format"; + break; + } + } + + // Message size validation + $msgSize = strlen($data['message']); + + if ($msgSize < MIN_MESSAGE_SIZE || $msgSize > MAX_MESSAGE_SIZE) { + $errors[] = "Invalid message size"; + randomDelay(0); + } + + // Hashcash verification (constant-time) + if (empty($errors) && !verifyHashcash($data['xhashcash'], $fromEmail)) { + $errors[] = "Invalid hashcash token"; + randomDelay(0); + } + + // Replay check (only if hashcash valid) + if (empty($errors) && checkHashcashReplay($data['xhashcash'])) { + $errors[] = "Token already used"; + randomDelay(0); + } + + return $errors; +} + +function addAdaptivePadding($message) { + // Size correlation attack prevention with adaptive padding + if (!PADDING_ENABLED) { + return $message; + } + + $msgLen = strlen($message); + $boundary = PADDING_BOUNDARY; + + // Don't pad if already near max size + if ($msgLen >= MAX_MESSAGE_SIZE * 0.95) { + return $message; + } + + // Round up to next boundary + $targetLen = (intdiv($msgLen, $boundary) + 1) * $boundary; + $paddingNeeded = $targetLen - $msgLen; + + // Add random jitter to prevent exact boundary detection + $paddingNeeded += cryptoRandInt(-50, 50); + $paddingNeeded = max(0, min($paddingNeeded, MAX_MESSAGE_SIZE - $msgLen)); + + if ($paddingNeeded > 0) { + // Use varied whitespace characters for padding + $paddingChars = [' ', "\t", "\n"]; + $padding = "\n\n"; // Start with double newline + + for ($i = 0; $i < $paddingNeeded - 2; $i++) { + $padding .= $paddingChars[cryptoRandInt(0, count($paddingChars) - 1)]; + } + + return $message . $padding; + } + + return $message; +} + +// ============================================================================ +// SOCKS5 IMPLEMENTATION - Pure PHP, Maximum Security +// ============================================================================ + +function socks5Connect($destHost, $destPort, $socksHost = SOCKS5_PROXY, $socksPort = SOCKS5_PORT, $timeout = SOCKS5_TIMEOUT) { + secureLog("Initiating SOCKS5 connection to $destHost:$destPort via $socksHost:$socksPort"); + + // Validate destination .onion address + if (!preg_match('/^[a-z2-7]{56}\.onion$/', $destHost)) { + secureLog("Invalid .onion address format", 'ERROR'); + return false; + } + + // Anti-timing: random delay before connection + randomDelay(0); + + // Create socket to SOCKS5 proxy + $socket = @stream_socket_client( + "tcp://$socksHost:$socksPort", + $errno, + $errstr, + $timeout, + STREAM_CLIENT_CONNECT ); + + if (!$socket) { + secureLog("Failed to connect to SOCKS5 proxy: $errstr ($errno)", 'ERROR'); + return false; + } + + // Set timeouts + stream_set_timeout($socket, $timeout); + stream_set_blocking($socket, true); + + secureLog("Connected to SOCKS5 proxy, initiating handshake"); + + // ===== SOCKS5 Handshake Step 1: Version/Auth negotiation ===== + // Format: [VER=0x05][NMETHODS=0x01][METHOD=0x00 (no auth)] + $request = pack('C3', 0x05, 0x01, 0x00); + + if (fwrite($socket, $request) === false) { + secureLog("Failed to write SOCKS5 handshake", 'ERROR'); + fclose($socket); + return false; + } + + // Read response: [VER][METHOD] + $response = fread($socket, 2); + + if (strlen($response) !== 2) { + secureLog("Invalid SOCKS5 handshake response length", 'ERROR'); + fclose($socket); + return false; + } + + $data = unpack('Cver/Cmethod', $response); + + if ($data['ver'] !== 0x05 || $data['method'] !== 0x00) { + secureLog("SOCKS5 handshake failed: unsupported version or auth method", 'ERROR'); + fclose($socket); + return false; + } + + secureLog("SOCKS5 handshake successful, sending connect request"); + + // ===== SOCKS5 Step 2: Connection request ===== + // Format: [VER=0x05][CMD=0x01 (CONNECT)][RSV=0x00][ATYP=0x03 (DOMAIN)] + // [DLEN][DOMAIN][PORT (2 bytes, big-endian)] + + $domainLen = strlen($destHost); + $request = pack('C4', 0x05, 0x01, 0x00, 0x03); // Version, Connect, Reserved, Domain type + $request .= pack('C', $domainLen); // Domain length + $request .= $destHost; // Domain name + $request .= pack('n', $destPort); // Port (network byte order) + + if (fwrite($socket, $request) === false) { + secureLog("Failed to write SOCKS5 connect request", 'ERROR'); + fclose($socket); + return false; + } + + // Read response: [VER][REP][RSV][ATYP][BIND_ADDR][BIND_PORT] + // We need at least 4 bytes to check the result + $response = fread($socket, 4); + + if (strlen($response) < 4) { + secureLog("Invalid SOCKS5 connect response length", 'ERROR'); + fclose($socket); + return false; + } + + $data = unpack('Cver/Crep/Crsv/Catyp', $response); + + if ($data['ver'] !== 0x05) { + secureLog("Invalid SOCKS5 response version", 'ERROR'); + fclose($socket); + return false; + } + + if ($data['rep'] !== 0x00) { + // Error codes + $errors = [ + 0x01 => 'General SOCKS server failure', + 0x02 => 'Connection not allowed by ruleset', + 0x03 => 'Network unreachable', + 0x04 => 'Host unreachable', + 0x05 => 'Connection refused', + 0x06 => 'TTL expired', + 0x07 => 'Command not supported', + 0x08 => 'Address type not supported' + ]; + + $errorMsg = $errors[$data['rep']] ?? "Unknown error code: {$data['rep']}"; + secureLog("SOCKS5 connection failed: $errorMsg", 'ERROR'); + fclose($socket); + return false; + } + + // Read the rest of the response (bind address + port) + // The format depends on ATYP + if ($data['atyp'] === 0x01) { + // IPv4: 4 bytes + 2 bytes port + fread($socket, 6); + } elseif ($data['atyp'] === 0x03) { + // Domain: 1 byte length + domain + 2 bytes port + $len = ord(fread($socket, 1)); + fread($socket, $len + 2); + } elseif ($data['atyp'] === 0x04) { + // IPv6: 16 bytes + 2 bytes port + fread($socket, 18); + } + + secureLog("SOCKS5 connection established successfully"); + + return $socket; +} + +function smtpSendCommand($socket, $command, $expectedCode = null) { + // Send SMTP command and read response + secureLog("SMTP >> " . trim($command)); + + if (fwrite($socket, $command) === false) { + secureLog("Failed to write SMTP command", 'ERROR'); + return false; + } + + // Read response (with size limit) + $response = ''; + $bytesRead = 0; + + while (!feof($socket) && $bytesRead < MAX_SMTP_RESPONSE_SIZE) { + $line = fgets($socket, 1024); + + if ($line === false) { + break; + } + + $response .= $line; + $bytesRead += strlen($line); + + // SMTP multiline response ends when line starts with code and space (not dash) + if (preg_match('/^\d{3} /', $line)) { + break; + } + } + + secureLog("SMTP << " . trim($response)); + + if ($expectedCode !== null) { + if (!preg_match("/^$expectedCode/", $response)) { + secureLog("SMTP error: expected $expectedCode, got: " . substr($response, 0, 100), 'ERROR'); + return false; + } + } + + return $response; +} + +function sendViaNativePHPSMTP($data, $smtpRelay, $smtpPort, $mail2newsAddress) { + secureLog("=== Starting native PHP SMTP delivery ==="); + secureLog("Target: $smtpRelay:$smtpPort → $mail2newsAddress"); + + // Anti-timing: random delay before connection + $delay = cryptoRandInt(MIN_RANDOM_DELAY_MS, MAX_RANDOM_DELAY_MS) / 1000; + randomDelay($delay); + + $startTime = microtime(true); + + // Connect via SOCKS5 + $socket = socks5Connect($smtpRelay, $smtpPort); + + if (!$socket) { + secureLog("SOCKS5 connection failed", 'ERROR'); + return ['success' => false, 'message' => 'Gateway connection failed']; + } + + $connectTime = microtime(true) - $startTime; + secureLog(sprintf("SOCKS5 connection established in %.2fs", $connectTime)); + + // Set read timeout for SMTP operations + stream_set_timeout($socket, SMTP_READ_TIMEOUT); + + try { + // Read SMTP greeting (220) + $greeting = smtpSendCommand($socket, '', 220); + + if ($greeting === false) { + throw new Exception("No SMTP greeting received"); + } + + // HELO + $response = smtpSendCommand($socket, "HELO m2usenet.local\r\n", 250); + + if ($response === false) { + throw new Exception("HELO failed"); + } + + // Extract sender email + preg_match('/<([^>]+)>/', $data['from'], $matches); + $fromEmail = $matches[1] ?? 'noreply@m2usenet.local'; + + // MAIL FROM + $response = smtpSendCommand($socket, "MAIL FROM:<$fromEmail>\r\n", 250); + + if ($response === false) { + throw new Exception("MAIL FROM rejected"); + } + + // RCPT TO + $response = smtpSendCommand($socket, "RCPT TO:<$mail2newsAddress>\r\n", 250); + + if ($response === false) { + throw new Exception("RCPT TO rejected"); + } + + // DATA + $response = smtpSendCommand($socket, "DATA\r\n", 354); + + if ($response === false) { + throw new Exception("DATA command rejected"); + } + + // Build message + $messageId = generateSecureMessageID(); + + $headers = [ + sprintf("From: %s", $data['from']), + sprintf("To: %s", $mail2newsAddress), + sprintf("Subject: %s", $data['subject']), + sprintf("Message-ID: %s", $messageId), + sprintf("Date: %s", gmdate('r')), + sprintf("Newsgroups: %s", $data['newsgroups']), + sprintf("X-Hashcash: %s", $data['xhashcash']) + ]; + + if (!empty($data['x-ed25519-pub'])) { + $headers[] = sprintf("X-Ed25519-Pub: %s", $data['x-ed25519-pub']); + } + if (!empty($data['x-ed25519-sig'])) { + $headers[] = sprintf("X-Ed25519-Sig: %s", $data['x-ed25519-sig']); + } + if (!empty($data['references'])) { + $refs = trim($data['references']); + if (strpos($refs, '<') === false) $refs = '<' . $refs; + if (strpos($refs, '>') === false) $refs .= '>'; + $headers[] = sprintf("References: %s", $refs); + $headers[] = sprintf("In-Reply-To: %s", $refs); + } + + $headers[] = "MIME-Version: 1.0"; + $headers[] = "Content-Type: text/plain; charset=utf-8"; + $headers[] = "Content-Transfer-Encoding: 8bit"; + $headers[] = "User-Agent: m2usenet-web v3.0.0"; + $headers[] = "X-No-Archive: Yes"; + + $body = addAdaptivePadding($data['message']); + $fullMessage = implode("\r\n", $headers) . "\r\n\r\n" . $body . "\r\n"; + + secureLog("Sending message body (" . strlen($fullMessage) . " bytes)"); + + // Send message body + if (fwrite($socket, $fullMessage) === false) { + throw new Exception("Failed to write message body"); + } + + // End DATA with . + $response = smtpSendCommand($socket, ".\r\n", 250); + + if ($response === false) { + throw new Exception("Message rejected"); + } + + // QUIT + smtpSendCommand($socket, "QUIT\r\n", 221); + + $totalTime = microtime(true) - $startTime; + secureLog(sprintf("=== Message delivered successfully in %.2fs ===", $totalTime)); + + fclose($socket); + + return [ + 'success' => true, + 'message' => 'Message sent via secure onion gateway', + 'messageId' => $messageId, + 'gateway' => $mail2newsAddress + ]; + + } catch (Exception $e) { + secureLog("SMTP error: " . $e->getMessage(), 'ERROR'); + fclose($socket); + return ['success' => false, 'message' => 'SMTP protocol error']; + } +} + +function sendMessage($data) { + $result = sendViaNativePHPSMTP($data, PRIMARY_SMTP_RELAY, PRIMARY_SMTP_PORT, PRIMARY_MAIL2NEWS); + + // Retry logic with exponential backoff + $retries = 0; + while (!$result['success'] && $retries < MAX_RETRIES) { + $retries++; + $backoff = RETRY_DELAY_BASE * pow(2, $retries - 1); + $backoff += cryptoRandInt(0, 2); // Add jitter + + secureLog("Retry $retries/" . MAX_RETRIES . " after {$backoff}s", 'WARNING'); + sleep($backoff); + + $result = sendViaNativePHPSMTP($data, PRIMARY_SMTP_RELAY, PRIMARY_SMTP_PORT, PRIMARY_MAIL2NEWS); + } + + return $result; } -// Limit newsgroups to 3 -$groups = array_slice( - array_filter(array_map('trim', explode(',', $data['newsgroups']))), - 0, 3 -); -$data['newsgroups'] = implode(', ', $groups); - -// Build raw email -$hdr = []; -$hdr[] = 'From: ' . $data['from']; -$hdr[] = 'To: mail2news@localhost'; // dummy for sendmail -t -$hdr[] = 'Newsgroups: ' . $data['newsgroups']; -$hdr[] = 'Subject: ' . $data['subject']; -if (!empty($data['references'])) $hdr[] = 'References: ' . $data['references']; -$hdr[] = 'X-Hashcash: ' . $data['xhashcash']; -$hdr[] = 'X-Ed25519-Sig: ' . $data['x-ed25519-sig']; -if (!empty($data['x-ed25519-pub'])) { - $hdr[] = 'X-Ed25519-Pub: ' . $data['x-ed25519-pub']; -} -$hdr[] = 'X-No-Archive: Yes'; -$hdr[] = 'Mime-Version: 1.0'; -$hdr[] = 'Content-Type: text/plain; charset=UTF-8'; -$hdr[] = 'Content-Transfer-Encoding: 7bit'; -$hdr[] = ''; // blank line before body - -$raw = implode("\r\n", $hdr) . "\r\n" . $data['message'] . "\r\n"; - -// Send via sendmail using the custom transport -$sendmail = '/usr/sbin/sendmail'; -$cmd = escapeshellcmd($sendmail) . ' -i -oTransport=mail2news -t'; - -$descriptors = [ - 0 => ['pipe', 'r'], - 1 => ['pipe', 'w'], - 2 => ['pipe', 'w'], -]; - -$proc = proc_open($cmd, $descriptors, $pipes); -if (!is_resource($proc)) showError('Failed to invoke sendmail.'); - -fwrite($pipes[0], $raw); -fclose($pipes[0]); - -$stderr = stream_get_contents($pipes[2]); -fclose($pipes[1]); -fclose($pipes[2]); - -$code = proc_close($proc); -if ($code !== 0) { - showError('Sendmail error (code ' . $code . '):<br>' . nl2br(htmlspecialchars($stderr))); -} - -// ------------------ success page ------------------ -?> +function errorResponse($message = "Request failed") { + http_response_code(400); + ?> <!DOCTYPE html> -<html lang="en"> +<html> <head> - <meta charset="UTF-8"> - <title>Post Sent</title> - <style> - body { font-family:sans-serif; text-align:center; padding:50px; } - a.button { display:inline-block; margin-top:20px; padding:10px 20px; - background:#4caf50; color:#fff; border-radius:4px; text-decoration:none; } - a.button:hover { background:#45a049; } - </style> + <title>Error</title> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <style> + body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + padding: 20px; background: #f5f5f5; margin: 0; } + .container { max-width: 600px; margin: 40px auto; } + .error { background: #fff; border-left: 4px solid #dc3545; + padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } + a { color: #007bff; text-decoration: none; } + a:hover { text-decoration: underline; } + </style> </head> <body> - <h1>Message Sent Successfully</h1> - <p>Your article was handed off to <code>mail2news</code> and should appear in the newsgroups soon.</p> - <a class="button" href="https://m2usenet.virebent.art">Return to Home</a> + <div class="container"> + <div class="error"> + <h2>Request Failed</h2> + <p><?php echo htmlspecialchars($message); ?></p> + <p><a href="index.php">← Back</a></p> + </div> + </div> </body> </html> + <?php + exit; +} -<?php -// -------- helper -------- -function showError(string $msg): void { - http_response_code(400); +function successResponse($messageId, $gateway) { + http_response_code(200); ?> - <!DOCTYPE html> - <html lang="en"><head> - <meta charset="UTF-8"><title>Error</title> - <style> - body { font-family:sans-serif; text-align:center; padding:50px; color:#d00; } - a { color:#337ab7; } - </style> - </head><body> - <h1>Submission Error</h1> - <p><?= $msg ?></p> - <p><a href="https://m2usenet.virebent.art">Return to Home</a></p> - </body></html> +<!DOCTYPE html> +<html> +<head> + <title>Message Sent</title> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <style> + body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + padding: 20px; background: #f5f5f5; margin: 0; } + .container { max-width: 600px; margin: 40px auto; } + .success { background: #fff; border-left: 4px solid #28a745; + padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } + .message-id { font-family: monospace; background: #f8f9fa; + padding: 10px; border-radius: 4px; word-break: break-all; + margin: 10px 0; } + a { color: #007bff; text-decoration: none; } + a:hover { text-decoration: underline; } + .info { font-size: 0.9em; color: #6c757d; margin-top: 15px; } + </style> +</head> +<body> + <div class="container"> + <div class="success"> + <h2>✓ Message Sent</h2> + <p><strong>Message-ID:</strong></p> + <div class="message-id"><?php echo htmlspecialchars($messageId); ?></div> + <p>Your message has been successfully delivered to the Usenet network.</p> + <div class="info"> + <p><strong>Gateway:</strong> <?php echo htmlspecialchars($gateway); ?></p> + <p><small>Message routed via secure Tor SOCKS5 with PHP native implementation</small></p> + </div> + <p><a href="index.php">Send another message</a></p> + </div> + </div> +</body> +</html> <?php exit; } -?> + +// ============================================================================ +// MAIN REQUEST HANDLER +// ============================================================================ + +try { + secureLog("=== New request received ==="); + + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + secureLog("Invalid request method: " . $_SERVER['REQUEST_METHOD'], 'WARNING'); + http_response_code(405); + die("Method not allowed"); + } + + if (!checkRateLimit()) { + secureLog("Rate limit exceeded", 'WARNING'); + errorResponse("Too many requests. Please try again later."); + } + + $data = $_POST; + + secureLog("POST data keys: " . implode(', ', array_keys($data))); + secureLog("Message length: " . strlen($data['message'] ?? '')); + + $errors = validateInput($data); + + if (!empty($errors)) { + secureLog("Validation failed: " . implode('; ', $errors), 'WARNING'); + errorResponse("Validation failed"); + } + + secureLog("Input validation passed"); + + // Mark token as used AFTER successful validation + markHashcashUsed($data['xhashcash']); + secureLog("Hashcash token marked as used"); + + $result = sendMessage($data); + + if ($result['success']) { + secureLog("Request completed successfully"); + successResponse($result['messageId'], $result['gateway']); + } else { + secureLog("Request failed: " . $result['message'], 'ERROR'); + errorResponse($result['message']); + } + +} catch (Exception $e) { + secureLog("=== Fatal exception: " . $e->getMessage() . " ===", 'CRITICAL'); + secureLog("Stack trace: " . $e->getTraceAsString(), 'ERROR'); + errorResponse("System error occurred"); +} |
