refactor: network - harden TCP/IP and unify HTTP clients

This commit is contained in:
2026-07-29 15:35:17 +01:00
parent 75ae7ede56
commit a288dee7df
28 changed files with 1852 additions and 1361 deletions
+4 -8
View File
@@ -31,25 +31,23 @@ run-hdd: run-hdd-$(ARCH)
.PHONY: run-x86_64
run-x86_64: $(IMAGE_NAME).iso
sudo ./scripts/net-setup.sh
qemu-system-$(ARCH) \
-enable-kvm \
-M q35 \
-bios /usr/share/ovmf/OVMF.fd \
-cdrom $(IMAGE_NAME).iso \
-device e1000,netdev=net0,mac=52:54:00:68:00:99 \
-netdev tap,id=net0,ifname=tap0,script=no,downscript=no \
-netdev user,id=net0 \
$(QEMUFLAGS)
.PHONY: run-hdd-x86_64
run-hdd-x86_64: $(IMAGE_NAME).hdd
sudo ./scripts/net-setup.sh
qemu-system-$(ARCH) \
-M q35 \
-bios /usr/share/ovmf/OVMF.fd \
-hda $(IMAGE_NAME).hdd \
-device e1000,netdev=net0,mac=52:54:00:68:00:99 \
-netdev tap,id=net0,ifname=tap0,script=no,downscript=no \
-netdev user,id=net0 \
$(QEMUFLAGS)
.PHONY: run-aarch64
@@ -133,23 +131,21 @@ run-hdd-loongarch64: $(IMAGE_NAME).hdd
.PHONY: run-bios
run-bios: $(IMAGE_NAME).iso
sudo ./scripts/net-setup.sh
qemu-system-$(ARCH) \
-M q35 \
-cdrom $(IMAGE_NAME).iso \
-boot d \
-device e1000,netdev=net0,mac=52:54:00:68:00:99 \
-netdev tap,id=net0,ifname=tap0,script=no,downscript=no \
-netdev user,id=net0 \
$(QEMUFLAGS)
.PHONY: run-hdd-bios
run-hdd-bios: $(IMAGE_NAME).hdd
sudo ./scripts/net-setup.sh
qemu-system-$(ARCH) \
-M q35 \
-hda $(IMAGE_NAME).hdd \
-device e1000,netdev=net0,mac=52:54:00:68:00:99 \
-netdev tap,id=net0,ifname=tap0,script=no,downscript=no \
-netdev user,id=net0 \
$(QEMUFLAGS)
.PHONY: toolchain
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once
#define MONTAUK_BUILD_NUMBER 7
#define MONTAUK_BUILD_NUMBER 8
+5 -2
View File
@@ -1382,7 +1382,8 @@ namespace Ipc {
Net::Tcp::Connection* conn = socket->tcpConn;
socket->socketLock.Release();
if (conn == nullptr) return -1;
return Net::Tcp::Send(conn, data, (uint16_t)len);
if (len > 0x7FFFFFFFu) return -1;
return Net::Tcp::Send(conn, data, len);
}
int SocketRecvHandle(int handle, uint8_t* buffer, uint32_t maxLen) {
@@ -1399,13 +1400,15 @@ namespace Ipc {
socket->socketLock.Release();
if (conn == nullptr) return -1;
int result = Net::Tcp::ReceiveNonBlocking(conn, buffer, (uint16_t)maxLen);
uint16_t cappedLen = maxLen > 0xFFFFu ? 0xFFFFu : (uint16_t)maxLen;
int result = Net::Tcp::ReceiveNonBlocking(conn, buffer, cappedLen);
if (result != 0) NotifyObjectChanged((Object*)socket);
return result;
}
int SocketSendToHandle(int handle, const uint8_t* data, uint32_t len, uint32_t destIp, uint16_t destPort) {
if (data == nullptr) return -1;
if (len > 1472) return -1;
if (len > 0 && !montauk::abi::UserMemory::Range((uint64_t)data, len, false)) return -1;
Socket* socket = nullptr;
+72 -11
View File
@@ -15,6 +15,7 @@
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <Timekeeping/ApicTimer.hpp>
#include <CppLib/Spinlock.hpp>
using namespace Kt;
@@ -38,6 +39,16 @@ namespace Net::Arp {
static constexpr uint64_t ARP_CACHE_TIMEOUT_MS = 60000; // 60 seconds
static CacheEntry g_cache[ARP_CACHE_SIZE] = {};
static kcp::Spinlock g_cacheLock;
struct PendingRequest {
uint32_t Ip;
uint64_t Timestamp;
bool Valid;
};
static constexpr uint32_t PENDING_REQUEST_SIZE = 16;
static constexpr uint64_t REQUEST_RETRY_MS = 1000;
static PendingRequest g_pendingRequests[PENDING_REQUEST_SIZE] = {};
void Initialize() {
for (uint32_t i = 0; i < ARP_CACHE_SIZE; i++) {
@@ -47,43 +58,91 @@ namespace Net::Arp {
}
static void CacheInsert(uint32_t ip, const uint8_t* mac) {
g_cacheLock.Acquire();
// Look for existing entry or empty slot
uint32_t emptySlot = ARP_CACHE_SIZE;
uint32_t oldestSlot = 0;
uint64_t oldestTimestamp = ~0ULL;
for (uint32_t i = 0; i < ARP_CACHE_SIZE; i++) {
if (g_cache[i].Valid && g_cache[i].Ip == ip) {
// Update existing entry
memcpy(g_cache[i].Mac, mac, 6);
g_cache[i].Timestamp = Timekeeping::GetMilliseconds();
for (uint32_t j = 0; j < PENDING_REQUEST_SIZE; ++j)
if (g_pendingRequests[j].Valid && g_pendingRequests[j].Ip == ip)
g_pendingRequests[j].Valid = false;
g_cacheLock.Release();
return;
}
if (!g_cache[i].Valid && emptySlot == ARP_CACHE_SIZE) {
emptySlot = i;
}
if (g_cache[i].Valid && g_cache[i].Timestamp < oldestTimestamp) {
oldestTimestamp = g_cache[i].Timestamp;
oldestSlot = i;
}
}
if (emptySlot < ARP_CACHE_SIZE) {
g_cache[emptySlot].Ip = ip;
memcpy(g_cache[emptySlot].Mac, mac, 6);
g_cache[emptySlot].Timestamp = Timekeeping::GetMilliseconds();
g_cache[emptySlot].Valid = true;
}
uint32_t slot = emptySlot < ARP_CACHE_SIZE ? emptySlot : oldestSlot;
g_cache[slot].Ip = ip;
memcpy(g_cache[slot].Mac, mac, 6);
g_cache[slot].Timestamp = Timekeeping::GetMilliseconds();
g_cache[slot].Valid = true;
for (uint32_t j = 0; j < PENDING_REQUEST_SIZE; ++j)
if (g_pendingRequests[j].Valid && g_pendingRequests[j].Ip == ip)
g_pendingRequests[j].Valid = false;
g_cacheLock.Release();
}
static bool CacheLookup(uint32_t ip, uint8_t* outMac) {
g_cacheLock.Acquire();
uint64_t now = Timekeeping::GetMilliseconds();
for (uint32_t i = 0; i < ARP_CACHE_SIZE; i++) {
if (g_cache[i].Valid && g_cache[i].Ip == ip) {
if ((now - g_cache[i].Timestamp) > ARP_CACHE_TIMEOUT_MS) {
g_cache[i].Valid = false;
g_cacheLock.Release();
return false;
}
memcpy(outMac, g_cache[i].Mac, 6);
g_cacheLock.Release();
return true;
}
}
g_cacheLock.Release();
return false;
}
static bool ShouldSendRequest(uint32_t ip) {
g_cacheLock.Acquire();
uint64_t now = Timekeeping::GetMilliseconds();
uint32_t slot = PENDING_REQUEST_SIZE;
uint32_t oldest = 0;
uint64_t oldestTimestamp = ~0ULL;
for (uint32_t i = 0; i < PENDING_REQUEST_SIZE; ++i) {
if (g_pendingRequests[i].Valid && g_pendingRequests[i].Ip == ip) {
if (now - g_pendingRequests[i].Timestamp < REQUEST_RETRY_MS) {
g_cacheLock.Release();
return false;
}
slot = i;
break;
}
if (!g_pendingRequests[i].Valid && slot == PENDING_REQUEST_SIZE) slot = i;
if (g_pendingRequests[i].Valid &&
g_pendingRequests[i].Timestamp < oldestTimestamp) {
oldestTimestamp = g_pendingRequests[i].Timestamp;
oldest = i;
}
}
if (slot == PENDING_REQUEST_SIZE) slot = oldest;
g_pendingRequests[slot].Ip = ip;
g_pendingRequests[slot].Timestamp = now;
g_pendingRequests[slot].Valid = true;
g_cacheLock.Release();
return true;
}
void OnPacketReceived(const uint8_t* data, uint16_t length) {
if (length < sizeof(Packet)) {
return;
@@ -92,10 +151,14 @@ namespace Net::Arp {
const Packet* pkt = (const Packet*)data;
if (Ntohs(pkt->HardwareType) != HW_TYPE_ETHERNET ||
Ntohs(pkt->ProtocolType) != PROTO_TYPE_IPV4) {
Ntohs(pkt->ProtocolType) != PROTO_TYPE_IPV4 ||
pkt->HardwareAddrLen != 6 || pkt->ProtocolAddrLen != 4) {
return;
}
uint16_t op = Ntohs(pkt->Operation);
if (op != OP_REQUEST && op != OP_REPLY) return;
uint32_t senderIp = pkt->SenderIp; // Already in network byte order in struct
uint32_t targetIp = pkt->TargetIp;
@@ -103,8 +166,6 @@ namespace Net::Arp {
CacheInsert(senderIp, pkt->SenderMac);
Ipv4::FlushPending();
uint16_t op = Ntohs(pkt->Operation);
if (op == OP_REQUEST && targetIp == GetIpAddress()) {
// Someone is asking for our MAC address -- send a reply
Packet reply;
@@ -135,8 +196,8 @@ namespace Net::Arp {
return true;
}
// Not in cache, send a request
SendRequest(ip);
// Coalesce bursts of packets awaiting the same next hop.
if (ShouldSendRequest(ip)) SendRequest(ip);
return false;
}
+113 -54
View File
@@ -13,6 +13,7 @@
#include <Timekeeping/ApicTimer.hpp>
#include <Sched/Scheduler.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Spinlock.hpp>
namespace Net::Dns {
@@ -36,16 +37,22 @@ namespace Net::Dns {
};
static CacheEntry g_cache[CACHE_SIZE] = {};
static kcp::Spinlock g_cacheLock;
static bool streq(const char* a, const char* b) {
while (*a && *b) {
if (*a != *b) return false;
char ca = *a;
char cb = *b;
if (ca >= 'A' && ca <= 'Z') ca += 'a' - 'A';
if (cb >= 'A' && cb <= 'Z') cb += 'a' - 'A';
if (ca != cb) return false;
a++; b++;
}
return *a == *b;
}
static uint32_t CacheLookup(const char* hostname) {
g_cacheLock.Acquire();
uint64_t now = Timekeeping::GetMilliseconds();
for (int i = 0; i < CACHE_SIZE; i++) {
if (!g_cache[i].valid) continue;
@@ -53,17 +60,22 @@ namespace Net::Dns {
// Check TTL
uint64_t elapsed = (now - g_cache[i].timestamp) / 1000;
if (elapsed < g_cache[i].ttl) {
return g_cache[i].ip;
uint32_t ip = g_cache[i].ip;
g_cacheLock.Release();
return ip;
}
// Expired
g_cache[i].valid = false;
g_cacheLock.Release();
return 0;
}
g_cacheLock.Release();
return 0;
}
static void CacheStore(const char* hostname, uint32_t ip, uint32_t ttl) {
if (ttl == 0) ttl = 60; // Minimum 60s TTL
g_cacheLock.Acquire();
// Find free or oldest slot
int slot = 0;
@@ -85,6 +97,7 @@ namespace Net::Dns {
e.ttl = ttl;
e.timestamp = Timekeeping::GetMilliseconds();
e.valid = true;
g_cacheLock.Release();
}
// ---- DNS query building ----
@@ -178,8 +191,10 @@ namespace Net::Dns {
maxJumps--;
continue;
}
if ((len & 0xC0) != 0) return -1;
// Regular label
if (offset + 1 + len > packetLen) return -1;
offset += 1 + len;
maxJumps--;
}
@@ -209,6 +224,7 @@ namespace Net::Dns {
// Check RCODE (must be 0 = no error)
uint8_t rcode = packet[3] & 0x0F;
if (rcode != 0) return result;
if (packet[2] & 0x02) return result; // Truncated UDP response
uint16_t qdcount = ((uint16_t)packet[4] << 8) | packet[5];
uint16_t ancount = ((uint16_t)packet[6] << 8) | packet[7];
@@ -228,7 +244,7 @@ namespace Net::Dns {
if (offset < 0 || offset + 10 > packetLen) return result;
uint16_t atype = ((uint16_t)packet[offset] << 8) | packet[offset + 1];
// uint16_t aclass = ((uint16_t)packet[offset + 2] << 8) | packet[offset + 3];
uint16_t aclass = ((uint16_t)packet[offset + 2] << 8) | packet[offset + 3];
uint32_t attl = ((uint32_t)packet[offset + 4] << 24) |
((uint32_t)packet[offset + 5] << 16) |
((uint32_t)packet[offset + 6] << 8) |
@@ -238,7 +254,7 @@ namespace Net::Dns {
if (offset + rdlen > packetLen) return result;
if (atype == DNS_QTYPE_A && rdlen == 4) {
if (atype == DNS_QTYPE_A && aclass == DNS_QCLASS_IN && rdlen == 4) {
// A record: 4-byte IPv4 address (already in network byte order)
result.ip = ((uint32_t)packet[offset])
| ((uint32_t)packet[offset + 1] << 8)
@@ -255,26 +271,38 @@ namespace Net::Dns {
return result;
}
// ---- Resolve state (shared with UDP callback) ----
// ---- Concurrent resolve state (shared with UDP callback) ----
static volatile bool g_gotResponse = false;
static volatile uint16_t g_currentId = 0;
static uint8_t g_responseBuffer[512];
static volatile int g_responseLen = 0;
static constexpr int MAX_QUERIES = 8;
struct Query {
volatile bool active;
volatile bool gotResponse;
uint16_t id;
uint16_t localPort;
uint32_t serverIp;
uint8_t response[512];
volatile int responseLen;
};
static Query g_queries[MAX_QUERIES] = {};
static kcp::Spinlock g_queriesLock;
static void DnsRecvCallback(uint32_t srcIp, uint16_t srcPort,
uint16_t dstPort,
const uint8_t* data, uint16_t length) {
(void)srcIp;
(void)srcPort;
(void)dstPort;
if (g_gotResponse) return; // Already got a response
if (length > sizeof(g_responseBuffer)) length = sizeof(g_responseBuffer);
memcpy(g_responseBuffer, data, length);
g_responseLen = length;
g_gotResponse = true;
if (srcPort != DNS_PORT || length < 2) return;
uint16_t responseId = ((uint16_t)data[0] << 8) | data[1];
for (int i = 0; i < MAX_QUERIES; ++i) {
Query& query = g_queries[i];
if (!query.active || query.gotResponse ||
query.localPort != dstPort || query.serverIp != srcIp ||
query.id != responseId) continue;
if (length > sizeof(query.response)) length = sizeof(query.response);
memcpy(query.response, data, length);
query.responseLen = length;
asm volatile("" ::: "memory");
query.gotResponse = true;
return;
}
}
// ---- Simple PRNG for transaction IDs ----
@@ -288,21 +316,26 @@ namespace Net::Dns {
// ---- Check if string is already an IP address ----
static bool IsIpAddress(const char* s) {
int dotCount = 0;
bool hasDigit = false;
for (int i = 0; s[i]; i++) {
if (s[i] >= '0' && s[i] <= '9') {
hasDigit = true;
} else if (s[i] == '.') {
if (!hasDigit) return false;
dotCount++;
hasDigit = false;
} else {
static bool ParseIpAddress(const char* s, uint32_t* out) {
uint32_t ip = 0;
for (int octet = 0; octet < 4; ++octet) {
if (*s < '0' || *s > '9') return false;
uint32_t value = 0;
int digits = 0;
while (*s >= '0' && *s <= '9') {
value = value * 10 + (uint32_t)(*s - '0');
if (value > 255 || ++digits > 3) return false;
++s;
}
ip |= value << (octet * 8);
if (octet < 3) {
if (*s++ != '.') return false;
} else if (*s != '\0') {
return false;
}
}
return hasDigit && dotCount == 3;
*out = ip;
return true;
}
// ---- Public API ----
@@ -310,8 +343,8 @@ namespace Net::Dns {
uint32_t Resolve(const char* hostname, uint32_t timeoutMs) {
if (hostname == nullptr || hostname[0] == '\0') return 0;
// Don't try to resolve IP addresses
if (IsIpAddress(hostname)) return 0;
uint32_t literalIp = 0;
if (ParseIpAddress(hostname, &literalIp)) return literalIp;
// Check cache first
uint32_t cached = CacheLookup(hostname);
@@ -321,51 +354,77 @@ namespace Net::Dns {
uint32_t dnsServer = Net::GetDnsServer();
if (dnsServer == 0) return 0;
// Pick a local port for receiving the response (ephemeral range)
uint16_t localPort = 10000 + (NextId() % 50000);
uint16_t txId = NextId();
g_queriesLock.Acquire();
Query* query = nullptr;
for (int i = 0; i < MAX_QUERIES; ++i) {
if (!g_queries[i].active) {
query = &g_queries[i];
query->active = true;
break;
}
}
if (!query) {
g_queriesLock.Release();
return 0;
}
query->gotResponse = false;
query->responseLen = 0;
query->serverIp = dnsServer;
query->id = NextId();
uint16_t txId = query->id;
g_queriesLock.Release();
// Build DNS query
uint8_t queryPacket[512];
int queryLen = BuildQuery(txId, hostname, queryPacket, sizeof(queryPacket));
if (queryLen == 0) return 0;
if (queryLen == 0) {
query->active = false;
return 0;
}
// Reset response state
g_gotResponse = false;
g_responseLen = 0;
g_currentId = txId;
// Bind our receive port
if (!Net::Udp::Bind(localPort, DnsRecvCallback)) {
// Port might be in use, try another
localPort = 10000 + (NextId() % 50000);
if (!Net::Udp::Bind(localPort, DnsRecvCallback)) {
return 0;
bool bound = false;
for (int attempt = 0; attempt < 16; ++attempt) {
g_queriesLock.Acquire();
uint16_t localPort = (uint16_t)(10000 + (NextId() % 50000));
g_queriesLock.Release();
if (Net::Udp::Bind(localPort, DnsRecvCallback)) {
query->localPort = localPort;
bound = true;
break;
}
}
if (!bound) {
query->active = false;
return 0;
}
// Send the query to DNS server port 53
bool sent = Net::Udp::Send(dnsServer, localPort, DNS_PORT, queryPacket, (uint16_t)queryLen);
bool sent = Net::Udp::Send(dnsServer, query->localPort, DNS_PORT,
queryPacket, (uint16_t)queryLen);
if (!sent) {
Net::Udp::Unbind(localPort);
Net::Udp::Unbind(query->localPort);
query->active = false;
return 0;
}
// Wait for response with timeout
uint64_t start = Timekeeping::GetMilliseconds();
while (!g_gotResponse) {
while (!query->gotResponse) {
if (Timekeeping::GetMilliseconds() - start >= timeoutMs) {
Net::Udp::Unbind(localPort);
Net::Udp::Unbind(query->localPort);
query->active = false;
return 0;
}
Sched::Schedule();
}
// Unbind the port
Net::Udp::Unbind(localPort);
Net::Udp::Unbind(query->localPort);
// Parse the response
DnsAnswer answer = ParseResponse(txId, g_responseBuffer, g_responseLen);
asm volatile("" ::: "memory");
DnsAnswer answer = ParseResponse(txId, query->response, query->responseLen);
query->active = false;
if (!answer.found) return 0;
// Cache the result
+53 -6
View File
@@ -15,6 +15,8 @@
#include <Libraries/Memory.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <CppLib/Spinlock.hpp>
#include <Timekeeping/ApicTimer.hpp>
using namespace Kt;
@@ -28,11 +30,15 @@ namespace Net::Ipv4 {
uint8_t Protocol;
uint8_t Data[Ethernet::MAX_PAYLOAD_SIZE - HEADER_SIZE];
uint16_t Length;
uint64_t Timestamp;
bool Active;
bool Processing;
};
static constexpr uint32_t PENDING_QUEUE_SIZE = 8;
static constexpr uint32_t PENDING_QUEUE_SIZE = 64;
static constexpr uint64_t PENDING_TIMEOUT_MS = 30000;
static PendingPacket g_pendingQueue[PENDING_QUEUE_SIZE] = {};
static kcp::Spinlock g_pendingLock;
void Initialize() {
g_identification = 0;
@@ -125,6 +131,11 @@ namespace Net::Ipv4 {
return;
}
// Fragment reassembly is not implemented yet. Never pass a fragment
// to TCP/UDP as though it were a complete transport packet.
uint16_t fragment = Ntohs(hdr->FlagsFragment);
if (fragment & 0x3FFFu) return;
// Check destination: accept packets addressed to us or broadcast
uint32_t ourIp = GetIpAddress();
if (hdr->DstIp != ourIp && hdr->DstIp != 0xFFFFFFFF) {
@@ -158,7 +169,9 @@ namespace Net::Ipv4 {
hdr->VersionIhl = (4 << 4) | 5; // IPv4, 5 dwords (20 bytes)
hdr->Tos = 0;
hdr->TotalLength = Htons(HEADER_SIZE + payloadLen);
hdr->Identification = Htons(g_identification++);
uint16_t identification =
__atomic_fetch_add(&g_identification, 1, __ATOMIC_RELAXED);
hdr->Identification = Htons(identification);
hdr->FlagsFragment = 0;
hdr->Ttl = DEFAULT_TTL;
hdr->Protocol = protocol;
@@ -187,16 +200,25 @@ namespace Net::Ipv4 {
}
// ARP request already sent by Resolve(), queue the packet for later
g_pendingLock.Acquire();
uint64_t now = Timekeeping::GetMilliseconds();
for (uint32_t i = 0; i < PENDING_QUEUE_SIZE; i++) {
if (g_pendingQueue[i].Active && !g_pendingQueue[i].Processing &&
now - g_pendingQueue[i].Timestamp >= PENDING_TIMEOUT_MS)
g_pendingQueue[i].Active = false;
if (!g_pendingQueue[i].Active) {
g_pendingQueue[i].DestIp = destIp;
g_pendingQueue[i].Protocol = protocol;
g_pendingQueue[i].Length = payloadLen;
g_pendingQueue[i].Timestamp = now;
memcpy(g_pendingQueue[i].Data, payload, payloadLen);
g_pendingQueue[i].Active = true;
g_pendingQueue[i].Processing = false;
g_pendingLock.Release();
return true;
}
}
g_pendingLock.Release();
// Queue full, drop the packet
return false;
@@ -204,17 +226,42 @@ namespace Net::Ipv4 {
void FlushPending() {
for (uint32_t i = 0; i < PENDING_QUEUE_SIZE; i++) {
if (!g_pendingQueue[i].Active) {
uint32_t destIp;
uint8_t protocol;
uint16_t length;
uint8_t data[Ethernet::MAX_PAYLOAD_SIZE - HEADER_SIZE];
g_pendingLock.Acquire();
if (!g_pendingQueue[i].Active || g_pendingQueue[i].Processing) {
g_pendingLock.Release();
continue;
}
if (Timekeeping::GetMilliseconds() - g_pendingQueue[i].Timestamp >=
PENDING_TIMEOUT_MS) {
g_pendingQueue[i].Active = false;
g_pendingLock.Release();
continue;
}
g_pendingQueue[i].Processing = true;
destIp = g_pendingQueue[i].DestIp;
protocol = g_pendingQueue[i].Protocol;
length = g_pendingQueue[i].Length;
memcpy(data, g_pendingQueue[i].Data, length);
g_pendingLock.Release();
uint32_t nextHop = GetNextHop(g_pendingQueue[i].DestIp);
uint32_t nextHop = GetNextHop(destIp);
uint8_t destMac[6];
if (Arp::Resolve(nextHop, destMac)) {
SendDirect(g_pendingQueue[i].DestIp, g_pendingQueue[i].Protocol,
destMac, g_pendingQueue[i].Data, g_pendingQueue[i].Length);
g_pendingLock.Acquire();
g_pendingQueue[i].Active = false;
g_pendingQueue[i].Processing = false;
g_pendingLock.Release();
SendDirect(destIp, protocol, destMac, data, length);
} else {
g_pendingLock.Acquire();
g_pendingQueue[i].Processing = false;
g_pendingLock.Release();
}
}
}
+141 -45
View File
@@ -23,7 +23,8 @@ namespace Net::Tcp {
// Receive buffer size per connection
static constexpr uint16_t RECV_BUFFER_SIZE = 32768;
static constexpr uint16_t WINDOW_SIZE = RECV_BUFFER_SIZE;
static constexpr uint32_t MAX_CONNECTIONS = 16;
static constexpr uint32_t MAX_CONNECTIONS = 64;
static constexpr uint8_t ACCEPT_BACKLOG_SIZE = 8;
static constexpr uint64_t RETRANSMIT_TIMEOUT_MS = 1000;
static constexpr int MAX_RETRANSMITS = 5;
static constexpr uint64_t TIME_WAIT_MS = 2000;
@@ -51,12 +52,18 @@ namespace Net::Tcp {
uint16_t RetransmitLen;
uint64_t RetransmitTime;
int RetransmitCount;
bool SendBusy;
// For Listen/Accept
bool PendingAccept;
uint32_t PendingRemoteIp;
uint16_t PendingRemotePort;
uint32_t PendingSeq;
struct PendingSyn {
uint32_t RemoteIp;
uint16_t RemotePort;
uint32_t Seq;
};
PendingSyn Pending[ACCEPT_BACKLOG_SIZE];
uint8_t PendingHead;
uint8_t PendingTail;
uint8_t PendingCount;
bool Active;
@@ -71,6 +78,17 @@ namespace Net::Tcp {
return (uint32_t)(Timekeeping::GetMilliseconds() * 2654435761u);
}
// TCP sequence numbers are compared modulo 2^32. This is valid for
// ranges smaller than 2^31 bytes, which all of our send windows are.
static bool SeqLessOrEqual(uint32_t a, uint32_t b) {
return (int32_t)(a - b) <= 0;
}
static bool AckIsValid(const Connection* conn, uint32_t ack) {
return SeqLessOrEqual(conn->SendUnack, ack) &&
SeqLessOrEqual(ack, conn->SendNext);
}
static Connection* FindConnection(uint32_t remoteIp, uint16_t remotePort,
uint16_t localPort) {
for (uint32_t i = 0; i < MAX_CONNECTIONS; i++) {
@@ -109,6 +127,13 @@ namespace Net::Tcp {
return nullptr;
}
static void ReleaseConnection(Connection* conn) {
if (!conn) return;
g_connectionsLock.Acquire();
conn->Active = false;
g_connectionsLock.Release();
}
static bool SendSegment(Connection* conn, uint8_t flags,
const uint8_t* payload, uint16_t payloadLen) {
uint8_t packet[1500];
@@ -213,15 +238,35 @@ namespace Net::Tcp {
if (flags & FLAG_SYN) {
Connection* listener = FindListener(dstPort);
if (listener != nullptr) {
// Signal the listener about this incoming connection
// Queue the SYN for accept(). Retransmitted SYNs refresh
// their sequence number instead of consuming backlog.
listener->Lock.Acquire();
listener->PendingAccept = true;
listener->PendingRemoteIp = srcIp;
listener->PendingRemotePort = srcPort;
listener->PendingSeq = seqNum;
bool duplicate = false;
for (uint8_t i = 0; i < listener->PendingCount; ++i) {
uint8_t index = (uint8_t)((listener->PendingHead + i) %
ACCEPT_BACKLOG_SIZE);
Connection::PendingSyn& pending = listener->Pending[index];
if (pending.RemoteIp == srcIp && pending.RemotePort == srcPort) {
pending.Seq = seqNum;
duplicate = true;
break;
}
}
if (!duplicate && listener->PendingCount < ACCEPT_BACKLOG_SIZE) {
Connection::PendingSyn& pending =
listener->Pending[listener->PendingTail];
pending.RemoteIp = srcIp;
pending.RemotePort = srcPort;
pending.Seq = seqNum;
listener->PendingTail =
(uint8_t)((listener->PendingTail + 1) % ACCEPT_BACKLOG_SIZE);
listener->PendingCount++;
}
listener->Lock.Release();
Sched::WakeObjectWaiters(listener);
Ipc::NotifyTcpConnectionChanged(listener);
if (duplicate || listener->PendingCount > 0) {
Sched::WakeObjectWaiters(listener);
Ipc::NotifyTcpConnectionChanged(listener);
}
return;
}
}
@@ -246,7 +291,9 @@ namespace Net::Tcp {
// RST handling
if (flags & FLAG_RST) {
conn->CurrentState = State::Closed;
conn->Active = false;
// Keep the slot reserved until its socket handle is closed.
// Reusing it here would leave that handle pointing at an
// unrelated future connection.
conn->Lock.Release();
Sched::WakeObjectWaiters(conn);
Ipc::NotifyTcpConnectionChanged(conn);
@@ -285,8 +332,10 @@ namespace Net::Tcp {
case State::Established: {
// Handle incoming data
if (flags & FLAG_ACK) {
conn->SendUnack = ackNum;
notify = true;
if (AckIsValid(conn, ackNum)) {
conn->SendUnack = ackNum;
notify = true;
}
}
uint16_t accepted = 0;
@@ -365,9 +414,9 @@ namespace Net::Tcp {
}
case State::LastAck: {
if (flags & FLAG_ACK) {
if ((flags & FLAG_ACK) && ackNum == conn->SendNext) {
conn->SendUnack = ackNum;
conn->CurrentState = State::Closed;
conn->Active = false;
notify = true;
}
break;
@@ -401,7 +450,9 @@ namespace Net::Tcp {
conn->LocalIp = Net::GetIpAddress();
conn->LocalPort = port;
conn->CurrentState = State::Listen;
conn->PendingAccept = false;
conn->PendingHead = 0;
conn->PendingTail = 0;
conn->PendingCount = 0;
KernelLogStream(INFO, "Net") << "TCP listening on port " << base::dec << (uint64_t)port;
return conn;
@@ -415,12 +466,15 @@ namespace Net::Tcp {
// Block until a SYN arrives
while (true) {
listener->Lock.Acquire();
if (listener->PendingAccept) {
listener->PendingAccept = false;
if (listener->PendingCount > 0) {
Connection::PendingSyn pending = listener->Pending[listener->PendingHead];
listener->PendingHead =
(uint8_t)((listener->PendingHead + 1) % ACCEPT_BACKLOG_SIZE);
listener->PendingCount--;
uint32_t remoteIp = listener->PendingRemoteIp;
uint16_t remotePort = listener->PendingRemotePort;
uint32_t remoteSeq = listener->PendingSeq;
uint32_t remoteIp = pending.RemoteIp;
uint16_t remotePort = pending.RemotePort;
uint32_t remoteSeq = pending.Seq;
listener->Lock.Release();
// Allocate a new connection for this client
@@ -480,9 +534,9 @@ namespace Net::Tcp {
}
// Timed out waiting for ACK
conn->Active = false;
Sched::WakeObjectWaiters(conn);
Ipc::NotifyTcpConnectionChanged(conn);
ReleaseConnection(conn);
return nullptr;
}
listener->Lock.Release();
@@ -568,25 +622,56 @@ namespace Net::Tcp {
}
// Failed to connect
conn->Active = false;
Sched::WakeObjectWaiters(conn);
Ipc::NotifyTcpConnectionChanged(conn);
ReleaseConnection(conn);
return nullptr;
}
int Send(Connection* conn, const uint8_t* data, uint16_t length) {
if (conn == nullptr || conn->CurrentState != State::Established) {
return -1;
int Send(Connection* conn, const uint8_t* data, uint32_t length) {
if (conn == nullptr || data == nullptr) return -1;
if (length == 0) return 0;
// A connection has one send sequence/retransmission stream. Serialize
// writers so concurrent threads cannot overwrite its in-flight
// retransmit buffer or allocate overlapping sequence numbers.
while (true) {
uint64_t flags;
asm volatile("pushfq; pop %0; cli" : "=r"(flags) :: "memory");
conn->Lock.Acquire();
if (conn->CurrentState != State::Established) {
conn->Lock.Release();
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
return -1;
}
if (!conn->SendBusy) {
conn->SendBusy = true;
conn->Lock.Release();
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
break;
}
conn->Lock.Release();
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
Sched::BlockOnObject(conn, 50);
}
auto finishSend = [&]() {
uint64_t flags;
asm volatile("pushfq; pop %0; cli" : "=r"(flags) :: "memory");
conn->Lock.Acquire();
conn->SendBusy = false;
conn->Lock.Release();
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
Sched::WakeObjectWaiters(conn);
Ipc::NotifyTcpConnectionChanged(conn);
};
constexpr uint16_t MSS = 1460;
uint16_t sent = 0;
uint32_t sent = 0;
while (sent < length) {
uint16_t segLen = length - sent;
if (segLen > MSS) {
segLen = MSS;
}
uint32_t remaining = length - sent;
uint16_t segLen = remaining > MSS ? MSS : (uint16_t)remaining;
uint32_t segSeq = 0;
uint32_t expectedAck = 0;
@@ -597,7 +682,9 @@ namespace Net::Tcp {
if (conn->CurrentState != State::Established) {
conn->Lock.Release();
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
return sent > 0 ? sent : -1;
int result = sent > 0 ? (int)sent : -1;
finishSend();
return result;
}
segSeq = conn->SendNext;
@@ -607,7 +694,9 @@ namespace Net::Tcp {
if (!ok) {
conn->Lock.Release();
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
return sent > 0 ? sent : -1;
int result = sent > 0 ? (int)sent : -1;
finishSend();
return result;
}
conn->SendNext = expectedAck;
@@ -626,7 +715,7 @@ namespace Net::Tcp {
asm volatile("pushfq; pop %0; cli" : "=r"(flags) :: "memory");
conn->Lock.Acquire();
if (conn->SendUnack >= expectedAck) {
if (SeqLessOrEqual(expectedAck, conn->SendUnack)) {
conn->RetransmitLen = 0;
conn->Lock.Release();
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
@@ -639,7 +728,9 @@ namespace Net::Tcp {
conn->RetransmitLen = 0;
conn->Lock.Release();
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
return sent > 0 ? sent : -1;
int result = sent > 0 ? (int)sent : -1;
finishSend();
return result;
}
uint64_t now = Timekeeping::GetMilliseconds();
@@ -653,7 +744,9 @@ namespace Net::Tcp {
conn->RetransmitLen = 0;
conn->Lock.Release();
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
return sent > 0 ? sent : -1;
int result = sent > 0 ? (int)sent : -1;
finishSend();
return result;
}
uint32_t savedNext = conn->SendNext;
@@ -667,7 +760,9 @@ namespace Net::Tcp {
conn->RetransmitLen = 0;
conn->Lock.Release();
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
return sent > 0 ? sent : -1;
int result = sent > 0 ? (int)sent : -1;
finishSend();
return result;
}
}
@@ -684,7 +779,8 @@ namespace Net::Tcp {
}
}
return sent;
finishSend();
return (int)sent;
}
int Receive(Connection* conn, uint8_t* buffer, uint16_t bufferSize) {
@@ -815,9 +911,9 @@ namespace Net::Tcp {
}
Sched::BlockOnObject(conn, 50);
}
conn->Active = false;
Sched::WakeObjectWaiters(conn);
Ipc::NotifyTcpConnectionChanged(conn);
ReleaseConnection(conn);
return;
}
@@ -835,29 +931,29 @@ namespace Net::Tcp {
}
Sched::BlockOnObject(conn, 50);
}
conn->Active = false;
Sched::WakeObjectWaiters(conn);
Ipc::NotifyTcpConnectionChanged(conn);
ReleaseConnection(conn);
return;
}
case State::Listen:
case State::SynSent: {
conn->CurrentState = State::Closed;
conn->Active = false;
conn->Lock.Release();
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
Sched::WakeObjectWaiters(conn);
Ipc::NotifyTcpConnectionChanged(conn);
ReleaseConnection(conn);
return;
}
default:
conn->Lock.Release();
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
conn->Active = false;
Sched::WakeObjectWaiters(conn);
Ipc::NotifyTcpConnectionChanged(conn);
ReleaseConnection(conn);
return;
}
}
@@ -875,7 +971,7 @@ namespace Net::Tcp {
bool HasPendingAccept(Connection* conn) {
if (conn == nullptr) return false;
conn->Lock.Acquire();
bool pending = conn->PendingAccept;
bool pending = conn->PendingCount > 0;
conn->Lock.Release();
return pending;
}
+1 -1
View File
@@ -65,7 +65,7 @@ namespace Net::Tcp {
Connection* Connect(uint32_t destIp, uint16_t destPort, uint16_t srcPort);
// Send data on an established connection. Returns number of bytes sent.
int Send(Connection* conn, const uint8_t* data, uint16_t length);
int Send(Connection* conn, const uint8_t* data, uint32_t length);
// Receive data from an established connection. Returns number of bytes received.
// Blocks until data is available or connection is closed.
+20 -4
View File
@@ -11,6 +11,7 @@
#include <Libraries/Memory.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <CppLib/Spinlock.hpp>
using namespace Kt;
@@ -22,8 +23,9 @@ namespace Net::Udp {
bool Active;
};
static constexpr uint32_t MAX_BINDINGS = 16;
static constexpr uint32_t MAX_BINDINGS = 64;
static PortBinding g_bindings[MAX_BINDINGS] = {};
static kcp::Spinlock g_bindingsLock;
void Initialize() {
for (uint32_t i = 0; i < MAX_BINDINGS; i++) {
@@ -58,13 +60,18 @@ namespace Net::Udp {
const uint8_t* payload = data + HEADER_SIZE;
uint16_t payloadLen = udpLen - HEADER_SIZE;
// Dispatch to bound callback
// Snapshot the callback under the binding lock, then invoke it after
// releasing the lock so callbacks may safely bind/unbind other ports.
RecvCallback callback = nullptr;
g_bindingsLock.Acquire();
for (uint32_t i = 0; i < MAX_BINDINGS; i++) {
if (g_bindings[i].Active && g_bindings[i].Port == dstPort) {
g_bindings[i].Callback(srcIp, srcPort, dstPort, payload, payloadLen);
return;
callback = g_bindings[i].Callback;
break;
}
}
g_bindingsLock.Release();
if (callback) callback(srcIp, srcPort, dstPort, payload, payloadLen);
}
bool Send(uint32_t destIp, uint16_t srcPort, uint16_t destPort,
@@ -96,9 +103,12 @@ namespace Net::Udp {
}
bool Bind(uint16_t port, RecvCallback callback) {
if (port == 0 || callback == nullptr) return false;
g_bindingsLock.Acquire();
// Check for duplicate
for (uint32_t i = 0; i < MAX_BINDINGS; i++) {
if (g_bindings[i].Active && g_bindings[i].Port == port) {
g_bindingsLock.Release();
return false;
}
}
@@ -109,19 +119,25 @@ namespace Net::Udp {
g_bindings[i].Port = port;
g_bindings[i].Callback = callback;
g_bindings[i].Active = true;
g_bindingsLock.Release();
return true;
}
}
g_bindingsLock.Release();
return false;
}
void Unbind(uint16_t port) {
g_bindingsLock.Acquire();
for (uint32_t i = 0; i < MAX_BINDINGS; i++) {
if (g_bindings[i].Active && g_bindings[i].Port == port) {
g_bindings[i].Active = false;
g_bindings[i].Callback = nullptr;
g_bindingsLock.Release();
return;
}
}
g_bindingsLock.Release();
}
}
+536 -277
View File
@@ -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
+25 -252
View File
@@ -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;
+1
View File
@@ -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);
Binary file not shown.
Binary file not shown.
+105 -38
View File
@@ -19,8 +19,8 @@ extern "C" {
namespace {
struct DerAccum { unsigned char* data; size_t len, cap; };
struct DnAccum { unsigned char* data; size_t len, cap; };
struct DerAccum { unsigned char* data; size_t len, cap; bool failed; };
struct DnAccum { unsigned char* data; size_t len, cap; bool failed; };
void der_append(void* ctx, const void* buf, size_t len) {
DerAccum* a = (DerAccum*)ctx;
@@ -28,7 +28,7 @@ void der_append(void* ctx, const void* buf, size_t len) {
size_t nc = a->cap * 2;
if (nc < a->len + len) nc = a->len + len + 4096;
unsigned char* nb = (unsigned char*)malloc(nc);
if (!nb) return;
if (!nb) { a->failed = true; return; }
if (a->data) { memcpy(nb, a->data, a->len); free(a->data); }
a->data = nb; a->cap = nc;
}
@@ -42,7 +42,7 @@ void dn_append(void* ctx, const void* buf, size_t len) {
size_t nc = a->cap * 2;
if (nc < a->len + len) nc = a->len + len + 256;
unsigned char* nb = (unsigned char*)malloc(nc);
if (!nb) return;
if (!nb) { a->failed = true; return; }
if (a->data) { memcpy(nb, a->data, a->len); free(a->data); }
a->data = nb; a->cap = nc;
}
@@ -50,53 +50,73 @@ void dn_append(void* ctx, const void* buf, size_t len) {
a->len += len;
}
void ta_add(tls::TrustAnchors* tas, const br_x509_trust_anchor* ta) {
bool ta_add(tls::TrustAnchors* tas, const br_x509_trust_anchor* ta) {
if (tas->count >= tas->capacity) {
size_t nc = tas->capacity == 0 ? 64 : tas->capacity * 2;
br_x509_trust_anchor* na = (br_x509_trust_anchor*)malloc(nc * sizeof(*na));
if (!na) return;
if (!na) return false;
if (tas->anchors) { memcpy(na, tas->anchors, tas->count * sizeof(*na)); free(tas->anchors); }
tas->anchors = na; tas->capacity = nc;
}
tas->anchors[tas->count++] = *ta;
return true;
}
bool process_cert_der(tls::TrustAnchors* tas, const unsigned char* der, size_t der_len) {
static br_x509_decoder_context dc; // ~2KB+, keep off stack
DnAccum dn = {nullptr, 0, 0};
br_x509_decoder_init(&dc, dn_append, &dn);
br_x509_decoder_push(&dc, der, der_len);
br_x509_pkey* pk = br_x509_decoder_get_pkey(&dc);
if (!pk) { if (dn.data) free(dn.data); return false; }
br_x509_decoder_context* dc =
(br_x509_decoder_context*)malloc(sizeof(br_x509_decoder_context));
if (!dc) return false;
DnAccum dn = {nullptr, 0, 0, false};
br_x509_decoder_init(dc, dn_append, &dn);
br_x509_decoder_push(dc, der, der_len);
br_x509_pkey* pk = br_x509_decoder_get_pkey(dc);
if (!pk || dn.failed) { free(dc); if (dn.data) free(dn.data); return false; }
br_x509_trust_anchor ta;
memset(&ta, 0, sizeof(ta));
ta.dn.data = dn.data; ta.dn.len = dn.len; ta.flags = 0;
if (br_x509_decoder_isCA(&dc)) ta.flags |= BR_X509_TA_CA;
if (br_x509_decoder_isCA(dc)) ta.flags |= BR_X509_TA_CA;
switch (pk->key_type) {
case BR_KEYTYPE_RSA:
ta.pkey.key_type = BR_KEYTYPE_RSA;
ta.pkey.key.rsa.nlen = pk->key.rsa.nlen;
ta.pkey.key.rsa.n = (unsigned char*)malloc(pk->key.rsa.nlen);
if (ta.pkey.key.rsa.n) memcpy(ta.pkey.key.rsa.n, pk->key.rsa.n, pk->key.rsa.nlen);
if (!ta.pkey.key.rsa.n) { free(dc); free(dn.data); return false; }
memcpy(ta.pkey.key.rsa.n, pk->key.rsa.n, pk->key.rsa.nlen);
ta.pkey.key.rsa.elen = pk->key.rsa.elen;
ta.pkey.key.rsa.e = (unsigned char*)malloc(pk->key.rsa.elen);
if (ta.pkey.key.rsa.e) memcpy(ta.pkey.key.rsa.e, pk->key.rsa.e, pk->key.rsa.elen);
if (!ta.pkey.key.rsa.e) {
free(ta.pkey.key.rsa.n);
free(dc);
free(dn.data);
return false;
}
memcpy(ta.pkey.key.rsa.e, pk->key.rsa.e, pk->key.rsa.elen);
break;
case BR_KEYTYPE_EC:
ta.pkey.key_type = BR_KEYTYPE_EC;
ta.pkey.key.ec.curve = pk->key.ec.curve;
ta.pkey.key.ec.qlen = pk->key.ec.qlen;
ta.pkey.key.ec.q = (unsigned char*)malloc(pk->key.ec.qlen);
if (ta.pkey.key.ec.q) memcpy(ta.pkey.key.ec.q, pk->key.ec.q, pk->key.ec.qlen);
if (!ta.pkey.key.ec.q) { free(dc); free(dn.data); return false; }
memcpy(ta.pkey.key.ec.q, pk->key.ec.q, pk->key.ec.qlen);
break;
default:
free(dc);
if (dn.data) free(dn.data);
return false;
}
ta_add(tas, &ta);
return true;
free(dc);
if (ta_add(tas, &ta)) return true;
free(ta.dn.data);
if (ta.pkey.key_type == BR_KEYTYPE_RSA) {
free(ta.pkey.key.rsa.n);
free(ta.pkey.key.rsa.e);
} else {
free(ta.pkey.key.ec.q);
}
return false;
}
} // anonymous namespace
@@ -116,36 +136,65 @@ TrustAnchors load_trust_anchors() {
unsigned char* pem = (unsigned char*)malloc(fsize + 1);
if (!pem) { montauk::close(fh); return tas; }
montauk::read(fh, pem, 0, fsize);
uint64_t readOffset = 0;
while (readOffset < fsize) {
int n = montauk::read(fh, pem + readOffset, readOffset, fsize - readOffset);
if (n <= 0) break;
readOffset += (uint64_t)n;
}
montauk::close(fh);
pem[fsize] = 0;
if (readOffset != fsize) { free(pem); return tas; }
pem[readOffset] = 0;
static br_pem_decoder_context pc; // keep off stack
br_pem_decoder_init(&pc);
DerAccum der = {nullptr, 0, 0};
br_pem_decoder_context* pc =
(br_pem_decoder_context*)malloc(sizeof(br_pem_decoder_context));
if (!pc) { free(pem); return tas; }
br_pem_decoder_init(pc);
DerAccum der = {nullptr, 0, 0, false};
bool inCert = false;
size_t offset = 0;
while (offset < fsize) {
size_t pushed = br_pem_decoder_push(&pc, pem + offset, fsize - offset);
size_t pushed = br_pem_decoder_push(pc, pem + offset, fsize - offset);
offset += pushed;
int ev = br_pem_decoder_event(&pc);
int ev = br_pem_decoder_event(pc);
if (ev == BR_PEM_BEGIN_OBJ) {
inCert = (strcmp(br_pem_decoder_name(&pc), "CERTIFICATE") == 0);
br_pem_decoder_setdest(&pc, inCert ? der_append : nullptr, inCert ? &der : nullptr);
if (inCert) der.len = 0;
inCert = (strcmp(br_pem_decoder_name(pc), "CERTIFICATE") == 0);
br_pem_decoder_setdest(pc, inCert ? der_append : nullptr, inCert ? &der : nullptr);
if (inCert) { der.len = 0; der.failed = false; }
} else if (ev == BR_PEM_END_OBJ) {
if (inCert && der.len > 0) process_cert_der(&tas, der.data, der.len);
inCert = false;
} else if (ev == BR_PEM_ERROR) {
break;
}
if (der.failed) break;
if (pushed == 0 && ev == 0) break;
}
if (der.data) free(der.data);
free(pc);
free(pem);
return tas;
}
void free_trust_anchors(TrustAnchors* tas) {
if (!tas) return;
for (size_t i = 0; i < tas->count; ++i) {
br_x509_trust_anchor& ta = tas->anchors[i];
free(ta.dn.data);
if (ta.pkey.key_type == BR_KEYTYPE_RSA) {
free(ta.pkey.key.rsa.n);
free(ta.pkey.key.rsa.e);
} else if (ta.pkey.key_type == BR_KEYTYPE_EC) {
free(ta.pkey.key.ec.q);
}
}
free(tas->anchors);
tas->anchors = nullptr;
tas->count = 0;
tas->capacity = 0;
}
void get_bearssl_time(uint32_t* days, uint32_t* seconds) {
montauk::abi::DateTime dt;
montauk::gettime(&dt);
@@ -167,7 +216,15 @@ int tls_send_all(int fd, const unsigned char* data, size_t len) {
int r = montauk::send(fd, data + sent, (uint32_t)(len - sent));
if (r > 0) { sent += r; deadline = montauk::get_milliseconds() + 15000; }
else if (r < 0) return -1;
else { if (montauk::get_milliseconds() >= deadline) return -1; montauk::sleep_ms(1); }
else {
uint64_t now = montauk::get_milliseconds();
if (now >= deadline) return -1;
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 -1;
}
}
return (int)sent;
}
@@ -178,8 +235,14 @@ int tls_recv_some(int fd, unsigned char* buf, size_t maxlen) {
int r = montauk::recv(fd, buf, (uint32_t)maxlen);
if (r > 0) return r;
if (r < 0) return -1;
if (montauk::get_milliseconds() >= deadline) return -1;
montauk::sleep_ms(1);
uint64_t now = montauk::get_milliseconds();
if (now >= deadline) return -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) return -1;
if ((signals & montauk::abi::IPC_SIGNAL_PEER_CLOSED) &&
!(signals & montauk::abi::IPC_SIGNAL_READABLE)) return -1;
}
}
@@ -187,7 +250,7 @@ int tls_exchange(int fd, br_ssl_engine_context* eng,
const char* request, int reqLen,
char* respBuf, int respMax,
AbortCheckFn abort_check) {
bool requestSent = false;
int requestOffset = 0;
int respLen = 0;
uint64_t deadline = montauk::get_milliseconds() + 30000;
@@ -217,14 +280,14 @@ int tls_exchange(int fd, br_ssl_engine_context* eng,
br_ssl_engine_recvapp_ack(eng, len);
deadline = montauk::get_milliseconds() + 30000; continue;
}
if ((state & BR_SSL_SENDAPP) && !requestSent) {
if ((state & BR_SSL_SENDAPP) && requestOffset < reqLen) {
size_t len; unsigned char* buf = br_ssl_engine_sendapp_buf(eng, &len);
size_t toWrite = (size_t)reqLen;
size_t toWrite = (size_t)(reqLen - requestOffset);
if (toWrite > len) toWrite = len;
memcpy(buf, request, toWrite);
memcpy(buf, request + requestOffset, toWrite);
br_ssl_engine_sendapp_ack(eng, toWrite);
br_ssl_engine_flush(eng, 0);
requestSent = true;
requestOffset += (int)toWrite;
if (requestOffset == reqLen) br_ssl_engine_flush(eng, 0);
deadline = montauk::get_milliseconds() + 30000; continue;
}
if (state & BR_SSL_RECVREC) {
@@ -244,6 +307,8 @@ int https_fetch(const char* host, uint32_t ip, uint16_t port,
const TrustAnchors& tas,
char* respBuf, int respMax,
AbortCheckFn abort_check) {
if (!host || !*host || ip == 0 || port == 0 || !request || reqLen <= 0 ||
!respBuf || respMax < 2 || !tas.anchors || tas.count == 0) return -1;
int fd = montauk::socket(montauk::abi::SOCK_TCP);
if (fd < 0) return -1;
if (montauk::connect(fd, ip, port) < 0) { montauk::closesocket(fd); return -1; }
@@ -262,7 +327,9 @@ int https_fetch(const char* host, uint32_t ip, uint16_t port,
br_x509_minimal_set_time(xc, days, secs);
unsigned char seed[32];
montauk::getrandom(seed, sizeof(seed));
if (montauk::getrandom(seed, sizeof(seed)) != (int64_t)sizeof(seed)) {
montauk::closesocket(fd); free(cc); free(xc); free(iobuf); return -1;
}
br_ssl_engine_set_buffer(&cc->eng, iobuf, BR_SSL_BUFSIZE_BIDI, 1);
br_ssl_engine_inject_entropy(&cc->eng, seed, sizeof(seed));
+29 -179
View File
@@ -8,7 +8,7 @@
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <tls/tls.hpp>
#include <http/http.hpp>
extern "C" {
#include <string.h>
@@ -56,11 +56,6 @@ static bool parse_uint16(const char* s, uint16_t* out) {
return true;
}
static void format_ip(char* buf, uint32_t ip) {
snprintf(buf, 32, "%u.%u.%u.%u",
ip & 0xFF, (ip >> 8) & 0xFF, (ip >> 16) & 0xFF, (ip >> 24) & 0xFF);
}
// ---- URL parser ----
struct ParsedUrl {
@@ -124,38 +119,6 @@ static ParsedUrl parse_url(const char* url) {
return u;
}
// ---- HTTP response parser ----
static int find_header_end(const char* buf, int len) {
for (int i = 0; i + 3 < len; i++) {
if (buf[i] == '\r' && buf[i+1] == '\n' && buf[i+2] == '\r' && buf[i+3] == '\n')
return i + 4;
}
return -1;
}
static int parse_status_code(const char* buf, int len) {
int i = 0;
while (i < len && buf[i] != ' ') i++;
if (i >= len) return -1;
i++;
if (i + 2 >= len) return -1;
if (buf[i] < '0' || buf[i] > '9') return -1;
return (buf[i] - '0') * 100 + (buf[i+1] - '0') * 10 + (buf[i+2] - '0');
}
static void parse_status_text(const char* buf, int len, char* out, int outMax) {
int i = 0;
while (i < len && buf[i] != ' ') i++;
i++;
while (i < len && buf[i] != ' ') i++;
i++;
int j = 0;
while (i < len && buf[i] != '\r' && buf[i] != '\n' && j < outMax - 1)
out[j++] = buf[i++];
out[j] = '\0';
}
// ---- Keyboard abort check for TLS ----
static bool check_keyboard_abort() {
@@ -167,95 +130,23 @@ static bool check_keyboard_abort() {
return false;
}
// ---- Plain HTTP exchange (no TLS) ----
static int plain_http_exchange(int fd, const char* request, int reqLen,
char* respBuf, int respMax) {
// Send request
int sent = 0;
uint64_t deadline = montauk::get_milliseconds() + 15000;
while (sent < reqLen) {
int r = montauk::send(fd, request + sent, reqLen - sent);
if (r > 0) { sent += r; deadline = montauk::get_milliseconds() + 15000; }
else if (r < 0) return -1;
else {
if (montauk::get_milliseconds() >= deadline) return -1;
montauk::sleep_ms(1);
}
}
// Receive response
int respLen = 0;
deadline = montauk::get_milliseconds() + 15000;
while (respLen < respMax - 1) {
if (montauk::is_key_available()) {
montauk::abi::KeyEvent ev;
montauk::getkey(&ev);
if (ev.pressed && ev.ctrl && ev.ascii == 'q') return -2; // aborted
}
int r = montauk::recv(fd, respBuf + respLen, respMax - 1 - respLen);
if (r > 0) { respLen += r; deadline = montauk::get_milliseconds() + 15000; }
else if (r < 0) break;
else {
uint64_t now = montauk::get_milliseconds();
if (now >= deadline) break;
uint32_t signals = montauk::wait_handle(
fd,
montauk::abi::IPC_SIGNAL_READABLE | montauk::abi::IPC_SIGNAL_PEER_CLOSED,
deadline - now
);
if (signals == 0 || signals == (uint32_t)-1) break;
}
}
return respLen;
}
// ---- Print response body ----
static void print_response(const char* respBuf, int respLen, bool verbose) {
if (respLen <= 0) {
montauk::print("Error: empty response\n");
return;
}
int headerEnd = find_header_end(respBuf, respLen);
if (headerEnd < 0) {
montauk::print("Warning: malformed response (no header boundary)\n\n");
// Print raw
char chunk[512];
int printed = 0;
while (printed < respLen) {
int n = respLen - printed;
if (n > 511) n = 511;
memcpy(chunk, respBuf + printed, n);
chunk[n] = '\0';
montauk::print(chunk);
printed += n;
}
montauk::putchar('\n');
return;
}
int statusCode = parse_status_code(respBuf, headerEnd);
char statusText[64];
parse_status_text(respBuf, headerEnd, statusText, sizeof(statusText));
int bodyLen = respLen - headerEnd;
static void print_response(const http::Response& response, bool verbose) {
if (verbose) {
char msg[256];
snprintf(msg, sizeof(msg), "HTTP %d %s (%d bytes)\n\n", statusCode, statusText, bodyLen);
char msg[128];
snprintf(msg, sizeof(msg), "HTTP %d (%d bytes)\n\n",
response.status, response.body_len);
montauk::print(msg);
}
if (bodyLen > 0) {
const char* body = respBuf + headerEnd;
if (response.body_len > 0) {
char chunk[512];
int printed = 0;
while (printed < bodyLen) {
int n = bodyLen - printed;
while (printed < response.body_len) {
int n = response.body_len - printed;
if (n > 511) n = 511;
memcpy(chunk, body + printed, n);
memcpy(chunk, response.body + printed, n);
chunk[n] = '\0';
montauk::print(chunk);
printed += n;
@@ -339,20 +230,8 @@ extern "C" void _start() {
}
}
// Resolve host to IP
uint32_t serverIp;
if (!parse_ip(hostStr, &serverIp)) {
serverIp = montauk::resolve(hostStr);
if (serverIp == 0) {
montauk::print("Error: could not resolve ");
montauk::print(hostStr);
montauk::putchar('\n');
montauk::exit(1);
}
}
char ipStr[32];
format_ip(ipStr, serverIp);
uint32_t numericIp = 0;
parse_ip(hostStr, &numericIp);
if (verbose) {
char msg[256];
@@ -361,16 +240,6 @@ extern "C" void _start() {
montauk::print(msg);
}
// Build HTTP request
char request[1024];
int reqLen = snprintf(request, sizeof(request),
"GET %s HTTP/1.0\r\n"
"Host: %s\r\n"
"User-Agent: MontaukOS/1.0\r\n"
"Connection: close\r\n"
"\r\n",
path, hostStr);
if (verbose) {
char msg[128];
snprintf(msg, sizeof(msg), "GET %s\n", path);
@@ -385,11 +254,9 @@ extern "C" void _start() {
montauk::exit(1);
}
int respLen;
tls::TrustAnchors tas = {};
if (useHttps) {
// ---- TLS handshake and exchange ----
tls::TrustAnchors tas = tls::load_trust_anchors();
tas = tls::load_trust_anchors();
if (verbose) {
char msg[64];
snprintf(msg, sizeof(msg), "Loaded %u trust anchors\n", (unsigned)tas.count);
@@ -415,43 +282,26 @@ extern "C" void _start() {
montauk::print("TLS handshake...\n");
}
respLen = tls::https_fetch(hostStr, serverIp, port,
request, reqLen, tas,
respBuf, RESP_MAX, check_keyboard_abort);
if (verbose && respLen > 0) {
montauk::print("TLS connection established\n");
}
} else {
// ---- Plain HTTP ----
int fd = montauk::socket(montauk::abi::SOCK_TCP);
if (fd < 0) {
montauk::print("Error: failed to create socket\n");
montauk::exit(1);
}
if (montauk::connect(fd, serverIp, port) < 0) {
montauk::print("Error: connection failed\n");
montauk::closesocket(fd);
montauk::exit(1);
}
respLen = plain_http_exchange(fd, request, reqLen, respBuf, RESP_MAX);
montauk::closesocket(fd);
if (respLen == -2) {
montauk::print("\nAborted.\n");
montauk::exit(0);
}
}
if (respLen <= 0) {
montauk::print("Error: no response received\n");
http::RequestOptions options;
options.secure = useHttps;
options.port = port;
options.resolved_ip = numericIp;
options.extra_headers = "User-Agent: MontaukOS/1.0\r\n";
options.abort_check = check_keyboard_abort;
options.timeout_ms = 15000;
http::Response response = http::request_into(
"GET", hostStr, path, nullptr, nullptr, 0, useHttps ? &tas : nullptr,
respBuf, RESP_MAX, options);
if (response.error != http::Error::NONE) {
montauk::print("Error: ");
montauk::print(http::error_string(response.error));
montauk::putchar('\n');
montauk::exit(1);
}
respBuf[respLen] = '\0';
print_response(respBuf, respLen, verbose);
if (verbose && useHttps) montauk::print("TLS connection established\n");
print_response(response, verbose);
montauk::exit(0);
}
+16 -48
View File
@@ -14,7 +14,7 @@
#include <gui/svg.hpp>
#include <gui/truetype.hpp>
#include <gui/mtk.hpp>
#include <tls/tls.hpp>
#include <http/http.hpp>
extern "C" {
#include <string.h>
@@ -77,26 +77,6 @@ static bool g_tls_ready = false;
static uint32_t g_server_ip = 0;
static tls::TrustAnchors g_tas = {nullptr, 0, 0};
// ============================================================================
// HTTP parsing
// ============================================================================
static int find_header_end(const char* buf, int len) {
for (int i = 0; i + 3 < len; i++)
if (buf[i]=='\r' && buf[i+1]=='\n' && buf[i+2]=='\r' && buf[i+3]=='\n')
return i + 4;
return -1;
}
static int parse_status_code(const char* buf, int len) {
int i = 0;
while (i < len && buf[i] != ' ') i++;
if (i >= len || i + 3 >= len) return -1;
i++;
if (buf[i] < '0' || buf[i] > '9') return -1;
return (buf[i]-'0')*100 + (buf[i+1]-'0')*10 + (buf[i+2]-'0');
}
// ============================================================================
// JSON parsing
// ============================================================================
@@ -237,39 +217,27 @@ static void do_fetch() {
g_tls_ready = true;
}
static char request[512];
int reqLen = snprintf(request, sizeof(request),
"GET /?format=j1 HTTP/1.0\r\n"
"Host: %s\r\n"
http::RequestOptions options;
options.resolved_ip = g_server_ip;
options.extra_headers =
"User-Agent: MontaukOS/1.0 weather\r\n"
"Accept: application/json\r\n"
"Connection: close\r\n"
"\r\n",
WTTR_HOST);
int respLen = tls::https_fetch(WTTR_HOST, g_server_ip, 443,
request, reqLen, g_tas,
g_resp_buf, RESP_MAX);
if (respLen <= 0) {
snprintf(g_status, sizeof(g_status), "Error: no response from server");
"Accept: application/json\r\n";
http::Response response = http::request_into(
"GET", WTTR_HOST, "/?format=j1", nullptr, nullptr, 0, &g_tas,
g_resp_buf, RESP_MAX, options);
if (response.error != http::Error::NONE) {
snprintf(g_status, sizeof(g_status), "Error: %s",
http::error_string(response.error));
g_phase = AppPhase::ERR; return;
}
g_resp_buf[respLen] = '\0';
int headerEnd = find_header_end(g_resp_buf, respLen);
if (headerEnd < 0) {
snprintf(g_status, sizeof(g_status), "Error: malformed HTTP response");
if (response.status != 200) {
snprintf(g_status, sizeof(g_status), "Error: HTTP %d from server",
response.status);
g_phase = AppPhase::ERR; return;
}
int status = parse_status_code(g_resp_buf, headerEnd);
if (status != 200) {
snprintf(g_status, sizeof(g_status), "Error: HTTP %d from server", status);
g_phase = AppPhase::ERR; return;
}
const char* body = g_resp_buf + headerEnd;
int bodyLen = respLen - headerEnd;
const char* body = response.body;
int bodyLen = response.body_len;
// Extract core weather fields
static char temp_raw[16], feels_raw[16], code_raw[8];
+28 -85
View File
@@ -10,7 +10,7 @@
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <tls/tls.hpp>
#include <http/http.hpp>
extern "C" {
#include <string.h>
@@ -82,39 +82,15 @@ static bool check_keyboard_abort() {
// ---- HTTPS fetch wrapper ----
static int wiki_fetch(const char* path, char* respBuf, int respMax) {
static char request[2560]; // keep off stack
int reqLen = snprintf(request, sizeof(request),
"GET %s HTTP/1.0\r\n"
"Host: %s\r\n"
static http::Response wiki_fetch(const char* path, char* respBuf, int respMax) {
http::RequestOptions options;
options.resolved_ip = g_serverIp;
options.extra_headers =
"User-Agent: MontaukOS/1.0 wiki\r\n"
"Accept: application/json\r\n"
"Connection: close\r\n"
"\r\n",
path, WIKI_HOST);
return tls::https_fetch(WIKI_HOST, g_serverIp, 443,
request, reqLen, g_tas,
respBuf, respMax, check_keyboard_abort);
}
// ---- HTTP response parsing ----
static int find_header_end(const char* buf, int len) {
for (int i = 0; i + 3 < len; i++) {
if (buf[i] == '\r' && buf[i+1] == '\n' && buf[i+2] == '\r' && buf[i+3] == '\n')
return i + 4;
}
return -1;
}
static int parse_status_code(const char* buf, int len) {
int i = 0;
while (i < len && buf[i] != ' ') i++;
if (i >= len) return -1;
i++;
if (i + 2 >= len) return -1;
if (buf[i] < '0' || buf[i] > '9') return -1;
return (buf[i] - '0') * 100 + (buf[i+1] - '0') * 10 + (buf[i+2] - '0');
"Accept: application/json\r\n";
options.abort_check = check_keyboard_abort;
return http::request_into("GET", WIKI_HOST, path, nullptr, nullptr, 0,
&g_tas, respBuf, respMax, options);
}
// ---- URL encoding ----
@@ -725,23 +701,13 @@ extern "C" void _start() {
"/w/api.php?action=query&format=json&formatversion=2"
"&prop=extracts&explaintext=1&titles=%s", encoded);
int respLen = wiki_fetch(path, respBuf, RESP_MAX);
if (respLen <= 0) {
http::Response response = wiki_fetch(path, respBuf, RESP_MAX);
if (response.error != http::Error::NONE) {
montauk::print("\x01"); // error sentinel
montauk::sleep_ms(100);
montauk::exit(1);
}
respBuf[respLen] = '\0';
int headerEnd = find_header_end(respBuf, respLen);
if (headerEnd < 0) {
montauk::print("\x01");
montauk::sleep_ms(100);
montauk::exit(1);
}
int statusCode = parse_status_code(respBuf, headerEnd);
if (statusCode == 404) {
if (response.status == 404) {
montauk::print("\x01");
montauk::sleep_ms(100);
montauk::exit(1);
@@ -749,8 +715,8 @@ extern "C" void _start() {
// Output raw JSON body in chunks to avoid overflowing
// the 4KB kernel ring buffer (parent polls at ~60fps)
const char* body = respBuf + headerEnd;
int bodyLen = respLen - headerEnd;
const char* body = response.body;
int bodyLen = response.body_len;
static char chunk[2049];
int sent = 0;
while (sent < bodyLen) {
@@ -774,21 +740,13 @@ extern "C" void _start() {
"/w/api.php?action=opensearch&search=%s&limit=10&format=json",
encoded);
int respLen = wiki_fetch(path, respBuf, RESP_MAX);
if (respLen <= 0) {
http::Response response = wiki_fetch(path, respBuf, RESP_MAX);
if (response.error != http::Error::NONE) {
montauk::print("\033[1;31mError:\033[0m no response from Wikipedia\n");
montauk::exit(1);
}
respBuf[respLen] = '\0';
int headerEnd = find_header_end(respBuf, respLen);
if (headerEnd < 0) {
montauk::print("\033[1;31mError:\033[0m malformed response\n");
montauk::exit(1);
}
const char* body = respBuf + headerEnd;
int bodyLen = respLen - headerEnd;
const char* body = response.body;
int bodyLen = response.body_len;
static char titles[MAX_SEARCH_RESULTS][256];
int titleCount = parse_search_titles(body, bodyLen, titles, MAX_SEARCH_RESULTS);
@@ -831,8 +789,8 @@ extern "C" void _start() {
snprintf(articlePath, sizeof(articlePath),
"/api/rest_v1/page/summary/%s", articleEncoded);
respLen = wiki_fetch(articlePath, respBuf, RESP_MAX);
if (respLen <= 0) {
response = wiki_fetch(articlePath, respBuf, RESP_MAX);
if (response.error != http::Error::NONE) {
sb_reset();
sb_cursor_to(infoRow, 3);
sb_puts("\033[2K\033[1;31mFetch failed. Press any key.\033[0m");
@@ -841,16 +799,10 @@ extern "C" void _start() {
montauk::abi::KeyEvent ev; montauk::getkey(&ev);
continue;
}
respBuf[respLen] = '\0';
body = response.body;
bodyLen = response.body_len;
headerEnd = find_header_end(respBuf, respLen);
if (headerEnd < 0) continue;
int statusCode = parse_status_code(respBuf, headerEnd);
body = respBuf + headerEnd;
bodyLen = respLen - headerEnd;
if (statusCode == 404) {
if (response.status == 404) {
sb_reset();
sb_cursor_to(infoRow, 3);
sb_puts("\033[2K\033[1;31mArticle not found. Press any key.\033[0m");
@@ -893,24 +845,15 @@ extern "C" void _start() {
"&prop=extracts&explaintext=1&titles=%s", encoded);
}
int respLen = wiki_fetch(path, respBuf, RESP_MAX);
if (respLen <= 0) {
http::Response response = wiki_fetch(path, respBuf, RESP_MAX);
if (response.error != http::Error::NONE) {
montauk::print("\033[1;31mError:\033[0m no response from Wikipedia\n");
montauk::exit(1);
}
respBuf[respLen] = '\0';
const char* body = response.body;
int bodyLen = response.body_len;
int headerEnd = find_header_end(respBuf, respLen);
if (headerEnd < 0) {
montauk::print("\033[1;31mError:\033[0m malformed response\n");
montauk::exit(1);
}
int statusCode = parse_status_code(respBuf, headerEnd);
const char* body = respBuf + headerEnd;
int bodyLen = respLen - headerEnd;
if (statusCode == 404) {
if (response.status == 404) {
montauk::print("\033[1;31mArticle not found:\033[0m ");
montauk::print(query);
montauk::putchar('\n');
+19 -43
View File
@@ -6,19 +6,14 @@
#include "wikipedia.h"
int wiki_fetch(const char* path, char* respBuf, int respMax) {
static char request[2560];
int reqLen = snprintf(request, sizeof(request),
"GET %s HTTP/1.0\r\n"
"Host: %s\r\n"
http::Response wiki_fetch(const char* path, char* respBuf, int respMax) {
http::RequestOptions options;
options.resolved_ip = g_server_ip;
options.extra_headers =
"User-Agent: MontaukOS/1.0 wikipedia\r\n"
"Accept: application/json\r\n"
"Connection: close\r\n"
"\r\n",
path, WIKI_HOST);
return tls::https_fetch(WIKI_HOST, g_server_ip, 443,
request, reqLen, g_tas,
respBuf, respMax);
"Accept: application/json\r\n";
return http::request_into("GET", WIKI_HOST, path, nullptr, nullptr, 0,
&g_tas, respBuf, respMax, options);
}
bool ensure_wiki_tls_ready(char* err, int err_len) {
@@ -74,27 +69,16 @@ void do_welcome_fetch() {
return;
}
int respLen = wiki_fetch(path, g_resp_buf, RESP_MAX);
if (respLen <= 0) {
http::Response response = wiki_fetch(path, g_resp_buf, RESP_MAX);
if (response.error != http::Error::NONE) {
snprintf(g_welcome_status, sizeof(g_welcome_status),
"Daily featured article unavailable; search is ready.");
build_welcome_lines(true);
return;
}
g_resp_buf[respLen] = '\0';
int headerEnd = find_header_end(g_resp_buf, respLen);
if (headerEnd < 0) {
snprintf(g_welcome_status, sizeof(g_welcome_status),
"Daily featured article unavailable; search is ready.");
build_welcome_lines(true);
return;
}
int status = parse_status_code(g_resp_buf, headerEnd);
const char* body = g_resp_buf + headerEnd;
int bodyLen = respLen - headerEnd;
if (status < 200 || status >= 300) {
const char* body = response.body;
int bodyLen = response.body_len;
if (response.status < 200 || response.status >= 300) {
snprintf(g_welcome_status, sizeof(g_welcome_status),
"Daily featured article unavailable; search is ready.");
build_welcome_lines(true);
@@ -148,24 +132,16 @@ void do_search(const char* query) {
"/w/api.php?action=query&format=json&formatversion=2"
"&prop=extracts&explaintext=1&titles=%s", encoded);
int respLen = wiki_fetch(path, g_resp_buf, RESP_MAX);
if (respLen <= 0) {
snprintf(g_status, sizeof(g_status), "Error: no response from Wikipedia");
http::Response response = wiki_fetch(path, g_resp_buf, RESP_MAX);
if (response.error != http::Error::NONE) {
snprintf(g_status, sizeof(g_status), "Error: %s",
http::error_string(response.error));
g_phase = AppPhase::ERR; return;
}
g_resp_buf[respLen] = '\0';
const char* body = response.body;
int bodyLen = response.body_len;
int headerEnd = find_header_end(g_resp_buf, respLen);
if (headerEnd < 0) {
snprintf(g_status, sizeof(g_status), "Error: malformed HTTP response");
g_phase = AppPhase::ERR; return;
}
int status = parse_status_code(g_resp_buf, headerEnd);
const char* body = g_resp_buf + headerEnd;
int bodyLen = respLen - headerEnd;
if (status == 404) {
if (response.status == 404) {
snprintf(g_status, sizeof(g_status), "Article not found: %s", query);
g_phase = AppPhase::ERR; return;
}
-20
View File
@@ -140,26 +140,6 @@ unsigned decode_utf8_codepoint(const char* buf, int len, int* consumed) {
return '?';
}
// ============================================================================
// HTTP parsing
// ============================================================================
int find_header_end(const char* buf, int len) {
for (int i = 0; i + 3 < len; i++)
if (buf[i]=='\r' && buf[i+1]=='\n' && buf[i+2]=='\r' && buf[i+3]=='\n')
return i + 4;
return -1;
}
int parse_status_code(const char* buf, int len) {
int i = 0;
while (i < len && buf[i] != ' ') i++;
if (i >= len || i + 3 >= len) return -1;
i++;
if (buf[i] < '0' || buf[i] > '9') return -1;
return (buf[i]-'0')*100 + (buf[i+1]-'0')*10 + (buf[i+2]-'0');
}
int find_substr(const char* buf, int len, const char* needle) {
int nlen = (int)strlen(needle);
if (!buf || !needle || nlen <= 0 || nlen > len) return -1;
+2 -4
View File
@@ -14,7 +14,7 @@
#include <gui/standalone.hpp>
#include <gui/svg.hpp>
#include <gui/truetype.hpp>
#include <tls/tls.hpp>
#include <http/http.hpp>
extern "C" {
#include <string.h>
@@ -190,8 +190,6 @@ void underscores_to_spaces(char* text);
bool is_main_page_query(const char* query);
unsigned decode_utf8_codepoint(const char* buf, int len, int* consumed);
int find_header_end(const char* buf, int len);
int parse_status_code(const char* buf, int len);
int find_substr(const char* buf, int len, const char* needle);
int url_encode_title(const char* in, char* out, int maxLen);
@@ -219,7 +217,7 @@ bool handle_reader_option_click(int mx, int my);
// network.cpp -- TLS fetch, welcome fetch, search
// ============================================================================
int wiki_fetch(const char* path, char* respBuf, int respMax);
http::Response wiki_fetch(const char* path, char* respBuf, int respMax);
bool ensure_wiki_tls_ready(char* err, int err_len);
bool welcome_feed_path(char* path, int path_len);
void do_welcome_fetch();
+34 -4
View File
@@ -643,11 +643,16 @@ The allocator uses size-class buckets (32 to 4096 bytes) with an overflow list f
## Networking and HTTPS
MontaukOS provides a shared TLS library (`tls/tls.hpp`) backed by BearSSL, and the MontaukAI dev environment adds a higher-level HTTP wrapper (`http/http.hpp`) on top. Build with `USE_TLS=1` to link TLS support.
MontaukOS provides a shared TLS library (`tls/tls.hpp`) backed by BearSSL and
a higher-level HTTP client (`http/http.hpp`) used by system and third-party
applications. Build with `USE_TLS=1` to link TLS support.
### HTTP Wrapper (`http/http.hpp`)
Header-only library that handles DNS resolution, request building, TLS, response parsing, and cleanup. All functions return an `http::Response` struct.
Header-only library that handles DNS resolution, request building, TLS,
response parsing, and cleanup. It decodes chunked responses, validates
`Content-Length`, handles informational responses and partial I/O, and reports
a specific `http::Error`. All functions return an `http::Response`.
#### Setup
@@ -656,13 +661,14 @@ Header-only library that handles DNS resolution, request building, TLS, response
// Load CA certificates once at startup (required for HTTPS)
tls::TrustAnchors tas = tls::load_trust_anchors();
// Call tls::free_trust_anchors(&tas) during application shutdown.
```
#### GET
```cpp
auto resp = http::get("api.example.com", "/v1/data", tas);
if (resp.status == 200) {
if (resp.error == http::Error::NONE && resp.status == 200) {
// resp.body is a pointer to the response body
// resp.body_len is its length
}
@@ -743,6 +749,28 @@ http::free_response(&resp);
Set `g_quit = true` from your keyboard handler (e.g., on Escape) to cancel mid-request.
#### Generic HTTP/HTTPS Requests
Use `RequestOptions` for custom ports, cached DNS results, plain HTTP, or a
caller-owned response buffer:
```cpp
char response_buffer[65536];
http::RequestOptions options;
options.secure = true;
options.port = 8443;
options.resolved_ip = cached_ip; // zero asks the library to resolve the host
options.extra_headers = "Accept: application/json\r\n";
auto resp = http::request_into(
"GET", "api.example.com", "/large", nullptr, nullptr, 0, &tas,
response_buffer, sizeof(response_buffer), options);
if (resp.error != http::Error::NONE) {
// http::error_string(resp.error) is suitable for diagnostics
}
// response_buffer is caller-owned, so do not free_response(&resp).
```
#### Response Struct Reference
```cpp
@@ -754,6 +782,8 @@ struct http::Response {
int body_len;
char* raw; // Owned buffer — freed by free_response()
int raw_len;
http::Error error;
bool owns_raw;
};
```
@@ -798,7 +828,7 @@ int http::parse_response(char* buf, int len, http::Response* out);
bool http::get_header(const http::Response* resp, const char* name,
char* out_val, int max_len);
// Free the response's raw buffer
// Free the response's raw buffer when owns_raw is true
void http::free_response(http::Response* resp);
```
+7 -2
View File
@@ -376,7 +376,10 @@ The optional `AbortCheckFn` callback (e.g., `bool check_quit()`) lets terminal/G
### HTTP Wrapper (`http/http.hpp`)
The MontaukAI dev environment includes a higher-level HTTP wrapper built on top of `tls::https_fetch()`. It handles DNS, request building, TLS, and response parsing automatically. See the "Networking and HTTPS" section in `gui-apps.md` for full documentation and examples.
The MontaukOS SDK includes a higher-level HTTP client built on top of the TLS
and socket layers. It handles DNS, request construction, transport, response
framing, and parsing. See the "Networking and HTTPS" section in `gui-apps.md`
for full documentation and examples.
```cpp
#include <http/http.hpp>
@@ -384,7 +387,9 @@ The MontaukAI dev environment includes a higher-level HTTP wrapper built on top
tls::TrustAnchors tas = tls::load_trust_anchors();
auto resp = http::get("api.example.com", "/v1/data", tas);
if (resp.status == 200) { /* resp.body, resp.body_len */ }
if (resp.error == http::Error::NONE && resp.status == 200) {
/* resp.body, resp.body_len */
}
http::free_response(&resp);
auto resp2 = http::post("api.example.com", "/v1/submit",
+536 -277
View File
@@ -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
+1
View File
@@ -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);
Binary file not shown.
+18
View File
@@ -0,0 +1,18 @@
CXX ?= g++
CXXFLAGS ?= -std=c++20 -O2 -Wall -Wextra
HTTP_TEST := http_test
.PHONY: all check clean
all: $(HTTP_TEST)
$(HTTP_TEST): http_test.cpp
$(CXX) $(CXXFLAGS) -I../programs/include \
-I../programs/lib/bearssl/inc $< -o $@
check: $(HTTP_TEST)
./$(HTTP_TEST)
clean:
rm -f $(HTTP_TEST)
+85
View File
@@ -0,0 +1,85 @@
#include <http/http.hpp>
static bool bytes_equal(const char* a, const char* b, int len) {
for (int i = 0; i < len; ++i)
if (a[i] != b[i]) return false;
return true;
}
static int test_content_length() {
char raw[] =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: 5\r\n\r\n"
"helloignored";
http::Response response = {};
if (http::parse_response(raw, sizeof(raw) - 1, &response) != 200) return 1;
if (response.error != http::Error::NONE || response.body_len != 5) return 2;
if (!bytes_equal(response.body, "hello", 5)) return 3;
char type[32];
if (!http::get_header(&response, "content-type", type, sizeof(type))) return 4;
if (!bytes_equal(type, "text/plain", 10) || type[10] != '\0') return 5;
return 0;
}
static int test_chunked() {
char raw[] =
"HTTP/1.1 200 OK\r\n"
"Transfer-Encoding: Chunked\r\n\r\n"
"4\r\nWiki\r\n5;extension=yes\r\npedia\r\n0\r\nX-Trailer: yes\r\n\r\n";
http::Response response = {};
if (http::parse_response(raw, sizeof(raw) - 1, &response) != 200) return 1;
if (response.error != http::Error::NONE || response.body_len != 9) return 2;
if (!bytes_equal(response.body, "Wikipedia", 9)) return 3;
return 0;
}
static int test_informational() {
char raw[] =
"HTTP/1.1 100 Continue\r\nHeader: value\r\n\r\n"
"HTTP/1.1 201 Created\r\nContent-Length: 2\r\n\r\nok";
http::Response response = {};
if (http::parse_response(raw, sizeof(raw) - 1, &response) != 201) return 1;
if (response.status != 201 || response.body_len != 2) return 2;
if (!bytes_equal(response.body, "ok", 2)) return 3;
return 0;
}
static int test_rejections() {
char truncated[] = "HTTP/1.1 200 OK\r\nContent-Length: 8\r\n\r\nshort";
http::Response response = {};
if (http::parse_response(truncated, sizeof(truncated) - 1, &response) != 200) return 1;
if (response.error != http::Error::TRUNCATED_RESPONSE) return 2;
char malformed[] = "not http\r\n\r\n";
response = {};
if (http::parse_response(malformed, sizeof(malformed) - 1, &response) >= 0) return 3;
if (response.error != http::Error::INVALID_RESPONSE) return 4;
char request[128];
if (http::build_request(request, sizeof(request), "GET", "safe\r\nInjected: yes",
"/", nullptr, nullptr, 0, nullptr) >= 0) return 5;
if (http::build_request(request, 16, "GET", "example.com", "/",
nullptr, nullptr, 0, nullptr) >= 0) return 6;
char bad_chunk[] =
"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n";
response = {};
http::parse_response(bad_chunk, sizeof(bad_chunk) - 1, &response);
if (response.error != http::Error::TRUNCATED_RESPONSE) return 7;
if (http::build_request(request, sizeof(request), "GET", "example.com", "/",
nullptr, nullptr, 0, "Safe: yes\r\n\r\nGET /evil") >= 0)
return 8;
return 0;
}
int main() {
int result = test_content_length();
if (result) return 10 + result;
result = test_chunked();
if (result) return 20 + result;
result = test_informational();
if (result) return 30 + result;
result = test_rejections();
if (result) return 40 + result;
return 0;
}