PHP 8.3.30
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

Name Size Perms Modified Actions
4.52 KB lrw-rw-rw- 2025-10-03 12:43:56
Edit Download

If ZipArchive is unavailable, a .tar will be created (no compression).