From d039e5082a7296d8de46fa6af71c2472efb443b6 Mon Sep 17 00:00:00 2001 From: DoarMihai Date: Sun, 30 Mar 2025 22:52:35 +0300 Subject: [PATCH] [#2][#5][#7] dev al arround to add lots of functionality, still have some work on the online proxy --- app/Console/Commands/TestProxies.php | 50 ++- app/Http/Controllers/ProxyController.php | 93 ++++ app/Http/Controllers/ProxyListController.php | 13 + .../Controllers/SubscriptionController.php | 13 + app/Services/ProxyService.php | 424 ++++++++++++++++++ resources/views/layouts/navigation.blade.php | 126 ++++-- resources/views/layouts/proxy.blade.php | 76 ++++ resources/views/proxies/index.blade.php | 18 + resources/views/proxy/browse.blade.php | 25 ++ resources/views/proxy/home.blade.php | 31 ++ resources/views/subscriptions/index.blade.php | 18 + routes/web.php | 13 + 12 files changed, 832 insertions(+), 68 deletions(-) create mode 100644 app/Http/Controllers/ProxyController.php create mode 100644 app/Http/Controllers/ProxyListController.php create mode 100644 app/Http/Controllers/SubscriptionController.php create mode 100644 app/Services/ProxyService.php create mode 100644 resources/views/layouts/proxy.blade.php create mode 100644 resources/views/proxies/index.blade.php create mode 100644 resources/views/proxy/browse.blade.php create mode 100644 resources/views/proxy/home.blade.php create mode 100644 resources/views/subscriptions/index.blade.php diff --git a/app/Console/Commands/TestProxies.php b/app/Console/Commands/TestProxies.php index 824a573..6aeb3e1 100644 --- a/app/Console/Commands/TestProxies.php +++ b/app/Console/Commands/TestProxies.php @@ -81,42 +81,50 @@ class TestProxies extends Command protected function testProxy($proxy, &$results) { - return new \GuzzleHttp\Promise\Promise(function() use ($proxy, &$results) { - try { - $client = new \GuzzleHttp\Client([ - 'proxy' => $proxy->proxy, - 'timeout' => $this->timeout, - 'verify' => false, - 'http_errors' => false - ]); + $client = new \GuzzleHttp\Client([ + 'timeout' => $this->timeout, + 'verify' => false, + 'http_errors' => false + ]); - $response = $client->get($this->testUrl); + return $client->getAsync($this->testUrl, [ + 'proxy' => $proxy->proxy + ])->then( + function($response) use ($proxy, &$results) { $body = json_decode($response->getBody(), true); if ($response->getStatusCode() === 200 && isset($body['unique_id'])) { // Proxy is working, increase score $proxy->increment('score'); + $proxy->update(['last_tested_at' => now()]); $results['working']++; } else { // Proxy returned invalid response $proxy->decrement('score'); + $proxy->update(['last_tested_at' => now()]); $results['failed']++; } - } catch (\GuzzleHttp\Exception\ConnectException $e) { - // Connection failed - $proxy->decrement('score'); - $results['failed']++; - } catch (\GuzzleHttp\Exception\RequestException $e) { - if ($e->getCode() === CURLE_OPERATION_TIMEDOUT) { - // Request timed out - $proxy->decrement('score'); - $results['timeout']++; - } else { - // Other request error + }, + function($exception) use ($proxy, &$results) { + if ($exception instanceof \GuzzleHttp\Exception\ConnectException) { + // Connection failed $proxy->decrement('score'); + $proxy->update(['last_tested_at' => now()]); $results['failed']++; + } elseif ($exception instanceof \GuzzleHttp\Exception\RequestException) { + if ($exception->getCode() === CURLE_OPERATION_TIMEDOUT) { + // Request timed out + $proxy->decrement('score'); + $proxy->update(['last_tested_at' => now()]); + $results['timeout']++; + } else { + // Other request error + $proxy->decrement('score'); + $proxy->update(['last_tested_at' => now()]); + $results['failed']++; + } } } - }); + ); } } diff --git a/app/Http/Controllers/ProxyController.php b/app/Http/Controllers/ProxyController.php new file mode 100644 index 0000000..e4f3cc7 --- /dev/null +++ b/app/Http/Controllers/ProxyController.php @@ -0,0 +1,93 @@ +proxyService = $proxyService; + } + + public function home() + { + return view('proxy.home'); + } + + public function browse(Request $request) + { + $request->validate([ + 'url' => 'required' + ]); + + try { + // Decode the URL if it's base64 encoded + $url = $request->url; + if (base64_encode(base64_decode($url, true)) === $url) { + // Decode base64 and then URL decode + $url = urldecode(base64_decode($url)); + } + + // Log the URL processing + \Log::info('Processing URL', [ + 'original' => $request->url, + 'decoded' => $url + ]); + + // Validate the decoded URL + if (!filter_var($url, FILTER_VALIDATE_URL)) { + throw new \Exception('Invalid URL format: ' . $url); + } + + $content = $this->proxyService->fetchUrl($url); + + // Get content type from response + $contentType = $this->proxyService->getLastContentType(); + + // Log the content type for debugging + \Log::info('Content type received', [ + 'url' => $url, + 'content_type' => $contentType, + 'is_css' => strpos($contentType, 'text/css') !== false || + strpos($contentType, 'application/css') !== false || + strpos($url, '.css') !== false + ]); + + // If it's a CSS file, serve it with the correct content type + if (strpos($contentType, 'text/css') !== false || + strpos($contentType, 'application/css') !== false || + strpos($url, '.css') !== false) { + + \Log::info('Serving CSS file', [ + 'url' => $url, + 'content_type' => $contentType, + 'content_length' => strlen($content) + ]); + + return response($content) + ->header('Content-Type', 'text/css') + ->header('Cache-Control', 'public, max-age=3600') + ->header('Access-Control-Allow-Origin', '*') + ->header('X-Content-Type-Options', 'nosniff'); + } + + // For HTML content, return the view + return view('proxy.browse', [ + 'url' => $url, + 'content' => $content + ]); + } catch (\Exception $e) { + \Log::error('Proxy error', [ + 'url' => $request->url, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + return back()->withErrors(['url' => 'Failed to fetch URL: ' . $e->getMessage()]); + } + } +} diff --git a/app/Http/Controllers/ProxyListController.php b/app/Http/Controllers/ProxyListController.php new file mode 100644 index 0000000..22b9658 --- /dev/null +++ b/app/Http/Controllers/ProxyListController.php @@ -0,0 +1,13 @@ +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; + } +} diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index c64bf64..acdb926 100644 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -15,41 +15,54 @@ {{ __('Dashboard') }} + + {{ __('Free Proxies') }} + + + {{ __('Subscriptions') }} + @@ -70,31 +83,50 @@ {{ __('Dashboard') }} + + {{ __('Free Proxies') }} + + + {{ __('Subscriptions') }} + -
-
-
{{ Auth::user()->name }}
-
{{ Auth::user()->email }}
-
+ @auth +
+
+
{{ Auth::user()->name }}
+
{{ Auth::user()->email }}
+
-
- - {{ __('Profile') }} - - - -
- @csrf - - - {{ __('Log Out') }} +
+ + {{ __('Profile') }} - + + +
+ @csrf + + + {{ __('Log Out') }} + +
+
-
+ @else +
+
+ + {{ __('Login') }} + + + {{ __('Register') }} + +
+
+ @endauth
diff --git a/resources/views/layouts/proxy.blade.php b/resources/views/layouts/proxy.blade.php new file mode 100644 index 0000000..aca428a --- /dev/null +++ b/resources/views/layouts/proxy.blade.php @@ -0,0 +1,76 @@ + + + + + + Web Proxy - @yield('title') + @vite(['resources/css/app.css', 'resources/js/app.js']) + + +
+ + + + +
+
+ @yield('content') +
+
+
+ + diff --git a/resources/views/proxies/index.blade.php b/resources/views/proxies/index.blade.php new file mode 100644 index 0000000..d12b2bf --- /dev/null +++ b/resources/views/proxies/index.blade.php @@ -0,0 +1,18 @@ + + +

+ {{ __('Free Proxies') }} +

+
+ +
+
+
+
+

Available Free Proxies

+

This page will display a list of free proxies. Content coming soon.

+
+
+
+
+
\ No newline at end of file diff --git a/resources/views/proxy/browse.blade.php b/resources/views/proxy/browse.blade.php new file mode 100644 index 0000000..628638b --- /dev/null +++ b/resources/views/proxy/browse.blade.php @@ -0,0 +1,25 @@ +@extends('layouts.proxy') + +@section('title', 'Browsing: ' . $url) + +@section('content') +
+
+
+
+ @csrf + + +
+
+ +
+ {!! $content !!} +
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/proxy/home.blade.php b/resources/views/proxy/home.blade.php new file mode 100644 index 0000000..9ab8117 --- /dev/null +++ b/resources/views/proxy/home.blade.php @@ -0,0 +1,31 @@ +@extends('layouts.proxy') + +@section('title', 'Home') + +@section('content') +
+
+

Web Proxy

+ +
+ @csrf +
+ +
+ + +
+ @error('url') +

{{ $message }}

+ @enderror +
+
+
+
+@endsection diff --git a/resources/views/subscriptions/index.blade.php b/resources/views/subscriptions/index.blade.php new file mode 100644 index 0000000..644254e --- /dev/null +++ b/resources/views/subscriptions/index.blade.php @@ -0,0 +1,18 @@ + + +

+ {{ __('Subscriptions') }} +

+
+ +
+
+
+
+

Available Subscriptions

+

This page will display available subscription plans. Content coming soon.

+
+
+
+
+
\ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 0ba88c2..57c72c8 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,9 @@ name('proxy.home'); +Route::match(['get', 'post'], '/browse', [ProxyController::class, 'browse'])->name('proxy.browse'); + +// Free Proxies routes +Route::get('/proxies', [ProxyListController::class, 'index'])->name('proxies.index'); + +// Subscription routes +Route::get('/subscriptions', [SubscriptionController::class, 'index'])->name('subscriptions.index'); + require __DIR__.'/auth.php';