Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/telegram_proxy/index.php
Babak Alizadeh c3d23c29b5 security: contain Telegram bot token leak and harden EU proxy
Remove the production bot token from example configs, disable public
proxy test endpoints, restrict proxied Bot API methods, and mask
notification secrets in the admin API after the @Hesabix_bot compromise.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 09:24:42 +00:00

731 lines
28 KiB
PHP
Executable file

<?php
declare(strict_types=1);
/**
* پروکسی سبک PHP برای عبور درخواست‌های تلگرام.
*
* نحوه استفاده:
* - فایل config.php را مطابق نمونه پر کنید.
* - این فایل را روی هاست PHP خود (با mod_php یا FPM) قرار دهید.
* - درخواست‌ها:
* POST /telegram/send → بدنه JSON شامل { "method": "sendMessage", "payload": {...} }
* POST /telegram/webhook → همان payload دریافتی از تلگرام را ارسال کنید.
* - هدر X-Proxy-Key در صورت تنظیم، برای احراز استفاده می‌شود.
*/
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/logger.php';
// مقداردهی اولیه لاگر
TelegramProxyLogger::init();
header('Content-Type: application/json; charset=utf-8');
// بررسی health endpoint
$healthPath = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH);
if ($_SERVER['REQUEST_METHOD'] === 'GET' && ($healthPath === '/health' || strpos($healthPath, '/health') !== false)) {
echo json_encode(['ok' => true]);
return;
}
if (!defined('TG_PROXY_CONFIG')) {
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'CONFIG_NOT_DEFINED']);
return;
}
$config = TG_PROXY_CONFIG;
$botToken = $config['telegram_bot_token'] ?? null;
$telegramBase = rtrim($config['telegram_api_base'] ?? 'https://api.telegram.org', '/');
$internalWebhook = $config['internal_webhook_url'] ?? null;
$proxyKey = $config['proxy_api_key'] ?? null;
// لاگ تنظیمات (بدون نمایش مقادیر حساس)
TelegramProxyLogger::info("Proxy configuration loaded", [
'has_bot_token' => !empty($botToken),
'bot_id_preview' => ($botToken && strpos($botToken, ':') !== false) ? (explode(':', $botToken, 2)[0] . ':***') : null,
'telegram_api_base' => $telegramBase,
'has_internal_webhook' => !empty($internalWebhook),
'internal_webhook_host' => $internalWebhook ? (parse_url($internalWebhook, PHP_URL_HOST) ?: null) : null,
'has_proxy_key' => !empty($proxyKey),
]);
// بررسی تنظیمات ضروری
if (!$botToken || $botToken === 'REPLACE_WITH_BOT_TOKEN') {
TelegramProxyLogger::error("Bot token not configured", [
'config_keys' => array_keys($config),
'bot_token_exists' => isset($config['telegram_bot_token']),
]);
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'BOT_TOKEN_NOT_SET']);
return;
}
if (!$proxyKey || $proxyKey === 'REPLACE_WITH_INDEPENDENT_PROXY_API_KEY' || $proxyKey === 'change-me') {
TelegramProxyLogger::error("Proxy API key not configured securely");
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'PROXY_KEY_NOT_SET']);
return;
}
/** متدهای مجاز Bot API که اپلیکیشن واقعاً استفاده می‌کند */
$TG_PROXY_ALLOWED_METHODS = [
'sendMessage',
'sendChatAction',
'editMessageText',
'editMessageReplyMarkup',
'answerCallbackQuery',
'setWebhook',
'deleteWebhook',
'setMyCommands',
'getWebhookInfo',
'getMe',
];
// بررسی internal_webhook_url
if (!$internalWebhook) {
TelegramProxyLogger::warning("Internal webhook URL not configured", [
'config_keys' => array_keys($config),
'internal_webhook_exists' => isset($config['internal_webhook_url']),
]);
} else {
// بررسی فرمت internal_webhook_url
$webhookParts = parse_url($internalWebhook);
$webhookSecret = basename($webhookParts['path'] ?? '');
TelegramProxyLogger::info("Internal webhook URL analysis", [
'internal_webhook_url' => $internalWebhook,
'parsed_host' => $webhookParts['host'] ?? null,
'parsed_path' => $webhookParts['path'] ?? null,
'extracted_secret' => $webhookSecret,
'secret_length' => strlen($webhookSecret),
'secret_contains_colon' => strpos($webhookSecret, ':') !== false,
'is_valid_format' => preg_match('/^\/api\/v1\/integrations\/telegram\/webhook\/[^\/]+$/', $webhookParts['path'] ?? ''),
]);
// هشدار اگر secret شامل کاراکترهای مشکوک باشد
if (strpos($webhookSecret, ':') !== false) {
TelegramProxyLogger::error("⚠️ WARNING: Internal webhook URL contains colon (:) - might be using bot token instead of webhook secret!", [
'internal_webhook_url' => $internalWebhook,
'extracted_secret_preview' => substr($webhookSecret, 0, 20) . '...',
'issue' => 'URL probably contains bot token instead of telegram_webhook_secret',
'expected_format' => 'https://domain.com/api/v1/integrations/telegram/webhook/{WEBHOOK_SECRET}',
'current_secret' => $webhookSecret,
]);
}
}
function requireAuth(?string $expectedKey): bool {
// بدون کلید پیکربندی‌شده، دسترسی مجاز نیست (قبلاً به‌اشتباه true برمی‌گشت)
if (!$expectedKey) {
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'PROXY_KEY_NOT_SET']);
return false;
}
$headerKey = $_SERVER['HTTP_X_PROXY_KEY'] ?? null;
if (!is_string($headerKey) || !hash_equals($expectedKey, $headerKey)) {
http_response_code(401);
echo json_encode(['ok' => false, 'error' => 'INVALID_PROXY_KEY']);
return false;
}
return true;
}
function isAllowedTelegramMethod(string $method): bool {
global $TG_PROXY_ALLOWED_METHODS;
return in_array($method, $TG_PROXY_ALLOWED_METHODS ?? [], true);
}
function readJsonBody(): array {
$raw = file_get_contents('php://input') ?: '';
$data = json_decode($raw, true);
return is_array($data) ? $data : [];
}
function callTelegramApi(string $url, array $payload): array {
$startTime = microtime(true);
TelegramProxyLogger::debug("Calling Telegram API", [
'url' => $url,
'method' => parse_url($url, PHP_URL_PATH) ?: 'unknown',
'payload_size' => strlen(json_encode($payload, JSON_UNESCAPED_UNICODE))
]);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_UNICODE));
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
$curlErrno = curl_errno($ch);
$duration = round((microtime(true) - $startTime) * 1000, 2);
if ($response === false) {
curl_close($ch);
$errorMsg = $curlError ?: 'Unknown error';
TelegramProxyLogger::error("Failed to call Telegram API", [
'url' => $url,
'error' => $errorMsg,
'curl_errno' => $curlErrno,
'http_code' => $httpCode ?: 0,
'duration_ms' => $duration
]);
return ['ok' => false, 'error' => $errorMsg, 'status' => $httpCode ?: 500];
}
curl_close($ch);
$json = json_decode($response, true);
if (!is_array($json)) {
TelegramProxyLogger::error("Invalid JSON response from Telegram API", [
'url' => $url,
'http_code' => $httpCode,
'response_preview' => substr($response, 0, 500),
'response_length' => strlen($response),
'duration_ms' => $duration
]);
return ['ok' => false, 'error' => 'INVALID_RESPONSE', 'raw_response' => substr($response, 0, 200), 'status' => $httpCode ?: 500];
}
$json['status'] = $httpCode ?: 200;
// لاگ پاسخ (خصوصاً برای setWebhook)
if (isset($json['ok'])) {
if (!$json['ok']) {
// استخراج اطلاعات خطا
$errorCode = $json['error_code'] ?? null;
$description = $json['description'] ?? null;
$parameters = $json['parameters'] ?? null;
TelegramProxyLogger::error("Telegram API returned error", [
'url' => $url,
'http_code' => $httpCode,
'error_code' => $errorCode,
'description' => $description,
'parameters' => $parameters,
'duration_ms' => $duration,
'full_response' => $json
]);
} else {
TelegramProxyLogger::info("Telegram API call successful", [
'url' => $url,
'http_code' => $httpCode,
'description' => $json['description'] ?? null,
'result' => $json['result'] ?? null,
'duration_ms' => $duration
]);
}
}
return $json;
}
function forwardWebhook(string $url, string $body, ?string $proxyKey): array {
$startTime = microtime(true);
// تحلیل URL مقصد
$urlParts = parse_url($url);
$targetSecret = basename($urlParts['path'] ?? '');
// تحلیل headers
$secretTokenHeader = $_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] ?? null;
TelegramProxyLogger::info("Starting webhook forward - DETAILED INFO", [
'target_url' => $url,
'target_host' => $urlParts['host'] ?? null,
'target_path' => $urlParts['path'] ?? null,
'target_secret' => $targetSecret,
'target_secret_length' => strlen($targetSecret),
'body_size' => strlen($body),
'headers_to_send' => [
'content_type' => 'application/json',
'x_telegram_proxy' => 'true',
'has_secret_token_header' => !empty($secretTokenHeader),
'secret_token_header_preview' => $secretTokenHeader ? substr($secretTokenHeader, 0, 10) . '...' : null,
'has_proxy_key' => !empty($proxyKey),
'proxy_key_preview' => $proxyKey ? substr($proxyKey, 0, 10) . '...' : null,
],
]);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30); // افزایش timeout
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // timeout برای اتصال
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
$headers = ['Content-Type: application/json', 'X-Telegram-Proxy: true'];
// Forward header X-Telegram-Bot-Api-Secret-Token از تلگرام به سرور اصلی
if ($secretTokenHeader) {
$headers[] = 'X-Telegram-Bot-Api-Secret-Token: ' . $secretTokenHeader;
TelegramProxyLogger::debug("Forwarding secret token header", [
'has_token' => true,
'token_preview' => substr($secretTokenHeader, 0, 10) . '...',
'token_length' => strlen($secretTokenHeader),
]);
} else {
TelegramProxyLogger::debug("No secret token header to forward", [
'telegram_headers' => array_filter($_SERVER, function($key) {
return strpos($key, 'HTTP_X_TELEGRAM') === 0;
}, ARRAY_FILTER_USE_KEY),
]);
}
if ($proxyKey) {
$headers[] = 'X-Proxy-Key: ' . $proxyKey;
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
$curlErrno = curl_errno($ch);
$duration = round((microtime(true) - $startTime) * 1000, 2);
curl_close($ch);
if ($response === false) {
// خطاهای رایج curl
$errorMsg = $curlError ?: 'Unknown error';
if ($curlErrno == CURLE_COULDNT_CONNECT) {
$errorMsg = 'Could not connect to server. Check firewall and network settings.';
} elseif ($curlErrno == CURLE_OPERATION_TIMEOUTED) {
$errorMsg = 'Connection timeout. Server may be slow or unreachable.';
} elseif ($curlErrno == CURLE_SSL_CONNECT_ERROR) {
$errorMsg = 'SSL connection error. Check certificate.';
}
TelegramProxyLogger::error("❌ Failed to forward webhook - DETAILED ERROR", [
'target_url' => $url,
'target_host' => $urlParts['host'] ?? null,
'target_secret' => $targetSecret,
'error' => $errorMsg,
'curl_errno' => $curlErrno,
'curl_error_code' => $curlErrno,
'body_length' => strlen($body),
'duration_ms' => $duration,
'headers_sent' => $headers,
'possible_causes' => [
'firewall_blocking' => $curlErrno == CURLE_COULDNT_CONNECT,
'server_unreachable' => $curlErrno == CURLE_COULDNT_CONNECT,
'timeout' => $curlErrno == CURLE_OPERATION_TIMEOUTED,
'ssl_issue' => $curlErrno == CURLE_SSL_CONNECT_ERROR,
],
'solution' => $curlErrno == CURLE_COULDNT_CONNECT ? 'Whitelist proxy IP in main server firewall' : 'Check server connectivity and SSL certificate',
]);
return ['ok' => false, 'error' => $errorMsg, 'curl_errno' => $curlErrno, 'status' => 500];
}
// تحلیل پاسخ
$responseData = json_decode($response, true);
$isJson = json_last_error() === JSON_ERROR_NONE;
TelegramProxyLogger::info("Webhook forward completed", [
'target_url' => $url,
'target_secret' => $targetSecret,
'http_code' => $httpCode,
'response_length' => strlen($response),
'is_json' => $isJson,
'response_preview' => substr($response, 0, 200),
'duration_ms' => $duration,
'status_meaning' => $httpCode == 200 ? 'Success' : ($httpCode == 403 ? 'Forbidden (check secret)' : ($httpCode == 404 ? 'Not Found' : 'Error')),
]);
// بررسی خطای 403
if ($httpCode == 403) {
TelegramProxyLogger::error("❌ Webhook forward returned 403 Forbidden - DETAILED ANALYSIS", [
'target_url' => $url,
'target_secret_used' => $targetSecret,
'target_secret_length' => strlen($targetSecret),
'target_secret_preview' => substr($targetSecret, 0, 30),
'secret_token_header_sent' => !empty($secretTokenHeader),
'secret_token_header_preview' => $secretTokenHeader ? substr($secretTokenHeader, 0, 10) . '...' : null,
'response_body' => $response,
'possible_causes' => [
'wrong_webhook_secret' => 'The webhook secret in URL does not match telegram_webhook_secret in database',
'missing_secret_header' => 'telegram_secret_header is set but header was not forwarded correctly',
'wrong_secret_header' => 'telegram_secret_header does not match what was sent in header',
],
'✅ CHECK' => [
'1' => 'Verify telegram_webhook_secret in database matches the secret in URL',
'2' => 'Verify telegram_secret_header is set correctly (if used)',
'3' => 'Check if secret token header is being forwarded correctly',
],
]);
}
// حتی اگر HTTP code خطا باشد، اگر response دریافت شده باشد، forward موفق بوده
return ['ok' => true, 'status' => $httpCode ?: 200, 'response' => $response];
}
// دریافت مسیر درخواست - پشتیبانی از subdirectory
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
$scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
// اگر در subdirectory هستیم، مسیر را normalize کنیم
$path = parse_url($requestUri, PHP_URL_PATH);
// حذف subdirectory از مسیر اگر وجود دارد
if ($scriptName && strpos($scriptName, '/') !== false) {
$scriptDir = dirname($scriptName);
if ($scriptDir !== '.' && $scriptDir !== '/' && strpos($path, $scriptDir) === 0) {
$path = substr($path, strlen($scriptDir));
}
}
// اطمینان از شروع با /
if ($path === '' || $path[0] !== '/') {
$path = '/' . $path;
}
// برای تست و دیباگ (اختیاری - می‌توانید حذف کنید)
if (isset($_GET['debug'])) {
echo json_encode([
'ok' => true,
'debug' => [
'request_uri' => $requestUri,
'script_name' => $scriptName,
'path' => $path,
'method' => $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN',
]
]);
return;
}
// Endpoint برای چک کردن وضعیت webhook (GET request)
if ($path === '/telegram/webhook/status' && $_SERVER['REQUEST_METHOD'] === 'GET') {
// احراز هویت اختیاری برای این endpoint
if ($proxyKey && !requireAuth($proxyKey)) {
return;
}
$url = sprintf('%s/bot%s/getWebhookInfo', $telegramBase, $botToken);
$result = callTelegramApi($url, []);
http_response_code($result['status'] ?? 200);
echo json_encode($result);
return;
}
if ($path === '/telegram/send' && $_SERVER['REQUEST_METHOD'] === 'POST') {
if (!requireAuth($proxyKey)) {
return;
}
$body = readJsonBody();
$method = $body['method'] ?? 'sendMessage';
$payload = $body['payload'] ?? [];
if (!is_string($method) || !isAllowedTelegramMethod($method)) {
TelegramProxyLogger::warning("Rejected disallowed Telegram method", [
'method' => is_string($method) ? $method : gettype($method),
]);
http_response_code(403);
echo json_encode(['ok' => false, 'error' => 'METHOD_NOT_ALLOWED']);
return;
}
TelegramProxyLogger::info("Proxy request received", [
'method' => $method,
'payload_keys' => is_array($payload) ? array_keys($payload) : []
]);
// برای setWebhook، باید مطمئن شویم که URL صحیح است
if ($method === 'setWebhook' && isset($payload['url'])) {
$webhookUrl = $payload['url'];
$secretToken = $payload['secret_token'] ?? null;
// لاگ مقدار secret_token (بدون نمایش کامل برای امنیت)
$secretTokenPreview = $secretToken ? substr($secretToken, 0, 10) . '...' : null;
$secretTokenFull = $secretToken; // برای تحلیل
$secretTokenChars = $secretToken ? str_split($secretToken) : [];
$hasInvalidChars = false;
$invalidChars = [];
$charAnalysis = [];
if ($secretToken) {
// بررسی کاراکترهای غیرمجاز در secret_token
// Telegram فقط کاراکترهای alphanumeric و dash, underscore, dot, tilde را می‌پذیرد
foreach ($secretTokenChars as $index => $char) {
$isValid = preg_match('/^[a-zA-Z0-9\-_\.~]$/', $char);
if (!$isValid) {
$hasInvalidChars = true;
if (!in_array($char, $invalidChars)) {
$invalidChars[] = $char;
}
}
// آنالیز کاراکترها (فقط 10 کاراکتر اول)
if ($index < 10) {
$charAnalysis[] = [
'pos' => $index,
'char' => $char === ' ' ? '[SPACE]' : ($char === "\n" ? '[NEWLINE]' : $char),
'is_valid' => $isValid,
'char_code' => ord($char),
];
}
}
}
// تحلیل دقیق secret_token
$secretAnalysis = [
'length' => strlen($secretToken ?? ''),
'contains_colon' => strpos($secretToken ?? '', ':') !== false,
'contains_slash' => strpos($secretToken ?? '', '/') !== false,
'contains_space' => strpos($secretToken ?? '', ' ') !== false,
'starts_with_bot_id' => preg_match('/^\d{10}:/', $secretToken ?? ''),
'is_bot_token_format' => preg_match('/^\d{10}:[A-Za-z0-9_-]+$/', $secretToken ?? ''),
'first_20_chars' => substr($secretToken ?? '', 0, 20),
];
TelegramProxyLogger::info("Processing setWebhook request - DETAILED ANALYSIS", [
'webhook_url' => $webhookUrl,
'drop_pending_updates' => $payload['drop_pending_updates'] ?? false,
'has_secret_token' => isset($payload['secret_token']),
'secret_token_info' => [
'length' => $secretToken ? strlen($secretToken) : 0,
'preview_first_10' => $secretTokenPreview,
'preview_last_10' => $secretToken ? '...' . substr($secretToken, -10) : null,
'full_preview' => $secretToken ? substr($secretToken, 0, 50) . (strlen($secretToken) > 50 ? '...' : '') : null,
],
'secret_token_analysis' => $secretAnalysis,
'char_analysis_first_10' => $charAnalysis,
'validation' => [
'has_invalid_chars' => $hasInvalidChars,
'invalid_chars' => $invalidChars,
'invalid_char_count' => count($invalidChars),
],
'⚠️ WARNING' => $secretAnalysis['contains_colon'] ? 'Secret token contains colon (:) - might be bot token instead of webhook secret!' : null,
'⚠️ ISSUE' => $secretAnalysis['is_bot_token_format'] ? 'Secret token looks like bot token format (BOT_ID:TOKEN) - this is WRONG!' : null,
]);
// بررسی اینکه URL با HTTPS شروع می‌شود
if (!preg_match('/^https:\/\//i', $webhookUrl)) {
TelegramProxyLogger::error("setWebhook URL validation failed", [
'webhook_url' => $webhookUrl,
'reason' => 'URL must use HTTPS'
]);
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Webhook URL must use HTTPS']);
return;
}
// بررسی secret_token: باید فقط شامل کاراکترهای مجاز باشد
if ($secretToken !== null && $hasInvalidChars) {
TelegramProxyLogger::error("❌ setWebhook secret_token validation FAILED - DETAILED ERROR", [
'secret_token_preview' => $secretTokenPreview,
'secret_token_length' => strlen($secretToken),
'secret_token_full_preview' => substr($secretToken, 0, 100),
'invalid_chars' => $invalidChars,
'invalid_char_count' => count($invalidChars),
'char_positions' => $charAnalysis,
'analysis' => $secretAnalysis,
'reason' => 'Secret token contains unallowed characters. Only alphanumeric, dash (-), underscore (_), dot (.), and tilde (~) are allowed.',
'❌ PROBLEM' => $secretAnalysis['contains_colon'] ? 'Secret token contains colon (:) - this is probably a BOT TOKEN, not a webhook secret!' : 'Secret token contains invalid characters',
'✅ SOLUTION' => 'Go to admin panel > Notifications Settings > Set telegram_webhook_secret to a valid value (without : or other special chars)',
'example_valid_secret' => 'my-webhook-secret-123',
]);
http_response_code(400);
echo json_encode([
'ok' => false,
'error' => 'Bad Request: secret token contains unallowed characters',
'description' => 'Secret token contains unallowed characters. Only alphanumeric, dash (-), underscore (_), dot (.), and tilde (~) are allowed.',
'invalid_chars' => array_values($invalidChars),
'secret_token_preview' => $secretTokenPreview,
'issue' => $secretAnalysis['contains_colon'] ? 'Secret token appears to be a bot token (contains colon). Use telegram_webhook_secret instead.' : null,
]);
return;
}
// بررسی طول: Telegram محدودیتی برای طول ندارد اما باید معقول باشد
if ($secretToken && strlen($secretToken) > 256) {
TelegramProxyLogger::error("setWebhook secret_token validation failed", [
'secret_token_length' => strlen($secretToken),
'reason' => 'Secret token is too long (max 256 characters)'
]);
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Secret token is too long']);
return;
}
}
$url = sprintf('%s/bot%s/%s', $telegramBase, $botToken, $method);
TelegramProxyLogger::info("Calling Telegram API", [
'method' => $method,
'telegram_host' => $telegramBase,
'payload_size' => strlen(json_encode($payload))
]);
$result = callTelegramApi($url, $payload);
TelegramProxyLogger::info("Telegram API response received", [
'method' => $method,
'ok' => $result['ok'] ?? false,
'status_code' => $result['status'] ?? null,
'error' => $result['error'] ?? null,
'description' => $result['description'] ?? null
]);
// اگر خطا داریم، اطلاعات بیشتر برگردان
if (!$result['ok'] && isset($result['error'])) {
$result['error_detail'] = $result['error'];
TelegramProxyLogger::error("Telegram API call failed", [
'method' => $method,
'error' => $result['error'],
'status_code' => $result['status'] ?? null,
'raw_response' => isset($result['raw_response']) ? substr($result['raw_response'], 0, 500) : null
]);
}
http_response_code($result['status'] ?? 200);
echo json_encode($result);
return;
}
// مسیر webhook - باید بدون نیاز به authentication کار کند (تلگرام مستقیماً به این مسیر درخواست می‌دهد)
if ($path === '/telegram/webhook' && $_SERVER['REQUEST_METHOD'] === 'POST') {
// این endpoint توسط تلگرام برای تست webhook و ارسال پیام‌ها استفاده می‌شود
// تلگرام secret header را خودکار ارسال می‌کند (اگر تنظیم شده باشد)
// لاگ جزئیات درخواست
$requestHeaders = [];
foreach ($_SERVER as $key => $value) {
if (strpos($key, 'HTTP_') === 0 || in_array($key, ['REQUEST_METHOD', 'REQUEST_URI', 'SERVER_NAME'])) {
$requestHeaders[$key] = $value;
}
}
TelegramProxyLogger::info("Webhook endpoint called", [
'path' => $path,
'method' => $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN',
'remote_addr' => $_SERVER['REMOTE_ADDR'] ?? null,
'has_internal_webhook' => !empty($internalWebhook),
'internal_webhook_url' => $internalWebhook,
]);
if (!$internalWebhook) {
TelegramProxyLogger::error("❌ Internal webhook URL not configured - DETAILED ERROR", [
'config_loaded' => defined('TG_PROXY_CONFIG'),
'config_keys' => defined('TG_PROXY_CONFIG') ? array_keys($config) : [],
'internal_webhook_in_config' => isset($config['internal_webhook_url']),
'internal_webhook_value' => $config['internal_webhook_url'] ?? null,
'problem' => 'internal_webhook_url is not set in config.php',
'solution' => 'Set internal_webhook_url in config.php to: https://hsxn.hesabix.ir/api/v1/integrations/telegram/webhook/{WEBHOOK_SECRET}',
]);
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'INTERNAL_WEBHOOK_NOT_SET']);
return;
}
// تحلیل internal_webhook_url
$webhookParts = parse_url($internalWebhook);
$webhookSecret = basename($webhookParts['path'] ?? '');
$secretTokenHeader = $_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] ?? null;
TelegramProxyLogger::info("Webhook forwarding details", [
'internal_webhook_url' => $internalWebhook,
'parsed_url' => [
'scheme' => $webhookParts['scheme'] ?? null,
'host' => $webhookParts['host'] ?? null,
'path' => $webhookParts['path'] ?? null,
'extracted_secret' => $webhookSecret,
'secret_length' => strlen($webhookSecret),
],
'telegram_headers' => [
'has_secret_token_header' => !empty($secretTokenHeader),
'secret_token_header_preview' => $secretTokenHeader ? substr($secretTokenHeader, 0, 10) . '...' : null,
'x_telegram_proxy' => isset($_SERVER['HTTP_X_TELEGRAM_PROXY']),
],
'proxy_config' => [
'has_proxy_key' => !empty($proxyKey),
'will_forward_proxy_key' => !empty($proxyKey) && isset($_SERVER['HTTP_X_TELEGRAM_PROXY']),
],
]);
$rawBody = file_get_contents('php://input') ?: '';
// اگر body خالی است (مثلاً برای تست webhook)، پاسخ موفق برگردان
if (empty($rawBody)) {
TelegramProxyLogger::info("Webhook test request received (empty body)", [
'internal_webhook_url' => $internalWebhook,
'will_forward' => false,
]);
http_response_code(200);
echo json_encode(['ok' => true]);
return;
}
// لاگ درخواست webhook
$bodyData = json_decode($rawBody, true);
$updateId = $bodyData['update_id'] ?? null;
$messageText = $bodyData['message']['text'] ?? null;
TelegramProxyLogger::info("Webhook request received - DETAILED", [
'update_id' => $updateId,
'message_text' => $messageText ? substr($messageText, 0, 50) : null,
'body_size' => strlen($rawBody),
'message_type' => isset($bodyData['message']) ? 'message' : (isset($bodyData['callback_query']) ? 'callback_query' : 'unknown'),
'internal_webhook_url' => $internalWebhook,
'extracted_webhook_secret' => $webhookSecret,
]);
// بررسی اینکه آیا این درخواست از تلگرام است یا از پروکسی داخلی
// اگر header X-Telegram-Proxy وجود دارد، از proxyKey استفاده می‌کنیم
$isFromProxy = isset($_SERVER['HTTP_X_TELEGRAM_PROXY']);
$forwardKey = ($isFromProxy && $proxyKey) ? $proxyKey : null;
TelegramProxyLogger::info("Forwarding webhook to internal server", [
'target_url' => $internalWebhook,
'target_secret' => $webhookSecret,
'has_forward_key' => !empty($forwardKey),
'will_forward_telegram_secret_header' => !empty($secretTokenHeader),
'body_size' => strlen($rawBody),
]);
// Forward webhook به سرور اصلی
$result = forwardWebhook($internalWebhook, $rawBody, $forwardKey);
// برای تلگرام، همیشه باید پاسخ موفق برگردانیم (حتی اگر forward ناموفق باشد)
// چون تلگرام اگر پاسخ ناموفق بگیرد، webhook را reject می‌کند
http_response_code(200);
if ($result['ok']) {
// Forward موفق بود
TelegramProxyLogger::info("Webhook forwarded successfully", [
'update_id' => $updateId,
'status_code' => $result['status'] ?? 200
]);
echo json_encode(['ok' => true]);
} else {
// Forward ناموفق بود، اما برای تلگرام پاسخ موفق می‌دهیم
TelegramProxyLogger::error("Webhook forward failed", [
'update_id' => $updateId,
'error' => $result['error'] ?? 'Unknown error',
'target_url' => $internalWebhook
]);
echo json_encode(['ok' => true]);
}
return;
}
http_response_code(404);
echo json_encode([
'ok' => false,
'error' => 'NOT_FOUND',
'path' => $path,
'method' => $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN',
]);