diff --git a/app/Support/Navigation.php b/app/Support/Navigation.php index d6d7617..39de0f3 100644 --- a/app/Support/Navigation.php +++ b/app/Support/Navigation.php @@ -54,6 +54,13 @@ final class Navigation ['admin.maintenance', 'alert-triangle', 'maintenance', null], ['admin.incidents', 'bell', 'incidents', null], ['admin.capacity', 'database', 'capacity', 'hosts.manage'], + // Hostnamen und ihre Zertifikate. Unter Betrieb und nicht unter + // System: was hier steht, entscheidet, ob eine Adresse + // ANTWORTET — das ist kein Einrichten, das ist Betrieb. Ohne + // diesen Eintrag war die Seite zwar vorhanden, aber über die + // Navigation nicht erreichbar, und eine Seite, die man nur mit + // der URL findet, gibt es für den Betreiber nicht. + ['admin.proxy-hosts', 'globe', 'proxy_hosts', 'site.manage'], ['admin.revenue', 'trending-up', 'revenue', null], // Its own entry, not a section of Settings: what is set here // appears on a legal document, and burying it beside the diff --git a/lang/de/admin.php b/lang/de/admin.php index 49895a1..4b7f82a 100644 --- a/lang/de/admin.php +++ b/lang/de/admin.php @@ -11,6 +11,7 @@ return [ ], 'nav' => [ + 'proxy_hosts' => 'Hostnamen', 'dpa' => 'AV-Vertrag', 'mail_preview' => 'E-Mail-Vorschau', 'inbox' => 'Posteingang', diff --git a/lang/en/admin.php b/lang/en/admin.php index 212d315..30b9d03 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -11,6 +11,7 @@ return [ ], 'nav' => [ + 'proxy_hosts' => 'Hostnames', 'dpa' => 'Processing agreement', 'mail_preview' => 'Mail preview', 'inbox' => 'Inbox', diff --git a/routes/web.php b/routes/web.php index 0055745..f2f3df2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -182,6 +182,22 @@ if ($filesHost !== '') { Route::domain($filesHost)->group(function () { Route::get('/bootstrap.tar.gz', BootstrapArchiveController::class)->name('bootstrap.archive'); Route::get('/{file}', PublicFileController::class)->name('files.public'); + + // Und sonst NICHTS. Beide Zeilen sind nötig, weil sie zwei verschiedene + // Löcher schließen: `/` trifft der Platzhalter oben nicht (er verlangt + // ein Segment), und ein mehrteiliger Pfad ebenfalls nicht. + // + // Ohne sie fiel eine Anfrage an diesen Hostnamen auf die + // host-unabhängigen Routen durch und landete beim Portal — wer + // `files.clupilot.com` aufrief, wurde nach `app.` weitergeleitet. Das + // ist nicht nur unschön: es sagt jedem, der den Namen probiert, wo das + // Portal liegt und dass beide dieselbe Maschine sind. + // + // 404 statt Weiterleitung, dieselbe Haltung wie überall sonst in dieser + // Datei: eine Adresse, die nichts anzubieten hat, hat auch nichts zu + // erzählen. + Route::get('/', fn () => abort(404))->name('files.root'); + Route::any('/{any}', fn () => abort(404))->where('any', '.*'); }); } else { Route::get('/bootstrap.tar.gz', BootstrapArchiveController::class)->name('bootstrap.archive'); diff --git a/tests/Feature/Host/FilesHostTest.php b/tests/Feature/Host/FilesHostTest.php index 824853a..985c54d 100644 --- a/tests/Feature/Host/FilesHostTest.php +++ b/tests/Feature/Host/FilesHostTest.php @@ -147,3 +147,31 @@ it('serves downloads even while the site is hidden', function () { // hätte die Ausnahme mehr aufgemacht als sie sollte. $this->withServerVariables($vonAussen)->get('http://localhost/')->assertStatus(503); }); + +/** + * Der Datei-Hostname bietet genau zwei Dinge an — und sonst nichts. + * + * Ohne diese Grenze fiel jede andere Anfrage auf die host-unabhängigen Routen + * durch und landete beim Portal: wer `files.…` aufrief, wurde nach `app.…` + * weitergeleitet. Das sagt jedem, der den Namen probiert, wo das Portal liegt + * und dass beide auf derselben Maschine sitzen. + */ +it('offers nothing but the archive and the public files', function () { + foreach (['/', '/dashboard', '/login', '/admin', '/tief/verschachtelt/pfad'] as $pfad) { + $antwort = $this->get('http://files.clupilot.test'.$pfad); + + $antwort->assertNotFound(); + // Und ausdrücklich KEINE Weiterleitung: eine 302 auf app.… wäre genau + // der Hinweis, den dieser Hostname nicht geben soll. + expect($antwort->headers->get('Location'))->toBeNull(); + } +}); + +it('still serves its two things after that boundary', function () { + file_put_contents($this->publicDir.'/av-2026-01.pdf', '%PDF-1.4'); + + $this->get('http://files.clupilot.test/av-2026-01.pdf')->assertOk(); + $this->get('http://files.clupilot.test/bootstrap.tar.gz?code='.issuedCode())->assertOk(); + + unlink($this->publicDir.'/av-2026-01.pdf'); +});