From 897392041ab19e071e50fa1fb68c2427153d2ccd Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Apr 2026 05:41:42 +0000 Subject: [PATCH] Fix session cookie not persisting due to Partitioned attribute The TOKEN cookie set by UniFi OS includes the 'Partitioned' attribute (CHIPS), which some libcurl versions do not write to the Netscape cookie jar file. This caused every API request to go out unauthenticated, resulting in 401/403 errors even after a successful login. Fix: extract the TOKEN value directly from the Set-Cookie response header in login() and pass it via CURLOPT_COOKIE in apiRequest(), bypassing the broken file-based cookie jar. Cookie file remains as fallback for environments where extraction fails. Also update test.php section 8 to validate this fix and show the extracted cookie value. https://claude.ai/code/session_01UsuvFAmmeagtQa14QA4iaq --- includes/UniFiController.php | 22 +++++++++++++++++++--- test.php | 33 ++++++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/includes/UniFiController.php b/includes/UniFiController.php index 58ed54f..90dce55 100644 --- a/includes/UniFiController.php +++ b/includes/UniFiController.php @@ -6,6 +6,7 @@ class UniFiController { private $siteId; private $cookieFile; private $csrfToken = null; + private $sessionCookie = null; public function __construct($controllerUrl, $username, $password, $siteId) { $this->controllerUrl = rtrim($controllerUrl, '/'); @@ -46,10 +47,18 @@ class UniFiController { 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); @@ -100,12 +109,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 6beca36..485c158 100644 --- a/test.php +++ b/test.php @@ -190,29 +190,52 @@ try { if (file_exists($cookieFile)) { $cookieContents = file_get_contents($cookieFile); echo "Cookie-Datei:
";
-            echo htmlspecialchars($cookieContents ?: '(leer)');
+            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):
"; + 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(); - curl_setopt_array($ch2, [ + $apiOpts = [ CURLOPT_URL => $controllerUrl . "/proxy/network/api/s/" . $site['site_id'] . "/stat/voucher", CURLOPT_HTTPGET => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => false, - CURLOPT_COOKIEFILE => $cookieFile, 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);