/* * http.hpp * Shared HTTP/1.1 client for MontaukOS. * * Owns request construction, DNS/transport selection, bounded response * collection, response parsing, and chunked-transfer decoding. Applications * should use this layer instead of constructing HTTP messages themselves. */ #pragma once #include #include #include #include namespace http { enum class Error { NONE = 0, INVALID_ARGUMENT, DNS_FAILED, NO_MEMORY, REQUEST_TOO_LARGE, SOCKET_FAILED, CONNECT_FAILED, SEND_FAILED, RECEIVE_FAILED, TLS_FAILED, INVALID_RESPONSE, RESPONSE_TOO_LARGE, TRUNCATED_RESPONSE }; inline const char* error_string(Error error) { switch (error) { case Error::NONE: return "no error"; case Error::INVALID_ARGUMENT: return "invalid HTTP request"; case Error::DNS_FAILED: return "DNS resolution failed"; case Error::NO_MEMORY: return "out of memory"; case Error::REQUEST_TOO_LARGE: return "HTTP request is too large"; case Error::SOCKET_FAILED: return "could not create socket"; case Error::CONNECT_FAILED: return "connection failed"; case Error::SEND_FAILED: return "request send failed"; case Error::RECEIVE_FAILED: return "response receive failed"; case Error::TLS_FAILED: return "TLS exchange failed"; case Error::INVALID_RESPONSE: return "invalid HTTP response"; case Error::RESPONSE_TOO_LARGE: return "HTTP response exceeded the buffer"; case Error::TRUNCATED_RESPONSE: return "truncated HTTP response"; } return "unknown HTTP error"; } struct Response { int status; // HTTP status code, or -1 before a response is parsed const char* headers; // Pointers into raw int headers_len; const char* body; int body_len; char* raw; int raw_len; Error error; bool owns_raw; }; struct RequestOptions { bool secure; // true for HTTPS, false for HTTP uint16_t port; // 0 selects 443 or 80 uint32_t resolved_ip; // 0 performs DNS resolution int response_buffer_size; // used by request(); includes trailing NUL const char* host_header; // optional Host authority (e.g. host:port) const char* extra_headers; // complete CRLF-terminated header lines tls::AbortCheckFn abort_check; uint64_t timeout_ms; // inactivity timeout for plain HTTP RequestOptions() : secure(true), port(0), resolved_ip(0), response_buffer_size(32768), host_header(nullptr), extra_headers(nullptr), abort_check(nullptr), timeout_ms(30000) {} }; inline Response empty_response(Error error = Error::NONE) { Response resp = {}; resp.status = -1; resp.error = error; return resp; } inline const char* find_header_block_end(const char* start, const char* end) { if (!start || !end || start >= end) return nullptr; for (const char* p = start; p + 3 < end; ++p) { if (p[0] == '\r' && p[1] == '\n' && p[2] == '\r' && p[3] == '\n') return p + 4; } for (const char* p = start; p + 1 < end; ++p) { if (p[0] == '\n' && p[1] == '\n') return p + 2; } return nullptr; } inline int parse_status_code(const char* start, const char* end) { if (!start || !end || end - start < 12) return -1; if (start[0] != 'H' || start[1] != 'T' || start[2] != 'T' || start[3] != 'P' || start[4] != '/' || start[5] != '1' || start[6] != '.' || (start[7] != '0' && start[7] != '1') || start[8] != ' ') return -1; const char* p = start; while (p < end && *p != ' ' && *p != '\r' && *p != '\n') ++p; if (p >= end || *p++ != ' ') return -1; if (end - p < 3 || p[0] < '0' || p[0] > '9' || p[1] < '0' || p[1] > '9' || p[2] < '0' || p[2] > '9') return -1; return (p[0] - '0') * 100 + (p[1] - '0') * 10 + (p[2] - '0'); } inline const char* find_final_response_start(const char* buf, int len) { if (!buf || len <= 0) return nullptr; const char* start = buf; const char* end = buf + len; for (;;) { int code = parse_status_code(start, end); if (code < 0) return nullptr; if (code < 100 || code == 101 || code >= 200) return start; const char* next = find_header_block_end(start, end); if (!next || next >= end) return nullptr; start = next; } } inline const char* skip_informational_responses(const char* buf, int len) { const char* start = find_final_response_start(buf, len); return start ? start : buf; } inline bool ascii_equal_ci(char a, char b) { if (a >= 'A' && a <= 'Z') a += 'a' - 'A'; if (b >= 'A' && b <= 'Z') b += 'a' - 'A'; return a == b; } inline bool get_header(const Response* resp, const char* name, char* out_val, int max_len) { if (!resp || !resp->headers || resp->headers_len <= 0 || !name || !out_val || max_len <= 0) return false; int name_len = montauk::slen(name); const char* p = resp->headers; const char* end = p + resp->headers_len; while (p < end) { const char* line_end = p; while (line_end < end && *line_end != '\r' && *line_end != '\n') ++line_end; bool match = line_end - p > name_len && p[name_len] == ':'; for (int i = 0; match && i < name_len; ++i) if (!ascii_equal_ci(p[i], name[i])) match = false; if (match) { const char* value = p + name_len + 1; while (value < line_end && (*value == ' ' || *value == '\t')) ++value; while (line_end > value && (line_end[-1] == ' ' || line_end[-1] == '\t')) --line_end; int n = (int)(line_end - value); if (n >= max_len) n = max_len - 1; if (n > 0) montauk::memcpy(out_val, value, n); out_val[n] = '\0'; return true; } p = line_end; while (p < end && (*p == '\r' || *p == '\n')) ++p; } return false; } inline bool header_has_token(const Response* resp, const char* name, const char* token) { char value[128]; if (!get_header(resp, name, value, sizeof(value))) return false; int token_len = montauk::slen(token); for (int i = 0; value[i];) { while (value[i] == ' ' || value[i] == '\t' || value[i] == ',') ++i; int start = i; while (value[i] && value[i] != ',' && value[i] != ' ' && value[i] != '\t') ++i; int len = i - start; bool match = len == token_len; for (int j = 0; match && j < len; ++j) if (!ascii_equal_ci(value[start + j], token[j])) match = false; if (match) return true; while (value[i] && value[i] != ',') ++i; } return false; } inline int parse_decimal(const char* value) { if (!value || !*value) return -1; int result = 0; for (int i = 0; value[i]; ++i) { if (value[i] < '0' || value[i] > '9') return -1; if (result > 214748364 || (result == 214748364 && value[i] > '7')) return -1; result = result * 10 + value[i] - '0'; } return result; } inline int decode_chunked_body(char* body, int encoded_len) { int read_pos = 0; int write_pos = 0; for (;;) { unsigned chunk_size = 0; int digits = 0; while (read_pos < encoded_len && body[read_pos] != '\r' && body[read_pos] != '\n') { char c = body[read_pos++]; if (c == ';') { while (read_pos < encoded_len && body[read_pos] != '\r' && body[read_pos] != '\n') ++read_pos; break; } unsigned digit; if (c >= '0' && c <= '9') digit = (unsigned)(c - '0'); else if (c >= 'a' && c <= 'f') digit = (unsigned)(c - 'a' + 10); else if (c >= 'A' && c <= 'F') digit = (unsigned)(c - 'A' + 10); else return -1; if (chunk_size > 0x0FFFFFFFu) return -1; chunk_size = chunk_size * 16 + digit; ++digits; } if (digits == 0 || read_pos >= encoded_len) return -1; if (body[read_pos] == '\r') { if (read_pos + 1 >= encoded_len || body[read_pos + 1] != '\n') return -1; read_pos += 2; } else { ++read_pos; } if (chunk_size == 0) { // A zero chunk is followed by either an empty trailer line or a // trailer header block. Do not accept a response cut at "0\r\n". if (read_pos < encoded_len && body[read_pos] == '\n') return write_pos; if (read_pos + 1 < encoded_len && body[read_pos] == '\r' && body[read_pos + 1] == '\n') return write_pos; if (find_header_block_end(body + read_pos, body + encoded_len)) return write_pos; return -1; } if (chunk_size > (unsigned)(encoded_len - read_pos)) return -1; montauk::memmove(body + write_pos, body + read_pos, chunk_size); write_pos += (int)chunk_size; read_pos += (int)chunk_size; if (read_pos >= encoded_len) return -1; if (body[read_pos] == '\r') { if (read_pos + 1 >= encoded_len || body[read_pos + 1] != '\n') return -1; read_pos += 2; } else if (body[read_pos] == '\n') { ++read_pos; } else { return -1; } } } // Parses and normalizes a response in place. Chunked bodies are decoded in // the same buffer. Content-Length mismatches are reported as truncation. inline int parse_response(char* buf, int len, Response* out) { if (!out) return -1; bool owns_raw = out->owns_raw; *out = empty_response(); out->raw = buf; out->raw_len = len; out->owns_raw = owns_raw; if (!buf || len <= 0) { out->error = Error::INVALID_RESPONSE; return -1; } const char* start = find_final_response_start(buf, len); const char* end = buf + len; if (!start) { out->error = Error::INVALID_RESPONSE; return -1; } int code = parse_status_code(start, end); if (code < 0) { out->error = Error::INVALID_RESPONSE; return -1; } const char* status_end = start; while (status_end < end && *status_end != '\n') ++status_end; if (status_end >= end) { out->error = Error::INVALID_RESPONSE; return -1; } const char* headers = status_end + 1; const char* body = find_header_block_end(headers, end); if (!body) { out->error = Error::INVALID_RESPONSE; return -1; } const char* headers_end = body; if (headers_end - headers >= 4 && headers_end[-4] == '\r') headers_end -= 4; else headers_end -= 2; out->status = code; out->headers = headers; out->headers_len = (int)(headers_end - headers); out->body = body; out->body_len = (int)(end - body); if (header_has_token(out, "Transfer-Encoding", "chunked")) { int decoded = decode_chunked_body((char*)out->body, out->body_len); if (decoded < 0) { out->error = Error::TRUNCATED_RESPONSE; return code; } out->body_len = decoded; ((char*)out->body)[decoded] = '\0'; } else { char value[32]; if (get_header(out, "Content-Length", value, sizeof(value))) { int expected = parse_decimal(value); if (expected < 0) { out->error = Error::INVALID_RESPONSE; return code; } if (out->body_len < expected) { out->error = Error::TRUNCATED_RESPONSE; return code; } out->body_len = expected; } } return code; } inline void free_response(Response* resp) { if (!resp) return; if (resp->raw && resp->owns_raw) montauk::mfree(resp->raw); *resp = empty_response(); } inline bool contains_crlf(const char* value) { if (!value) return false; for (; *value; ++value) if (*value == '\r' || *value == '\n') return true; return false; } inline bool contains_header_terminator(const char* value) { if (!value) return false; for (int i = 0; value[i]; ++i) { if (value[i] == '\n' && value[i + 1] == '\n') return true; if (value[i] == '\r' && value[i + 1] == '\n' && value[i + 2] == '\r' && value[i + 3] == '\n') return true; } return false; } struct RequestWriter { char* p; char* end; bool overflow; void text(const char* value) { if (!value) return; while (*value) { if (p >= end) { overflow = true; return; } *p++ = *value++; } } void number(int value) { char digits[16]; int n = 0; if (value == 0) digits[n++] = '0'; while (value > 0 && n < (int)sizeof(digits)) { digits[n++] = (char)('0' + value % 10); value /= 10; } while (n > 0) { if (p >= end) { overflow = true; return; } *p++ = digits[--n]; } } }; inline int build_request(char* buf, int buf_size, const char* method, const char* host, const char* path, const char* content_type, const char* body_data, int body_len, const char* extra_headers) { if (!buf || buf_size <= 0 || !method || !*method || !host || !*host || !path || path[0] != '/' || body_len < 0 || (body_len > 0 && !body_data) || contains_crlf(method) || contains_crlf(host) || contains_crlf(path) || (content_type && contains_crlf(content_type)) || contains_header_terminator(extra_headers)) return -1; RequestWriter w = {buf, buf + buf_size, false}; w.text(method); w.text(" "); w.text(path); w.text(" HTTP/1.1\r\n"); w.text("Host: "); w.text(host); w.text("\r\n"); if (body_data || body_len > 0) { if (content_type) { w.text("Content-Type: "); w.text(content_type); w.text("\r\n"); } w.text("Content-Length: "); w.number(body_len); w.text("\r\n"); } if (extra_headers) { w.text(extra_headers); int n = montauk::slen(extra_headers); if (n > 0 && extra_headers[n - 1] != '\n') w.text("\r\n"); } w.text("Connection: close\r\n\r\n"); if (!w.overflow && body_len > 0) { if (w.end - w.p < body_len) w.overflow = true; else { montauk::memcpy(w.p, body_data, body_len); w.p += body_len; } } return w.overflow ? -1 : (int)(w.p - buf); } inline bool plain_send_all(int fd, const char* data, int len, uint64_t timeout_ms, tls::AbortCheckFn abort_check) { int sent = 0; uint64_t deadline = montauk::get_milliseconds() + timeout_ms; while (sent < len) { if (abort_check && abort_check()) return false; int n = montauk::send(fd, data + sent, (uint32_t)(len - sent)); if (n < 0) return false; if (n > 0) { sent += n; deadline = montauk::get_milliseconds() + timeout_ms; continue; } uint64_t now = montauk::get_milliseconds(); if (now >= deadline) return false; uint32_t signals = montauk::wait_handle( fd, montauk::abi::IPC_SIGNAL_WRITABLE | montauk::abi::IPC_SIGNAL_PEER_CLOSED, deadline - now); if (signals == (uint32_t)-1 || (signals & montauk::abi::IPC_SIGNAL_PEER_CLOSED)) return false; } return true; } inline int plain_receive(int fd, char* buf, int capacity, uint64_t timeout_ms, tls::AbortCheckFn abort_check, Error* error) { int total = 0; uint64_t deadline = montauk::get_milliseconds() + timeout_ms; while (total < capacity) { if (abort_check && abort_check()) { *error = Error::RECEIVE_FAILED; return -1; } int n = montauk::recv(fd, buf + total, (uint32_t)(capacity - total)); if (n > 0) { total += n; deadline = montauk::get_milliseconds() + timeout_ms; continue; } if (n < 0) return total; uint64_t now = montauk::get_milliseconds(); if (now >= deadline) { *error = Error::RECEIVE_FAILED; return total > 0 ? total : -1; } uint32_t signals = montauk::wait_handle( fd, montauk::abi::IPC_SIGNAL_READABLE | montauk::abi::IPC_SIGNAL_PEER_CLOSED, deadline - now); if (signals == (uint32_t)-1) { *error = Error::RECEIVE_FAILED; return total > 0 ? total : -1; } if ((signals & montauk::abi::IPC_SIGNAL_PEER_CLOSED) && !(signals & montauk::abi::IPC_SIGNAL_READABLE)) return total; } *error = Error::RESPONSE_TOO_LARGE; return total; } // Generic request using a caller-owned response buffer. inline Response request_into(const char* method, const char* host, const char* path, const char* content_type, const char* body_data, int body_len, const tls::TrustAnchors* tas, char* response_buffer, int response_buffer_size, const RequestOptions& options = RequestOptions()) { Response resp = empty_response(); if (!response_buffer || response_buffer_size < 2 || !method || !host || !path || body_len < 0 || (body_len > 0 && !body_data) || (options.secure && (!tas || tas->count == 0))) { resp.error = Error::INVALID_ARGUMENT; return resp; } uint64_t request_size64 = 160u + (uint64_t)montauk::slen(method) + (uint64_t)montauk::slen(host) + (uint64_t)montauk::slen(path) + (uint64_t)(content_type ? montauk::slen(content_type) : 0) + (uint64_t)(options.extra_headers ? montauk::slen(options.extra_headers) : 0) + (uint64_t)body_len; if (request_size64 > 8u * 1024u * 1024u) { resp.error = Error::REQUEST_TOO_LARGE; return resp; } int request_size = (int)request_size64; char* request_data = (char*)montauk::malloc(request_size); if (!request_data) { resp.error = Error::NO_MEMORY; return resp; } int request_len = build_request(request_data, request_size, method, options.host_header ? options.host_header : host, path, content_type, body_data, body_len, options.extra_headers); if (request_len < 0) { montauk::mfree(request_data); resp.error = Error::REQUEST_TOO_LARGE; return resp; } uint32_t ip = options.resolved_ip ? options.resolved_ip : montauk::resolve(host); if (!ip) { montauk::mfree(request_data); resp.error = Error::DNS_FAILED; return resp; } uint16_t port = options.port ? options.port : (options.secure ? 443 : 80); int received = -1; Error transport_error = Error::NONE; if (options.secure) { received = tls::https_fetch(host, ip, port, request_data, request_len, *tas, response_buffer, response_buffer_size, options.abort_check); if (received < 0) transport_error = Error::TLS_FAILED; else if (received >= response_buffer_size - 1) transport_error = Error::RESPONSE_TOO_LARGE; } else { int fd = montauk::socket(montauk::abi::SOCK_TCP); if (fd < 0) { transport_error = Error::SOCKET_FAILED; } else if (montauk::connect(fd, ip, port) < 0) { transport_error = Error::CONNECT_FAILED; montauk::closesocket(fd); } else { if (!plain_send_all(fd, request_data, request_len, options.timeout_ms, options.abort_check)) { transport_error = Error::SEND_FAILED; } else { received = plain_receive(fd, response_buffer, response_buffer_size - 1, options.timeout_ms, options.abort_check, &transport_error); } montauk::closesocket(fd); } } montauk::mfree(request_data); if (received <= 0) { resp.error = transport_error == Error::NONE ? Error::RECEIVE_FAILED : transport_error; return resp; } response_buffer[received] = '\0'; resp.owns_raw = false; parse_response(response_buffer, received, &resp); if (resp.error == Error::NONE && transport_error != Error::NONE) resp.error = transport_error; return resp; } // Generic request with a library-owned response buffer. inline Response request(const char* method, const char* host, const char* path, const char* content_type, const char* body_data, int body_len, const tls::TrustAnchors* tas, const RequestOptions& options = RequestOptions()) { if (options.response_buffer_size < 2) return empty_response(Error::INVALID_ARGUMENT); char* buffer = (char*)montauk::malloc(options.response_buffer_size); if (!buffer) return empty_response(Error::NO_MEMORY); Response resp = request_into(method, host, path, content_type, body_data, body_len, tas, buffer, options.response_buffer_size, options); if (!resp.raw) { montauk::mfree(buffer); } else { resp.owns_raw = true; } return resp; } inline Response get(const char* host, const char* path, const tls::TrustAnchors& tas, int response_buffer_size = 32768, const char* extra_headers = nullptr, tls::AbortCheckFn abort_check = nullptr) { RequestOptions options; options.response_buffer_size = response_buffer_size; options.extra_headers = extra_headers; options.abort_check = abort_check; return request("GET", host, path, nullptr, nullptr, 0, &tas, options); } inline Response post(const char* host, const char* path, const char* content_type, const char* body_data, int body_len, const tls::TrustAnchors& tas, int response_buffer_size = 32768, const char* extra_headers = nullptr, tls::AbortCheckFn abort_check = nullptr) { RequestOptions options; options.response_buffer_size = response_buffer_size; options.extra_headers = extra_headers; options.abort_check = abort_check; return request("POST", host, path, content_type, body_data, body_len, &tas, options); } inline Response request(const char* method, const char* host, const char* path, const char* content_type, const char* body_data, int body_len, const tls::TrustAnchors& tas, int response_buffer_size = 32768, const char* extra_headers = nullptr, tls::AbortCheckFn abort_check = nullptr) { RequestOptions options; options.response_buffer_size = response_buffer_size; options.extra_headers = extra_headers; options.abort_check = abort_check; return request(method, host, path, content_type, body_data, body_len, &tas, options); } inline Response get_plain(const char* host, const char* path, int response_buffer_size = 32768, const char* extra_headers = nullptr) { RequestOptions options; options.secure = false; options.response_buffer_size = response_buffer_size; options.extra_headers = extra_headers; return request("GET", host, path, nullptr, nullptr, 0, nullptr, options); } } // namespace http