Monorepo for Aesthetic.Computer aesthetic.computer

fix: captive portal auto-accept — extract form action + POST submit

GettyLink returned HTTP 200 with a login page but the old code just
re-fetched the connectivity check URL. New flow:
1. Detect portal (connectivity check != 204)
2. Follow redirects, save portal page HTML
3. Extract form action URL from the page
4. POST to the form action (accepts terms on most portals)
5. Re-check connectivity after 1s propagation delay

Logs from USB showed: portal detected → auto-accept did nothing →
WebSocket connections got 302 Captive Portal endlessly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+53 -7
+53 -7
fedac/native/src/wifi.c
··· 392 392 393 393 // Captive portal detection + auto-accept 394 394 { 395 - char portal_cmd[256]; 395 + char portal_cmd[512]; 396 + // Step 1: Check connectivity (expect 204 = no portal) 396 397 snprintf(portal_cmd, sizeof(portal_cmd), 397 398 "curl -sL -o /dev/null -w '%%{http_code}' " 398 399 "--max-time 5 --connect-timeout 3 " ··· 408 409 wifi_log(wifi, "Internet: OK (no captive portal)"); 409 410 } else if (code[0]) { 410 411 wifi_log(wifi, "Captive portal detected (HTTP %s), trying auto-accept...", code); 411 - // Follow redirect and accept — many portals just need a GET 412 + 413 + // Step 2: Get the portal redirect URL 412 414 snprintf(portal_cmd, sizeof(portal_cmd), 413 - "curl -sL --max-time 10 --connect-timeout 5 " 414 - "-o /dev/null -w '%%{http_code}' " 415 - "http://connectivitycheck.gstatic.com/generate_204 " 415 + "curl -s --max-time 8 --connect-timeout 5 " 416 + "-o /tmp/portal_page.html -w '%%{url_effective}' " 417 + "-L http://connectivitycheck.gstatic.com/generate_204 " 416 418 "2>/dev/null"); 419 + FILE *uf = popen(portal_cmd, "r"); 420 + char portal_url[256] = ""; 421 + if (uf) { 422 + if (fgets(portal_url, sizeof(portal_url), uf)) 423 + portal_url[strcspn(portal_url, "\n")] = 0; 424 + pclose(uf); 425 + } 426 + wifi_log(wifi, "Portal URL: %s", portal_url); 427 + 428 + // Step 3: Extract form action and submit it 429 + // Many portals (GettyLink, hotel WiFi) have a form 430 + // with action URL — POST to it to accept terms 431 + snprintf(portal_cmd, sizeof(portal_cmd), 432 + "sh -c '" 433 + "ACTION=$(grep -oi \"action=\\\"[^\\\"]*\\\"\" /tmp/portal_page.html 2>/dev/null " 434 + "| head -1 | sed \"s/action=\\\"//;s/\\\"//\"); " 435 + "if [ -n \"$ACTION\" ]; then " 436 + " case \"$ACTION\" in http*) URL=\"$ACTION\" ;; /*) " 437 + " HOST=$(echo \"%s\" | sed \"s|^\\(https\\?://[^/]*\\).*|\\1|\"); " 438 + " URL=\"${HOST}${ACTION}\" ;; " 439 + " *) URL=\"%s\" ;; esac; " 440 + " curl -sL -o /dev/null -w \"%%{http_code}\" " 441 + " --max-time 10 -X POST \"$URL\" 2>/dev/null; " 442 + "else " 443 + " curl -sL -o /dev/null -w \"%%{http_code}\" " 444 + " --max-time 10 \"%s\" 2>/dev/null; " 445 + "fi'", 446 + portal_url, portal_url, portal_url); 417 447 FILE *af = popen(portal_cmd, "r"); 418 448 if (af) { 419 449 char acode[8] = ""; 420 450 if (fgets(acode, sizeof(acode), af)) 421 451 acode[strcspn(acode, "\n")] = 0; 422 452 pclose(af); 423 - if (strcmp(acode, "204") == 0) 453 + wifi_log(wifi, "Portal submit response: HTTP %s", acode); 454 + } 455 + 456 + // Step 4: Re-check connectivity 457 + usleep(1000000); // 1s for portal to propagate 458 + snprintf(portal_cmd, sizeof(portal_cmd), 459 + "curl -sL -o /dev/null -w '%%{http_code}' " 460 + "--max-time 5 --connect-timeout 3 " 461 + "http://connectivitycheck.gstatic.com/generate_204 " 462 + "2>/dev/null"); 463 + FILE *cf = popen(portal_cmd, "r"); 464 + if (cf) { 465 + char ccode[8] = ""; 466 + if (fgets(ccode, sizeof(ccode), cf)) 467 + ccode[strcspn(ccode, "\n")] = 0; 468 + pclose(cf); 469 + if (strcmp(ccode, "204") == 0) 424 470 wifi_log(wifi, "Captive portal cleared!"); 425 471 else 426 - wifi_log(wifi, "Captive portal may need manual login (HTTP %s)", acode); 472 + wifi_log(wifi, "Portal still active (HTTP %s) — may need manual login", ccode); 427 473 } 428 474 } else { 429 475 wifi_log(wifi, "Connectivity check failed (no response)");