diff --git a/includes/UniFiController.php b/includes/UniFiController.php index 9ee254f..d0da04b 100644 --- a/includes/UniFiController.php +++ b/includes/UniFiController.php @@ -6,6 +6,8 @@ class UniFiController { private $siteId; private $cookieFile; private $csrfToken = null; + private $sessionCookie = null; + private $loggedIn = false; public function __construct($controllerUrl, $username, $password, $siteId) { $this->controllerUrl = rtrim($controllerUrl, '/'); @@ -23,6 +25,10 @@ class UniFiController { // Login zum Controller private function login() { + if ($this->loggedIn) { + return true; + } + $ch = curl_init(); curl_setopt_array($ch, [ @@ -38,14 +44,26 @@ class UniFiController { CURLOPT_COOKIEFILE => $this->cookieFile, CURLOPT_TIMEOUT => 10, CURLOPT_CONNECTTIMEOUT => 5, - CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'Origin: ' . $this->controllerUrl, + 'Referer: ' . $this->controllerUrl . '/login', + ], CURLOPT_HEADERFUNCTION => function($ch, $header) { $parts = explode(':', $header, 2); if (count($parts) === 2) { - $name = trim($parts[0]); + $name = strtolower(trim($parts[0])); $value = trim($parts[1]); - if (strtolower($name) === 'x-csrf-token') { + if ($name === 'x-csrf-token') { $this->csrfToken = $value; + } elseif ($name === 'set-cookie') { + // Extract TOKEN value directly — cookie jar may not persist + // cookies with the 'Partitioned' attribute on some libcurl versions + $cookieParts = explode(';', $value); + $first = trim($cookieParts[0]); + if (strpos($first, 'TOKEN=') === 0) { + $this->sessionCookie = $first; + } } } return strlen($header); @@ -57,7 +75,9 @@ class UniFiController { curl_close($ch); if ($httpCode !== 200) { - throw new Exception("Login fehlgeschlagen: HTTP $httpCode"); + $body = json_decode($response, true); + $detail = $body['meta']['msg'] ?? $body['errors'][0] ?? substr(strip_tags($response), 0, 120); + throw new Exception("Login fehlgeschlagen: HTTP $httpCode" . ($detail ? " – $detail" : '')); } $data = json_decode($response, true); @@ -75,6 +95,7 @@ class UniFiController { $this->csrfToken = $this->readCsrfFromCookieFile(); } + $this->loggedIn = true; return true; } @@ -94,12 +115,19 @@ class UniFiController { CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => false, - CURLOPT_COOKIEFILE => $this->cookieFile, CURLOPT_TIMEOUT => 10, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_HTTPHEADER => $headers ]; + // Prefer manually extracted cookie over file-based jar — the Partitioned + // attribute on the TOKEN cookie prevents some libcurl versions from writing it + if ($this->sessionCookie !== null) { + $options[CURLOPT_COOKIE] = $this->sessionCookie; + } else { + $options[CURLOPT_COOKIEFILE] = $this->cookieFile; + } + if ($method === 'GET') { $options[CURLOPT_HTTPGET] = true; } elseif ($method === 'POST') { diff --git a/test.php b/test.php index 472b59c..485c158 100644 --- a/test.php +++ b/test.php @@ -84,6 +84,180 @@ echo "

7. Session

"; echo "Session Status: " . session_status() . " (1=disabled, 2=active)
"; echo "Session ID: " . session_id() . "
"; +// 8. UniFi API Diagnose +echo "

8. UniFi API Diagnose

"; +try { + $db2 = Database::getInstance(); + $site = $db2->fetchOne("SELECT * FROM sites WHERE is_active = 1 ORDER BY id ASC LIMIT 1"); + if (!$site) { + echo "✗ Keine aktive Site in der Datenbank gefunden
"; + } else { + echo "Site: " . htmlspecialchars($site['name']) . "
"; + echo "Controller URL: " . htmlspecialchars($site['unifi_controller_url']) . "
"; + echo "Site ID: " . htmlspecialchars($site['site_id']) . "
"; + echo "Username: " . htmlspecialchars($site['unifi_username']) . "
"; + echo "
"; + + // --- Raw Login Test --- + echo "Login-Test (raw cURL):
"; + $cookieFile = tempnam(sys_get_temp_dir(), 'UNIFI_TEST_'); + $csrfToken = null; + $controllerUrl = rtrim($site['unifi_controller_url'], '/'); + + $ch = curl_init(); + $responseHeaders = []; + curl_setopt_array($ch, [ + CURLOPT_URL => $controllerUrl . "/api/auth/login", + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode([ + 'username' => $site['unifi_username'], + 'password' => $site['unifi_password'] + ]), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_COOKIEJAR => $cookieFile, + CURLOPT_COOKIEFILE => $cookieFile, + CURLOPT_TIMEOUT => 10, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'Origin: ' . $controllerUrl, + 'Referer: ' . $controllerUrl . '/login', + ], + CURLOPT_HEADERFUNCTION => function($ch, $header) use (&$csrfToken, &$responseHeaders) { + $responseHeaders[] = rtrim($header); + $parts = explode(':', $header, 2); + if (count($parts) === 2 && strtolower(trim($parts[0])) === 'x-csrf-token') { + $csrfToken = trim($parts[1]); + } + return strlen($header); + } + ]); + + $body = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlErr = curl_error($ch); + $info = curl_getinfo($ch); + curl_close($ch); + + if ($curlErr) { + echo "✗ cURL Fehler: " . htmlspecialchars($curlErr) . "
"; + } else { + $icon = ($httpCode === 200) ? '✓' : '✗'; + echo "$icon HTTP Code: $httpCode
"; + echo "Effective URL: " . htmlspecialchars($info['url']) . "
"; + } + + // Response headers + echo "
Response-Header:
";
+        foreach ($responseHeaders as $h) {
+            if ($h !== '') echo htmlspecialchars($h) . "\n";
+        }
+        echo "
"; + + // CSRF token + if ($csrfToken !== null) { + echo "✓ X-CSRF-Token aus Header: " . htmlspecialchars($csrfToken) . "
"; + } else { + echo "✗ Kein X-CSRF-Token im Login-Response-Header gefunden
"; + // Check cookie file fallback + if (file_exists($cookieFile)) { + $tokenFromCookie = null; + foreach (file($cookieFile) as $line) { + $line = trim($line); + if ($line === '' || $line[0] === '#') continue; + $parts = explode("\t", $line); + if (count($parts) >= 7 && strtoupper($parts[5]) === 'TOKEN') { + $tokenFromCookie = $parts[6]; + } + } + if ($tokenFromCookie) { + echo "✓ CSRF-Token aus Cookie-Datei (Fallback): " . htmlspecialchars($tokenFromCookie) . "
"; + $csrfToken = $tokenFromCookie; + } else { + echo "✗ Auch kein TOKEN-Cookie in der Cookie-Datei gefunden
"; + } + } + } + + // Response body + echo "
Login Response Body:
";
+        echo htmlspecialchars(substr($body, 0, 2000));
+        echo "
"; + + // Cookie file contents + if (file_exists($cookieFile)) { + $cookieContents = file_get_contents($cookieFile); + echo "Cookie-Datei:
";
+            echo htmlspecialchars($cookieContents ?: '(leer — TOKEN hat Partitioned-Attribut, libcurl schreibt es nicht in die Jar-Datei)');
+            echo "
"; + } + + // Extract TOKEN from Set-Cookie header (the fix for Partitioned cookie issue) + $tokenCookie = null; + foreach ($responseHeaders as $h) { + if (stripos($h, 'set-cookie:') === 0) { + $cookieVal = trim(substr($h, strlen('set-cookie:'))); + $cookieParts = explode(';', $cookieVal); + $first = trim($cookieParts[0]); + if (strpos($first, 'TOKEN=') === 0) { + $tokenCookie = $first; + } + } + } + if ($tokenCookie) { + echo "✓ TOKEN aus Set-Cookie-Header extrahiert: " . htmlspecialchars(substr($tokenCookie, 0, 40)) . "…
"; + } else { + echo "✗ TOKEN nicht in Set-Cookie-Header gefunden
"; + } + + // --- API Test (only if login succeeded) --- + if ($httpCode === 200) { + echo "
API-Test (stat/voucher GET) — mit extrahiertem Cookie:
"; + $apiHeaders = ['Content-Type: application/json']; + if ($csrfToken !== null) { + $apiHeaders[] = 'X-CSRF-Token: ' . $csrfToken; + } + + $ch2 = curl_init(); + $apiOpts = [ + CURLOPT_URL => $controllerUrl . "/proxy/network/api/s/" . $site['site_id'] . "/stat/voucher", + CURLOPT_HTTPGET => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_TIMEOUT => 10, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_HTTPHEADER => $apiHeaders, + ]; + if ($tokenCookie !== null) { + $apiOpts[CURLOPT_COOKIE] = $tokenCookie; + } else { + $apiOpts[CURLOPT_COOKIEFILE] = $cookieFile; + } + curl_setopt_array($ch2, $apiOpts); + $apiBody = curl_exec($ch2); + $apiCode = curl_getinfo($ch2, CURLINFO_HTTP_CODE); + $apiErr = curl_error($ch2); + curl_close($ch2); + + if ($apiErr) { + echo "✗ cURL Fehler: " . htmlspecialchars($apiErr) . "
"; + } else { + $icon2 = ($apiCode === 200) ? '✓' : '✗'; + echo "$icon2 HTTP Code: $apiCode
"; + } + echo "API Response Body:
";
+            echo htmlspecialchars(substr($apiBody, 0, 2000));
+            echo "
"; + } + + @unlink($cookieFile); + } +} catch (Exception $e) { + echo "✗ Diagnose-Fehler: " . htmlspecialchars($e->getMessage()) . "
"; +} + echo "
"; echo "

✓ Test abgeschlossen

"; echo "

Zum Login | Zur Startseite

";