client = new Client([ 'timeout' => 30, 'verify' => false, 'http_errors' => false, 'headers' => [ 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language' => 'en-US,en;q=0.5', 'Connection' => 'keep-alive', 'Upgrade-Insecure-Requests' => '1' ], 'curl' => [ CURLOPT_DNS_USE_GLOBAL_CACHE => true, CURLOPT_DNS_CACHE_TIMEOUT => 3600, CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false ] ]); } public function getLastContentType() { return $this->lastContentType; } public function fetchUrl($url) { try { // Normalize and encode the URL $url = $this->normalizeUrl($url); // Log the request details \Log::info('Fetching URL', [ 'url' => $url ]); try { $response = $this->client->get($url); } catch (RequestException $e) { // Log the specific error \Log::error('Request failed with specific error', [ 'url' => $url, 'error' => $e->getMessage(), 'code' => $e->getCode() ]); // If it's a DNS resolution error, try with IP resolution if (strpos($e->getMessage(), 'Could not resolve host') !== false) { try { // Try to resolve the hostname first $parsedUrl = parse_url($url); $host = $parsedUrl['host']; $ip = gethostbyname($host); if ($ip === $host) { throw new \Exception("Could not resolve hostname: $host"); } // Replace hostname with IP in URL $url = str_replace($host, $ip, $url); // Add Host header to maintain virtual hosting $response = $this->client->get($url, [ 'headers' => [ 'Host' => $host ] ]); } catch (\Exception $dnsError) { \Log::error('DNS resolution failed', [ 'url' => $url, 'error' => $dnsError->getMessage() ]); throw $dnsError; } } else { throw $e; } } // Store the content type $this->lastContentType = $response->getHeaderLine('Content-Type'); // Log the response details \Log::info('Response received', [ 'url' => $url, 'status' => $response->getStatusCode(), 'content_type' => $this->lastContentType, 'headers' => $response->getHeaders() ]); $content = $response->getBody()->getContents(); // Process the content to rewrite URLs $content = $this->processContent($content, $url); return $content; } catch (RequestException $e) { // Log the error \Log::error('Request failed', [ 'url' => $url, 'error' => $e->getMessage() ]); throw $e; } } protected function normalizeUrl($url) { // Remove any existing encoding $url = urldecode($url); // Parse the URL $parsedUrl = parse_url($url); // Reconstruct the URL with proper encoding $scheme = $parsedUrl['scheme'] ?? 'http'; $host = $parsedUrl['host'] ?? ''; $path = $parsedUrl['path'] ?? '/'; $query = $parsedUrl['query'] ?? ''; $fragment = $parsedUrl['fragment'] ?? ''; // Encode path components $path = implode('/', array_map('rawurlencode', explode('/', $path))); // Build the URL $url = $scheme . '://' . $host . $path; if ($query) { $url .= '?' . $query; } if ($fragment) { $url .= '#' . $fragment; } return $url; } protected function processContent($content, $baseUrl) { // Log the content type we're processing \Log::info('Processing content', [ 'url' => $baseUrl, 'content_type' => $this->lastContentType ]); // If the content is CSS, process it differently if (strpos($this->lastContentType, 'text/css') !== false || strpos($this->lastContentType, 'application/css') !== false || strpos($baseUrl, '.css') !== false) { \Log::info('Processing CSS content', [ 'url' => $baseUrl, 'content_type' => $this->lastContentType ]); return $this->processCssContent($content, $baseUrl); } // For HTML content, process only anchor tags with external URLs $patterns = [ // External links in href attributes (only for anchor tags) '/(]*href)=["\'](https?:\/\/[^"\']+)["\']/i' ]; foreach ($patterns as $pattern) { $content = preg_replace_callback( $pattern, function($matches) use ($baseUrl) { $url = $matches[2]; // Skip URLs that are already going through our proxy if (strpos($url, route('proxy.browse')) === 0) { return $matches[0]; } // Only proxy external URLs if (strpos($url, 'http') === 0 && strpos($url, parse_url($baseUrl, PHP_URL_HOST)) === false) { $url = route('proxy.browse') . '?url=' . base64_encode($url); } // Log the URL transformation \Log::info('URL transformed', [ 'original' => $matches[0], 'transformed' => $url ]); return $matches[1] . '="' . htmlspecialchars($url) . '"'; }, $content ); } // Add JavaScript to handle dynamic links and resources $content = $this->addLinkHandlerScript($content); return $content; } protected function processCssContent($content, $baseUrl) { // Log the CSS processing start \Log::info('Starting CSS processing', [ 'url' => $baseUrl, 'content_length' => strlen($content) ]); // Process @import statements $content = preg_replace_callback( '/@import\s+["\'](?!http|https|data:)([^"\']+)["\']/i', function($matches) use ($baseUrl) { $url = $matches[1]; if (strpos($url, '/') === 0) { $parsedUrl = parse_url($baseUrl); $url = $parsedUrl['scheme'] . '://' . $parsedUrl['host'] . $url; } else { $url = rtrim($baseUrl, '/') . '/' . ltrim($url, '/'); } $url = $this->normalizeUrl($url); \Log::info('CSS @import processed', [ 'original' => $matches[0], 'transformed' => $url ]); return '@import "' . htmlspecialchars($url) . '"'; }, $content ); // Process url() functions $content = preg_replace_callback( '/url\(["\']?(?!http|https|data:)([^"\'\)]+)["\']?\)/i', function($matches) use ($baseUrl) { $url = $matches[1]; if (strpos($url, '/') === 0) { $parsedUrl = parse_url($baseUrl); $url = $parsedUrl['scheme'] . '://' . $parsedUrl['host'] . $url; } else { $url = rtrim($baseUrl, '/') . '/' . ltrim($url, '/'); } $url = $this->normalizeUrl($url); \Log::info('CSS url() processed', [ 'original' => $matches[0], 'transformed' => $url ]); return 'url("' . htmlspecialchars($url) . '")'; }, $content ); // Log the processed CSS content \Log::info('CSS content processed', [ 'url' => $baseUrl, 'content_length' => strlen($content), 'content_type' => $this->lastContentType ]); return $content; } protected function processSrcset($srcset, $baseUrl) { // Split srcset into individual sources $sources = preg_split('/,\s*/', $srcset); $processedSources = []; foreach ($sources as $source) { // Split into URL and descriptor $parts = preg_split('/\s+/', trim($source)); $url = $parts[0]; $descriptor = isset($parts[1]) ? ' ' . $parts[1] : ''; // Skip data URLs if (strpos($url, 'data:') === 0) { $processedSources[] = $source; continue; } // Convert relative URL to absolute if (strpos($url, '/') === 0) { $parsedUrl = parse_url($baseUrl); $url = $parsedUrl['scheme'] . '://' . $parsedUrl['host'] . $url; } else { $url = rtrim($baseUrl, '/') . '/' . ltrim($url, '/'); } // Encode the URL $url = $this->normalizeUrl($url); // Rewrite external URLs to go through our proxy if (strpos($url, 'http') === 0 && strpos($url, parse_url($baseUrl, PHP_URL_HOST)) === false) { $url = route('proxy.browse') . '?url=' . base64_encode($url); } $processedSources[] = htmlspecialchars($url) . $descriptor; } return 'srcset="' . implode(', ', $processedSources) . '"'; } protected function addLinkHandlerScript($content) { // Add script before closing body tag $script = << document.addEventListener('DOMContentLoaded', function() { // Function to encode URL using base64 function encodeUrl(url) { // First encode the URL to handle special characters var encoded = encodeURIComponent(url); // Then convert to base64 return btoa(encoded); } // Function to convert relative URL to absolute function toAbsoluteUrl(url) { if (url.startsWith('http')) return url; if (url.startsWith('//')) return window.location.protocol + url; if (url.startsWith('/')) return window.location.origin + url; return window.location.origin + '/' + url; } // Handle dynamically added links document.addEventListener('click', function(e) { var target = e.target; while (target && target.tagName !== 'A') { target = target.parentNode; } if (target && target.tagName === 'A') { var href = target.getAttribute('href'); if (href) { href = toAbsoluteUrl(href); if (href.startsWith('http') && !href.includes(window.location.hostname)) { e.preventDefault(); var encodedUrl = encodeUrl(href); window.location.href = '{{ route('proxy.browse') }}?url=' + encodedUrl; } } } }); // Handle form submissions document.addEventListener('submit', function(e) { var form = e.target; var action = form.getAttribute('action'); if (action) { action = toAbsoluteUrl(action); if (action.startsWith('http') && !action.includes(window.location.hostname)) { e.preventDefault(); var encodedUrl = encodeUrl(action); form.action = '{{ route('proxy.browse') }}?url=' + encodedUrl; form.submit(); } } }); // Process existing anchor tags function processExistingLinks() { document.querySelectorAll('a[href]').forEach(function(link) { var href = link.getAttribute('href'); if (href) { href = toAbsoluteUrl(href); if (href.startsWith('http') && !href.includes(window.location.hostname)) { var encodedUrl = encodeUrl(href); link.setAttribute('href', '{{ route('proxy.browse') }}?url=' + encodedUrl); } } }); } // Process existing links immediately processExistingLinks(); // Handle dynamically added links var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { mutation.addedNodes.forEach(function(node) { if (node.nodeType === 1) { // Element node // Only process anchor tags if (node.tagName === 'A') { var href = node.getAttribute('href'); if (href) { href = toAbsoluteUrl(href); if (href.startsWith('http') && !href.includes(window.location.hostname)) { var encodedUrl = encodeUrl(href); node.setAttribute('href', '{{ route('proxy.browse') }}?url=' + encodedUrl); } } } } }); }); }); // Start observing the document with the configured parameters observer.observe(document.documentElement, { childList: true, subtree: true }); }); EOT; // Insert script before closing body tag $content = preg_replace('/<\/body>/i', $script . '', $content); return $content; } }