49 lines
1.8 KiB
PHP
49 lines
1.8 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use Closure;
|
|
use Illuminate\Http\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
/**
|
|
* Serves the operator console only on the hostnames listed in ADMIN_HOSTS.
|
|
*
|
|
* Responds 404 (not 403) on any other host: a public domain must not even
|
|
* disclose that an admin console exists here. Empty list = no restriction, so
|
|
* this can be rolled out without locking anyone out.
|
|
*
|
|
* Host-based rather than IP-based on purpose: behind a reverse proxy without
|
|
* TrustProxies, request->ip() is the proxy's address, so an IP allowlist would
|
|
* be misleading. The real network-level control stays in the proxy.
|
|
*/
|
|
class RestrictAdminHost
|
|
{
|
|
public function handle(Request $request, Closure $next): Response
|
|
{
|
|
// Path-scoped and prepended to the `web` group rather than attached to
|
|
// the admin route group: route middleware is reordered by Laravel's
|
|
// priority list, which puts `auth` first — a guest would then be
|
|
// redirected to /login and thereby learn the console exists. Running
|
|
// ahead of the whole route stack makes the 404 deterministic.
|
|
if (! $request->is('admin', 'admin/*')) {
|
|
return $next($request);
|
|
}
|
|
|
|
// Lowercased here as well as in the config: DNS names are
|
|
// case-insensitive and getHost() always returns lowercase, so a
|
|
// capitalised ADMIN_HOSTS entry must not lock operators out — whatever
|
|
// route the value took into the config (env, cached config, runtime set).
|
|
$allowed = array_map(
|
|
fn ($host) => strtolower((string) $host),
|
|
(array) config('admin_access.hosts', []),
|
|
);
|
|
|
|
if ($allowed !== [] && ! in_array($request->getHost(), $allowed, true)) {
|
|
abort(404);
|
|
}
|
|
|
|
return $next($request);
|
|
}
|
|
}
|