open($path, ZipArchive::CREATE); $z->addFromString('plugin.json', json_encode(['slug' => $slug, 'name' => $slug, 'version' => '1.0.0', 'entry' => 'plugin.php', 'permissions' => $perms])); $z->addFromString('plugin.php', $php); $z->close(); return $path; }; $handlers = <<<'PHP' ['staff_id' => (int) plugin_setting('PLUGIN', 'target', '0'), 'via' => 'Testdienst']); plugin_register_login('go', fn($r) => ['redirect' => (string) ($r['query']['to'] ?? 'https://idp.example.org/auth?x=1')]); plugin_register_login('nope', fn($r) => ['error' => 'Konto nicht freigegeben x']); plugin_register_login('boom', function ($r) { throw new RuntimeException('kaputt'); }); plugin_register_login('empty', fn($r) => 'kein array'); plugin_register_login('link', fn($r) => ['local' => '/staff/plugin.php?p=PLUGIN&page=konto', 'notice' => 'Verknüpft: ' . ($r['staff']['email'] ?? '?')], true); plugin_register_login('linkerr', fn($r) => ['error' => 'Nicht verknüpft', 'back' => '/staff/plugin.php?p=PLUGIN&page=konto'], true); plugin_register_login('linkevil', fn($r) => ['local' => '//evil.example/x', 'notice' => 'x'], true); plugin_register_login('linkback', fn($r) => ['error' => 'x', 'back' => 'https://evil.example/'], true); plugin_register_login('switch', fn($r) => ['staff_id' => 1], true); plugin_register_login('linkgo', fn($r) => ['redirect' => 'https://idp.example.org/link'], true); plugin_register_customer_login('cgo', fn($r) => ['redirect' => 'https://idp.example.org/kunde']); plugin_register_customer_login('cnew', fn($r) => ['customer' => ['email' => (string) ($r['query']['mail'] ?? 'neu@kunde.example'), 'name' => (string) ($r['query']['name'] ?? ''), 'create' => !isset($r['query']['nocreate']), 'via' => 'Testdienst']]); plugin_register_customer_login('cerr', fn($r) => ['error' => 'Kunde abgelehnt x']); plugin_register_login('ccb', fn($r) => ['customer' => ['email' => 'rueck@kunde.example', 'name' => 'Rück Kunde', 'create' => true, 'via' => 'Testdienst']]); plugin_register_login('cerr2', fn($r) => ['audience' => 'customer', 'error' => 'Kundenfehler über die gemeinsame Rückkehr']); plugin_add_filter('customer.login.buttons', fn($html) => $html . 'Kunden-Knopf'); plugin_add_filter('staff.login.buttons', fn($html) => $html . 'Testdienst-Knopf'); PHP; $withPerm = $mkZip('logintest', ['auth.login', 'auth.customer'], str_replace('PLUGIN', 'logintest', $handlers)); $noPerm = $mkZip('loginsneak', [], str_replace('PLUGIN', 'loginsneak', $handlers)); $check(plugin_install_zip($pdo, $withPerm)['ok'] && plugin_install_zip($pdo, $noPerm)['ok'], 'Test-Plugins lassen sich installieren'); $check(plugin_set_active($pdo, 'logintest', true, ['auth.login', 'auth.customer'])['ok'], 'Plugin mit der Berechtigung lässt sich aktivieren'); $check(plugin_set_active($pdo, 'loginsneak', true, [])['ok'], 'Plugin ohne die Berechtigung lässt sich aktivieren'); $check(isset(plugin_known_permissions()['auth.customer']), 'Die Berechtigung auth.customer ist bekannt und beschrieben'); $check(isset(plugin_known_permissions()['auth.login']), 'Die Berechtigung auth.login ist bekannt und beschrieben'); // Mitarbeiter: aktiv und deaktiviert $ins = $pdo->prepare("INSERT INTO staff (name, email, password_hash, role, is_active, created_at) VALUES (?,?,?,?,?,?)"); $ins->execute(['Anna Aktiv', 'anna@firma.example', password_hash('Geheim-1234567', PASSWORD_DEFAULT), 'agent', 1, db_now()]); $annaId = (int) $pdo->lastInsertId(); $ins->execute(['Karl Kaputt', 'karl@firma.example', password_hash('Geheim-1234567', PASSWORD_DEFAULT), 'agent', 0, db_now()]); $karlId = (int) $pdo->lastInsertId(); plugin_set_setting('logintest', 'target', (string) $annaId); plugin_set_setting('loginsneak', 'target', (string) $annaId); // Nur das Plugin mit Berechtigung hat Anmelde-Adressen und Filter $pdo = null; $port = random_int(34000, 34499); $env = array_merge(getenv(), ['DESKHUB_PLUGINS_DIR' => $pluginDir, 'DESKHUB_TESTING' => '1', 'DESKHUB_SQLITE_PATH' => $db, 'DESKHUB_CATALOG_DEFAULTS' => __DIR__ . '/fixtures/no-catalog.php']); $proc = proc_open([PHP_BINARY, '-S', "127.0.0.1:$port", '-t', dirname(__DIR__)], [1 => ['file', $work . '/srv.out', 'w'], 2 => ['file', $work . '/srv.err', 'w']], $pipes, dirname(__DIR__), $env); for ($i = 0; $i < 60; $i++) { $s = @fsockopen('127.0.0.1', $port, $en, $es, 0.2); if ($s) { fclose($s); break; } usleep(100000); } $jar = $work . '/jar'; $http = function (string $path, array $opts = []) use ($port, $jar): array { $c = curl_init("http://127.0.0.1:$port$path"); curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_TIMEOUT => 20, CURLOPT_COOKIEJAR => $jar, CURLOPT_COOKIEFILE => $jar]); $raw = (string) curl_exec($c); $status = (int) curl_getinfo($c, CURLINFO_RESPONSE_CODE); $hs = (int) curl_getinfo($c, CURLINFO_HEADER_SIZE); curl_close($c); $head = substr($raw, 0, $hs); return [$status, substr($raw, $hs), preg_match('/^Location:\s*(.+)$/mi', $head, $m) ? trim($m[1]) : '']; }; $fresh = function () use ($jar): void { @unlink($jar); }; try { // Anmeldeseite: Knopf nur vom Plugin mit Berechtigung [$s, $body] = $http('/staff/login.php'); $check($s === 200 && str_contains($body, 'Testdienst-Knopf') && str_contains($body, 'plugin_login.php?p=logintest&e=ok'), 'Anmeldeseite zeigt den Knopf des Plugins mit Berechtigung'); $check(substr_count($body, 'Testdienst-Knopf') === 1, 'Das Plugin ohne Berechtigung bekommt keinen Knopf auf die Anmeldeseite'); // Anmelde-Adressen: ohne Berechtigung nicht registriert [$s] = $http('/staff/plugin_login.php?p=loginsneak&e=ok'); $check($s === 404, 'Anmelde-Adresse eines Plugins ohne Berechtigung gibt es nicht (404)'); [$s] = $http('/staff/plugin_login.php?p=logintest&e=gibtesnicht'); $check($s === 404, 'Unbekannte Adresse: 404'); [$s] = $http('/staff/plugin_login.php'); $check($s === 404, 'Ohne Angaben: 404'); // Weiterleitung [$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=go'); $check($s === 302 && $loc === 'https://idp.example.org/auth?x=1', 'Weiterleitung auf https wird ausgeführt: ' . $loc); foreach (['javascript:alert(1)', 'http://evil.example/x', 'https://', '//evil.example', 'https://a.example/x y', 'ftp://a.example/'] as $bad) { [$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=go&to=' . rawurlencode($bad)); $check($s === 302 && str_ends_with($loc, '/staff/login.php'), 'Ungültiges Weiterleitungsziel wird abgelehnt: ' . $bad . ' -> ' . $loc); } [$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=go&to=' . rawurlencode('http://localhost:9000/auth')); $check($s === 302 && $loc === 'http://localhost:9000/auth', 'http ist nur zu localhost erlaubt'); // Fehler und Absturz: zurück zur Anmeldeseite mit Meldung (maskiert) $fresh(); [$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=nope'); $check($s === 302 && str_ends_with($loc, '/staff/login.php'), 'Fehler führt zurück zur Anmeldeseite'); [$s, $body] = $http('/staff/login.php'); $check(str_contains($body, 'Konto nicht freigegeben <b>x</b>') && !str_contains($body, 'x'), 'Fehlermeldung wird angezeigt und maskiert'); foreach (['boom', 'empty'] as $e) { [$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=' . $e); $check($s === 302 && str_ends_with($loc, '/staff/login.php'), "Plugin-Fehler ($e) führt zurück zur Anmeldeseite"); } // Anmeldung: nicht angemeldet → Weiterleitung auf die Anmeldeseite; nach Plugin-Anmeldung eingeloggt $fresh(); [$s, , $loc] = $http('/staff/reports.php'); $check($s === 302 && str_contains($loc, 'login'), 'ohne Anmeldung ist der Mitarbeiterbereich gesperrt'); [$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=ok'); $check($s === 302 && str_ends_with($loc, '/staff/reports.php'), 'Geprüfter Mitarbeiter wird angemeldet und weitergeleitet: ' . $loc); [$s, $body] = $http('/staff/reports.php'); $check($s === 200 && !str_contains($body, 'name="password"'), 'danach ist der Mitarbeiterbereich offen'); $log = (string) get_db()->query("SELECT GROUP_CONCAT(action, ' | ') FROM admin_audit_log WHERE action LIKE 'Anmeldung über%'")->fetchColumn(); $check(str_contains($log, 'Anmeldung über Testdienst (Plugin logintest)'), 'Die Anmeldung steht im Admin-Protokoll: ' . $log); // Schon angemeldet: normale Anmelde-Adressen führen nur zurück in den Bereich; Adressen mit „whenLoggedIn“ laufen [$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=ok'); $check($s === 302 && str_ends_with($loc, '/staff/reports.php'), 'Angemeldet: eine normale Anmelde-Adresse meldet nicht noch einmal an'); [$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=go'); $check($s === 302 && str_ends_with($loc, '/staff/reports.php'), 'Angemeldet: auch eine Weiterleitungs-Adresse ohne Freigabe wird ignoriert'); [$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=linkgo'); $check($s === 302 && $loc === 'https://idp.example.org/link', 'Angemeldet: freigegebene Adresse darf weiter zum Anmeldedienst leiten'); [$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=link'); $check($s === 302 && str_ends_with($loc, '/staff/plugin.php?p=logintest&page=konto'), 'Angemeldet: „local“ führt zurück in den Mitarbeiterbereich: ' . $loc); [$s, $body] = $http('/staff/reports.php'); $check(str_contains($body, 'Verknüpft: anna@firma.example'), 'Der Hinweis erscheint und das Plugin kennt den angemeldeten Mitarbeiter'); [$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=linkevil'); $check($s === 302 && str_ends_with($loc, '/staff/reports.php') && !str_contains($loc, 'evil'), 'Ein ungültiges lokales Ziel (//evil) wird nicht angenommen: ' . $loc); [$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=linkerr'); $check($s === 302 && str_ends_with($loc, '/staff/plugin.php?p=logintest&page=konto'), 'Fehler mit „back“ führt zur angegebenen Seite zurück: ' . $loc); [$s, $body] = $http('/staff/reports.php'); $check(str_contains($body, 'Nicht verknüpft'), 'Die Fehlermeldung wird angezeigt'); [$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=linkback'); $check($s === 302 && str_ends_with($loc, '/staff/reports.php'), 'Ein fremdes „back“-Ziel wird nicht angenommen: ' . $loc); [$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=switch'); $check($s === 302 && str_ends_with($loc, '/staff/reports.php'), 'Angemeldet kann nie ein anderer Mitarbeiter angemeldet werden'); [$s, $body] = $http('/staff/reports.php'); $check(str_contains($body, 'keinen anderen Mitarbeiter') && str_contains($body, 'Anna Aktiv') || str_contains($body, 'anna@firma.example') || $s === 200, 'Danach bleibt Anna angemeldet'); // Deaktivierter Mitarbeiter wird nie angemeldet $fresh(); plugin_set_setting('logintest', 'target', (string) $karlId); [$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=ok'); $check($s === 302 && str_ends_with($loc, '/staff/login.php'), 'Deaktivierter Mitarbeiter kommt nicht hinein'); [$s] = $http('/staff/reports.php'); $check($s === 302, 'und bleibt ausgesperrt'); plugin_set_setting('logintest', 'target', '999999'); [$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=ok'); $check($s === 302 && str_ends_with($loc, '/staff/login.php'), 'Unbekannte Mitarbeiter-ID wird abgelehnt'); // ── Kunden ── $fresh(); [$s, $body] = $http('/login.php'); $check($s === 200 && str_contains($body, 'Kunden-Knopf') && str_contains($body, '/plugin_login.php?p=logintest&e=cnew') && substr_count($body, 'Kunden-Knopf') === 1, 'Kunden-Anmeldeseite zeigt nur den Knopf des Plugins mit auth.customer'); $check(!str_contains($body, 'Testdienst-Knopf'), 'Mitarbeiter-Knöpfe erscheinen dort nicht'); [$s, $body] = $http('/staff/login.php'); $check(!str_contains($body, 'Kunden-Knopf'), 'Und Kunden-Knöpfe nicht bei den Mitarbeitern'); [$s] = $http('/plugin_login.php?p=loginsneak&e=cnew'); $check($s === 404, 'Kunden-Adresse eines Plugins ohne auth.customer gibt es nicht'); [$s] = $http('/plugin_login.php?p=logintest&e=ok'); $check($s === 404, 'Eine Mitarbeiter-Adresse ist über den Kunden-Einstieg nicht erreichbar'); [$s, , $loc] = $http('/plugin_login.php?p=logintest&e=cgo'); $check($s === 302 && $loc === 'https://idp.example.org/kunde', 'Kunden-Weiterleitung auf https wird ausgeführt'); // Neues Konto wird angelegt und angemeldet [$s, , $loc] = $http('/plugin_login.php?p=logintest&e=cnew&mail=' . rawurlencode('Neu.Kunde@Kunde.Example') . '&name=' . rawurlencode('Nora Neu')); $check($s === 302 && str_ends_with($loc, '/home.php'), 'Neuer Kunde wird angemeldet: ' . $loc); $u = get_db()->query("SELECT * FROM users WHERE email = 'neu.kunde@kunde.example'")->fetch(); $check($u && $u['name'] === 'Nora Neu' && strlen((string) $u['password_hash']) > 20, 'Kundenkonto wurde angelegt (E-Mail klein geschrieben, Name ohne HTML, Passwort zufällig)'); [$s, $body] = $http('/home.php'); $check(str_contains($body, 'Ihr Konto wurde erstellt'), 'Kunde sieht den Willkommens-Hinweis'); $log = (string) get_db()->query("SELECT GROUP_CONCAT(action, ' | ') FROM admin_audit_log WHERE action LIKE 'Kundenkonto über%'")->fetchColumn(); $check(str_contains($log, 'Kundenkonto über Testdienst angelegt: neu.kunde@kunde.example'), 'Das Anlegen steht im Admin-Protokoll: ' . $log); // Zweites Mal: vorhandenes Konto, kein zweites $fresh(); $http('/plugin_login.php?p=logintest&e=cnew&mail=' . rawurlencode('neu.kunde@kunde.example')); $check((int) get_db()->query("SELECT COUNT(*) FROM users WHERE email = 'neu.kunde@kunde.example'")->fetchColumn() === 1, 'Ein vorhandenes Konto wird nicht doppelt angelegt'); // Ohne Erlaubnis zum Anlegen $fresh(); [$s, , $loc] = $http('/plugin_login.php?p=logintest&e=cnew&nocreate=1&mail=' . rawurlencode('unbekannt@kunde.example')); $check($s === 302 && str_ends_with($loc, '/login.php') && (int) get_db()->query("SELECT COUNT(*) FROM users WHERE email = 'unbekannt@kunde.example'")->fetchColumn() === 0, 'Ohne „create“ wird kein Konto angelegt'); [$s, $body] = $http('/login.php'); $check(str_contains($body, 'kein Kundenkonto'), 'und die Meldung erscheint'); // Ungültige und gesperrte Adressen foreach (['kein-mail', 'a@b', str_repeat('x', 160) . '@kunde.example'] as $bad) { [$s, , $loc] = $http('/plugin_login.php?p=logintest&e=cnew&mail=' . rawurlencode($bad)); $check($s === 302 && str_ends_with($loc, '/login.php'), 'Ungültige E-Mail-Adresse wird abgelehnt: ' . substr($bad, 0, 20)); } get_db()->prepare("INSERT INTO banlist (match_type, value, reason, created_at) VALUES ('email', ?, 'Test', ?)")->execute(['gesperrt@kunde.example', db_now()]); [$s, , $loc] = $http('/plugin_login.php?p=logintest&e=cnew&mail=' . rawurlencode('gesperrt@kunde.example')); $check($s === 302 && str_ends_with($loc, '/login.php') && (int) get_db()->query("SELECT COUNT(*) FROM users WHERE email = 'gesperrt@kunde.example'")->fetchColumn() === 0, 'Eine gesperrte E-Mail-Adresse kommt nicht hinein und bekommt kein Konto'); // Fehler (maskiert) [$s, , $loc] = $http('/plugin_login.php?p=logintest&e=cerr'); [$s, $body] = $http('/login.php'); $check(str_contains($body, 'Kunde abgelehnt <i>x</i>') && !str_contains($body, 'x'), 'Fehlermeldung für Kunden wird maskiert angezeigt'); // Rückkehr über die gemeinsame Adresse (staff/plugin_login.php) $fresh(); [$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=ccb'); $check($s === 302 && str_ends_with($loc, '/home.php') && (int) get_db()->query("SELECT COUNT(*) FROM users WHERE email = 'rueck@kunde.example'")->fetchColumn() === 1, 'Die gemeinsame Rückkehr-Adresse kann eine Kunden-Anmeldung beenden und legt das Konto an'); [$s, $body] = $http('/staff/reports.php'); $check($s === 302, 'und macht dabei niemanden zum Mitarbeiter'); $fresh(); [$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=cerr2'); $check($s === 302 && str_ends_with($loc, '/login.php') && !str_contains($loc, 'staff'), 'Kunden-Fehler über die gemeinsame Adresse führen auf die Kunden-Anmeldeseite'); // Plugin ohne auth.customer darf keine Kunden anmelden, auch nicht über die gemeinsame Adresse $fresh(); // (loginsneak hat keine Mitarbeiter-Adressen registriert, daher gibt es hier nichts zu erreichen: 404 ist die richtige Antwort) [$s] = $http('/staff/plugin_login.php?p=loginsneak&e=ccb'); $check($s === 404, 'Ein Plugin ohne die Berechtigungen kommt nicht einmal bis zum Ergebnis'); $st0 = &plugin_state(); $st0['booted'] = false; plugins_boot(); $check(!in_array('auth.customer', plugin_state()['perms']['loginsneak'] ?? [], true) && in_array('auth.customer', plugin_state()['perms']['logintest'] ?? [], true), 'Nur das Plugin mit Freigabe hat auth.customer in seinen Rechten'); // Begrenzung $fresh(); $limited = false; for ($i = 0; $i < 60 && !$limited; $i++) { [$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=go'); if (str_ends_with($loc, '/staff/login.php')) $limited = true; } $check($limited, 'Zu viele Aufrufe werden begrenzt'); } finally { proc_terminate($proc); proc_close($proc); } usleep(300000); foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($work, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST) as $f) { $f->isDir() ? @rmdir($f->getPathname()) : @unlink($f->getPathname()); } @rmdir($work); if ($failures) { fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n"); exit(1); } echo "Plugin login checks passed.\n";