diff --git a/gui/gui_app_netmon.c b/gui/gui_app_netmon.c index 974480a..34795d0 100644 --- a/gui/gui_app_netmon.c +++ b/gui/gui_app_netmon.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #define NETMON_HISTORY 64 @@ -46,8 +47,57 @@ typedef struct { bool primed; // a baseline reading has been taken int scroll; // first connection row shown + + // Internet reachability probe (async, via the HTTP worker). + http_fetch_t* probe; + int inet_state; // 0 unknown, 1 checking, 2 online, 3 offline + char inet_detail[72]; + bool auto_checked; } netmon_data_t; +#define INET_UNKNOWN 0 +#define INET_CHECKING 1 +#define INET_ONLINE 2 +#define INET_OFFLINE 3 + +#define NETMON_PROBE_HOST "example.com/" // a plain-HTTP host to reach for + +// Advance the reachability probe: collect a finished result, or note that one +// is still in flight. Runs every paint; the fetch itself is on the worker. +static void netmon_poll_probe(netmon_data_t* nd) { + if (!nd->probe) return; + + int st = nd->probe->state; + if (st == HTTP_STATE_RUNNING) { nd->inet_state = INET_CHECKING; return; } + + if (st == HTTP_STATE_DONE && nd->probe->status_code > 0) { + char ipx[16]; + ip_to_str(nd->probe->ip, ipx); + nd->inet_state = INET_ONLINE; + snprintf(nd->inet_detail, sizeof(nd->inet_detail), "%s HTTP %d %u ms", + ipx, nd->probe->status_code, nd->probe->elapsed_ms); + } else { + nd->inet_state = INET_OFFLINE; + snprintf(nd->inet_detail, sizeof(nd->inet_detail), "%s", + st == HTTP_STATE_ERROR ? nd->probe->err : "no response"); + } + + http_fetch_release(nd->probe); + nd->probe = NULL; +} + +static void netmon_start_probe(netmon_data_t* nd) { + if (nd->probe) return; // one in flight already + nd->probe = http_fetch_start(NETMON_PROBE_HOST, 4096); + if (nd->probe) { + nd->inet_state = INET_CHECKING; + } else { + // The single worker slot is busy (e.g. the Web app is fetching). + nd->inet_state = INET_UNKNOWN; + snprintf(nd->inet_detail, sizeof(nd->inet_detail), "worker busy, try again"); + } +} + // =========================================================================== // Sampling // =========================================================================== @@ -217,8 +267,32 @@ static void netmon_paint(gui_window_t* win) { (unsigned)tcp_get_connections_count(), tcp_get_listener_count()); gui_label_aligned(0, row + 12, w - 12, buf, GUI_THEME_TEXT_MUTED, GUI_ALIGN_RIGHT); + // --- internet reachability ------------------------------------------- + // Kick off one probe automatically the first time the window paints, then + // let the user re-check on demand. The fetch runs on the worker thread, so + // this stays responsive while a check is in flight. + if (!nd->auto_checked) { nd->auto_checked = true; netmon_start_probe(nd); } + netmon_poll_probe(nd); + + int irow = row + 28; + static const char* const inet_labels[] = { "UNKNOWN", "checking...", "ONLINE", "OFFLINE" }; + uint32_t inet_col = (nd->inet_state == INET_ONLINE) ? GUI_THEME_SUCCESS + : (nd->inet_state == INET_OFFLINE) ? GUI_THEME_DANGER + : (nd->inet_state == INET_CHECKING)? GUI_THEME_WARNING + : GUI_THEME_TEXT_DIM; + gui_label(10, irow, "Internet:", GUI_THEME_TEXT_MUTED); + gui_badge(84, irow - 1, inet_labels[nd->inet_state], + inet_col, GUI_COLOR_BLACK); + if (nd->inet_detail[0]) { + gui_label(150, irow, nd->inet_detail, GUI_THEME_TEXT_DIM); + } + if (gui_button(5, w - 82, irow - 3, 72, 18, + nd->probe ? "checking" : "Check")) { + netmon_start_probe(nd); + } + // --- connection table ------------------------------------------------- - int tbl_y = row + 30; + int tbl_y = row + 46; gui_separator(10, tbl_y - 4, w - 20); gui_label_bold(10, tbl_y, "PROTO LOCAL", GUI_THEME_TEXT_DIM); gui_label(w / 2 + 6, tbl_y, "REMOTE", GUI_THEME_TEXT_DIM); @@ -306,7 +380,9 @@ static void netmon_event(gui_window_t* win, const gui_event_t* ev) { gui_widget_feed_event(win, ev); if (ev->type == GUI_EVENT_CLOSE && win->user_data) { - kfree(win->user_data); + netmon_data_t* nd = (netmon_data_t*)win->user_data; + if (nd->probe) http_fetch_release(nd->probe); // safe mid-fetch + kfree(nd); win->user_data = NULL; } } diff --git a/include/net/http_client.h b/include/net/http_client.h index e46342d..fc40cd5 100644 --- a/include/net/http_client.h +++ b/include/net/http_client.h @@ -31,6 +31,7 @@ typedef struct http_fetch { char host[128]; char path[256]; uint16_t port; + bool tls; // the URL asked for https, which we cannot speak // Result. `state` is the only field the requester may read while the fetch // is in flight; everything else is valid once state != RUNNING. @@ -41,10 +42,11 @@ typedef struct http_fetch { int len; // bytes stored int cap; // buffer capacity - uint32_t ip; // resolved peer address + uint32_t ip; // resolved peer address (of the final hop) int status_code; // parsed HTTP status, 0 if not found - uint32_t elapsed_ms; // wall time of the fetch - char err[64]; // human-readable failure reason + uint32_t elapsed_ms; // wall time of the whole fetch + int redirects; // number of 3xx hops followed + char err[96]; // human-readable failure reason } http_fetch_t; // Start the worker thread. Call once at boot, after the TCP stack is up. diff --git a/net/http_client.c b/net/http_client.c index 70e66b2..3ca87f6 100644 --- a/net/http_client.c +++ b/net/http_client.c @@ -37,41 +37,119 @@ static http_fetch_t* volatile mailbox = NULL; // job awaiting the worker // Anything it cannot make sense of leaves the job unusable and is reported as // an error rather than guessed at. // =========================================================================== -static void parse_url(const char* url, http_fetch_t* f) { - f->host[0] = '\0'; - strcpy(f->path, "/"); - f->port = 80; +// Parse an absolute URL into its parts. Returns false only when there is no +// host to be found. `is_https` reports the scheme so the caller can refuse a +// TLS target cleanly rather than speaking plaintext to port 443. +static bool split_url(const char* url, char* host, size_t host_cap, + char* path, size_t path_cap, uint16_t* port, bool* is_https) { + host[0] = '\0'; + strcpy(path, "/"); + *port = 80; + *is_https = false; - if (!url) return; - if (strncmp(url, "http://", 7) == 0) url += 7; - else if (strncmp(url, "https://", 8) == 0) url += 8; // no TLS; try plain + if (!url) return false; + if (strncmp(url, "http://", 7) == 0) { + url += 7; + } else if (strncmp(url, "https://", 8) == 0) { + url += 8; + *is_https = true; + *port = 443; + } // Host (and optional :port) up to the first '/'. - int i = 0; char hostport[192]; + int i = 0; while (url[i] && url[i] != '/' && i < (int)sizeof(hostport) - 1) { hostport[i] = url[i]; i++; } hostport[i] = '\0'; - const char* rest = url + i; // "" or "/path..." - // Split host:port. char* colon = strchr(hostport, ':'); if (colon) { *colon = '\0'; int p = 0; for (const char* c = colon + 1; *c >= '0' && *c <= '9'; c++) p = p * 10 + (*c - '0'); - if (p > 0 && p < 65536) f->port = (uint16_t)p; + if (p > 0 && p < 65536) *port = (uint16_t)p; } - strncpy(f->host, hostport, sizeof(f->host) - 1); - f->host[sizeof(f->host) - 1] = '\0'; + strncpy(host, hostport, host_cap - 1); + host[host_cap - 1] = '\0'; if (rest[0] == '/') { - strncpy(f->path, rest, sizeof(f->path) - 1); - f->path[sizeof(f->path) - 1] = '\0'; + strncpy(path, rest, path_cap - 1); + path[path_cap - 1] = '\0'; } + return host[0] != '\0'; +} + +static void parse_url(const char* url, http_fetch_t* f) { + split_url(url, f->host, sizeof(f->host), f->path, sizeof(f->path), &f->port, &f->tls); +} + +// Find a header's value in the response, case-insensitively. Copies the value +// (trimmed, up to the CRLF) into out. Returns false if the header is absent. +static bool find_header(const char* buf, int len, const char* name, + char* out, size_t out_cap) { + size_t nlen = strlen(name); + // Scan line by line; the status line is skipped naturally (no match). + for (int i = 0; i + (int)nlen < len; i++) { + if (i != 0 && buf[i - 1] != '\n') continue; // only at a line start + + // Case-insensitive compare of the header name. + size_t k = 0; + while (k < nlen) { + char a = buf[i + k], b = name[k]; + if (a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a'); + if (b >= 'A' && b <= 'Z') b = (char)(b - 'A' + 'a'); + if (a != b) break; + k++; + } + if (k != nlen || buf[i + k] != ':') continue; + + const char* v = buf + i + nlen + 1; + const char* end = buf + len; + while (v < end && (*v == ' ' || *v == '\t')) v++; + size_t o = 0; + while (v < end && *v != '\r' && *v != '\n' && o < out_cap - 1) out[o++] = *v++; + out[o] = '\0'; + return true; + } + return false; +} + +// Resolve a Location value against the current request. Absolute URLs replace +// host/port/path; a path-only Location keeps the host. Returns false and sets +// f->err when the target is HTTPS, which this TLS-less client cannot follow. +static bool apply_redirect(http_fetch_t* f, const char* location) { + if (location[0] == '/' || location[0] == '\0') { + // Same host, new path. + strncpy(f->path, location[0] ? location : "/", sizeof(f->path) - 1); + f->path[sizeof(f->path) - 1] = '\0'; + return true; + } + + if (strncmp(location, "http://", 7) != 0 && strncmp(location, "https://", 8) != 0) { + // A bare relative reference; treat it as a rooted path. + snprintf(f->path, sizeof(f->path), "/%s", location); + return true; + } + + bool https; + char host[128], path[256]; + uint16_t port; + if (!split_url(location, host, sizeof(host), path, sizeof(path), &port, &https)) { + snprintf(f->err, sizeof(f->err), "redirect to a URL we cannot parse"); + return false; + } + if (https) { + snprintf(f->err, sizeof(f->err), "redirect to HTTPS (TLS unsupported): %s", host); + return false; + } + strcpy(f->host, host); + strcpy(f->path, path); + f->port = port; + return true; } // Parse the numeric status out of "HTTP/1.x NNN ...", or 0 if absent. @@ -96,22 +174,23 @@ static int parse_status(const char* buf, int len) { // finalize would then read freed memory. Returning the state instead keeps the // job private to the worker until the atomic hand-off. // =========================================================================== -static int do_fetch(http_fetch_t* f) { - uint64_t t0 = pit_get_ticks(); +#define HTTP_MAX_REDIRECTS 5 - // Resolve: a dotted quad is taken as-is, otherwise the DNS resolver runs - // (itself bounded, so an unreachable server times out rather than hangs). +// Perform one request/response against the current host/path/port. Returns the +// HTTP status code, or a negative error code; the raw response lands in f->buf. +static int do_one_request(http_fetch_t* f) { f->ip = ip_parse(f->host); if (f->ip == 0) f->ip = dns_resolve(f->host); if (f->ip == 0) { snprintf(f->err, sizeof(f->err), "cannot resolve %s", f->host); - return HTTP_STATE_ERROR; + return -1; } tcp_conn_t* c = tcp_connect(f->ip, f->port, HTTP_CONNECT_TIMEOUT_MS); if (!c) { - snprintf(f->err, sizeof(f->err), "connect to :%u refused/timed out", f->port); - return HTTP_STATE_ERROR; + snprintf(f->err, sizeof(f->err), "connect to %s:%u refused/timed out", + f->host, f->port); + return -1; } char req[512]; @@ -121,10 +200,9 @@ static int do_fetch(http_fetch_t* f) { if (tcp_send(c, req, (uint16_t)rl) < 0) { snprintf(f->err, sizeof(f->err), "send failed"); tcp_close(c); - return HTTP_STATE_ERROR; + return -1; } - // Drain the response until the peer closes or the buffer fills. f->len = 0; while (f->len < f->cap - 1) { int n = tcp_recv(c, f->buf + f->len, (uint16_t)(f->cap - 1 - f->len), @@ -135,14 +213,51 @@ static int do_fetch(http_fetch_t* f) { f->buf[f->len] = '\0'; tcp_close(c); - f->status_code = parse_status(f->buf, f->len); - f->elapsed_ms = (uint32_t)((pit_get_ticks() - t0) * 10); - if (f->len == 0) { snprintf(f->err, sizeof(f->err), "no data received"); + return -1; + } + return parse_status(f->buf, f->len); +} + +// Fills in the result fields but deliberately does NOT touch f->state: +// publishing the terminal state is finalize's job, done under interrupts-off. +// Returning the state keeps the job private to the worker until that hand-off. +// +// A 3xx response with a Location header is followed, up to a bounded number of +// hops, so a real-world URL that redirects (a bare host to a path, http to a +// canonical host) resolves to actual content. An https target is reported +// rather than followed, since this client has no TLS. +static int do_fetch(http_fetch_t* f) { + uint64_t t0 = pit_get_ticks(); + f->redirects = 0; + + // The client has no TLS, so an https URL cannot be served. Say so plainly + // rather than sending a plaintext request to port 443 and reporting a + // confusing timeout. + if (f->tls) { + snprintf(f->err, sizeof(f->err), "%s is HTTPS; this client has no TLS", f->host); return HTTP_STATE_ERROR; } - return HTTP_STATE_DONE; + + for (;;) { + int status = do_one_request(f); + if (status < 0) return HTTP_STATE_ERROR; // f->err already set + + bool is_redirect = (status == 301 || status == 302 || status == 303 || + status == 307 || status == 308); + char location[512]; + if (is_redirect && f->redirects < HTTP_MAX_REDIRECTS && + find_header(f->buf, f->len, "Location", location, sizeof(location))) { + if (!apply_redirect(f, location)) return HTTP_STATE_ERROR; // e.g. HTTPS + f->redirects++; + continue; + } + + f->status_code = status; + f->elapsed_ms = (uint32_t)((pit_get_ticks() - t0) * 10); + return HTTP_STATE_DONE; + } } static void free_job(http_fetch_t* f) { diff --git a/userland/lazybox/lazybox.c b/userland/lazybox/lazybox.c index eb76c41..d306e83 100644 --- a/userland/lazybox/lazybox.c +++ b/userland/lazybox/lazybox.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -2488,6 +2489,86 @@ static int applet_webget(int argc, char** argv) { return 0; } +// End-to-end connectivity check: link, address, gateway, DNS, and a real HTTP +// fetch, each reported PASS/FAIL. This is the one-command answer to "is the +// internet working?" -- it exercises every layer the stack depends on. +static int applet_netcheck(int argc, char** argv) { + const char* probe = (argc >= 2) ? argv[1] : "example.com"; + + printk(ANSI_BRIGHT_CYAN "=== SUB-OS Connectivity Check ===\n" ANSI_RESET); + + #define OKMARK ANSI_BRIGHT_GREEN "[ ok ]" ANSI_RESET + #define FAILMARK ANSI_BRIGHT_RED "[fail]" ANSI_RESET + + int failures = 0; + + // 1. Physical link. + bool link = e1000_is_link_up(); + printk(" %s link eth0 %s\n", link ? OKMARK : FAILMARK, + link ? "UP" : "DOWN"); + if (!link) failures++; + + // 2. Interface address. + net_if_t* nif = net_get_primary_if(); + bool has_ip = nif && nif->ip != 0; + char ips[16], gws[16], dnss[16]; + ip_to_str(nif ? nif->ip : 0, ips); + ip_to_str(nif ? nif->gateway : 0, gws); + ip_to_str(nif ? nif->dns : 0, dnss); + printk(" %s address %s gw %s dns %s\n", has_ip ? OKMARK : FAILMARK, + ips, gws, dnss); + if (!has_ip) failures++; + + // 3. Gateway reachability (one quiet ping). + int recv = (nif && nif->gateway) ? net_ping(nif->gateway, 1, 1000) : 0; + printk(" %s gateway %s %s\n", recv > 0 ? OKMARK : FAILMARK, gws, + recv > 0 ? "reachable" : "no reply"); + if (recv <= 0) failures++; + + // 4. DNS resolution. + uint32_t rip = dns_resolve(probe); + char rips[16]; + ip_to_str(rip, rips); + printk(" %s dns %s -> %s\n", rip ? OKMARK : FAILMARK, probe, + rip ? rips : "no answer"); + if (!rip) failures++; + + // 5. A real HTTP fetch, on the async worker, polled to completion. + char url[160]; + snprintf(url, sizeof(url), "%s/", probe); + http_fetch_t* f = http_fetch_start(url, 8192); + if (!f) { + printk(" %s http could not start fetch\n", FAILMARK); + failures++; + } else { + uint64_t t0 = pit_get_ticks(); + while (f->state == HTTP_STATE_RUNNING && (pit_get_ticks() - t0) < 1500) { + sched_yield(); + } + if (f->state == HTTP_STATE_DONE && f->status_code > 0) { + printk(" %s http GET %s -> %d, %d bytes, %u ms%s\n", OKMARK, + probe, f->status_code, f->len, f->elapsed_ms, + f->redirects ? " (redirected)" : ""); + } else { + printk(" %s http %s\n", FAILMARK, + f->state == HTTP_STATE_ERROR ? f->err : "timed out"); + failures++; + } + http_fetch_release(f); + } + + if (failures == 0) { + printk(ANSI_BRIGHT_GREEN "\n Internet: ONLINE -- all checks passed.\n" ANSI_RESET); + } else { + printk(ANSI_YELLOW "\n Internet: DEGRADED -- %d check(s) failed.\n" ANSI_RESET, + failures); + } + + #undef OKMARK + #undef FAILMARK + return failures == 0 ? 0 : 1; +} + // A minimal HTTP/1.0 client over the active-open path. static int applet_httpget(int argc, char** argv) { if (argc < 2) { @@ -2860,6 +2941,7 @@ static const lazybox_applet_t applets[] = { {"tcpconnect", applet_tcpconnect, "tcpconnect [text]", "Open an outbound TCP stream", "Network"}, {"httpget", applet_httpget, "httpget [path] [port]", "Fetch a URL over TCP", "Network"}, {"webget", applet_webget, "webget ", "Fetch a URL via the async worker", "Network"}, + {"netcheck", applet_netcheck, "netcheck [host]", "End-to-end internet connectivity test", "Network"}, {"tcpserve", applet_tcpserve, "tcpserve ", "Accept one inbound TCP stream", "Network"}, // Storage & Devices