Preview: AccessControlMiddleware.php
Size: 4.52 KB
/home/getspomw/royalsquad.us/app/Http/Middleware/AccessControlMiddleware.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use App\Models\AccessControl;
use Stevebauman\Location\Facades\Location;
use Symfony\Component\HttpFoundation\Response;
class AccessControlMiddleware
{
// Deny list behavior: only block when rule exists AND is_allowed = false
// Never block by default.
// Common SEO bots we’ll verify by reverse DNS
protected $botUAs = [
'Googlebot','AdsBot-Google','APIs-Google',
'Bingbot','DuckDuckBot','AhrefsBot','SemrushBot',
'facebookexternalhit','Twitterbot','LinkedInBot',
];
public function handle(Request $request, Closure $next): Response
{
// 0) Always allow SEO-critical endpoints
if ($this->isSeoEndpoint($request)) {
return $next($request);
}
// 1) Allow verified crawlers (prevents 403 to Googlebot etc.)
if ($this->isLikelyCrawler($request) && $this->isVerifiedCrawler($request->ip())) {
return $next($request);
}
$ip = $request->ip();
// 2) Explicit IP rule takes precedence
$ipRule = AccessControl::where('type', 'ip')
->where('value', $ip)
->first();
if ($ipRule) {
// If explicitly denied → block; if allowed → allow; if ambiguous → allow
if ($ipRule->is_allowed === 0) {
return $this->blockedResponse();
}
return $next($request); // explicitly allowed (or not denied)
}
// 3) Country rule (fail-open if lookup fails OR there is no rule)
$countryCode = $this->countryFromIp($ip); // returns 'IN', 'US', etc., or null
if ($countryCode) {
$countryRule = AccessControl::where('type', 'country')
->where('value', strtoupper($countryCode))
->first();
// Only block if there is an explicit deny rule for this country
if ($countryRule && (int)$countryRule->is_allowed === 0) {
return $this->blockedResponse();
}
// If rule says allowed or no rule found → allow
return $next($request);
}
// GeoIP failed → allow (do NOT block on failure)
return $next($request);
}
protected function isSeoEndpoint(Request $r): bool
{
$path = ltrim($r->path(), '/');
if ($path === 'robots.txt') return true;
if (preg_match('#^sitemap(\.xml|[-_].*\.xml)?$#i', $path)) return true;
if (preg_match('#^sitemap/.*\.xml$#i', $path)) return true;
return false;
}
protected function isLikelyCrawler(Request $r): bool
{
$ua = $r->userAgent() ?? '';
foreach ($this->botUAs as $needle) {
if (stripos($ua, $needle) !== false) return true;
}
return false;
}
/**
* Verify crawler by reverse DNS then forward DNS.
* Google: IP -> rdns ends with googlebot.com/google.com, then rdns -> A includes original IP.
*/
protected function isVerifiedCrawler(string $ip): bool
{
$rdns = @gethostbyaddr($ip);
if (!$rdns) return false;
$allowedDomains = [
'googlebot.com','google.com', // Google
'search.msn.com', // Bing
'ahrefs.com', // Ahrefs
'semrush.com', // Semrush
'duckduckgo.com', // DuckDuckGo
];
$matchesAllowed = false;
foreach ($allowedDomains as $suffix) {
if (preg_match('/\.' . preg_quote($suffix, '/') . '$/i', $rdns)) {
$matchesAllowed = true;
break;
}
}
if (!$matchesAllowed) return false;
$fwd = @gethostbynamel($rdns);
return $fwd && in_array($ip, $fwd, true);
}
protected function countryFromIp(string $ip): ?string
{
try {
// Optional: cache to reduce latency
return cache()->remember("geoip_cc:{$ip}", now()->addMinutes(10), function () use ($ip) {
$pos = Location::get($ip);
return $pos ? strtoupper((string)$pos->countryCode) : null;
});
} catch (\Throwable $e) {
\Log::warning('GeoIP failed: '.$e->getMessage());
return null; // fail open
}
}
protected function blockedResponse(): Response
{
// 451 is clearer than 403 for policy/geo-based blocks and less scary for SEO
return response()->view('maintenance', [], 451);
}
}
Directory Contents
Dirs: 0 × Files: 1