Files
MontaukOS/template/sysroot/include/http/http.hpp
T

378 lines
12 KiB
C++

/*
* http.hpp
* Simple HTTP request builder and response parser for MontaukOS
* Wraps tls::https_fetch() and raw sockets for ergonomic HTTP usage.
*/
#pragma once
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/heap.h>
#include <tls/tls.hpp>
namespace http {
// ----------------------------------------------------------------------------
// Response
// ----------------------------------------------------------------------------
struct Response {
int status; // HTTP status code (200, 404, etc.) or -1 on error
const char* headers; // Pointer into raw buffer (header block)
int headers_len;
const char* body; // Pointer into raw buffer (body)
int body_len;
char* raw; // Owned buffer — caller must free with montauk::mfree()
int raw_len;
};
inline const char* find_header_block_end(const char* start, const char* end) {
if (!start || !end || start >= end) return nullptr;
for (const char* s = start; s < end - 3; s++) {
if (s[0] == '\r' && s[1] == '\n' && s[2] == '\r' && s[3] == '\n')
return s + 4;
}
for (const char* s = start; s < end - 1; s++) {
if (s[0] == '\n' && s[1] == '\n')
return s + 2;
}
return nullptr;
}
inline int parse_status_code(const char* start, const char* end) {
if (!start || !end || end - start < 12) return -1;
const char* p = start;
while (p < end && *p && *p != ' ') p++;
if (p >= end || *p != ' ') return -1;
p++;
int code = 0;
int digits = 0;
while (digits < 3 && p < end && *p >= '0' && *p <= '9') {
code = code * 10 + (*p - '0');
p++;
digits++;
}
return digits == 3 ? code : -1;
}
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 >= 200) return start;
const char* next = find_header_block_end(start, end);
if (!next || next >= end) return nullptr;
if (end - next < 5) return nullptr;
if (!(next[0] == 'H' && next[1] == 'T' && next[2] == 'T' && next[3] == 'P' && next[4] == '/'))
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;
}
// Parse raw HTTP response in-place. Sets pointers into buf (does not copy).
// Skips leading informational 1xx responses such as "100 Continue".
// Returns status code, or -1 if unparseable.
inline int parse_response(char* buf, int len, Response* out) {
out->raw = buf;
out->raw_len = len;
out->status = -1;
out->headers = nullptr;
out->headers_len = 0;
out->body = nullptr;
out->body_len = 0;
const char* start = find_final_response_start(buf, len);
const char* end = buf + len;
if (!start || end - start < 12) return -1; // "HTTP/1.x NNN"
// Parse status code from "HTTP/1.x NNN"
int code = parse_status_code(start, end);
if (code < 0) return -1;
out->status = code;
// Headers start after the status line
const char* hdr_start = start;
while (hdr_start < end - 1) {
if (*hdr_start == '\r' && *(hdr_start + 1) == '\n') { hdr_start += 2; break; }
if (*hdr_start == '\n') { hdr_start++; break; }
hdr_start++;
}
out->headers = hdr_start;
// Find \r\n\r\n boundary between headers and body
const char* body = find_header_block_end(hdr_start, end);
if (body) {
if (body >= hdr_start + 4 &&
body[-4] == '\r' && body[-3] == '\n' && body[-2] == '\r' && body[-1] == '\n')
out->headers_len = (int)((body - 4) - hdr_start);
else if (body >= hdr_start + 2 &&
body[-2] == '\n' && body[-1] == '\n')
out->headers_len = (int)((body - 2) - hdr_start);
else
out->headers_len = (int)(body - hdr_start);
out->body = body;
out->body_len = len - (int)(out->body - buf);
return code;
}
// No body separator found — entire remainder is headers
out->headers_len = len - (int)(hdr_start - buf);
return code;
}
// Find a header value by name (case-insensitive match on the name).
// Writes value into out_val (up to max_len), returns true if found.
inline bool get_header(const Response* resp, const char* name, char* out_val, int max_len) {
if (!resp->headers || resp->headers_len == 0) return false;
int name_len = montauk::slen(name);
const char* p = resp->headers;
const char* end = resp->headers + resp->headers_len;
while (p < end) {
// Case-insensitive prefix match
bool match = true;
if (p + name_len >= end) { match = false; }
else {
for (int i = 0; i < name_len; i++) {
char a = p[i], b = name[i];
if (a >= 'A' && a <= 'Z') a += 32;
if (b >= 'A' && b <= 'Z') b += 32;
if (a != b) { match = false; break; }
}
if (match && p[name_len] != ':') match = false;
}
if (match) {
const char* v = p + name_len + 1;
while (v < end && *v == ' ') v++; // skip OWS
int i = 0;
while (v < end && *v != '\r' && *v != '\n' && i < max_len - 1)
out_val[i++] = *v++;
out_val[i] = 0;
return true;
}
// Skip to next line
while (p < end && *p != '\n') p++;
if (p < end) p++;
}
return false;
}
// Free a response's raw buffer.
inline void free_response(Response* resp) {
if (resp->raw) { montauk::mfree(resp->raw); resp->raw = nullptr; }
}
// ----------------------------------------------------------------------------
// Request builder (internal)
// ----------------------------------------------------------------------------
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) {
char* p = buf;
char* end = buf + buf_size - 1;
auto append = [&](const char* s) {
while (*s && p < end) *p++ = *s++;
};
auto append_int = [&](int n) {
char tmp[16]; int ti = 0;
if (n == 0) { if (p < end) *p++ = '0'; return; }
while (n > 0) { tmp[ti++] = '0' + (n % 10); n /= 10; }
for (int j = ti - 1; j >= 0 && p < end; j--) *p++ = tmp[j];
};
// Request line
append(method); append(" "); append(path); append(" HTTP/1.1\r\n");
// Host
append("Host: "); append(host); append("\r\n");
// Content headers (for POST/PUT/PATCH)
if (body_data && body_len > 0) {
if (content_type) {
append("Content-Type: "); append(content_type); append("\r\n");
}
append("Content-Length: "); append_int(body_len); append("\r\n");
}
// Extra headers (caller-supplied, must include \r\n terminators)
if (extra_headers) append(extra_headers);
append("Connection: close\r\n");
append("\r\n");
int header_len = (int)(p - buf);
// Append body
if (body_data && body_len > 0 && header_len + body_len < buf_size) {
montauk::memcpy(p, body_data, body_len);
p += body_len;
}
return (int)(p - buf);
}
// ----------------------------------------------------------------------------
// Public API
// ----------------------------------------------------------------------------
// GET request over HTTPS. Returns parsed response. Caller must free_response().
inline Response get(const char* host, const char* path,
const tls::TrustAnchors& tas,
int resp_buf_size = 32768,
const char* extra_headers = nullptr,
tls::AbortCheckFn abort_check = nullptr) {
Response resp = {};
resp.status = -1;
uint32_t ip = montauk::resolve(host);
if (!ip) return resp;
char req[1024];
int reqLen = build_request(req, sizeof(req), "GET", host, path,
nullptr, nullptr, 0, extra_headers);
char* buf = (char*)montauk::malloc(resp_buf_size);
if (!buf) return resp;
int n = tls::https_fetch(host, ip, 443, req, reqLen, tas,
buf, resp_buf_size - 1, abort_check);
if (n <= 0) { montauk::mfree(buf); return resp; }
buf[n] = 0;
parse_response(buf, n, &resp);
return resp;
}
// POST request over HTTPS. Returns parsed response. Caller must free_response().
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 resp_buf_size = 32768,
const char* extra_headers = nullptr,
tls::AbortCheckFn abort_check = nullptr) {
Response resp = {};
resp.status = -1;
uint32_t ip = montauk::resolve(host);
if (!ip) return resp;
int req_size = 1024 + body_len;
char* req = (char*)montauk::malloc(req_size);
if (!req) return resp;
int reqLen = build_request(req, req_size, "POST", host, path,
content_type, body_data, body_len,
extra_headers);
char* buf = (char*)montauk::malloc(resp_buf_size);
if (!buf) { montauk::mfree(req); return resp; }
int n = tls::https_fetch(host, ip, 443, req, reqLen, tas,
buf, resp_buf_size - 1, abort_check);
montauk::mfree(req);
if (n <= 0) { montauk::mfree(buf); return resp; }
buf[n] = 0;
parse_response(buf, n, &resp);
return resp;
}
// Generic request over HTTPS (PUT, PATCH, DELETE, etc.).
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 resp_buf_size = 32768,
const char* extra_headers = nullptr,
tls::AbortCheckFn abort_check = nullptr) {
Response resp = {};
resp.status = -1;
uint32_t ip = montauk::resolve(host);
if (!ip) return resp;
int req_size = 1024 + (body_len > 0 ? body_len : 0);
char* req = (char*)montauk::malloc(req_size);
if (!req) return resp;
int reqLen = build_request(req, req_size, method, host, path,
content_type, body_data, body_len,
extra_headers);
char* buf = (char*)montauk::malloc(resp_buf_size);
if (!buf) { montauk::mfree(req); return resp; }
int n = tls::https_fetch(host, ip, 443, req, reqLen, tas,
buf, resp_buf_size - 1, abort_check);
montauk::mfree(req);
if (n <= 0) { montauk::mfree(buf); return resp; }
buf[n] = 0;
parse_response(buf, n, &resp);
return resp;
}
// Plain HTTP (no TLS) GET over port 80.
inline Response get_plain(const char* host, const char* path,
int resp_buf_size = 32768,
const char* extra_headers = nullptr) {
Response resp = {};
resp.status = -1;
uint32_t ip = montauk::resolve(host);
if (!ip) return resp;
char req[1024];
int reqLen = build_request(req, sizeof(req), "GET", host, path,
nullptr, nullptr, 0, extra_headers);
int sock = montauk::socket(Montauk::SOCK_TCP);
if (sock < 0) return resp;
if (montauk::connect(sock, ip, 80) < 0) { montauk::closesocket(sock); return resp; }
montauk::send(sock, req, reqLen);
char* buf = (char*)montauk::malloc(resp_buf_size);
if (!buf) { montauk::closesocket(sock); return resp; }
int total = 0;
while (total < resp_buf_size - 1) {
int n = montauk::recv(sock, buf + total, resp_buf_size - 1 - total);
if (n <= 0) break;
total += n;
}
montauk::closesocket(sock);
if (total <= 0) { montauk::mfree(buf); return resp; }
buf[total] = 0;
parse_response(buf, total, &resp);
return resp;
}
} // namespace http