Files
webproxyscan/app/Services/ProxyService.php
T

425 lines
14 KiB
PHP
Raw Normal View History

<?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;
}
}