98 lines
3.9 KiB
PHP
98 lines
3.9 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use App\Support\AdminArea;
|
|
use Closure;
|
|
use Illuminate\Http\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
/**
|
|
* The console answers only on its own hostname — and that hostname answers
|
|
* only the console.
|
|
*
|
|
* Both halves matter. The first keeps the console off the public domains. The
|
|
* second is what actually separates the two products: binding the console
|
|
* routes to a hostname does not stop the CUSTOMER routes from answering there
|
|
* as well, because they are registered without a hostname. Without this,
|
|
* app.clupilot.com and admin.clupilot.com would still serve each other's pages
|
|
* wherever the paths did not collide.
|
|
*
|
|
* 404 in both directions, never 403: a stranger must not learn that a console
|
|
* lives here, and an operator on the wrong host should see the same nothing a
|
|
* stranger does.
|
|
*
|
|
* A handful of endpoints are shared by both sides and must answer on either
|
|
* host — Livewire's own endpoints and the authentication actions. They are
|
|
* listed explicitly below, because the failure mode of getting that list wrong
|
|
* is "the console is broken" rather than "the rule is wrong", and a list you
|
|
* can read is easier to correct than a rule you have to infer.
|
|
*/
|
|
class RestrictAdminHost
|
|
{
|
|
/**
|
|
* Endpoints both the console and the portal need, on whichever host the
|
|
* caller is currently on.
|
|
*
|
|
* `login`, `logout` and `two-factor-challenge` used to be here, from when
|
|
* one sign-in page served both. They are gone: the console has its own at
|
|
* `admin.login`, and leaving the portal's here is what put a registration
|
|
* link in front of an operator — with a 404 behind it, because `register`
|
|
* was never in this list and could not be, since the console has no
|
|
* registration by design.
|
|
*
|
|
* Livewire's component endpoint is guarded separately and more strictly:
|
|
* the console's own middleware is registered as persistent, so an action
|
|
* posted to /livewire/update is re-checked against the component's real
|
|
* route. Letting the path through here does not let anything through there.
|
|
*/
|
|
private const SHARED = [
|
|
'livewire/*',
|
|
'up',
|
|
];
|
|
|
|
public function handle(Request $request, Closure $next): Response
|
|
{
|
|
// Not host-bound (development, a fresh checkout): the console keeps its
|
|
// /admin prefix on any host and nothing is separated. Upgrading must
|
|
// not lock anyone out of a system that was working.
|
|
if (! AdminArea::isHostBound()) {
|
|
return $next($request);
|
|
}
|
|
|
|
$isConsoleRoute = $this->isConsoleRoute($request);
|
|
$onConsoleHost = AdminArea::covers($request->getHost());
|
|
|
|
// The console, reached through a hostname that is not the console's.
|
|
if ($isConsoleRoute && ! $onConsoleHost) {
|
|
abort(404);
|
|
}
|
|
|
|
// The portal or the public site, reached through the console's
|
|
// hostname. Only once exclusivity is switched on: a machine that lists
|
|
// its own IP in ADMIN_HOSTS so the console is reachable without DNS
|
|
// still has to serve the portal from that same address.
|
|
if (AdminArea::isExclusive() && $onConsoleHost && ! $isConsoleRoute && ! $request->is(...self::SHARED)) {
|
|
abort(404);
|
|
}
|
|
|
|
return $next($request);
|
|
}
|
|
|
|
/**
|
|
* Console routes are the ones named `admin.*`.
|
|
*
|
|
* By name rather than by path: the path is exactly what changes between
|
|
* host-bound and fallback mode, and a rule written against it would be
|
|
* wrong in one of them.
|
|
*/
|
|
private function isConsoleRoute(Request $request): bool
|
|
{
|
|
$name = $request->route()?->getName();
|
|
|
|
// Covers admin.* and the alternate-host registrations (admin.viaN.*),
|
|
// which are the same console reached by a recovery hostname.
|
|
return $name !== null && str_starts_with($name, 'admin.');
|
|
}
|
|
}
|