Add the central site registry and remote updates
Every installation now reports address and version to sites.kant-it.de every 15 minutes and picks up update orders there (ADR 123). Reports and answers are HMAC-signed with a per-installation key sent on first contact; an order can only install a published release with checksum. The next request after the file swap runs the migrations, so a remote update finishes without anyone in the admin. The platform itself lives in zentrale/ (plain PHP + SQLite, not part of the release archive): overview with outdated, silent and unassigned installations, single and bulk updates, per-site detail and history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
32cb77b72e
commit
723680b7de
39 files changed
+2355
-27
No files matched your search
@@ -315,6 +315,21 @@ final class ArchiveUpdate
|
||||
return ['ok' => true, 'steps' => $schritte, 'migrate' => true, 'token' => $token, 'fassung' => $ziel];
|
||||
}
|
||||
|
||||
/**
|
||||
* Kennzeichnet den offenen zweiten Schritt als von der Zentrale angestoßen (ADR 123).
|
||||
*
|
||||
* Dann wartet niemand in der Verwaltung, der ihn aufruft; die nächste Anfrage mit dem neuen
|
||||
* Code schließt ihn ab (@see RemoteUpdateFinishSubscriber).
|
||||
*/
|
||||
public function markiereFern(): void
|
||||
{
|
||||
$offen = $this->pending();
|
||||
if (null === $offen) {
|
||||
return;
|
||||
}
|
||||
@file_put_contents($this->root.'/'.self::MARKE, json_encode([...$offen, 'fern' => true], JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
/**
|
||||
* Steht ein zweiter Schritt aus? Gibt Fassung und Zeitpunkt zurück, sonst null.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ops;
|
||||
|
||||
use App\Content\PageRepository;
|
||||
use App\Content\SiteSettings;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
/**
|
||||
* Läuft die neue Fassung wirklich? Datenbank, Startseite in der Datenbank und die Startseite als
|
||||
* Seite - als Unteranfrage, die an der Wartungsseite vorbeigeht.
|
||||
*
|
||||
* Der Abschluss eines Updates ruft sie nach den Migrationen auf, aus der Verwaltung und nach
|
||||
* einem Auftrag der Zentrale. Sie braucht eine laufende Hauptanfrage: Die Unteranfrage hängt sich
|
||||
* an deren Request-Stack.
|
||||
*/
|
||||
class Funktionspruefung
|
||||
{
|
||||
public function __construct(private PageRepository $pages, private SiteSettings $settings, private HttpKernelInterface $kernel)
|
||||
{
|
||||
}
|
||||
|
||||
/** null heißt: alles gut. Sonst der Grund, warum die Fassung nicht läuft. */
|
||||
public function __invoke(): ?string
|
||||
{
|
||||
$home = $this->settings->homePath();
|
||||
if (null === $this->pages->findPublished($home)) {
|
||||
return sprintf('Die Startseite /%s ist nicht lesbar.', $home);
|
||||
}
|
||||
$antwort = $this->kernel->handle(Request::create('/'), HttpKernelInterface::SUB_REQUEST, false);
|
||||
if ($antwort->getStatusCode() >= 400) {
|
||||
return sprintf('Die Startseite antwortet mit %d.', $antwort->getStatusCode());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ops\Tasks;
|
||||
|
||||
use App\Ops\ArchiveUpdate;
|
||||
use App\Ops\Updater;
|
||||
use App\Ops\Zentrale;
|
||||
|
||||
/**
|
||||
* Meldet sich bei der Zentrale und führt einen dort angestoßenen Auftrag aus (ADR 123).
|
||||
*
|
||||
* Der einzige Auftrag ist „Update auf Fassung X“. Er läuft über denselben Weg wie der Knopf in
|
||||
* der Verwaltung: Sicherung, Archiv mit Prüfsumme, Dateien tauschen. Den zweiten Schritt - die
|
||||
* Migrationen mit dem neuen Code - erledigt die nächste Anfrage (@see RemoteUpdateFinishSubscriber);
|
||||
* diese Aufgabe stößt sie gleich selbst an, damit die Wartungsseite nicht bis zum nächsten
|
||||
* Besucher stehen bleibt.
|
||||
*/
|
||||
final class ZentraleTask implements Task
|
||||
{
|
||||
/** Zeitgrenze für den Anstoß des zweiten Schritts; die Migrationen laufen in dieser Anfrage. */
|
||||
private const ABSCHLUSS_SEKUNDEN = 300;
|
||||
|
||||
/** @param (\Closure(string): void)|null $anstossen ruft die Website einmal auf; Tests setzen ihn */
|
||||
public function __construct(
|
||||
private Zentrale $zentrale,
|
||||
private ArchiveUpdate $archive,
|
||||
private Updater $updater,
|
||||
private string $siteOrigin,
|
||||
private string $projectDir,
|
||||
private ?\Closure $anstossen = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'zentrale';
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return 'Bei der Zentrale melden';
|
||||
}
|
||||
|
||||
public function explanation(): string
|
||||
{
|
||||
return 'Meldet Adresse und Fassung an sites.kant-it.de und holt dort angestoßene Updates ab. Ohne sie fehlt die Website in der Übersicht, und ein Update aus der Ferne kommt nicht an.';
|
||||
}
|
||||
|
||||
public function interval(): int
|
||||
{
|
||||
return 900;
|
||||
}
|
||||
|
||||
public function run(): string
|
||||
{
|
||||
$grund = $this->zentrale->grundInaktiv();
|
||||
if (null !== $grund) {
|
||||
return $grund;
|
||||
}
|
||||
$this->ergebnisNachtragen();
|
||||
$antwort = $this->zentrale->melden();
|
||||
$auftrag = $antwort['auftrag'];
|
||||
if (null === $auftrag) {
|
||||
return sprintf('Gemeldet: Fassung %s.', $this->updater->version());
|
||||
}
|
||||
|
||||
return $this->ausfuehren($auftrag['id'], $auftrag['aktion'], $auftrag['fassung']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Auftrag, dessen zweiter Schritt in einer anderen Anfrage lief: Was ist daraus geworden?
|
||||
* Steht nichts mehr aus, verrät es der Fortschritt des Updates.
|
||||
*/
|
||||
private function ergebnisNachtragen(): void
|
||||
{
|
||||
$auftrag = $this->zentrale->offenerAuftrag();
|
||||
if (null === $auftrag || array_key_exists('ok', $auftrag) || null !== $this->archive->pending()) {
|
||||
return;
|
||||
}
|
||||
$status = $this->archive->status() ?? [];
|
||||
if ($status['laeuft'] ?? false) {
|
||||
return;
|
||||
}
|
||||
$phase = (string) ($status['phase'] ?? '');
|
||||
$fassung = (string) ($auftrag['fassung'] ?? '');
|
||||
$ok = 'fertig' === $phase || version_compare($this->updater->version(), $fassung, '>=');
|
||||
$this->zentrale->auftragErledigt($ok, (string) ($status['meldung'] ?? ($ok ? 'Fassung '.$this->updater->version().' läuft.' : 'Ohne Ergebnis beendet.')));
|
||||
}
|
||||
|
||||
private function ausfuehren(int $id, string $aktion, string $fassung): string
|
||||
{
|
||||
$this->zentrale->auftragAngenommen($id, $fassung);
|
||||
$ablehnen = function (string $grund): string {
|
||||
$this->zentrale->auftragErledigt(false, $grund);
|
||||
$this->zentrale->melden();
|
||||
|
||||
return 'Auftrag abgelehnt: '.$grund;
|
||||
};
|
||||
if ('update' !== $aktion) {
|
||||
return $ablehnen(sprintf('Unbekannter Auftrag „%s“.', $aktion));
|
||||
}
|
||||
if (1 !== preg_match('/^\d{1,4}\.\d{1,5}\.\d{1,6}$/D', $fassung)) {
|
||||
return $ablehnen('Die Fassung hat nicht die Form 1.2.3.');
|
||||
}
|
||||
if (version_compare($fassung, $this->updater->version(), '<=')) {
|
||||
$this->zentrale->auftragErledigt(true, sprintf('Bereits auf Fassung %s.', $this->updater->version()));
|
||||
$this->zentrale->melden();
|
||||
|
||||
return 'Auftrag erledigt: Diese Installation ist schon auf dem Stand.';
|
||||
}
|
||||
if (is_dir($this->projectDir.'/.git')) {
|
||||
return $ablehnen('Diese Installation ist eine Git-Arbeitskopie; sie wird über Git aktualisiert, nicht aus der Ferne.');
|
||||
}
|
||||
if (null !== $this->archive->pending() || is_file($this->projectDir.'/'.Updater::MAINTENANCE_FILE)) {
|
||||
return $ablehnen('Hier läuft gerade ein Update oder die Wartungsseite ist an.');
|
||||
}
|
||||
|
||||
// Ein Update dauert länger als eine gewöhnliche Aufgabe. Die Antwort an den Besucher ist
|
||||
// da schon unterwegs; abbrechen darf der Lauf jetzt nicht mehr.
|
||||
@ignore_user_abort(true);
|
||||
@set_time_limit(0);
|
||||
$ergebnis = $this->archive->run($fassung);
|
||||
if (!($ergebnis['migrate'] ?? false)) {
|
||||
$letzter = end($ergebnis['steps']) ?: ['detail' => ''];
|
||||
$this->zentrale->auftragErledigt($ergebnis['ok'], (string) $letzter['detail']);
|
||||
$this->zentrale->melden();
|
||||
|
||||
return ($ergebnis['ok'] ? 'Auftrag erledigt: ' : 'Update gescheitert: ').$letzter['detail'];
|
||||
}
|
||||
|
||||
// Die neuen Dateien liegen; die Migrationen gehören in eine Anfrage mit dem neuen Code.
|
||||
$this->archive->markiereFern();
|
||||
$this->anstossen();
|
||||
$this->ergebnisNachtragen();
|
||||
try {
|
||||
$this->zentrale->melden();
|
||||
} catch (\Throwable) {
|
||||
// Das Ergebnis bleibt in var/zentrale.json und geht mit der nächsten Meldung raus.
|
||||
}
|
||||
|
||||
return null === $this->archive->pending()
|
||||
? sprintf('Update auf %s aus der Ferne ausgeführt.', $fassung)
|
||||
: sprintf('Dateien für %s getauscht; die Migrationen laufen mit der nächsten Anfrage.', $fassung);
|
||||
}
|
||||
|
||||
/** Ruft die Website einmal auf. Gelingt das nicht, schließt der nächste Besucher ab. */
|
||||
private function anstossen(): void
|
||||
{
|
||||
$adresse = rtrim($this->siteOrigin, '/').'/';
|
||||
if (null !== $this->anstossen) {
|
||||
($this->anstossen)($adresse);
|
||||
|
||||
return;
|
||||
}
|
||||
if (function_exists('curl_init')) {
|
||||
$curl = curl_init($adresse);
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 2,
|
||||
CURLOPT_CONNECTTIMEOUT => 10,
|
||||
CURLOPT_TIMEOUT => self::ABSCHLUSS_SEKUNDEN,
|
||||
CURLOPT_USERAGENT => 'KantSites-Abschluss',
|
||||
]);
|
||||
curl_exec($curl);
|
||||
curl_close($curl);
|
||||
|
||||
return;
|
||||
}
|
||||
$kontext = stream_context_create(['http' => ['timeout' => self::ABSCHLUSS_SEKUNDEN, 'ignore_errors' => true, 'header' => "User-Agent: KantSites-Abschluss\r\n"]]);
|
||||
@file_get_contents($adresse, false, $kontext);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ops;
|
||||
|
||||
/**
|
||||
* Die Meldung an die Zentrale (ADR 123): welche Domain, welche Fassung - und ob ein Auftrag wartet.
|
||||
*
|
||||
* Jede Installation meldet sich regelmäßig bei sites.kant-it.de. Dort steht die Liste aller
|
||||
* Installationen; dort lässt sich ein Update anstoßen. Die Zentrale verbindet sich nie selbst mit
|
||||
* einer Website: Sie merkt sich den Auftrag, und die Website holt ihn mit der nächsten Meldung ab.
|
||||
* Damit braucht keine Installation eine Tür, die sich von außen öffnen lässt.
|
||||
*
|
||||
* Was gemeldet wird, steht in payload(): Adresse, Fassung, PHP, Datenbankart, Updatestand. Keine
|
||||
* Inhalte, keine Besucher, keine Zugangsdaten.
|
||||
*
|
||||
* Die Echtheit: Beim ersten Kontakt schickt die Installation einen zufälligen Schlüssel mit (über
|
||||
* HTTPS). Danach trägt jede Meldung eine HMAC-Unterschrift damit, und jede Antwort der Zentrale
|
||||
* ebenso. Wer die Kennung einer Installation kennt, kann ohne den Schlüssel weder für sie melden
|
||||
* noch ihr einen Auftrag unterschieben. Ein Auftrag kann ohnehin nur eine veröffentlichte Fassung
|
||||
* mit passender Prüfsumme einspielen (@see ArchiveUpdate).
|
||||
*/
|
||||
class Zentrale
|
||||
{
|
||||
/** Kennung, Schlüssel und offener Auftrag dieser Installation. Bleibt bei Updates erhalten. */
|
||||
public const DATEI = 'var/zentrale.json';
|
||||
|
||||
private const PFAD = '/api/melden';
|
||||
|
||||
/** @var (\Closure(string, string, array<string, string>): array{status: int, body: string, signatur: string})|null */
|
||||
private ?\Closure $senden;
|
||||
|
||||
public function __construct(
|
||||
private string $root,
|
||||
private Updater $updater,
|
||||
private ArchiveUpdate $archive,
|
||||
private string $url,
|
||||
private string $siteOrigin,
|
||||
private string $databaseUrl,
|
||||
?\Closure $senden = null,
|
||||
) {
|
||||
$this->senden = $senden;
|
||||
}
|
||||
|
||||
/**
|
||||
* Meldet diese Installation? Nein, wenn die Zentrale abgeschaltet ist (ZENTRALE_URL=aus) oder
|
||||
* die Website nur auf diesem Rechner läuft - Entwicklungs- und Messinstallationen gehören
|
||||
* nicht in die Liste.
|
||||
*/
|
||||
public function aktiv(): bool
|
||||
{
|
||||
return null === $this->grundInaktiv();
|
||||
}
|
||||
|
||||
public function grundInaktiv(): ?string
|
||||
{
|
||||
if (!str_starts_with($this->url, 'https://')) {
|
||||
return 'Abgeschaltet (ZENTRALE_URL ist nicht gesetzt oder keine HTTPS-Adresse).';
|
||||
}
|
||||
$host = strtolower((string) parse_url($this->siteOrigin, PHP_URL_HOST));
|
||||
if ('' === $host || in_array($host, ['localhost', '127.0.0.1', '[::1]', '::1'], true) || str_ends_with($host, '.localhost') || str_ends_with($host, '.test') || str_ends_with($host, '.invalid')) {
|
||||
return sprintf('Keine Meldung: Die Website läuft lokal (%s).', '' === $host ? 'ohne SITE_ORIGIN' : $host);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schickt die Meldung und gibt den Auftrag zurück, falls einer wartet.
|
||||
*
|
||||
* @return array{auftrag: ?array{id: int, aktion: string, fassung: string}, registriert: bool}
|
||||
*
|
||||
* @throws \RuntimeException wenn die Zentrale nicht oder nicht echt antwortet
|
||||
*/
|
||||
public function melden(): array
|
||||
{
|
||||
$ich = $this->identitaet();
|
||||
$erneut = false;
|
||||
while (true) {
|
||||
$daten = $this->payload($ich);
|
||||
$body = json_encode($daten, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
|
||||
$antwort = $this->post(rtrim($this->url, '/').self::PFAD, $body, [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-KantSites-Signatur' => hash_hmac('sha256', $body, $ich['schluessel']),
|
||||
]);
|
||||
// Die Zentrale kennt diese Kennung nicht (mehr) - etwa nach einem Neuaufsetzen dort.
|
||||
// Dann einmal mit Schlüssel neu anmelden.
|
||||
if (409 === $antwort['status'] && !$erneut) {
|
||||
$ich['registriert'] = false;
|
||||
$erneut = true;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (200 !== $antwort['status']) {
|
||||
throw new \RuntimeException(sprintf('Die Zentrale antwortet mit %d. %s', $antwort['status'], $this->fehlertext($antwort['body'])));
|
||||
}
|
||||
if (!hash_equals(hash_hmac('sha256', $antwort['body'], $ich['schluessel']), $antwort['signatur'])) {
|
||||
throw new \RuntimeException('Die Antwort der Zentrale trägt keine gültige Unterschrift und wird verworfen.');
|
||||
}
|
||||
$inhalt = json_decode($antwort['body'], true);
|
||||
if (!is_array($inhalt) || true !== ($inhalt['ok'] ?? null)) {
|
||||
throw new \RuntimeException('Die Antwort der Zentrale ist unverständlich.');
|
||||
}
|
||||
|
||||
$ich['registriert'] = true;
|
||||
$ich['gemeldet'] = gmdate('c');
|
||||
// Ein gemeldetes Ergebnis ist angekommen; der Auftrag ist damit erledigt.
|
||||
if (isset($daten['auftrag']['ok'])) {
|
||||
unset($ich['auftrag']);
|
||||
}
|
||||
$this->speichern($ich);
|
||||
|
||||
$auftrag = $inhalt['auftrag'] ?? null;
|
||||
if (!is_array($auftrag) || !is_int($auftrag['id'] ?? null) || !is_string($auftrag['aktion'] ?? null) || !is_string($auftrag['fassung'] ?? null)) {
|
||||
$auftrag = null;
|
||||
}
|
||||
|
||||
return ['auftrag' => null === $auftrag ? null : ['id' => $auftrag['id'], 'aktion' => $auftrag['aktion'], 'fassung' => $auftrag['fassung']], 'registriert' => true];
|
||||
}
|
||||
|
||||
/** Merkt sich einen angenommenen Auftrag, bis sein Ergebnis gemeldet ist. */
|
||||
public function auftragAngenommen(int $id, string $fassung): void
|
||||
{
|
||||
$ich = $this->identitaet();
|
||||
$ich['auftrag'] = ['id' => $id, 'fassung' => $fassung, 'seit' => gmdate('c')];
|
||||
$this->speichern($ich);
|
||||
}
|
||||
|
||||
/** Hält das Ergebnis eines Auftrags fest; die nächste Meldung trägt es zur Zentrale. */
|
||||
public function auftragErledigt(bool $ok, string $meldung): void
|
||||
{
|
||||
$ich = $this->identitaet();
|
||||
if (!isset($ich['auftrag'])) {
|
||||
return;
|
||||
}
|
||||
$ich['auftrag']['ok'] = $ok;
|
||||
$ich['auftrag']['meldung'] = mb_substr($meldung, 0, 500);
|
||||
$this->speichern($ich);
|
||||
}
|
||||
|
||||
/** @return array{id?: int, fassung?: string, seit?: string, ok?: bool, meldung?: string}|null */
|
||||
public function offenerAuftrag(): ?array
|
||||
{
|
||||
return $this->identitaet()['auftrag'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Was gemeldet wird.
|
||||
*
|
||||
* @param array<string, mixed> $ich
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function payload(array $ich): array
|
||||
{
|
||||
$status = $this->archive->status();
|
||||
$daten = [
|
||||
'id' => $ich['id'],
|
||||
'adresse' => rtrim($this->siteOrigin, '/'),
|
||||
'fassung' => $this->updater->version(),
|
||||
'php' => PHP_VERSION,
|
||||
'datenbank' => strtolower((string) strtok($this->databaseUrl, ':')),
|
||||
// Über Git aktualisierte Installationen nimmt die Zentrale nicht in Auftrag.
|
||||
'art' => is_dir($this->root.'/.git') ? 'git' : 'archiv',
|
||||
'wartung' => is_file($this->root.'/'.Updater::MAINTENANCE_FILE),
|
||||
'update' => null === $status ? null : [
|
||||
'phase' => (string) ($status['phase'] ?? ''),
|
||||
'meldung' => mb_substr((string) ($status['meldung'] ?? ''), 0, 300),
|
||||
'zeit' => (string) ($status['aktualisiert'] ?? ''),
|
||||
],
|
||||
];
|
||||
if (!($ich['registriert'] ?? false)) {
|
||||
$daten['schluessel'] = $ich['schluessel'];
|
||||
}
|
||||
$auftrag = $ich['auftrag'] ?? null;
|
||||
if (is_array($auftrag)) {
|
||||
$daten['auftrag'] = array_key_exists('ok', $auftrag)
|
||||
? ['id' => $auftrag['id'], 'ok' => $auftrag['ok'], 'meldung' => $auftrag['meldung'] ?? '']
|
||||
: ['id' => $auftrag['id']];
|
||||
}
|
||||
|
||||
return $daten;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function identitaet(): array
|
||||
{
|
||||
$daten = json_decode((string) @file_get_contents($this->root.'/'.self::DATEI), true);
|
||||
if (is_array($daten) && is_string($daten['id'] ?? null) && is_string($daten['schluessel'] ?? null)) {
|
||||
return $daten;
|
||||
}
|
||||
$daten = ['id' => bin2hex(random_bytes(16)), 'schluessel' => bin2hex(random_bytes(32)), 'registriert' => false];
|
||||
$this->speichern($daten);
|
||||
|
||||
return $daten;
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $daten */
|
||||
private function speichern(array $daten): void
|
||||
{
|
||||
$datei = $this->root.'/'.self::DATEI;
|
||||
@mkdir(dirname($datei), 0o775, true);
|
||||
$zwischen = $datei.'.'.bin2hex(random_bytes(4)).'.tmp';
|
||||
if (false === @file_put_contents($zwischen, json_encode($daten, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) || !@rename($zwischen, $datei)) {
|
||||
@unlink($zwischen);
|
||||
throw new \RuntimeException(self::DATEI.' ist nicht schreibbar.');
|
||||
}
|
||||
}
|
||||
|
||||
private function fehlertext(string $body): string
|
||||
{
|
||||
$daten = json_decode($body, true);
|
||||
|
||||
return is_array($daten) && is_string($daten['fehler'] ?? null) ? mb_substr($daten['fehler'], 0, 200) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* POST über HTTPS, ohne Weiterleitungen: Eine Meldung, die woanders hin umgelenkt wird, soll
|
||||
* scheitern und nicht ankommen.
|
||||
*
|
||||
* @param array<string, string> $kopf
|
||||
*
|
||||
* @return array{status: int, body: string, signatur: string}
|
||||
*/
|
||||
private function post(string $url, string $body, array $kopf): array
|
||||
{
|
||||
if (null !== $this->senden) {
|
||||
return ($this->senden)($url, $body, $kopf);
|
||||
}
|
||||
$zeilen = ['User-Agent: KantSites/'.$this->updater->version()];
|
||||
foreach ($kopf as $name => $wert) {
|
||||
$zeilen[] = $name.': '.$wert;
|
||||
}
|
||||
$signatur = '';
|
||||
if (function_exists('curl_init')) {
|
||||
$curl = curl_init($url);
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $body,
|
||||
CURLOPT_HTTPHEADER => $zeilen,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => false,
|
||||
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
CURLOPT_CONNECTTIMEOUT => 10,
|
||||
CURLOPT_TIMEOUT => 20,
|
||||
CURLOPT_HEADERFUNCTION => static function ($curl, string $zeile) use (&$signatur): int {
|
||||
if (1 === preg_match('/^X-KantSites-Signatur:\s*([a-f0-9]{64})\s*$/i', $zeile, $treffer)) {
|
||||
$signatur = strtolower($treffer[1]);
|
||||
}
|
||||
|
||||
return strlen($zeile);
|
||||
},
|
||||
]);
|
||||
$antwort = curl_exec($curl);
|
||||
$status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
|
||||
$fehler = curl_error($curl);
|
||||
curl_close($curl);
|
||||
if (!is_string($antwort)) {
|
||||
throw new \RuntimeException('Die Zentrale ist nicht erreichbar: '.$fehler);
|
||||
}
|
||||
|
||||
return ['status' => $status, 'body' => $antwort, 'signatur' => $signatur];
|
||||
}
|
||||
if (!filter_var(ini_get('allow_url_fopen'), FILTER_VALIDATE_BOOL)) {
|
||||
throw new \RuntimeException('Dieser Server darf keine Adressen abrufen: curl fehlt und allow_url_fopen ist abgeschaltet.');
|
||||
}
|
||||
$kontext = stream_context_create([
|
||||
'http' => ['method' => 'POST', 'header' => implode("\r\n", $zeilen), 'content' => $body, 'timeout' => 20, 'follow_location' => 0, 'ignore_errors' => true],
|
||||
'ssl' => ['verify_peer' => true, 'verify_peer_name' => true],
|
||||
]);
|
||||
$antwort = @file_get_contents($url, false, $kontext);
|
||||
$kopfzeilen = $http_response_header ?? [];
|
||||
if (!is_string($antwort) || [] === $kopfzeilen) {
|
||||
throw new \RuntimeException('Die Zentrale ist nicht erreichbar.');
|
||||
}
|
||||
preg_match('/^HTTP\/\S+\s+(\d{3})/', (string) $kopfzeilen[0], $treffer);
|
||||
foreach ($kopfzeilen as $zeile) {
|
||||
if (1 === preg_match('/^X-KantSites-Signatur:\s*([a-f0-9]{64})\s*$/i', $zeile, $s)) {
|
||||
$signatur = strtolower($s[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return ['status' => (int) ($treffer[1] ?? 0), 'body' => $antwort, 'signatur' => $signatur];
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user