refactor: network - harden TCP/IP and unify HTTP clients
This commit is contained in:
+536
-277
@@ -1,7 +1,10 @@
|
||||
/*
|
||||
* http.hpp
|
||||
* Simple HTTP request builder and response parser for MontaukOS
|
||||
* Wraps tls::https_fetch() and raw sockets for ergonomic HTTP usage.
|
||||
* 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
|
||||
@@ -13,67 +16,113 @@
|
||||
|
||||
namespace http {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Response
|
||||
// ----------------------------------------------------------------------------
|
||||
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 (200, 404, etc.) or -1 on error
|
||||
const char* headers; // Pointer into raw buffer (header block)
|
||||
int status; // HTTP status code, or -1 before a response is parsed
|
||||
const char* headers; // Pointers into raw
|
||||
int headers_len;
|
||||
const char* body; // Pointer into raw buffer (body)
|
||||
const char* body;
|
||||
int body_len;
|
||||
char* raw; // Owned buffer — caller must free with montauk::mfree()
|
||||
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* 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* 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* s = start; s < end - 1; s++) {
|
||||
if (s[0] == '\n' && s[1] == '\n')
|
||||
return s + 2;
|
||||
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 != ' ') 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;
|
||||
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 >= 200) return start;
|
||||
|
||||
if (code < 100 || code == 101 || 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;
|
||||
}
|
||||
}
|
||||
@@ -83,295 +132,505 @@ inline const char* skip_informational_responses(const char* buf, int 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;
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
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 = resp->headers + resp->headers_len;
|
||||
|
||||
const char* end = p + 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;
|
||||
}
|
||||
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* 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;
|
||||
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;
|
||||
}
|
||||
|
||||
// Skip to next line
|
||||
while (p < end && *p != '\n') p++;
|
||||
if (p < end) p++;
|
||||
p = line_end;
|
||||
while (p < end && (*p == '\r' || *p == '\n')) ++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; }
|
||||
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;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Request builder (internal)
|
||||
// ----------------------------------------------------------------------------
|
||||
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) {
|
||||
char* p = buf;
|
||||
char* end = buf + buf_size - 1;
|
||||
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;
|
||||
|
||||
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) {
|
||||
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) {
|
||||
append("Content-Type: "); append(content_type); append("\r\n");
|
||||
w.text("Content-Type: "); w.text(content_type); w.text("\r\n");
|
||||
}
|
||||
append("Content-Length: "); append_int(body_len); append("\r\n");
|
||||
w.text("Content-Length: "); w.number(body_len); w.text("\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;
|
||||
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");
|
||||
}
|
||||
|
||||
return (int)(p - buf);
|
||||
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);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ----------------------------------------------------------------------------
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 tls::TrustAnchors& tas, int response_buffer_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;
|
||||
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);
|
||||
}
|
||||
|
||||
// POST request over HTTPS. Returns parsed response. Caller must free_response().
|
||||
inline Response post(const char* host, const char* path,
|
||||
const char* content_type,
|
||||
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 tls::TrustAnchors& tas, int response_buffer_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;
|
||||
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);
|
||||
}
|
||||
|
||||
// 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,
|
||||
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) {
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
// Plain HTTP (no TLS) GET over port 80.
|
||||
inline Response get_plain(const char* host, const char* path,
|
||||
int resp_buf_size = 32768,
|
||||
int response_buffer_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::abi::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;
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user