bind-hosts nimmt keinen Wert mehr an, der kein Hostname sein kann

main
nexxo 2026-08-04 18:32:07 +02:00
parent 5967c56d16
commit 620539a512
2 changed files with 82 additions and 5 deletions

View File

@ -53,6 +53,15 @@ class BindHosts extends Command
'FILES_HOST' => (string) $this->option('files'),
];
foreach ($wanted as $key => $value) {
if ($value !== '' && ! $this->isHostList($value)) {
$this->error("{$key}: „{$value}“ ist kein Hostname.");
$this->line('Erwartet wird ein Name wie app.example.com, mehrere kommagetrennt.');
return self::FAILURE;
}
}
$missing = [];
foreach ($wanted as $key => $value) {
@ -113,6 +122,43 @@ class BindHosts extends Command
return self::SUCCESS;
}
/** Eine Komma-Liste, in der JEDER Eintrag ein Hostname ist. */
private function isHostList(string $value): bool
{
$names = array_map('trim', explode(',', $value));
if ($names === [] || in_array('', $names, true)) {
return false;
}
foreach ($names as $name) {
if (! $this->isHost($name)) {
return false;
}
}
return true;
}
/**
* Dasselbe Muster, das der root-eigene Helfer in `apply-proxy-hosts`
* benutzt, bevor er einen Namen in die Proxy-Konfiguration schreibt
* (deploy/install-agent.sh).
*
* Der Anlass ist konkret: `[www.example.com](https://www.example.com)` ist
* als Hostname offensichtlich Unsinn und wurde trotzdem geschrieben, weil
* die Zeile die Form KEY=value hatte. Das Repo kennt diese Falle schon
* RestrictConsoleNetwork::isNetwork() gibt es, weil ein Eintrag, der nichts
* trifft, sonst „stored happily and reports success".
*/
private function isHost(string $name): bool
{
return (bool) preg_match(
'/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/',
$name,
);
}
/**
* Der Wert eines Schlüssels, oder '' wenn er fehlt ODER leer ist.
*

View File

@ -111,20 +111,51 @@ it('sagt es, wenn schon alles steht, statt eine Sicherung anzulegen', function (
expect(glob($this->envPath.'.bak-*'))->toBeEmpty();
});
it('schreibt einen Wert wörtlich, auch wenn er wie ein Rückverweis aussieht', function () {
it('lehnt einen Wert ab, der wie ein Rückverweis aussieht, statt ihn zu verschlucken', function () {
// SITE_HOST steht schon leer da — genau das, was .env.example wirklich
// ausliefert, und der Pfad, auf dem apply() eine vorhandene Zeile per
// preg_replace ersetzt statt sie nur anzuhängen.
file_put_contents($this->envPath, "APP_URL=https://app.example.test\nSITE_HOST=\n");
// `$1` und `\1` sind das, was preg_replace im ERSATZ als Rückverweis
// deutet und still verschluckt. Diese Datei hält jedes Geheimnis der
// Installation — was hier ankommt, muss ankommen wie eingegeben.
// deutet und still verschluckt hätte — apply() schützt genau davor. Seit
// der Hostnamen-Prüfung kommt der Wert dort aber gar nicht mehr an: `$`
// und `\` sind in keinem Hostnamen erlaubt, also weist die Prüfung ihn
// vorher zurück, statt ihn (unbeschädigt oder nicht) zu schreiben.
$this->artisan('clupilot:bind-hosts', [
'--site' => 'www.example.test$1,foo\1bar',
'--force' => true,
])->assertSuccessful();
])->assertFailed();
expect(file_get_contents($this->envPath))
->toContain('SITE_HOST=www.example.test$1,foo\1bar');
->toBe("APP_URL=https://app.example.test\nSITE_HOST=\n")
->and(glob($this->envPath.'.bak-*'))->toBeEmpty();
});
it('lehnt ab, was kein Hostname sein kann, und schreibt nichts', function () {
file_put_contents($this->envPath, "APP_URL=https://app.example.test\n");
// Genau der Wert, der auf der Produktivmaschine landete. Er ist als
// Hostname offensichtlich Unsinn und wurde trotzdem geschrieben, weil
// EnvFileEditor nur prueft, ob die ZEILE die Form KEY=value hat.
$this->artisan('clupilot:bind-hosts', [
'--site' => '[www.example.test](https://www.example.test)',
'--force' => true,
])->assertFailed();
expect(file_get_contents($this->envPath))->not->toContain('SITE_HOST')
->and(glob($this->envPath.'.bak-*'))->toBeEmpty();
});
it('prüft jeden Namen einer Komma-Liste einzeln', function () {
file_put_contents($this->envPath, "APP_URL=https://app.example.test\n");
// Der erste Name ist gültig — geschrieben werden darf trotzdem nichts,
// sonst stünde die halbe Liste in der Datei.
$this->artisan('clupilot:bind-hosts', [
'--site' => 'www.example.test,nicht gültig',
'--force' => true,
])->assertFailed();
expect(file_get_contents($this->envPath))->not->toContain('SITE_HOST');
});