/* Native HTTP/HTTPS fetcher for MontaukOS. The OS exposes TCP and DNS as * syscalls rather than BSD sockets; HTTPS is provided by the OS BearSSL * helper and its system certificate bundle. */ #include #include #include #include #include #include #include #include #include #include "utils/corestrings.h" #include "utils/log.h" #include "utils/nsurl.h" #include "utils/ring.h" #include "content/fetch.h" #include "content/fetchers.h" #include "content/fetchers/montauk.h" #include "content/fetchers/montauk_bridge.h" #define RESPONSE_LIMIT (8u * 1024u * 1024u) #define IO_TIMEOUT_MS 30000u struct fetch_montauk_context { struct fetch *parent; nsurl *url; char *post; char **headers; volatile bool aborted; bool locked; volatile int worker_state; /* 0 queued, 1 running, 2 complete */ int worker_tid; void *worker_stack; char *response; size_t response_len; const char *transport_error; struct fetch_montauk_context *r_next, *r_prev; }; static struct fetch_montauk_context *ring; static bool worker_busy; static void callback(struct fetch_montauk_context *ctx, const fetch_msg *msg) { ctx->locked = true; fetch_send_callback(msg, ctx->parent); ctx->locked = false; } static void fail(struct fetch_montauk_context *ctx, const char *reason) { fetch_msg msg = { .type = FETCH_ERROR }; msg.data.error = reason; callback(ctx, &msg); } static bool send_all(int fd, const uint8_t *data, size_t len) { size_t off = 0; while (off < len) { int n = mtk_send(fd, data + off, len - off); if (n <= 0) return false; off += (size_t)n; } return true; } static bool append(char **buf, size_t *len, size_t *cap, const void *data, size_t amount) { if (amount > RESPONSE_LIMIT - *len) return false; if (*len + amount + 1 > *cap) { size_t next = *cap ? *cap * 2 : 32768; while (next < *len + amount + 1) next *= 2; if (next > RESPONSE_LIMIT + 1) next = RESPONSE_LIMIT + 1; char *replacement = realloc(*buf, next); if (replacement == NULL) return false; *buf = replacement; *cap = next; } memcpy(*buf + *len, data, amount); *len += amount; (*buf)[*len] = 0; return true; } static bool read_response(struct fetch_montauk_context *ctx, int fd, char **response, size_t *response_len) { char chunk[8192]; size_t cap = 0; uint64_t deadline = mtk_get_ms() + IO_TIMEOUT_MS; *response = NULL; *response_len = 0; for (;;) { if (ctx->aborted) return false; int n = mtk_recv(fd, chunk, sizeof(chunk)); if (n > 0) { if (!append(response, response_len, &cap, chunk, (size_t)n)) return false; deadline = mtk_get_ms() + IO_TIMEOUT_MS; } else if (n < 0) { return *response_len != 0; } else { if (mtk_get_ms() >= deadline) return false; mtk_sleep_ms(1); } } } static char *find_header_end(char *response) { char *p = strstr(response, "\r\n\r\n"); return p == NULL ? NULL : p + 4; } static bool header_value(const char *headers, const char *name, char *value, size_t value_size) { size_t name_len = strlen(name); const char *first_end = strstr(headers, "\r\n"); const char *line = first_end == NULL ? NULL : first_end + 2; while (line != NULL && *line != '\r' && *line != 0) { const char *end = strstr(line, "\r\n"); if (end == NULL) break; if ((size_t)(end - line) > name_len && strncasecmp(line, name, name_len) == 0 && line[name_len] == ':') { const char *start = line + name_len + 1; size_t len; while (*start == ' ' || *start == '\t') start++; len = (size_t)(end - start); if (len >= value_size) len = value_size - 1; memcpy(value, start, len); value[len] = 0; return true; } line = end + 2; } return false; } static bool dechunk(char *body, size_t body_len, size_t *decoded_len) { char *src = body, *dst = body, *limit = body + body_len; while (src < limit) { char *line_end = strstr(src, "\r\n"); char *endptr; unsigned long amount; if (line_end == NULL || line_end >= limit) return false; *line_end = 0; amount = strtoul(src, &endptr, 16); if (endptr == src) return false; src = line_end + 2; if (amount == 0) { *decoded_len = (size_t)(dst - body); return true; } if (amount > (unsigned long)(limit - src) || amount + 2 > (unsigned long)(limit - src)) return false; memmove(dst, src, amount); dst += amount; src += amount; if (src[0] != '\r' || src[1] != '\n') return false; src += 2; } return false; } static bool transport(struct fetch_montauk_context *ctx) { char *scheme = NULL, *host = NULL, *port_text = NULL, *path = NULL; size_t ignored; uint16_t port = 80; bool https = false; uint32_t ip; int fd = -1; char *request = NULL, *response = NULL; size_t request_len = 0, request_cap = 0, response_len = 0; char line[1024]; bool ok = false; if (nsurl_get(ctx->url, NSURL_SCHEME, &scheme, &ignored) != NSERROR_OK || nsurl_get(ctx->url, NSURL_HOST, &host, &ignored) != NSERROR_OK || nsurl_get(ctx->url, NSURL_PATH | NSURL_QUERY, &path, &ignored) != NSERROR_OK) { ctx->transport_error = "Invalid HTTP(S) URL"; goto done; } https = strcasecmp(scheme, "https") == 0; port = https ? 443 : 80; if (nsurl_has_component(ctx->url, NSURL_PORT) && nsurl_get(ctx->url, NSURL_PORT, &port_text, &ignored) == NSERROR_OK) port = (uint16_t)strtoul(port_text, NULL, 10); if (*path == 0) { free(path); path = strdup("/"); } ip = mtk_resolve(host); if (ip == 0) { ctx->transport_error = "DNS resolution failed"; goto done; } if (!https) { fd = mtk_socket(MTK_SOCK_TCP); if (fd < 0 || mtk_connect(fd, ip, port) < 0) { ctx->transport_error = "Could not connect to server"; goto done; } } int n = snprintf(line, sizeof(line), "%s %s HTTP/1.1\r\nHost: %s%s%s\r\nUser-Agent: NetSurf-MontaukOS\r\nAccept: */*\r\nAccept-Encoding: identity\r\nConnection: close\r\n", ctx->post ? "POST" : "GET", path, host, port == (https ? 443 : 80) ? "" : ":", port == (https ? 443 : 80) ? "" : (port_text ? port_text : (https ? "443" : "80"))); if (n < 0 || !append(&request, &request_len, &request_cap, line, (size_t)n)) goto oom; if (ctx->headers != NULL) { for (char **h = ctx->headers; *h != NULL; h++) { if (!append(&request, &request_len, &request_cap, *h, strlen(*h)) || !append(&request, &request_len, &request_cap, "\r\n", 2)) goto oom; } } if (ctx->post != NULL) { n = snprintf(line, sizeof(line), "Content-Type: application/x-www-form-urlencoded\r\nContent-Length: %zu\r\n", strlen(ctx->post)); if (n < 0 || !append(&request, &request_len, &request_cap, line, (size_t)n)) goto oom; } if (!append(&request, &request_len, &request_cap, "\r\n", 2) || (ctx->post != NULL && !append(&request, &request_len, &request_cap, ctx->post, strlen(ctx->post)))) goto oom; if (https) { response = malloc(RESPONSE_LIMIT + 1); if (response == NULL) goto oom; int received = mtk_netsurf_https_fetch(host, ip, port, request, request_len, response, RESPONSE_LIMIT + 1); if (received <= 0) { ctx->transport_error = "HTTPS transport or certificate verification failed"; goto done; } if ((size_t)received >= RESPONSE_LIMIT) { ctx->transport_error = "HTTP response exceeds size limit"; goto done; } response_len = (size_t)received; response[response_len] = 0; } else if (!send_all(fd, (const uint8_t *)request, request_len) || !read_response(ctx, fd, &response, &response_len)) { ctx->transport_error = ctx->aborted ? "Request cancelled" : "HTTP transport failed"; goto done; } ctx->response = response; ctx->response_len = response_len; response = NULL; ok = true; goto done; oom: ctx->transport_error = "Out of memory while fetching HTTP URL"; done: if (fd >= 0) mtk_closesocket(fd); free(scheme); free(host); free(port_text); free(path); free(request); free(response); return ok; } static bool deliver_response(struct fetch_montauk_context *ctx) { char *response = ctx->response; size_t response_len = ctx->response_len, body_len; char *body; char location[2048], transfer[64]; int status; fetch_msg msg; bool ok = false; body = find_header_end(response); char *status_space = strncmp(response, "HTTP/", 5) == 0 ? strchr(response, ' ') : NULL; if (body == NULL || status_space == NULL) { fail(ctx, "Malformed HTTP response"); goto done; } status = (int)strtoul(status_space + 1, NULL, 10); if (status < 100 || status > 599) { fail(ctx, "Malformed HTTP status"); goto done; } body_len = response_len - (size_t)(body - response); fetch_set_http_code(ctx->parent, status); if (status >= 300 && status < 400 && header_value(response, "Location", location, sizeof(location))) { msg.type = FETCH_REDIRECT; msg.data.redirect = location; callback(ctx, &msg); ok = true; goto done; } /* header_value temporarily terminates its matching line; find the header * boundary again only after looking up all values we need. */ if (header_value(response, "Transfer-Encoding", transfer, sizeof(transfer)) && strcasecmp(transfer, "chunked") == 0 && !dechunk(body, body_len, &body_len)) { fail(ctx, "Malformed chunked HTTP response"); goto done; } /* Match libcurl's header callback contract exactly: the status line, each * header, and the terminating blank line arrive separately and retain * their CRLF. llcache uses the status and blank lines to drive its header * state machine; omitting either makes an otherwise valid fetch fail. */ char *h = response; while (h != NULL && h < body && !ctx->aborted) { char *end = strstr(h, "\r\n"); if (end == NULL || end > body) break; msg.type = FETCH_HEADER; msg.data.header_or_data.buf = (const uint8_t *)h; msg.data.header_or_data.len = (size_t)(end - h) + 2; callback(ctx, &msg); h = end + 2; } if (!ctx->aborted && body_len != 0) { msg.type = FETCH_DATA; msg.data.header_or_data.buf = (const uint8_t *)body; msg.data.header_or_data.len = body_len; callback(ctx, &msg); } if (!ctx->aborted) { msg.type = FETCH_FINISHED; callback(ctx, &msg); } ok = true; goto done; done: return ok; } static void worker_entry(struct fetch_montauk_context *ctx) { transport(ctx); __atomic_store_n(&ctx->worker_state, 2, __ATOMIC_RELEASE); _mtk_syscall1(MTK_SYS_THREAD_EXIT, 0); __builtin_unreachable(); } static bool start_worker(struct fetch_montauk_context *ctx) { const size_t stack_size = 256u * 1024u; ctx->worker_stack = malloc(stack_size); if (ctx->worker_stack == NULL) return false; uintptr_t top = ((uintptr_t)ctx->worker_stack + stack_size) & ~(uintptr_t)15; ctx->worker_state = 1; ctx->worker_tid = (int)_mtk_syscall3(MTK_SYS_THREAD_SPAWN, (long)worker_entry, (long)ctx, (long)top); if (ctx->worker_tid < 0) { ctx->worker_state = 0; free(ctx->worker_stack); ctx->worker_stack = NULL; return false; } worker_busy = true; return true; } static void join_worker(struct fetch_montauk_context *ctx) { if (ctx->worker_tid > 0) { _mtk_syscall2(MTK_SYS_THREAD_JOIN, ctx->worker_tid, 0); ctx->worker_tid = 0; } free(ctx->worker_stack); ctx->worker_stack = NULL; worker_busy = false; } static bool initialise(lwc_string *scheme) { (void)scheme; return true; } static void finalise(lwc_string *scheme) { (void)scheme; } static bool acceptable(const nsurl *url) { (void)url; return true; } static void *setup(struct fetch *parent, nsurl *url, bool only_2xx, bool downgrade_tls, const char *post_urlenc, const struct fetch_multipart_data *post_multipart, const char **headers) { struct fetch_montauk_context *ctx = calloc(1, sizeof(*ctx)); (void)only_2xx; (void)downgrade_tls; (void)post_multipart; if (ctx == NULL) return NULL; ctx->parent = parent; ctx->url = nsurl_ref(url); ctx->post = post_urlenc == NULL ? NULL : strdup(post_urlenc); if (post_urlenc != NULL && ctx->post == NULL) { nsurl_unref(ctx->url); free(ctx); return NULL; } if (headers != NULL) { size_t count = 0; while (headers[count] != NULL) count++; ctx->headers = calloc(count + 1, sizeof(*ctx->headers)); if (ctx->headers == NULL) { nsurl_unref(ctx->url); free(ctx->post); free(ctx); return NULL; } for (size_t i = 0; i < count; i++) { ctx->headers[i] = strdup(headers[i]); if (ctx->headers[i] == NULL) { while (i != 0) free(ctx->headers[--i]); free(ctx->headers); nsurl_unref(ctx->url); free(ctx->post); free(ctx); return NULL; } } } RING_INSERT(ring, ctx); return ctx; } static bool start(void *ctx) { (void)ctx; return true; } static void abort_fetch(void *vctx) { ((struct fetch_montauk_context *)vctx)->aborted = true; } static void free_fetch(void *vctx) { struct fetch_montauk_context *ctx = vctx; if (__atomic_load_n(&ctx->worker_state, __ATOMIC_ACQUIRE) == 1) { ctx->aborted = true; _mtk_syscall2(MTK_SYS_THREAD_JOIN, ctx->worker_tid, 0); ctx->worker_tid = 0; free(ctx->worker_stack); ctx->worker_stack = NULL; worker_busy = false; } else if (ctx->worker_tid > 0) { join_worker(ctx); } nsurl_unref(ctx->url); free(ctx->post); free(ctx->response); if (ctx->headers != NULL) { for (size_t i = 0; ctx->headers[i] != NULL; i++) free(ctx->headers[i]); free(ctx->headers); } free(ctx); } static void poll_fetch(lwc_string *scheme) { struct fetch_montauk_context *ctx, *saved = NULL; (void)scheme; while (ring != NULL) { ctx = ring; RING_REMOVE(ring, ctx); if (ctx->locked) { RING_INSERT(saved, ctx); continue; } int state = __atomic_load_n(&ctx->worker_state, __ATOMIC_ACQUIRE); if (state == 2) { join_worker(ctx); if (!ctx->aborted) { if (ctx->transport_error != NULL) fail(ctx, ctx->transport_error); else deliver_response(ctx); } assert(!ctx->locked); fetch_remove_from_queues(ctx->parent); fetch_free(ctx->parent); } else if (state == 0 && ctx->aborted) { fetch_remove_from_queues(ctx->parent); fetch_free(ctx->parent); } else if (state == 0 && !worker_busy) { if (start_worker(ctx)) { RING_INSERT(saved, ctx); } else { fail(ctx, "Could not start network worker"); fetch_remove_from_queues(ctx->parent); fetch_free(ctx->parent); } } else { RING_INSERT(saved, ctx); } } ring = saved; } nserror fetch_montauk_register(void) { const struct fetcher_operation_table ops = { .initialise = initialise, .acceptable = acceptable, .setup = setup, .start = start, .abort = abort_fetch, .free = free_fetch, .poll = poll_fetch, .finalise = finalise }; nserror err = fetcher_add(lwc_string_ref(corestring_lwc_http), &ops); if (err != NSERROR_OK) return err; return fetcher_add(lwc_string_ref(corestring_lwc_https), &ops); }