This commit is contained in:
@@ -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']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\ProxyService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ProxyController extends Controller
|
||||
{
|
||||
protected $proxyService;
|
||||
|
||||
public function __construct(ProxyService $proxyService)
|
||||
{
|
||||
$this->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()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ProxyListController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return view('proxies.index');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SubscriptionController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return view('subscriptions.index');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Proxy;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
|
||||
class ProxyService
|
||||
{
|
||||
protected $client;
|
||||
protected $lastContentType;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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)
|
||||
'/(<a[^>]*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 = <<<EOT
|
||||
<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 });
|
||||
});
|
||||
</script>
|
||||
EOT;
|
||||
|
||||
// Insert script before closing body tag
|
||||
$content = preg_replace('/<\/body>/i', $script . '</body>', $content);
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user