Compare commits

..

2 Commits

Author SHA1 Message Date
nexxo dac84f024a Release v1.3.71
tests / pest (push) Failing after 8m16s Details
tests / assets (push) Successful in 21s Details
tests / release (push) Has been skipped Details
The hostnames page is now in the navigation under Betrieb — it had a route and
no entry, which meant it existed only for whoever knew the URL.

And files.… no longer redirects to app.… when called without a path. It answers
404, like every other address in this installation that has nothing to offer:
the redirect told anyone who tried the name where the portal lives and that both
share a machine.

2037 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 23:21:39 +02:00
nexxo 60c501e40d Put the page in the navigation, and stop files. handing out the portal
Two faults, both mine, both reported from the live server.

The page had a route and no navigation entry. A page reachable only by typing
its URL does not exist as far as the operator is concerned, and "Betrieb →
Hostnamen und Zertifikate" was exactly as findable as I had made it: not at all.
It sits under Betrieb rather than System because what is set there decides
whether an address ANSWERS — that is operations, not configuration.

And calling files.… without a path redirected to app.… The host-bound group only
claimed /bootstrap.tar.gz and /{file}; a bare / matches neither, so the request
fell through to the host-agnostic routes and landed on the portal. Two holes,
because / and a multi-segment path miss the placeholder for different reasons,
and both are closed now.

404 rather than a redirect, and that is the point rather than a detail. The
redirect told anybody who tried the name where the portal lives and that both
sit on the same machine — the one thing every other hostname in routes/web.php
is careful not to say. An address with nothing to offer has nothing to tell
either.

2037 tests pass, assets build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 23:21:15 +02:00
6 changed files with 54 additions and 1 deletions

View File

@ -1 +1 @@
1.3.70
1.3.71

View File

@ -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

View File

@ -11,6 +11,7 @@ return [
],
'nav' => [
'proxy_hosts' => 'Hostnamen',
'dpa' => 'AV-Vertrag',
'mail_preview' => 'E-Mail-Vorschau',
'inbox' => 'Posteingang',

View File

@ -11,6 +11,7 @@ return [
],
'nav' => [
'proxy_hosts' => 'Hostnames',
'dpa' => 'Processing agreement',
'mail_preview' => 'Mail preview',
'inbox' => 'Inbox',

View File

@ -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');

View File

@ -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');
});