Monorepo for Aesthetic.Computer
aesthetic.computer
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}