From 5bab0fedaf05048b97cb508af38da5b48c35c810 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Thu, 16 Jul 2026 21:28:06 +0000 Subject: [PATCH] Use Host-header loopback for billing force_ip. CURLOPT_RESOLVE was not reliably faster; rewrite the billing base URL to the forced IP and send the original Host header instead. --- app/Services/Billing/BillingClient.php | 50 +++++++++++++++++--------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/app/Services/Billing/BillingClient.php b/app/Services/Billing/BillingClient.php index 7b1ce74..7c62a4a 100644 --- a/app/Services/Billing/BillingClient.php +++ b/app/Services/Billing/BillingClient.php @@ -18,11 +18,36 @@ class BillingClient { private const BALANCE_CACHE_TTL_SECONDS = 8; - private function base(): string + private function configuredBase(): string { return rtrim((string) config('billing.api_url'), '/'); } + /** + * Public billing URL rewritten to a same-host IP when force_ip is set. + * Example: https://ladill.com/api/billing + 127.0.0.1 + * → https://127.0.0.1/api/billing (Host: ladill.com) + */ + private function base(): string + { + $base = $this->configuredBase(); + $forceIp = trim((string) config('billing.force_ip', '')); + if ($forceIp === '') { + return $base; + } + + $parts = parse_url($base); + if (! is_array($parts) || empty($parts['host'])) { + return $base; + } + + $scheme = $parts['scheme'] ?? 'https'; + $path = $parts['path'] ?? ''; + $port = isset($parts['port']) ? ':'.$parts['port'] : ''; + + return $scheme.'://'.$forceIp.$port.$path; + } + private function token(): string { return (string) (config('billing.api_key') ?? ''); @@ -40,29 +65,20 @@ class BillingClient return $pending; } - $host = parse_url($this->base(), PHP_URL_HOST); - $scheme = parse_url($this->base(), PHP_URL_SCHEME) ?: 'https'; - $port = parse_url($this->base(), PHP_URL_PORT); - if (! is_int($port) || $port <= 0) { - $port = $scheme === 'http' ? 80 : 443; - } - + $host = parse_url($this->configuredBase(), PHP_URL_HOST); if (! is_string($host) || $host === '') { return $pending; } - $options = [ - 'curl' => [ - CURLOPT_RESOLVE => [sprintf('%s:%d:%s', $host, $port, $forceIp)], - ], - ]; + $pending = $pending->withHeaders(['Host' => $host]); - // Loopback TLS often uses the public cert; skip verify only for forced local IP. - if (in_array($forceIp, ['127.0.0.1', '::1', 'localhost'], true)) { - $options['verify'] = false; + // Loopback TLS cert is issued for the public host, not the IP. + if (in_array($forceIp, ['127.0.0.1', '::1', 'localhost'], true) + || filter_var($forceIp, FILTER_VALIDATE_IP)) { + $pending = $pending->withOptions(['verify' => false]); } - return $pending->withOptions($options); + return $pending; } private function get(string $path, array $query): array