<?php
declare(strict_types=1);
if (!defined('STORE_ROOT')) { http_response_code(403); exit; }
class PaymentBridge {
private static int $timeout = 15;
private static ?array $configCache = null;
private static int $cacheTTL = 300;
private static function getPaymentNodeUrl(): string {
try {
$url = App::conf('payment_node_url', '');
if ($url && filter_var($url, FILTER_VALIDATE_URL)) return $url;
} catch (\Throwable $e) {}
return 'http://10.91.1.2:49629';
}
private static function loadHmacKey(): ?string {
try {
$key = App::conf('bridge_hmac', '');
if ($key) return $key;
} catch (\Throwable $e) {}
$paths = [
(defined('PRIVATE_DIR') ? PRIVATE_DIR : '/var/www/private') . '/bridge_hmac.key',
'/etc/bridge_hmac.key',
];
foreach ($paths as $p) {
if (file_exists($p)) {
$k = trim(file_get_contents($p));
if ($k) return $k;
}
}
return null;
}
public static function submit(array $orderData): array {
$hmacKey = self::loadHmacKey();
if (!$hmacKey) return ['ok' => false, 'error' => 'Bridge key not configured'];
$payload = json_encode($orderData);
$timestamp = (string)time();
$nonce = bin2hex(random_bytes(16));
$signature = hash_hmac('sha256', $timestamp . '.' . $nonce . '.' . $payload, $hmacKey);
$headers = [
'Content-Type: application/json',
'X-Bridge-Sig: ' . $signature,
'X-Bridge-Ts: ' . $timestamp,
'X-Bridge-Nonce: ' . $nonce,
'X-Bridge-Origin: origin-01',
];
$ch = curl_init(self::getPaymentNodeUrl() . '/api/process');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => self::$timeout,
CURLOPT_CONNECTTIMEOUT => 5,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error || $httpCode !== 200) {
error_log("[PAYMENT_BRIDGE] submit failed: HTTP {$httpCode} - {$error}");
return ['ok' => false, 'error' => 'Payment bridge unreachable'];
}
$decoded = json_decode($response, true);
if (!$decoded || !isset($decoded['sig'])) {
return ['ok' => false, 'error' => 'Invalid bridge response'];
}
$responseSig = $decoded['sig'];
unset($decoded['sig']);
$expectedSig = hash_hmac('sha256', json_encode($decoded), $hmacKey);
if (!hash_equals($expectedSig, $responseSig)) {
error_log("[PAYMENT_BRIDGE] Response signature mismatch");
return ['ok' => false, 'error' => 'Bridge signature invalid'];
}
return $decoded;
}
public static function getConfig(): array {
$cacheFile = (defined('PRIVATE_DIR') ? PRIVATE_DIR : '/var/www/private') . '/.payment_config_cache';
if (self::$configCache === null && file_exists($cacheFile)) {
$cached = json_decode(file_get_contents($cacheFile), true);
if ($cached && ($cached['ts'] ?? 0) > time() - self::$cacheTTL) {
self::$configCache = $cached['data'];
}
}
if (self::$configCache !== null) return self::$configCache;
$hmacKey = self::loadHmacKey();
if (!$hmacKey) return self::fallbackConfig();
$timestamp = (string)time();
$nonce = bin2hex(random_bytes(16));
$payload = 'config';
$signature = hash_hmac('sha256', $timestamp . '.' . $nonce . '.' . $payload, $hmacKey);
$headers = [
'X-Bridge-Sig: ' . $signature,
'X-Bridge-Ts: ' . $timestamp,
'X-Bridge-Nonce: ' . $nonce,
'X-Bridge-Origin: origin-01',
];
$ch = curl_init(self::getPaymentNodeUrl() . '/api/config');
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_CONNECTTIMEOUT => 3,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response && $httpCode === 200) {
$decoded = json_decode($response, true);
if ($decoded && isset($decoded['stripe_pk'])) {
self::$configCache = $decoded;
file_put_contents($cacheFile, json_encode(['ts' => time(), 'data' => $decoded]));
chmod($cacheFile, 0600);
return $decoded;
}
}
error_log("[PAYMENT_BRIDGE] getConfig: payment node returned HTTP {$httpCode}, using fallback");
return self::fallbackConfig();
}
public static function verifyCallback(string $rawBody, string $signature, string $timestamp, string $nonce): bool {
if (!$signature || !$timestamp || !$nonce) return false;
$hmacKey = self::loadHmacKey();
if (!$hmacKey) {
error_log('[PAYMENT_BRIDGE] verifyCallback: no HMAC key configured');
return false;
}
if (abs(time() - (int)$timestamp) > 300) {
error_log('[PAYMENT_BRIDGE] verifyCallback: timestamp too old');
return false;
}
$expected = hash_hmac('sha256', $timestamp . '.' . $nonce . '.' . $rawBody, $hmacKey);
return hash_equals($expected, $signature);
}
public static function clearConfigCache(): void {
self::$configCache = null;
$cacheFile = (defined('PRIVATE_DIR') ? PRIVATE_DIR : '/var/www/private') . '/.payment_config_cache';
if (file_exists($cacheFile)) unlink($cacheFile);
}
private static function fallbackConfig(): array {
$stripePk = App::conf('stripe_pk', '');
$xmr = App::conf('xmr_addr', '');
$usdt = App::conf('usdt_addr', '');
$usdc = App::conf('usdc_addr', '');
$etEmail = App::conf('et_email', '');
return [
'stripe_pk' => $stripePk,
'wallets' => [
'XMR' => ['address' => $xmr, 'network' => 'Monero (XMR mainnet)'],
'USDT' => ['address' => $usdt, 'network' => 'USDT (TRC-20)'],
'USDC' => ['address' => $usdc, 'network' => 'USDC (Polygon ERC20)'],
],
'etransfer' => [
'email' => $etEmail,
],
'fallback' => true,
];
}
}
