feat(net,gui): real internet access -- redirects, HTTPS guard, netcheck, and a live ONLINE indicator

The stack could already reach the real internet once the UDP-checksum and
scheduler fixes landed; this makes it work on real-world URLs and gives the
desktop a first-class "is the internet up?" answer.

HTTP client
  - Follows 3xx redirects (Location header, absolute or relative) up to five
    hops, so a bare host that 301s to a path or a canonical host resolves to
    actual content. Verified: `webget google.com/` follows the 301 to
    www.google.com and returns its HTTP 200 homepage.
  - Refuses https:// targets -- at the initial URL and on redirect -- with a
    clear message instead of speaking plaintext to port 443 and timing out.
  - Split the URL parser out so both the initial request and redirect targets
    go through the same path; added a case-insensitive header finder.

netcheck (CLI)
  One command that walks the whole chain -- link, address, gateway ping, DNS,
  and a real HTTP fetch -- and reports each PASS/FAIL, ending in ONLINE or
  DEGRADED. The one-shot answer to "is the internet working?".

Network Monitor (GUI)
  A live Internet: ONLINE/OFFLINE badge with the resolved IP, HTTP status and
  latency. It auto-probes once on open and re-checks on the Check button, all
  through the async HTTP worker so the window never blocks.

Verified against the real internet through QEMU's NAT: DNS resolved
example.com to a live Cloudflare address; `netcheck` passed all five layers
(Internet: ONLINE); the GUI badge showed "ONLINE 104.20.23.154 HTTP 200 240
ms"; redirect following and the HTTPS refusal both behaved. Builds clean on
x86_64, aarch64 and armv8i.

Known limit: no TLS, so HTTPS URLs are reported rather than fetched -- plain
HTTP only.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fh5z6V7nGPbHvqVG5p4CXX
This commit is contained in:
subhobhai943andClaude Opus 4.8 committed 2026-08-27 02:44:53 +05:30
1 parent dd4922767a
commit f6e4511047
4 files changed
+308 -33

No files matched your search

+143 -28
View File
@@ -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) {