169 lines
5.3 KiB
PHP
169 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use App\Models\AppSetting;
|
|
use Closure;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Inertia\Inertia;
|
|
|
|
class GeoBlockMiddleware
|
|
{
|
|
private const BYPASS_PATHS = [
|
|
'blocked', 'favicon.ico', 'up',
|
|
'admin', 'login', 'register',
|
|
];
|
|
|
|
public function handle(Request $request, Closure $next)
|
|
{
|
|
// Skip for admin users, static paths, and the blocked page itself
|
|
if ($this->shouldBypass($request)) {
|
|
return $next($request);
|
|
}
|
|
|
|
$settings = AppSetting::get('geo.settings', []);
|
|
|
|
if (empty($settings['enabled'])) {
|
|
return $next($request);
|
|
}
|
|
|
|
$ip = $request->ip();
|
|
|
|
// Never block localhost in dev
|
|
if (in_array($ip, ['127.0.0.1', '::1', '::ffff:127.0.0.1'])) {
|
|
return $next($request);
|
|
}
|
|
|
|
$geoData = $this->fetchGeoData($ip);
|
|
$country = $geoData['countryCode'] ?? null;
|
|
$isProxy = $geoData['proxy'] ?? false;
|
|
$isHosting = $geoData['hosting'] ?? false;
|
|
|
|
// VPN / Proxy check
|
|
if (!empty($settings['vpn_block'])) {
|
|
$vpnProvider = $settings['vpn_provider'] ?? 'none';
|
|
|
|
$isVpn = match ($vpnProvider) {
|
|
'ipqualityscore' => Cache::remember("geo_vpn_iqs_{$ip}", 3600, fn() => $this->checkIpQualityScore($ip, $settings['vpn_api_key'] ?? '')),
|
|
'proxycheck' => Cache::remember("geo_vpn_pc_{$ip}", 3600, fn() => $this->checkProxyCheck($ip, $settings['vpn_api_key'] ?? '')),
|
|
default => ($isProxy || $isHosting), // free ip-api.com fallback
|
|
};
|
|
|
|
if ($isVpn) {
|
|
return $this->blockResponse($request, $settings, 'vpn');
|
|
}
|
|
}
|
|
|
|
// Country check
|
|
if ($country) {
|
|
$mode = $settings['mode'] ?? 'blacklist';
|
|
$blocked = array_map('strtoupper', $settings['blocked_countries'] ?? []);
|
|
$allowed = array_map('strtoupper', $settings['allowed_countries'] ?? []);
|
|
$upper = strtoupper($country);
|
|
|
|
$isBlocked = match ($mode) {
|
|
'whitelist' => !empty($allowed) && !in_array($upper, $allowed),
|
|
default => !empty($blocked) && in_array($upper, $blocked),
|
|
};
|
|
|
|
if ($isBlocked) {
|
|
return $this->blockResponse($request, $settings, 'country');
|
|
}
|
|
}
|
|
|
|
return $next($request);
|
|
}
|
|
|
|
private function shouldBypass(Request $request): bool
|
|
{
|
|
$path = ltrim($request->path(), '/');
|
|
|
|
foreach (self::BYPASS_PATHS as $bypass) {
|
|
if ($path === $bypass || str_starts_with($path, $bypass . '/')) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Skip API routes
|
|
if ($request->is('api/*')) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function blockResponse(Request $request, array $settings, string $reason)
|
|
{
|
|
$message = $settings['block_message'] ?? 'This service is not available in your region.';
|
|
$redirectUrl = $settings['redirect_url'] ?? '';
|
|
|
|
if ($redirectUrl) {
|
|
return redirect()->away($redirectUrl);
|
|
}
|
|
|
|
if ($request->header('X-Inertia')) {
|
|
return Inertia::render('GeoBlocked', [
|
|
'message' => $message,
|
|
'reason' => $reason,
|
|
])->toResponse($request)->setStatusCode(403);
|
|
}
|
|
|
|
return Inertia::render('GeoBlocked', [
|
|
'message' => $message,
|
|
'reason' => $reason,
|
|
])->toResponse($request)->setStatusCode(403);
|
|
}
|
|
|
|
private function fetchGeoData(string $ip): array
|
|
{
|
|
return Cache::remember("geo_data_{$ip}", 3600, function () use ($ip) {
|
|
try {
|
|
$res = Http::timeout(3)->get("http://ip-api.com/json/{$ip}", [
|
|
'fields' => 'countryCode,proxy,hosting',
|
|
]);
|
|
if ($res->ok()) {
|
|
return $res->json() ?? [];
|
|
}
|
|
} catch (\Throwable) {}
|
|
return [];
|
|
});
|
|
}
|
|
|
|
private function checkIpQualityScore(string $ip, string $apiKey): bool
|
|
{
|
|
if (!$apiKey) return false;
|
|
try {
|
|
$res = Http::timeout(4)->get("https://ipqualityscore.com/api/json/ip/{$apiKey}/{$ip}", [
|
|
'strictness' => 1,
|
|
'allow_public_access_points' => 'true',
|
|
'fast' => 'true',
|
|
]);
|
|
if ($res->ok()) {
|
|
$data = $res->json();
|
|
return (bool) ($data['vpn'] ?? $data['proxy'] ?? $data['tor'] ?? false);
|
|
}
|
|
} catch (\Throwable) {}
|
|
return false;
|
|
}
|
|
|
|
private function checkProxyCheck(string $ip, string $apiKey): bool
|
|
{
|
|
if (!$apiKey) return false;
|
|
try {
|
|
$res = Http::timeout(4)->get("https://proxycheck.io/v2/{$ip}", [
|
|
'key' => $apiKey,
|
|
'vpn' => 1,
|
|
'asn' => 0,
|
|
]);
|
|
if ($res->ok()) {
|
|
$data = $res->json();
|
|
$entry = $data[$ip] ?? [];
|
|
return strtolower($entry['proxy'] ?? 'no') === 'yes';
|
|
}
|
|
} catch (\Throwable) {}
|
|
return false;
|
|
}
|
|
}
|