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 . '
+
+
+
+
+
+
+
+ @yield('content')
+
+
+
+', $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') }}
+
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 @@
+
+
+