This commit is contained in:
@@ -81,42 +81,50 @@ class TestProxies extends Command
|
|||||||
|
|
||||||
protected function testProxy($proxy, &$results)
|
protected function testProxy($proxy, &$results)
|
||||||
{
|
{
|
||||||
return new \GuzzleHttp\Promise\Promise(function() use ($proxy, &$results) {
|
|
||||||
try {
|
|
||||||
$client = new \GuzzleHttp\Client([
|
$client = new \GuzzleHttp\Client([
|
||||||
'proxy' => $proxy->proxy,
|
|
||||||
'timeout' => $this->timeout,
|
'timeout' => $this->timeout,
|
||||||
'verify' => false,
|
'verify' => false,
|
||||||
'http_errors' => 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);
|
$body = json_decode($response->getBody(), true);
|
||||||
|
|
||||||
if ($response->getStatusCode() === 200 && isset($body['unique_id'])) {
|
if ($response->getStatusCode() === 200 && isset($body['unique_id'])) {
|
||||||
// Proxy is working, increase score
|
// Proxy is working, increase score
|
||||||
$proxy->increment('score');
|
$proxy->increment('score');
|
||||||
|
$proxy->update(['last_tested_at' => now()]);
|
||||||
$results['working']++;
|
$results['working']++;
|
||||||
} else {
|
} else {
|
||||||
// Proxy returned invalid response
|
// Proxy returned invalid response
|
||||||
$proxy->decrement('score');
|
$proxy->decrement('score');
|
||||||
|
$proxy->update(['last_tested_at' => now()]);
|
||||||
$results['failed']++;
|
$results['failed']++;
|
||||||
}
|
}
|
||||||
} catch (\GuzzleHttp\Exception\ConnectException $e) {
|
},
|
||||||
|
function($exception) use ($proxy, &$results) {
|
||||||
|
if ($exception instanceof \GuzzleHttp\Exception\ConnectException) {
|
||||||
// Connection failed
|
// Connection failed
|
||||||
$proxy->decrement('score');
|
$proxy->decrement('score');
|
||||||
|
$proxy->update(['last_tested_at' => now()]);
|
||||||
$results['failed']++;
|
$results['failed']++;
|
||||||
} catch (\GuzzleHttp\Exception\RequestException $e) {
|
} elseif ($exception instanceof \GuzzleHttp\Exception\RequestException) {
|
||||||
if ($e->getCode() === CURLE_OPERATION_TIMEDOUT) {
|
if ($exception->getCode() === CURLE_OPERATION_TIMEDOUT) {
|
||||||
// Request timed out
|
// Request timed out
|
||||||
$proxy->decrement('score');
|
$proxy->decrement('score');
|
||||||
|
$proxy->update(['last_tested_at' => now()]);
|
||||||
$results['timeout']++;
|
$results['timeout']++;
|
||||||
} else {
|
} else {
|
||||||
// Other request error
|
// Other request error
|
||||||
$proxy->decrement('score');
|
$proxy->decrement('score');
|
||||||
|
$proxy->update(['last_tested_at' => now()]);
|
||||||
$results['failed']++;
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,11 +15,18 @@
|
|||||||
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
||||||
{{ __('Dashboard') }}
|
{{ __('Dashboard') }}
|
||||||
</x-nav-link>
|
</x-nav-link>
|
||||||
|
<x-nav-link :href="route('proxies.index')" :active="request()->routeIs('proxies.index')">
|
||||||
|
{{ __('Free Proxies') }}
|
||||||
|
</x-nav-link>
|
||||||
|
<x-nav-link :href="route('subscriptions.index')" :active="request()->routeIs('subscriptions.index')">
|
||||||
|
{{ __('Subscriptions') }}
|
||||||
|
</x-nav-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Settings Dropdown -->
|
<!-- Settings Dropdown -->
|
||||||
<div class="hidden sm:flex sm:items-center sm:ms-6">
|
<div class="hidden sm:flex sm:items-center sm:ms-6">
|
||||||
|
@auth
|
||||||
<x-dropdown align="right" width="48">
|
<x-dropdown align="right" width="48">
|
||||||
<x-slot name="trigger">
|
<x-slot name="trigger">
|
||||||
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none transition ease-in-out duration-150">
|
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none transition ease-in-out duration-150">
|
||||||
@@ -50,6 +57,12 @@
|
|||||||
</form>
|
</form>
|
||||||
</x-slot>
|
</x-slot>
|
||||||
</x-dropdown>
|
</x-dropdown>
|
||||||
|
@else
|
||||||
|
<div class="space-x-4">
|
||||||
|
<a href="{{ route('login') }}" class="text-sm text-gray-700 dark:text-gray-500 hover:text-gray-900 dark:hover:text-gray-300">Login</a>
|
||||||
|
<a href="{{ route('register') }}" class="text-sm text-gray-700 dark:text-gray-500 hover:text-gray-900 dark:hover:text-gray-300">Register</a>
|
||||||
|
</div>
|
||||||
|
@endauth
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hamburger -->
|
<!-- Hamburger -->
|
||||||
@@ -70,9 +83,16 @@
|
|||||||
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
||||||
{{ __('Dashboard') }}
|
{{ __('Dashboard') }}
|
||||||
</x-responsive-nav-link>
|
</x-responsive-nav-link>
|
||||||
|
<x-responsive-nav-link :href="route('proxies.index')" :active="request()->routeIs('proxies.index')">
|
||||||
|
{{ __('Free Proxies') }}
|
||||||
|
</x-responsive-nav-link>
|
||||||
|
<x-responsive-nav-link :href="route('subscriptions.index')" :active="request()->routeIs('subscriptions.index')">
|
||||||
|
{{ __('Subscriptions') }}
|
||||||
|
</x-responsive-nav-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Responsive Settings Options -->
|
<!-- Responsive Settings Options -->
|
||||||
|
@auth
|
||||||
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-600">
|
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-600">
|
||||||
<div class="px-4">
|
<div class="px-4">
|
||||||
<div class="font-medium text-base text-gray-800 dark:text-gray-200">{{ Auth::user()->name }}</div>
|
<div class="font-medium text-base text-gray-800 dark:text-gray-200">{{ Auth::user()->name }}</div>
|
||||||
@@ -96,5 +116,17 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-600">
|
||||||
|
<div class="mt-3 space-y-1">
|
||||||
|
<x-responsive-nav-link :href="route('login')">
|
||||||
|
{{ __('Login') }}
|
||||||
|
</x-responsive-nav-link>
|
||||||
|
<x-responsive-nav-link :href="route('register')">
|
||||||
|
{{ __('Register') }}
|
||||||
|
</x-responsive-nav-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endauth
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Web Proxy - @yield('title')</title>
|
||||||
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
</head>
|
||||||
|
<body class="antialiased">
|
||||||
|
<div class="min-h-screen bg-gray-100">
|
||||||
|
<!-- Navigation Bar -->
|
||||||
|
<nav class="bg-white shadow-lg">
|
||||||
|
<div class="max-w-7xl mx-auto px-4">
|
||||||
|
<div class="flex justify-between h-16">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0 flex items-center">
|
||||||
|
<a href="{{ route('proxy.home') }}" class="text-xl font-bold text-gray-800">
|
||||||
|
WebProxyScan
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<!-- Navigation Links -->
|
||||||
|
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
|
||||||
|
<a href="{{ route('dashboard') }}" class="inline-flex items-center px-1 pt-1 text-sm font-medium text-gray-900">
|
||||||
|
{{ __('Dashboard') }}
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('proxies.index') }}" class="inline-flex items-center px-1 pt-1 text-sm font-medium text-gray-900">
|
||||||
|
{{ __('Free Proxies') }}
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('subscriptions.index') }}" class="inline-flex items-center px-1 pt-1 text-sm font-medium text-gray-900">
|
||||||
|
{{ __('Subscriptions') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Navigation -->
|
||||||
|
<div class="hidden sm:flex sm:items-center sm:ml-6">
|
||||||
|
@auth
|
||||||
|
<div class="relative" x-data="{ open: false }">
|
||||||
|
<button @click="open = !open" class="flex items-center text-sm font-medium text-gray-900 hover:text-gray-700 focus:outline-none">
|
||||||
|
<span>{{ Auth::user()->name }}</span>
|
||||||
|
<svg class="ml-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div x-show="open" @click.away="open = false" class="absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5">
|
||||||
|
<div class="py-1">
|
||||||
|
<a href="{{ route('profile.edit') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Profile</a>
|
||||||
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||||
|
Log Out
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="space-x-4">
|
||||||
|
<a href="{{ route('login') }}" class="text-sm text-gray-700 hover:text-gray-900">Login</a>
|
||||||
|
<a href="{{ route('register') }}" class="text-sm text-gray-700 hover:text-gray-900">Register</a>
|
||||||
|
</div>
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="py-12">
|
||||||
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
@yield('content')
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<x-app-layout>
|
||||||
|
<x-slot name="header">
|
||||||
|
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||||
|
{{ __('Free Proxies') }}
|
||||||
|
</h2>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<div class="py-12">
|
||||||
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||||
|
<div class="p-6 text-gray-900">
|
||||||
|
<h3 class="text-lg font-medium mb-4">Available Free Proxies</h3>
|
||||||
|
<p class="text-gray-600">This page will display a list of free proxies. Content coming soon.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-app-layout>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
@extends('layouts.proxy')
|
||||||
|
|
||||||
|
@section('title', 'Browsing: ' . $url)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||||
|
<div class="p-6 text-gray-900">
|
||||||
|
<div class="mb-4">
|
||||||
|
<form action="{{ route('proxy.browse') }}" method="POST" class="flex space-x-2">
|
||||||
|
@csrf
|
||||||
|
<input type="url" name="url" value="{{ htmlspecialchars($url) }}"
|
||||||
|
class="flex-1 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
||||||
|
<button type="submit"
|
||||||
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||||
|
Go
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="prose max-w-none">
|
||||||
|
{!! $content !!}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
@extends('layouts.proxy')
|
||||||
|
|
||||||
|
@section('title', 'Home')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||||
|
<div class="p-6 text-gray-900">
|
||||||
|
<h1 class="text-2xl font-semibold mb-6">Web Proxy</h1>
|
||||||
|
|
||||||
|
<form action="{{ route('proxy.browse') }}" method="POST" class="space-y-4">
|
||||||
|
@csrf
|
||||||
|
<div>
|
||||||
|
<label for="url" class="block text-sm font-medium text-gray-700">Enter URL to browse</label>
|
||||||
|
<div class="mt-1 flex rounded-md shadow-sm">
|
||||||
|
<input type="url" name="url" id="url" required
|
||||||
|
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
|
||||||
|
placeholder="https://example.com"
|
||||||
|
value="{{ old('url') }}">
|
||||||
|
<button type="submit"
|
||||||
|
class="ml-3 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||||
|
Go
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@error('url')
|
||||||
|
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<x-app-layout>
|
||||||
|
<x-slot name="header">
|
||||||
|
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||||
|
{{ __('Subscriptions') }}
|
||||||
|
</h2>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<div class="py-12">
|
||||||
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||||
|
<div class="p-6 text-gray-900">
|
||||||
|
<h3 class="text-lg font-medium mb-4">Available Subscriptions</h3>
|
||||||
|
<p class="text-gray-600">This page will display available subscription plans. Content coming soon.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-app-layout>
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\ProfileController;
|
use App\Http\Controllers\ProfileController;
|
||||||
|
use App\Http\Controllers\ProxyController;
|
||||||
|
use App\Http\Controllers\ProxyListController;
|
||||||
|
use App\Http\Controllers\SubscriptionController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', function () {
|
||||||
@@ -25,4 +28,14 @@ Route::get('/proxy-test', function () {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Proxy routes
|
||||||
|
Route::get('/', [ProxyController::class, 'home'])->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';
|
require __DIR__.'/auth.php';
|
||||||
|
|||||||
Reference in New Issue
Block a user