Monorepo for Aesthetic.Computer aesthetic.computer
at main 1104 lines 44 kB view raw
1#include "wifi.h" 2#include <stdio.h> 3#include <stdlib.h> 4#include <string.h> 5#include <unistd.h> 6#include <signal.h> 7#include <sys/wait.h> 8#include <sys/stat.h> 9#include <fcntl.h> 10#include <errno.h> 11#include <dirent.h> 12#include <time.h> 13#include <stdarg.h> 14 15// Defined in ac-native.c 16extern void ac_log(const char *fmt, ...); 17 18// Write to both ac_log and the wifi ring buffer (readable from JS) 19static void wifi_log(ACWifi *wifi, const char *fmt, ...) __attribute__((format(printf, 2, 3))); 20static void wifi_log(ACWifi *wifi, const char *fmt, ...) { 21 char buf[128]; 22 va_list ap; 23 va_start(ap, fmt); 24 vsnprintf(buf, sizeof(buf), fmt, ap); 25 va_end(ap); 26 ac_log("[wifi] %s", buf); 27 if (wifi) { 28 int idx = wifi->log_count % 32; 29 strncpy(wifi->log[idx], buf, 127); 30 wifi->log[idx][127] = 0; 31 wifi->log_count++; 32 } 33} 34 35// ============================================================ 36// Helpers (called from wifi thread only — blocking is fine) 37// ============================================================ 38 39static int run_cmd(const char *cmd) { 40 ac_log("[wifi] exec: %s", cmd); 41 int r = system(cmd); 42 if (r != 0) ac_log("[wifi] cmd failed (%d): %s", r, cmd); 43 return r; 44} 45 46static int file_exists(const char *path) { 47 struct stat st; 48 return stat(path, &st) == 0; 49} 50 51static int detect_iface(char *out, int out_len) { 52 // Chromebooks (esp. HP 14 G7 / Jasper Lake w/ Intel AX201) often boot 53 // with WiFi rfkill-blocked at the ACPI / coreboot level — the iwlwifi 54 // driver loads but mac80211 refuses to register an interface, so 55 // `iw dev` returns nothing. Unblock unconditionally before we look 56 // for an interface; it's a no-op on hardware that wasn't blocked. 57 if (system("rfkill unblock all >/dev/null 2>&1") != 0) { 58 // rfkill binary not available — fall back to writing the soft 59 // flag on every rfkill node we can find. 60 DIR *rd = opendir("/sys/class/rfkill"); 61 if (rd) { 62 struct dirent *ent; 63 while ((ent = readdir(rd)) != NULL) { 64 if (ent->d_name[0] == '.') continue; 65 char path[256]; 66 snprintf(path, sizeof(path), "/sys/class/rfkill/%s/soft", ent->d_name); 67 FILE *f = fopen(path, "w"); 68 if (f) { fputs("0\n", f); fclose(f); } 69 } 70 closedir(rd); 71 } 72 } 73 74 FILE *fp = popen("iw dev 2>/dev/null | grep Interface | head -1 | awk '{print $2}'", "r"); 75 if (fp) { 76 char buf[32] = ""; 77 if (fgets(buf, sizeof(buf), fp)) { 78 buf[strcspn(buf, "\n")] = 0; 79 if (buf[0]) { 80 snprintf(out, out_len, "%s", buf); 81 pclose(fp); 82 return 1; 83 } 84 } 85 pclose(fp); 86 } 87 fp = popen("ls -d /sys/class/net/*/wireless 2>/dev/null | head -1", "r"); 88 if (fp) { 89 char buf[128] = ""; 90 if (fgets(buf, sizeof(buf), fp)) { 91 buf[strcspn(buf, "\n")] = 0; 92 char *start = strstr(buf, "/net/"); 93 if (start) { 94 start += 5; 95 char *end = strchr(start, '/'); 96 if (end) { 97 *end = 0; 98 snprintf(out, out_len, "%s", start); 99 pclose(fp); 100 return 1; 101 } 102 } 103 } 104 pclose(fp); 105 } 106 return 0; 107} 108 109// Thread-safe state update helpers 110static void wifi_set_state(ACWifi *wifi, WiFiState st) { 111 pthread_mutex_lock(&wifi->lock); 112 wifi->state = st; 113 pthread_mutex_unlock(&wifi->lock); 114} 115 116static void wifi_set_status(ACWifi *wifi, const char *msg) { 117 pthread_mutex_lock(&wifi->lock); 118 snprintf(wifi->status_msg, sizeof(wifi->status_msg), "%s", msg); 119 pthread_mutex_unlock(&wifi->lock); 120} 121 122static void wifi_set_state_and_status(ACWifi *wifi, WiFiState st, const char *msg) { 123 pthread_mutex_lock(&wifi->lock); 124 wifi->state = st; 125 snprintf(wifi->status_msg, sizeof(wifi->status_msg), "%s", msg); 126 pthread_mutex_unlock(&wifi->lock); 127} 128 129// ============================================================ 130// Scan (runs on wifi thread) 131// ============================================================ 132 133static void wifi_do_scan(ACWifi *wifi) { 134 if (!wifi->iface[0]) return; 135 136 wifi_set_state_and_status(wifi, WIFI_STATE_SCANNING, "scanning..."); 137 138 // Bring interface up + set regulatory domain (blocking — that's fine here) 139 char cmd[256]; 140 snprintf(cmd, sizeof(cmd), "ip link set %s up 2>/dev/null", wifi->iface); 141 run_cmd(cmd); 142 run_cmd("iw reg set US 2>/dev/null"); 143 144 // Remove stale scan files 145 unlink("/tmp/wifi_scan.txt"); 146 unlink("/tmp/wifi_scan_err.txt"); 147 148 // Run scan synchronously on this thread (no need for background shell) 149 snprintf(cmd, sizeof(cmd), 150 "iw dev %s scan > /tmp/wifi_scan.txt 2>/tmp/wifi_scan_err.txt", 151 wifi->iface); 152 int rc = run_cmd(cmd); 153 if (rc != 0) { 154 ac_log("[wifi] scan returned code %d", rc); 155 FILE *err = fopen("/tmp/wifi_scan_err.txt", "r"); 156 if (err) { 157 char errbuf[256] = ""; 158 while (fgets(errbuf, sizeof(errbuf), err)) { 159 errbuf[strcspn(errbuf, "\n")] = 0; 160 if (errbuf[0]) ac_log("[wifi] scan stderr: %s", errbuf); 161 } 162 fclose(err); 163 } 164 } 165 166 // Parse scan results 167 FILE *fp = fopen("/tmp/wifi_scan.txt", "r"); 168 if (!fp) { 169 wifi_set_state_and_status(wifi, WIFI_STATE_SCAN_DONE, "scan failed"); 170 return; 171 } 172 173 WiFiNetwork nets[WIFI_MAX_NETWORKS]; 174 int count = 0; 175 char line[512]; 176 int cur = -1; 177 178 while (fgets(line, sizeof(line), fp) && count < WIFI_MAX_NETWORKS) { 179 char bssid[18]; 180 if (sscanf(line, "BSS %17s", bssid) == 1) { 181 cur = count++; 182 memset(&nets[cur], 0, sizeof(WiFiNetwork)); 183 strncpy(nets[cur].bssid, bssid, 17); 184 nets[cur].signal = -100; 185 } 186 if (cur < 0) continue; 187 188 int sig; 189 if (sscanf(line, "\tsignal: %d", &sig) == 1) 190 nets[cur].signal = sig; 191 192 char ssid[64]; 193 if (sscanf(line, "\tSSID: %63[^\n]", ssid) == 1) 194 strncpy(nets[cur].ssid, ssid, WIFI_SSID_MAX - 1); 195 196 if (strstr(line, "WPA") || strstr(line, "RSN")) 197 nets[cur].encrypted = 1; 198 } 199 fclose(fp); 200 unlink("/tmp/wifi_scan.txt"); 201 202 // Sort by signal (strongest first) 203 for (int i = 0; i < count - 1; i++) 204 for (int j = i + 1; j < count; j++) 205 if (nets[j].signal > nets[i].signal) { 206 WiFiNetwork tmp = nets[i]; nets[i] = nets[j]; nets[j] = tmp; 207 } 208 209 // Remove empty SSIDs 210 int w = 0; 211 for (int r = 0; r < count; r++) 212 if (nets[r].ssid[0]) { if (w != r) nets[w] = nets[r]; w++; } 213 214 // Commit results under lock 215 pthread_mutex_lock(&wifi->lock); 216 memcpy(wifi->networks, nets, sizeof(WiFiNetwork) * w); 217 wifi->network_count = w; 218 wifi->state = WIFI_STATE_SCAN_DONE; 219 snprintf(wifi->status_msg, sizeof(wifi->status_msg), "%d networks", w); 220 pthread_mutex_unlock(&wifi->lock); 221 222 wifi_log(wifi, "Scan complete: %d networks", w); 223} 224 225// ============================================================ 226// Connect (runs on wifi thread) 227// ============================================================ 228 229static void wifi_do_connect(ACWifi *wifi, const char *ssid, const char *password) { 230 if (!ssid || !wifi->iface[0]) return; 231 232 wifi_set_state_and_status(wifi, WIFI_STATE_CONNECTING, "connecting..."); 233 wifi_log(wifi, "Connecting to '%s'", ssid); 234 235 // Kill any existing wpa_supplicant / dhclient 236 if (wifi->wpa_pid > 0) { 237 kill(wifi->wpa_pid, SIGTERM); 238 waitpid(wifi->wpa_pid, NULL, 0); 239 wifi->wpa_pid = 0; 240 } 241 if (wifi->dhcp_pid > 0) { 242 kill(wifi->dhcp_pid, SIGTERM); 243 waitpid(wifi->dhcp_pid, NULL, 0); 244 wifi->dhcp_pid = 0; 245 } 246 run_cmd("killall wpa_supplicant 2>/dev/null; killall dhclient 2>/dev/null"); 247 248 // Write wpa_supplicant config 249 FILE *fp = fopen("/tmp/wpa.conf", "w"); 250 if (!fp) { 251 wifi_set_state_and_status(wifi, WIFI_STATE_FAILED, "config error"); 252 return; 253 } 254 fprintf(fp, "ctrl_interface=/var/run/wpa_supplicant\n"); 255 fprintf(fp, "update_config=1\n\n"); 256 fprintf(fp, "network={\n"); 257 fprintf(fp, " ssid=\"%s\"\n", ssid); 258 if (password && password[0]) 259 fprintf(fp, " psk=\"%s\"\n", password); 260 else 261 fprintf(fp, " key_mgmt=NONE\n"); 262 fprintf(fp, "}\n"); 263 fclose(fp); 264 265 // Create required directories 266 mkdir("/var", 0755); 267 mkdir("/var/run", 0755); 268 mkdir("/var/run/wpa_supplicant", 0755); 269 270 // Remove stale ctrl_interface socket — wpa_supplicant refuses to start 271 // if /var/run/wpa_supplicant/<iface> already exists from a prior run 272 { 273 char sock_path[128]; 274 snprintf(sock_path, sizeof(sock_path), 275 "/var/run/wpa_supplicant/%s", wifi->iface); 276 if (unlink(sock_path) == 0) 277 wifi_log(wifi, "Removed stale socket: %s", sock_path); 278 } 279 280 // Start wpa_supplicant 281 pid_t pid = fork(); 282 if (pid == 0) { 283 const char *wpa_paths[] = { 284 "/bin/wpa_supplicant", "/usr/bin/wpa_supplicant", 285 "/usr/sbin/wpa_supplicant", "/sbin/wpa_supplicant", NULL 286 }; 287 for (int i = 0; wpa_paths[i]; i++) { 288 if (file_exists(wpa_paths[i])) { 289 execl(wpa_paths[i], "wpa_supplicant", 290 "-i", wifi->iface, "-c", "/tmp/wpa.conf", 291 "-B", "-P", "/tmp/wpa.pid", NULL); 292 } 293 } 294 _exit(1); 295 } 296 wifi->wpa_pid = pid; 297 waitpid(pid, NULL, 0); // Wait for wpa_supplicant to daemonize (-B) 298 299 pthread_mutex_lock(&wifi->lock); 300 strncpy(wifi->connected_ssid, ssid, WIFI_SSID_MAX - 1); 301 pthread_mutex_unlock(&wifi->lock); 302 303 // Poll for WPA completion + DHCP (blocking loop on this thread) 304 int connect_ticks = 0; 305 int dhcp_started = 0; 306 char last_wpa_state[64] = ""; 307 308 while (connect_ticks < 1200 && wifi->thread_running) { // ~60 seconds max (50ms polls) 309 // Check if a new command interrupted us 310 if (wifi->pending_cmd != WIFI_CMD_NONE) { 311 wifi_log(wifi, "Connect interrupted by new command"); 312 return; 313 } 314 315 usleep(50000); // 50ms between polls 316 connect_ticks++; 317 318 // Check wpa_supplicant status 319 char cmd[256]; 320 snprintf(cmd, sizeof(cmd), 321 "wpa_cli -i %s status 2>/dev/null | grep wpa_state", wifi->iface); 322 FILE *wfp = popen(cmd, "r"); 323 if (!wfp) continue; 324 325 char line[128] = ""; 326 fgets(line, sizeof(line), wfp); 327 pclose(wfp); 328 329 // Extract WPA state for logging/status 330 { 331 char *eq = strchr(line, '='); 332 const char *st = eq ? eq + 1 : line; 333 char state_str[64] = ""; 334 strncpy(state_str, st, sizeof(state_str) - 1); 335 state_str[strcspn(state_str, "\n\r")] = 0; 336 if (state_str[0] && strcmp(state_str, last_wpa_state) != 0) { 337 wifi_log(wifi, "WPA: %s (tick %d)", state_str, connect_ticks); 338 strncpy(last_wpa_state, state_str, sizeof(last_wpa_state) - 1); 339 // Update user-visible status with WPA state detail 340 if (strstr(state_str, "SCANNING")) 341 wifi_set_status(wifi, "scanning..."); 342 else if (strstr(state_str, "ASSOCIATING")) 343 wifi_set_status(wifi, "associating..."); 344 else if (strstr(state_str, "4WAY_HANDSHAKE")) 345 wifi_set_status(wifi, "authenticating..."); 346 else if (strstr(state_str, "GROUP_HANDSHAKE")) 347 wifi_set_status(wifi, "group handshake..."); 348 } 349 } 350 351 if (strstr(line, "COMPLETED")) { 352 // WPA connected — start DHCP if not already running 353 if (!dhcp_started) { 354 wifi_log(wifi, "WPA connected, starting DHCP"); 355 wifi_set_status(wifi, "getting IP..."); 356 357 pid_t dpid = fork(); 358 if (dpid == 0) { 359 int fd = open("/tmp/dhcp.log", O_WRONLY|O_CREAT|O_TRUNC, 0644); 360 if (fd >= 0) { dup2(fd, 2); dup2(fd, 1); close(fd); } 361 // Prefer udhcpc (busybox) — fast, no script needed 362 const char *udhcpc = NULL; 363 if (file_exists("/sbin/udhcpc")) udhcpc = "/sbin/udhcpc"; 364 else if (file_exists("/bin/udhcpc")) udhcpc = "/bin/udhcpc"; 365 else if (file_exists("/usr/bin/udhcpc")) udhcpc = "/usr/bin/udhcpc"; 366 if (udhcpc) { 367 fprintf(stderr, "[wifi] using udhcpc: %s\n", udhcpc); 368 execl(udhcpc, "udhcpc", "-i", wifi->iface, 369 "-n", // exit if no lease (don't background) 370 "-q", // quit after obtaining lease 371 "-s", "/usr/share/udhcpc/default.script", 372 "-t", "5", // 5 retries 373 "-T", "3", // 3 second timeout 374 NULL); 375 // execl failed 376 fprintf(stderr, "[wifi] udhcpc execl failed: %s\n", strerror(errno)); 377 } 378 // Fallback to dhclient 379 const char *dhc_paths[] = { 380 "/sbin/dhclient", "/usr/sbin/dhclient", NULL 381 }; 382 const char *script = "/sbin/dhclient-script"; 383 if (file_exists("/bin/dhclient-script")) script = "/bin/dhclient-script"; 384 for (int i = 0; dhc_paths[i]; i++) { 385 if (file_exists(dhc_paths[i])) { 386 execl(dhc_paths[i], "dhclient", 387 "-v", "-1", "-sf", script, 388 "-pf", "/tmp/dhclient.pid", 389 "-lf", "/tmp/dhclient.leases", 390 wifi->iface, NULL); 391 } 392 } 393 _exit(1); 394 } 395 wifi->dhcp_pid = dpid; 396 dhcp_started = 1; 397 } 398 399 // Check for IP address 400 snprintf(cmd, sizeof(cmd), 401 "ip addr show %s 2>/dev/null | grep 'inet ' | awk '{print $2}' | cut -d/ -f1", 402 wifi->iface); 403 FILE *ifp = popen(cmd, "r"); 404 if (ifp) { 405 char ip[32] = ""; 406 if (fgets(ip, sizeof(ip), ifp)) { 407 ip[strcspn(ip, "\n")] = 0; 408 if (ip[0] && strcmp(ip, "0.0.0.0") != 0) { 409 pthread_mutex_lock(&wifi->lock); 410 strncpy(wifi->ip_address, ip, sizeof(wifi->ip_address) - 1); 411 wifi->state = WIFI_STATE_CONNECTED; 412 snprintf(wifi->status_msg, sizeof(wifi->status_msg), "%s", ip); 413 pthread_mutex_unlock(&wifi->lock); 414 wifi_log(wifi, "Connected! IP: %s", ip); 415 416 // Captive portal detection + auto-accept 417 { 418 char portal_cmd[512]; 419 // Step 1: Check connectivity (expect 204 = no portal) 420 snprintf(portal_cmd, sizeof(portal_cmd), 421 "curl -sL -o /dev/null -w '%%{http_code}' " 422 "--max-time 5 --connect-timeout 3 " 423 "http://connectivitycheck.gstatic.com/generate_204 " 424 "2>/dev/null"); 425 FILE *pf = popen(portal_cmd, "r"); 426 if (pf) { 427 char code[8] = ""; 428 if (fgets(code, sizeof(code), pf)) 429 code[strcspn(code, "\n")] = 0; 430 pclose(pf); 431 if (strcmp(code, "204") == 0) { 432 wifi_log(wifi, "Internet: OK (no captive portal)"); 433 } else if (code[0]) { 434 wifi_log(wifi, "Captive portal detected (HTTP %s), trying auto-accept...", code); 435 436 // Step 2: Get the portal redirect URL + save HTML for debugging 437 snprintf(portal_cmd, sizeof(portal_cmd), 438 "curl -sk --max-time 8 --connect-timeout 5 " 439 "-o /tmp/portal_page.html -w '%%{url_effective}' " 440 "-L http://connectivitycheck.gstatic.com/generate_204 " 441 "2>/dev/null"); 442 FILE *uf = popen(portal_cmd, "r"); 443 char portal_url[256] = ""; 444 if (uf) { 445 if (fgets(portal_url, sizeof(portal_url), uf)) 446 portal_url[strcspn(portal_url, "\n")] = 0; 447 pclose(uf); 448 } 449 wifi_log(wifi, "Portal URL: %s", portal_url); 450 // Save portal HTML to USB for inspection 451 run_cmd("cp /tmp/portal_page.html /mnt/portal_page.html 2>/dev/null || true"); 452 453 // Step 3: Try multiple accept strategies 454 // Strategy A: ClearPass/Aruba — change cmd=login to cmd=authenticate 455 { 456 char auth_url[512] = ""; 457 const char *cmd_pos = strstr(portal_url, "cmd=login"); 458 if (cmd_pos) { 459 int prefix_len = (int)(cmd_pos - portal_url); 460 snprintf(auth_url, sizeof(auth_url), "%.*scmd=authenticate%s", 461 prefix_len, portal_url, cmd_pos + 9); 462 wifi_log(wifi, "ClearPass: trying %s", auth_url); 463 snprintf(portal_cmd, sizeof(portal_cmd), 464 "curl -skL -o /dev/null -w '%%{http_code}' " 465 "--max-time 10 '%s' 2>/dev/null", auth_url); 466 FILE *cf2 = popen(portal_cmd, "r"); 467 if (cf2) { 468 char cc[8] = ""; 469 if (fgets(cc, sizeof(cc), cf2)) cc[strcspn(cc, "\n")] = 0; 470 pclose(cf2); 471 wifi_log(wifi, "ClearPass auth response: HTTP %s", cc); 472 } 473 } 474 } 475 // Strategy B: Extract HTML form action and POST 476 snprintf(portal_cmd, sizeof(portal_cmd), 477 "sh -c '" 478 "ACTION=$(grep -oi \"action=\\\"[^\\\"]*\\\"\" /tmp/portal_page.html 2>/dev/null " 479 "| head -1 | sed \"s/action=\\\"//;s/\\\"//\"); " 480 "if [ -n \"$ACTION\" ]; then " 481 " case \"$ACTION\" in http*) URL=\"$ACTION\" ;; /*) " 482 " HOST=$(echo \"%s\" | sed \"s|^\\(https\\?://[^/]*\\).*|\\1|\"); " 483 " URL=\"${HOST}${ACTION}\" ;; " 484 " *) URL=\"%s\" ;; esac; " 485 " curl -skL -o /dev/null -w \"%%{http_code}\" " 486 " --max-time 10 -X POST \"$URL\" 2>/dev/null; " 487 "else " 488 " curl -skL -o /dev/null -w \"%%{http_code}\" " 489 " --max-time 10 \"%s\" 2>/dev/null; " 490 "fi'", 491 portal_url, portal_url, portal_url); 492 FILE *af = popen(portal_cmd, "r"); 493 if (af) { 494 char acode[8] = ""; 495 if (fgets(acode, sizeof(acode), af)) 496 acode[strcspn(acode, "\n")] = 0; 497 pclose(af); 498 wifi_log(wifi, "Portal form submit: HTTP %s", acode); 499 } 500 // Strategy C: POST to portal URL with common accept params 501 snprintf(portal_cmd, sizeof(portal_cmd), 502 "curl -skL -o /dev/null -w '%%{http_code}' " 503 "--max-time 10 -X POST " 504 "-d 'accept=true&cmd=authenticate&Login=Login' " 505 "'%s' 2>/dev/null", portal_url); 506 FILE *pf2 = popen(portal_cmd, "r"); 507 if (pf2) { 508 char pc[8] = ""; 509 if (fgets(pc, sizeof(pc), pf2)) pc[strcspn(pc, "\n")] = 0; 510 pclose(pf2); 511 wifi_log(wifi, "Portal POST accept: HTTP %s", pc); 512 } 513 514 // Step 4: Re-check connectivity 515 usleep(1000000); // 1s for portal to propagate 516 snprintf(portal_cmd, sizeof(portal_cmd), 517 "curl -sL -o /dev/null -w '%%{http_code}' " 518 "--max-time 5 --connect-timeout 3 " 519 "http://connectivitycheck.gstatic.com/generate_204 " 520 "2>/dev/null"); 521 FILE *cf = popen(portal_cmd, "r"); 522 if (cf) { 523 char ccode[8] = ""; 524 if (fgets(ccode, sizeof(ccode), cf)) 525 ccode[strcspn(ccode, "\n")] = 0; 526 pclose(cf); 527 if (strcmp(ccode, "204") == 0) 528 wifi_log(wifi, "Captive portal cleared!"); 529 else 530 wifi_log(wifi, "Portal still active (HTTP %s) — may need manual login", ccode); 531 } 532 } else { 533 wifi_log(wifi, "Connectivity check failed (no response)"); 534 } 535 } 536 } 537 538 // Save credentials for auto-reconnect 539 strncpy(wifi->last_ssid, ssid, WIFI_SSID_MAX - 1); 540 strncpy(wifi->last_pass, password ? password : "", WIFI_PASS_MAX - 1); 541 wifi->reconnect_failures = 0; 542 543 // Ensure resolv.conf has DNS fallbacks 544 mkdir("/etc", 0755); 545 FILE *rf = fopen("/etc/resolv.conf", "a"); 546 if (rf) { 547 fseek(rf, 0, SEEK_END); 548 if (ftell(rf) == 0) 549 fprintf(rf, "nameserver 8.8.8.8\nnameserver 1.1.1.1\n"); 550 fclose(rf); 551 } 552 pclose(ifp); 553 return; // Success! 554 } 555 } 556 pclose(ifp); 557 } 558 559 // Check if DHCP client died 560 if (wifi->dhcp_pid > 0) { 561 int status; 562 pid_t r = waitpid(wifi->dhcp_pid, &status, WNOHANG); 563 if (r > 0) { 564 if (WIFEXITED(status) && WEXITSTATUS(status) != 0) { 565 wifi_log(wifi, "DHCP exit %d", WEXITSTATUS(status)); 566 FILE *dlog = fopen("/tmp/dhcp.log", "r"); 567 if (dlog) { 568 char dline[256]; 569 while (fgets(dline, sizeof(dline), dlog)) 570 wifi_log(wifi, "dhclient: %s", dline); 571 fclose(dlog); 572 } 573 // Retry DHCP on next iteration 574 wifi->dhcp_pid = 0; 575 dhcp_started = 0; 576 } 577 if (WIFSIGNALED(status)) { 578 wifi_log(wifi, "dhclient killed by signal %d", WTERMSIG(status)); 579 wifi->dhcp_pid = 0; 580 dhcp_started = 0; 581 } 582 } 583 } 584 } else if (strstr(line, "DISCONNECTED") || strstr(line, "INTERFACE_DISABLED")) { 585 // Still waiting for WPA auth 586 if (connect_ticks > 200) { // ~10 seconds (50ms polls) 587 wifi_set_state_and_status(wifi, WIFI_STATE_FAILED, "auth failed"); 588 wifi_log(wifi, "Auth failed after %d ticks", connect_ticks); 589 return; 590 } 591 } 592 } 593 594 // Timed out 595 if (wifi->state == WIFI_STATE_CONNECTING) { 596 wifi_set_state_and_status(wifi, WIFI_STATE_FAILED, "timeout"); 597 wifi_log(wifi, "Connect timed out after 60s (last WPA: %s)", last_wpa_state); 598 } 599} 600 601// ============================================================ 602// Disconnect (runs on wifi thread) 603// ============================================================ 604 605static void wifi_do_disconnect(ACWifi *wifi) { 606 run_cmd("killall wpa_supplicant 2>/dev/null"); 607 run_cmd("killall dhclient 2>/dev/null"); 608 609 char cmd[128]; 610 snprintf(cmd, sizeof(cmd), "ip addr flush dev %s 2>/dev/null", wifi->iface); 611 run_cmd(cmd); 612 613 if (wifi->wpa_pid > 0) { 614 kill(wifi->wpa_pid, SIGTERM); 615 waitpid(wifi->wpa_pid, NULL, 0); 616 } 617 if (wifi->dhcp_pid > 0) { 618 kill(wifi->dhcp_pid, SIGTERM); 619 waitpid(wifi->dhcp_pid, NULL, 0); 620 } 621 622 pthread_mutex_lock(&wifi->lock); 623 wifi->wpa_pid = 0; 624 wifi->dhcp_pid = 0; 625 wifi->state = WIFI_STATE_OFF; 626 wifi->connected_ssid[0] = 0; 627 wifi->ip_address[0] = 0; 628 snprintf(wifi->status_msg, sizeof(wifi->status_msg), "disconnected"); 629 pthread_mutex_unlock(&wifi->lock); 630 631 wifi_log(wifi, "Disconnected"); 632} 633 634// ============================================================ 635// Auto-connect (runs on wifi thread) 636// ============================================================ 637 638// Hardcoded fallback SSID/pass (matches wifi.mjs AC_SSID/AC_PASS) 639#define AC_SSID "aesthetic.computer" 640#define AC_PASS "aesthetic.computer" 641 642static void wifi_do_autoconnect(ACWifi *wifi) { 643 wifi_log(wifi, "Auto-connect: scanning..."); 644 645 // Step 1: Scan 646 wifi_do_scan(wifi); 647 if (wifi->network_count == 0) { 648 wifi_log(wifi, "Auto-connect: no networks found"); 649 wifi_set_state_and_status(wifi, WIFI_STATE_SCAN_DONE, "no networks"); 650 return; 651 } 652 653 // Step 2: Read saved credentials from /mnt/wifi_creds.json 654 // Format: [{"ssid":"MyNet","pass":"secret"}, ...] 655 typedef struct { char ssid[WIFI_SSID_MAX]; char pass[WIFI_PASS_MAX]; } SavedCred; 656 SavedCred creds[16]; 657 int cred_count = 0; 658 659 // Always include preset networks 660 strncpy(creds[0].ssid, AC_SSID, WIFI_SSID_MAX - 1); 661 strncpy(creds[0].pass, AC_PASS, WIFI_PASS_MAX - 1); 662 cred_count = 1; 663 // ATT home network 664 strncpy(creds[cred_count].ssid, "ATT2AWTpcr", WIFI_SSID_MAX - 1); 665 strncpy(creds[cred_count].pass, "t84q%7%g2h8u", WIFI_PASS_MAX - 1); 666 cred_count++; 667 // GettyLink (open network, no password) 668 strncpy(creds[cred_count].ssid, "GettyLink", WIFI_SSID_MAX - 1); 669 creds[cred_count].pass[0] = '\0'; 670 cred_count++; 671 // Tondo_Guest 672 strncpy(creds[cred_count].ssid, "Tondo_Guest", WIFI_SSID_MAX - 1); 673 strncpy(creds[cred_count].pass, "California", WIFI_PASS_MAX - 1); 674 cred_count++; 675 676 FILE *fp = fopen("/mnt/wifi_creds.json", "r"); 677 if (fp) { 678 char buf[2048] = ""; 679 size_t n = fread(buf, 1, sizeof(buf) - 1, fp); 680 buf[n] = 0; 681 fclose(fp); 682 683 // Minimal JSON array parse: find each {"ssid":"...","pass":"..."} 684 char *p = buf; 685 while ((p = strstr(p, "\"ssid\"")) && cred_count < 16) { 686 char *sq = strchr(p + 6, '"'); // opening quote of ssid value 687 if (!sq) break; 688 sq++; 689 char *eq = strchr(sq, '"'); // closing quote 690 if (!eq) break; 691 692 int len = (int)(eq - sq); 693 if (len > 0 && len < WIFI_SSID_MAX) { 694 strncpy(creds[cred_count].ssid, sq, len); 695 creds[cred_count].ssid[len] = 0; 696 697 // Find corresponding "pass" value 698 creds[cred_count].pass[0] = 0; 699 char *pp = strstr(eq, "\"pass\""); 700 if (pp) { 701 char *pq = strchr(pp + 6, '"'); 702 if (pq) { 703 pq++; 704 char *pe = strchr(pq, '"'); 705 if (pe) { 706 int plen = (int)(pe - pq); 707 if (plen >= 0 && plen < WIFI_PASS_MAX) { 708 strncpy(creds[cred_count].pass, pq, plen); 709 creds[cred_count].pass[plen] = 0; 710 } 711 } 712 } 713 } 714 715 // Skip duplicates of AC preset 716 if (strcmp(creds[cred_count].ssid, AC_SSID) != 0) 717 cred_count++; 718 } 719 p = eq + 1; 720 } 721 wifi_log(wifi, "Auto-connect: %d saved creds", cred_count); 722 } else { 723 wifi_log(wifi, "Auto-connect: no saved creds, preset only"); 724 } 725 726 // Step 3: Match scanned networks against saved creds (by signal strength) 727 // Networks are already sorted by signal (strongest first) from wifi_do_scan 728 pthread_mutex_lock(&wifi->lock); 729 for (int i = 0; i < wifi->network_count; i++) { 730 for (int j = 0; j < cred_count; j++) { 731 if (strcmp(wifi->networks[i].ssid, creds[j].ssid) == 0) { 732 char ssid[WIFI_SSID_MAX], pass[WIFI_PASS_MAX]; 733 strncpy(ssid, creds[j].ssid, WIFI_SSID_MAX - 1); 734 ssid[WIFI_SSID_MAX - 1] = 0; 735 strncpy(pass, creds[j].pass, WIFI_PASS_MAX - 1); 736 pass[WIFI_PASS_MAX - 1] = 0; 737 pthread_mutex_unlock(&wifi->lock); 738 739 wifi_log(wifi, "Trying '%s' (%d dBm)", 740 ssid, wifi->networks[i].signal); 741 wifi_do_connect(wifi, ssid, pass); 742 743 if (wifi->state == WIFI_STATE_CONNECTED) { 744 wifi_log(wifi, "Auto-connect: success!"); 745 return; 746 } 747 wifi_log(wifi, "'%s' failed, trying next...", ssid); 748 pthread_mutex_lock(&wifi->lock); 749 break; // Move to next scanned network 750 } 751 } 752 } 753 pthread_mutex_unlock(&wifi->lock); 754 755 wifi_log(wifi, "Auto-connect: no match"); 756 wifi_set_state_and_status(wifi, WIFI_STATE_SCAN_DONE, "no saved network"); 757} 758 759// ============================================================ 760// Worker thread 761// ============================================================ 762 763// Check if interface still has an IP (connectivity watchdog) 764// Also updates signal_strength while connected. 765static int wifi_check_link(ACWifi *wifi) { 766 if (!wifi->iface[0]) return 0; 767 char cmd[256]; 768 snprintf(cmd, sizeof(cmd), 769 "ip -4 addr show %s 2>/dev/null | grep -q 'inet '", wifi->iface); 770 int has_ip = system(cmd) == 0; 771 772 // Update signal strength 773 if (has_ip) { 774 snprintf(cmd, sizeof(cmd), 775 "iw dev %s link 2>/dev/null | grep signal | awk '{print $2}'", 776 wifi->iface); 777 FILE *fp = popen(cmd, "r"); 778 if (fp) { 779 char buf[32] = ""; 780 if (fgets(buf, sizeof(buf), fp)) { 781 int sig = atoi(buf); 782 if (sig < 0) { 783 pthread_mutex_lock(&wifi->lock); 784 wifi->signal_strength = sig; 785 pthread_mutex_unlock(&wifi->lock); 786 } 787 } 788 pclose(fp); 789 } 790 } 791 792 return has_ip; 793} 794 795static void *wifi_thread_fn(void *arg) { 796 ACWifi *wifi = (ACWifi *)arg; 797 int watchdog_counter = 0; 798 799 while (wifi->thread_running) { 800 // Wait for a command (with 2s timeout for watchdog checks) 801 pthread_mutex_lock(&wifi->lock); 802 if (wifi->pending_cmd == WIFI_CMD_NONE) { 803 struct timespec ts; 804 clock_gettime(CLOCK_REALTIME, &ts); 805 ts.tv_sec += 2; // 2-second watchdog interval 806 pthread_cond_timedwait(&wifi->cond, &wifi->lock, &ts); 807 } 808 809 WiFiCommand cmd = wifi->pending_cmd; 810 wifi->pending_cmd = WIFI_CMD_NONE; 811 812 // Copy command args before releasing lock 813 char ssid[WIFI_SSID_MAX] = ""; 814 char pass[WIFI_PASS_MAX] = ""; 815 if (cmd == WIFI_CMD_CONNECT) { 816 strncpy(ssid, wifi->cmd_ssid, WIFI_SSID_MAX - 1); 817 strncpy(pass, wifi->cmd_pass, WIFI_PASS_MAX - 1); 818 } 819 WiFiState cur_state = wifi->state; 820 pthread_mutex_unlock(&wifi->lock); 821 822 // Execute command (blocking calls are fine — we're on the wifi thread) 823 switch (cmd) { 824 case WIFI_CMD_SCAN: wifi_do_scan(wifi); break; 825 case WIFI_CMD_CONNECT: wifi_do_connect(wifi, ssid, pass); break; 826 case WIFI_CMD_DISCONNECT: wifi_do_disconnect(wifi); break; 827 case WIFI_CMD_AUTOCONNECT: wifi_do_autoconnect(wifi); break; 828 default: break; 829 } 830 831 // Watchdog: check connectivity every ~10s (5 iterations × 2s) 832 if (cmd == WIFI_CMD_NONE && cur_state == WIFI_STATE_CONNECTED) { 833 watchdog_counter++; 834 if (watchdog_counter >= 5) { 835 watchdog_counter = 0; 836 if (!wifi_check_link(wifi)) { 837 wifi_log(wifi, "Connection lost — reconnecting '%s'", 838 wifi->last_ssid); 839 wifi_set_state_and_status(wifi, WIFI_STATE_CONNECTING, 840 "reconnecting..."); 841 wifi_do_connect(wifi, wifi->last_ssid, wifi->last_pass); 842 if (wifi->state != WIFI_STATE_CONNECTED) { 843 wifi->reconnect_failures++; 844 wifi_log(wifi, "Reconnect failed (%d)", 845 wifi->reconnect_failures); 846 // Back off: wait longer between retries 847 if (wifi->reconnect_failures > 3) 848 sleep(wifi->reconnect_failures * 2); 849 } 850 } 851 } 852 } else { 853 watchdog_counter = 0; 854 } 855 } 856 857 return NULL; 858} 859 860// ============================================================ 861// Public API (called from main thread — all non-blocking) 862// ============================================================ 863 864ACWifi *wifi_init(void) { 865 ACWifi *wifi = calloc(1, sizeof(ACWifi)); 866 if (!wifi) return NULL; 867 868 wifi->state = WIFI_STATE_OFF; 869 snprintf(wifi->status_msg, sizeof(wifi->status_msg), "initializing..."); 870 wifi->iface[0] = 0; 871 pthread_mutex_init(&wifi->lock, NULL); 872 pthread_cond_init(&wifi->cond, NULL); 873 874 // Check if iw exists 875 int has_iw = file_exists("/usr/sbin/iw") || file_exists("/sbin/iw") || 876 file_exists("/usr/bin/iw") || file_exists("/bin/iw"); 877 if (!has_iw) 878 has_iw = (system("which iw >/dev/null 2>&1") == 0); 879 if (!has_iw) { 880 snprintf(wifi->status_msg, sizeof(wifi->status_msg), "iw not found"); 881 ac_log("[wifi] iw binary not found"); 882 return wifi; 883 } 884 ac_log("[wifi] iw binary found"); 885 886 // Log diagnostic info 887 { 888 FILE *dbg; 889 dbg = popen("ls /sys/class/net/ 2>/dev/null", "r"); 890 if (dbg) { 891 char buf[256] = ""; 892 if (fgets(buf, sizeof(buf), dbg)) { 893 buf[strcspn(buf, "\n")] = 0; 894 ac_log("[wifi] net interfaces at start: %s", buf); 895 } 896 pclose(dbg); 897 } 898 dbg = popen("ls /lib/firmware/iwlwifi-*.ucode 2>/dev/null | head -3", "r"); 899 if (dbg) { 900 char buf[256] = ""; 901 while (fgets(buf, sizeof(buf), dbg)) { 902 buf[strcspn(buf, "\n")] = 0; 903 ac_log("[wifi] firmware: %s", buf); 904 } 905 pclose(dbg); 906 } 907 // Read kernel log for wireless-related messages 908 int kmsg_fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK); 909 if (kmsg_fd >= 0) { 910 lseek(kmsg_fd, 0, SEEK_SET); 911 char kbuf[512]; 912 int kmsg_count = 0, matched = 0; 913 ssize_t rr; 914 while ((rr = read(kmsg_fd, kbuf, sizeof(kbuf) - 1)) > 0 && kmsg_count < 2000) { 915 kbuf[rr] = 0; 916 if (strcasestr(kbuf, "iwl") || strcasestr(kbuf, "wifi") || 917 strcasestr(kbuf, "wlan") || strcasestr(kbuf, "80211") || 918 strcasestr(kbuf, "firmware") || 919 (strcasestr(kbuf, "pci") && kmsg_count < 100)) { 920 char *msg = strchr(kbuf, ';'); 921 if (msg) msg = strchr(msg + 1, ';'); 922 if (msg) msg = strchr(msg + 1, ';'); 923 if (msg) msg++; else msg = kbuf; 924 msg[strcspn(msg, "\n")] = 0; 925 ac_log("[wifi] kmsg: %s", msg); 926 matched++; 927 } 928 kmsg_count++; 929 } 930 ac_log("[wifi] kmsg: %d total messages, %d matched", kmsg_count, matched); 931 close(kmsg_fd); 932 } 933 // PCI device enumeration 934 dbg = popen("for d in /sys/bus/pci/devices/*; do echo \"$(basename $d) $(cat $d/vendor 2>/dev/null):$(cat $d/device 2>/dev/null)\"; done 2>/dev/null", "r"); 935 if (dbg) { 936 char buf[256] = ""; 937 while (fgets(buf, sizeof(buf), dbg)) { 938 buf[strcspn(buf, "\n")] = 0; 939 ac_log("[wifi] PCI id: %s", buf); 940 } 941 pclose(dbg); 942 } 943 } 944 945 // Wait for wireless interface (up to 3 seconds) 946 int found = 0; 947 for (int attempt = 0; attempt < 30; attempt++) { 948 if (detect_iface(wifi->iface, sizeof(wifi->iface))) { 949 found = 1; 950 break; 951 } 952 ac_log("[wifi] Waiting for wireless interface (attempt %d/30)...", attempt + 1); 953 usleep(100000); 954 } 955 956 if (!found) { 957 ac_log("[wifi] No wireless interface detected"); 958 FILE *fp = popen("ls /sys/class/net/ 2>/dev/null", "r"); 959 if (fp) { 960 char buf[256] = ""; 961 if (fgets(buf, sizeof(buf), fp)) { 962 buf[strcspn(buf, "\n")] = 0; 963 ac_log("[wifi] interfaces: %s", buf); 964 } 965 pclose(fp); 966 } 967 snprintf(wifi->status_msg, sizeof(wifi->status_msg), "no wifi hw"); 968 return wifi; 969 } 970 ac_log("[wifi] Detected interface: %s", wifi->iface); 971 972 // Unblock rfkill if needed 973 { 974 char cmd[256]; 975 snprintf(cmd, sizeof(cmd), 976 "cat /sys/class/net/%s/phy80211/rfkill*/soft 2>/dev/null | head -1", 977 wifi->iface); 978 FILE *fp = popen(cmd, "r"); 979 if (fp) { 980 char buf[8] = ""; 981 if (fgets(buf, sizeof(buf), fp) && buf[0] == '1') { 982 ac_log("[wifi] Soft-blocked, unblocking..."); 983 run_cmd("rfkill unblock wifi 2>/dev/null"); 984 usleep(200000); 985 } 986 pclose(fp); 987 } 988 } 989 990 // Bring up the interface 991 { 992 char cmd[128]; 993 snprintf(cmd, sizeof(cmd), "ip link set %s up 2>/dev/null", wifi->iface); 994 run_cmd(cmd); 995 996 snprintf(cmd, sizeof(cmd), "ip link show %s 2>/dev/null", wifi->iface); 997 FILE *fp = popen(cmd, "r"); 998 if (fp) { 999 char buf[256] = ""; 1000 if (fgets(buf, sizeof(buf), fp)) { 1001 buf[strcspn(buf, "\n")] = 0; 1002 ac_log("[wifi] link status: %s", buf); 1003 } 1004 pclose(fp); 1005 } 1006 } 1007 1008 snprintf(wifi->status_msg, sizeof(wifi->status_msg), "ready"); 1009 ac_log("[wifi] Interface %s is up", wifi->iface); 1010 1011 // Start worker thread 1012 wifi->thread_running = 1; 1013 if (pthread_create(&wifi->thread, NULL, wifi_thread_fn, wifi) != 0) { 1014 ac_log("[wifi] Failed to create wifi thread"); 1015 wifi->thread_running = 0; 1016 } else { 1017 ac_log("[wifi] Worker thread started"); 1018 } 1019 1020 return wifi; 1021} 1022 1023void wifi_scan(ACWifi *wifi) { 1024 if (!wifi || !wifi->iface[0] || !wifi->thread_running) return; 1025 1026 pthread_mutex_lock(&wifi->lock); 1027 // Don't interrupt scanning, connecting, or DHCP in progress 1028 if (wifi->state == WIFI_STATE_SCANNING || 1029 wifi->state == WIFI_STATE_CONNECTING || 1030 wifi->state == WIFI_STATE_CONNECTED) { 1031 pthread_mutex_unlock(&wifi->lock); 1032 return; 1033 } 1034 wifi->pending_cmd = WIFI_CMD_SCAN; 1035 pthread_cond_signal(&wifi->cond); 1036 pthread_mutex_unlock(&wifi->lock); 1037} 1038 1039void wifi_connect(ACWifi *wifi, const char *ssid, const char *password) { 1040 if (!wifi || !ssid || !wifi->iface[0] || !wifi->thread_running) return; 1041 1042 pthread_mutex_lock(&wifi->lock); 1043 strncpy(wifi->cmd_ssid, ssid, WIFI_SSID_MAX - 1); 1044 wifi->cmd_ssid[WIFI_SSID_MAX - 1] = 0; 1045 if (password) { 1046 strncpy(wifi->cmd_pass, password, WIFI_PASS_MAX - 1); 1047 wifi->cmd_pass[WIFI_PASS_MAX - 1] = 0; 1048 } else { 1049 wifi->cmd_pass[0] = 0; 1050 } 1051 wifi->pending_cmd = WIFI_CMD_CONNECT; 1052 pthread_cond_signal(&wifi->cond); 1053 pthread_mutex_unlock(&wifi->lock); 1054} 1055 1056void wifi_disconnect(ACWifi *wifi) { 1057 if (!wifi || !wifi->thread_running) return; 1058 1059 pthread_mutex_lock(&wifi->lock); 1060 wifi->pending_cmd = WIFI_CMD_DISCONNECT; 1061 pthread_cond_signal(&wifi->cond); 1062 pthread_mutex_unlock(&wifi->lock); 1063} 1064 1065// No-ops — polling now happens inside the wifi thread. 1066// Main thread just reads wifi->state / wifi->networks directly. 1067int wifi_scan_poll(ACWifi *wifi) { 1068 (void)wifi; 1069 return 0; 1070} 1071 1072int wifi_connect_poll(ACWifi *wifi) { 1073 (void)wifi; 1074 return 0; 1075} 1076 1077void wifi_autoconnect(ACWifi *wifi) { 1078 if (!wifi || !wifi->iface[0] || !wifi->thread_running) return; 1079 1080 pthread_mutex_lock(&wifi->lock); 1081 wifi->pending_cmd = WIFI_CMD_AUTOCONNECT; 1082 pthread_cond_signal(&wifi->cond); 1083 pthread_mutex_unlock(&wifi->lock); 1084} 1085 1086void wifi_destroy(ACWifi *wifi) { 1087 if (!wifi) return; 1088 1089 // Signal thread to stop 1090 wifi->thread_running = 0; 1091 pthread_mutex_lock(&wifi->lock); 1092 pthread_cond_signal(&wifi->cond); 1093 pthread_mutex_unlock(&wifi->lock); 1094 1095 // Wait for thread to finish 1096 if (wifi->thread) 1097 pthread_join(wifi->thread, NULL); 1098 1099 wifi_do_disconnect(wifi); 1100 1101 pthread_mutex_destroy(&wifi->lock); 1102 pthread_cond_destroy(&wifi->cond); 1103 free(wifi); 1104}