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
|
||||
|
||||
@@ -1272,197 +1272,17 @@ inline uint32_t next_request_id() {
|
||||
return request_id++;
|
||||
}
|
||||
|
||||
inline int send_all_plain(int fd, const uint8_t* data, int len) {
|
||||
static constexpr int MAX_SEND_CHUNK = 32768;
|
||||
uint64_t deadline = montauk::get_milliseconds() + 15000;
|
||||
int off = 0;
|
||||
while (off < len) {
|
||||
int chunk = len - off;
|
||||
if (chunk > MAX_SEND_CHUNK) chunk = MAX_SEND_CHUNK;
|
||||
|
||||
int n = montauk::send(fd, data + off, (uint32_t)chunk);
|
||||
if (n > 0) {
|
||||
off += n;
|
||||
deadline = montauk::get_milliseconds() + 15000;
|
||||
continue;
|
||||
}
|
||||
if (n < 0) return -1;
|
||||
|
||||
uint32_t sig = montauk::wait_handle(fd,
|
||||
montauk::abi::IPC_SIGNAL_WRITABLE | montauk::abi::IPC_SIGNAL_PEER_CLOSED,
|
||||
1000);
|
||||
if (sig & montauk::abi::IPC_SIGNAL_PEER_CLOSED) return -1;
|
||||
if (montauk::get_milliseconds() >= deadline) return -1;
|
||||
montauk::sleep_ms(1);
|
||||
}
|
||||
return off;
|
||||
}
|
||||
|
||||
inline bool response_has_no_body(int status) {
|
||||
return (status >= 100 && status < 200) || status == 204 || status == 304;
|
||||
}
|
||||
|
||||
inline int parse_content_length_value(const http::Response* resp) {
|
||||
char value[32] = {};
|
||||
if (!http::get_header(resp, "Content-Length", value, sizeof(value))) return -1;
|
||||
char* end = nullptr;
|
||||
long n = strtol(value, &end, 10);
|
||||
if (end == value || n < 0) return -1;
|
||||
return (int)n;
|
||||
}
|
||||
|
||||
inline bool chunked_body_complete(const char* src, int src_len) {
|
||||
if (src == nullptr || src_len <= 0) return false;
|
||||
|
||||
int pos = 0;
|
||||
while (pos < src_len) {
|
||||
int line_start = pos;
|
||||
while (pos < src_len && src[pos] != '\n') pos++;
|
||||
if (pos >= src_len) return false;
|
||||
|
||||
int line_end = pos;
|
||||
pos++;
|
||||
while (line_end > line_start && (src[line_end - 1] == '\r' || src[line_end - 1] == '\n'))
|
||||
line_end--;
|
||||
|
||||
char hex[16] = {};
|
||||
int hex_pos = 0;
|
||||
for (int i = line_start; i < line_end && hex_pos < (int)sizeof(hex) - 1; i++) {
|
||||
if (src[i] == ';') break;
|
||||
hex[hex_pos++] = src[i];
|
||||
}
|
||||
if (hex_pos == 0) return false;
|
||||
|
||||
char* end = nullptr;
|
||||
unsigned long chunk = strtoul(hex, &end, 16);
|
||||
if (end == hex) return false;
|
||||
|
||||
if (chunk == 0) {
|
||||
if (pos >= src_len) return false;
|
||||
if (src[pos] == '\n') return true;
|
||||
if (src[pos] == '\r' && pos + 1 < src_len && src[pos + 1] == '\n') return true;
|
||||
return http::find_header_block_end(src + pos, src + src_len) != nullptr;
|
||||
}
|
||||
|
||||
if (pos + (int)chunk > src_len) return false;
|
||||
pos += (int)chunk;
|
||||
if (pos < src_len && src[pos] == '\r') pos++;
|
||||
if (pos < src_len && src[pos] == '\n') pos++;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool response_is_chunked(const http::Response* resp);
|
||||
|
||||
inline bool http_response_complete(char* buf, int len, bool peer_closed) {
|
||||
if (buf == nullptr || len <= 0) return false;
|
||||
|
||||
http::Response resp = {};
|
||||
if (http::parse_response(buf, len, &resp) < 0) return false;
|
||||
if (resp.body == nullptr) return false;
|
||||
if (response_has_no_body(resp.status)) return true;
|
||||
if (response_is_chunked(&resp)) return chunked_body_complete(resp.body, resp.body_len);
|
||||
|
||||
int content_length = parse_content_length_value(&resp);
|
||||
if (content_length >= 0) return resp.body_len >= content_length;
|
||||
return peer_closed;
|
||||
}
|
||||
|
||||
inline int recv_http_plain(int fd, char* buf, int cap) {
|
||||
int total = 0;
|
||||
bool peer_closed = false;
|
||||
uint64_t deadline = montauk::get_milliseconds() + 90000;
|
||||
|
||||
while (total < cap - 1) {
|
||||
if (http_response_complete(buf, total, peer_closed)) break;
|
||||
if (montauk::get_milliseconds() >= deadline) break;
|
||||
|
||||
uint32_t sig = montauk::wait_handle(fd,
|
||||
montauk::abi::IPC_SIGNAL_READABLE | montauk::abi::IPC_SIGNAL_PEER_CLOSED,
|
||||
1000);
|
||||
if (sig == 0) continue;
|
||||
|
||||
if (sig & montauk::abi::IPC_SIGNAL_PEER_CLOSED)
|
||||
peer_closed = true;
|
||||
if (!(sig & montauk::abi::IPC_SIGNAL_READABLE)) {
|
||||
if (peer_closed) break;
|
||||
continue;
|
||||
}
|
||||
|
||||
int n = montauk::recv(fd, buf + total, (uint32_t)(cap - 1 - total));
|
||||
if (n < 0) break;
|
||||
if (n == 0) {
|
||||
if (peer_closed) break;
|
||||
continue;
|
||||
}
|
||||
total += n;
|
||||
deadline = montauk::get_milliseconds() + 90000;
|
||||
}
|
||||
buf[total] = '\0';
|
||||
return total;
|
||||
}
|
||||
|
||||
inline bool response_is_chunked(const http::Response* resp) {
|
||||
char value[64] = {};
|
||||
if (!http::get_header(resp, "Transfer-Encoding", value, sizeof(value))) return false;
|
||||
for (int i = 0; value[i]; i++) {
|
||||
if (value[i] >= 'A' && value[i] <= 'Z') value[i] = (char)(value[i] - 'A' + 'a');
|
||||
}
|
||||
return strstr(value, "chunked") != nullptr;
|
||||
}
|
||||
|
||||
inline bool extract_http_body(const http::Response* resp, uint8_t** out_body, int* out_len) {
|
||||
if (out_body) *out_body = nullptr;
|
||||
if (out_len) *out_len = 0;
|
||||
if (resp == nullptr || resp->body == nullptr || resp->body_len < 0) return false;
|
||||
|
||||
if (!response_is_chunked(resp)) {
|
||||
uint8_t* body = (uint8_t*)malloc((size_t)(resp->body_len > 0 ? resp->body_len : 1));
|
||||
if (!body) return false;
|
||||
if (resp->body_len > 0) memcpy(body, resp->body, (size_t)resp->body_len);
|
||||
if (out_body) *out_body = body;
|
||||
if (out_len) *out_len = resp->body_len;
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* src = resp->body;
|
||||
int src_len = resp->body_len;
|
||||
int pos = 0;
|
||||
int out_pos = 0;
|
||||
uint8_t* body = (uint8_t*)malloc((size_t)src_len);
|
||||
uint8_t* body = (uint8_t*)malloc((size_t)(resp->body_len > 0 ? resp->body_len : 1));
|
||||
if (!body) return false;
|
||||
|
||||
while (pos < src_len) {
|
||||
int line_start = pos;
|
||||
while (pos < src_len && src[pos] != '\n') pos++;
|
||||
int line_end = pos;
|
||||
if (pos < src_len && src[pos] == '\n') pos++;
|
||||
while (line_end > line_start && (src[line_end - 1] == '\r' || src[line_end - 1] == '\n'))
|
||||
line_end--;
|
||||
|
||||
char hex[16] = {};
|
||||
int hex_pos = 0;
|
||||
for (int i = line_start; i < line_end && hex_pos < (int)sizeof(hex) - 1; i++) {
|
||||
if (src[i] == ';') break;
|
||||
hex[hex_pos++] = src[i];
|
||||
}
|
||||
unsigned long chunk = strtoul(hex, nullptr, 16);
|
||||
if (chunk == 0) {
|
||||
if (out_body) *out_body = body;
|
||||
if (out_len) *out_len = out_pos;
|
||||
return true;
|
||||
}
|
||||
if (pos + (int)chunk > src_len) break;
|
||||
memcpy(body + out_pos, src + pos, chunk);
|
||||
out_pos += (int)chunk;
|
||||
pos += (int)chunk;
|
||||
if (pos < src_len && src[pos] == '\r') pos++;
|
||||
if (pos < src_len && src[pos] == '\n') pos++;
|
||||
}
|
||||
|
||||
free(body);
|
||||
return false;
|
||||
if (resp->body_len > 0) memcpy(body, resp->body, (size_t)resp->body_len);
|
||||
if (out_body) *out_body = body;
|
||||
if (out_len) *out_len = resp->body_len;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ipp_http_post(const IppUri* uri,
|
||||
@@ -1487,86 +1307,39 @@ inline bool ipp_http_post(const IppUri* uri,
|
||||
else
|
||||
snprintf(host_header, sizeof(host_header), "%s:%u", uri->host, (unsigned)uri->port);
|
||||
|
||||
int req_cap = body_len + 1024;
|
||||
char* req = (char*)malloc((size_t)req_cap);
|
||||
if (!req) {
|
||||
safe_copy(err, err_len, "out of memory");
|
||||
return false;
|
||||
}
|
||||
|
||||
int req_len = snprintf(req, (size_t)req_cap,
|
||||
"POST %s HTTP/1.1\r\n"
|
||||
"Host: %s\r\n"
|
||||
"User-Agent: MontaukOS Print/1.0\r\n"
|
||||
"Content-Type: application/ipp\r\n"
|
||||
"Content-Length: %d\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n",
|
||||
uri->path, host_header, body_len);
|
||||
if (req_len < 0 || req_len + body_len >= req_cap) {
|
||||
free(req);
|
||||
safe_copy(err, err_len, "IPP request is too large");
|
||||
return false;
|
||||
}
|
||||
memcpy(req + req_len, body, (size_t)body_len);
|
||||
req_len += body_len;
|
||||
|
||||
char* raw = (char*)malloc(HTTP_RESPONSE_MAX);
|
||||
if (!raw) {
|
||||
free(req);
|
||||
safe_copy(err, err_len, "out of memory");
|
||||
return false;
|
||||
}
|
||||
|
||||
int raw_len = -1;
|
||||
tls::TrustAnchors tas = {};
|
||||
if (uri->use_tls) {
|
||||
tls::TrustAnchors tas = tls::load_trust_anchors();
|
||||
raw_len = tls::https_fetch(uri->host, uri->ip, uri->port, req, req_len,
|
||||
tas, raw, HTTP_RESPONSE_MAX - 1);
|
||||
if (tas.anchors) free(tas.anchors);
|
||||
} else {
|
||||
int fd = montauk::socket(montauk::abi::SOCK_TCP);
|
||||
if (fd < 0) {
|
||||
tas = tls::load_trust_anchors();
|
||||
if (tas.count == 0) {
|
||||
free(raw);
|
||||
free(req);
|
||||
snprintf(err, (size_t)err_len, "failed to create socket for %s:%u", uri->host, (unsigned)uri->port);
|
||||
safe_copy(err, err_len, "no CA certificates loaded");
|
||||
return false;
|
||||
}
|
||||
if (montauk::connect(fd, uri->ip, uri->port) < 0) {
|
||||
montauk::closesocket(fd);
|
||||
free(raw);
|
||||
free(req);
|
||||
snprintf(err, (size_t)err_len, "failed to connect to %s (%s):%u",
|
||||
uri->host, ip_text, (unsigned)uri->port);
|
||||
return false;
|
||||
}
|
||||
if (send_all_plain(fd, (const uint8_t*)req, req_len) < 0) {
|
||||
montauk::closesocket(fd);
|
||||
free(raw);
|
||||
free(req);
|
||||
snprintf(err, (size_t)err_len, "failed to send print request to %s (%s):%u",
|
||||
uri->host, ip_text, (unsigned)uri->port);
|
||||
return false;
|
||||
}
|
||||
raw_len = recv_http_plain(fd, raw, HTTP_RESPONSE_MAX);
|
||||
montauk::closesocket(fd);
|
||||
}
|
||||
|
||||
free(req);
|
||||
|
||||
if (raw_len <= 0) {
|
||||
http::RequestOptions options;
|
||||
options.secure = uri->use_tls;
|
||||
options.port = uri->port;
|
||||
options.resolved_ip = uri->ip;
|
||||
options.host_header = host_header;
|
||||
options.extra_headers = "User-Agent: MontaukOS Print/1.0\r\n";
|
||||
options.timeout_ms = 90000;
|
||||
http::Response resp = http::request_into(
|
||||
"POST", uri->host, uri->path, "application/ipp",
|
||||
(const char*)body, body_len, uri->use_tls ? &tas : nullptr,
|
||||
raw, HTTP_RESPONSE_MAX, options);
|
||||
tls::free_trust_anchors(&tas);
|
||||
if (resp.error != http::Error::NONE) {
|
||||
free(raw);
|
||||
snprintf(err, (size_t)err_len, "printer returned no response from %s (%s):%u",
|
||||
uri->host, ip_text, (unsigned)uri->port);
|
||||
return false;
|
||||
}
|
||||
|
||||
raw[raw_len] = '\0';
|
||||
http::Response resp = {};
|
||||
if (http::parse_response(raw, raw_len, &resp) < 0) {
|
||||
free(raw);
|
||||
snprintf(err, (size_t)err_len, "printer returned no final HTTP response from %s (%s):%u",
|
||||
uri->host, ip_text, (unsigned)uri->port);
|
||||
snprintf(err, (size_t)err_len, "%s from %s (%s):%u",
|
||||
http::error_string(resp.error), uri->host, ip_text,
|
||||
(unsigned)uri->port);
|
||||
return false;
|
||||
}
|
||||
if (out_http_status) *out_http_status = resp.status;
|
||||
|
||||
@@ -23,6 +23,7 @@ struct TrustAnchors {
|
||||
};
|
||||
|
||||
TrustAnchors load_trust_anchors();
|
||||
void free_trust_anchors(TrustAnchors* tas);
|
||||
void get_bearssl_time(uint32_t* days, uint32_t* seconds);
|
||||
int tls_send_all(int fd, const unsigned char* data, size_t len);
|
||||
int tls_recv_some(int fd, unsigned char* buf, size_t maxlen);
|
||||
|
||||
Reference in New Issue
Block a user