diff --git a/GNUmakefile b/GNUmakefile index 6dc134f..dd0c71d 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -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 diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 4adff58..b3a52e6 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 7 +#define MONTAUK_BUILD_NUMBER 8 diff --git a/kernel/src/Ipc/Ipc.cpp b/kernel/src/Ipc/Ipc.cpp index c933f12..4e84bba 100644 --- a/kernel/src/Ipc/Ipc.cpp +++ b/kernel/src/Ipc/Ipc.cpp @@ -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; diff --git a/kernel/src/Net/Arp.cpp b/kernel/src/Net/Arp.cpp index 934c4c9..ed34fe2 100644 --- a/kernel/src/Net/Arp.cpp +++ b/kernel/src/Net/Arp.cpp @@ -15,6 +15,7 @@ #include #include #include +#include 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; } diff --git a/kernel/src/Net/Dns.cpp b/kernel/src/Net/Dns.cpp index 34a15a2..95f2eca 100644 --- a/kernel/src/Net/Dns.cpp +++ b/kernel/src/Net/Dns.cpp @@ -13,6 +13,7 @@ #include #include #include +#include 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 diff --git a/kernel/src/Net/Ipv4.cpp b/kernel/src/Net/Ipv4.cpp index 929dd68..5444f97 100644 --- a/kernel/src/Net/Ipv4.cpp +++ b/kernel/src/Net/Ipv4.cpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include 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(); } } } diff --git a/kernel/src/Net/Tcp.cpp b/kernel/src/Net/Tcp.cpp index a7af84e..1eb0754 100644 --- a/kernel/src/Net/Tcp.cpp +++ b/kernel/src/Net/Tcp.cpp @@ -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; } diff --git a/kernel/src/Net/Tcp.hpp b/kernel/src/Net/Tcp.hpp index 09de699..25efca0 100644 --- a/kernel/src/Net/Tcp.hpp +++ b/kernel/src/Net/Tcp.hpp @@ -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. diff --git a/kernel/src/Net/Udp.cpp b/kernel/src/Net/Udp.cpp index 4c1cbb2..348d5cc 100644 --- a/kernel/src/Net/Udp.cpp +++ b/kernel/src/Net/Udp.cpp @@ -11,6 +11,7 @@ #include #include #include +#include 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(); } } diff --git a/programs/include/http/http.hpp b/programs/include/http/http.hpp index e84c358..14ee092 100644 --- a/programs/include/http/http.hpp +++ b/programs/include/http/http.hpp @@ -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 diff --git a/programs/include/print/print.hpp b/programs/include/print/print.hpp index 37dc439..49f5817 100644 --- a/programs/include/print/print.hpp +++ b/programs/include/print/print.hpp @@ -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; diff --git a/programs/include/tls/tls.hpp b/programs/include/tls/tls.hpp index 9555a49..64fff35 100644 --- a/programs/include/tls/tls.hpp +++ b/programs/include/tls/tls.hpp @@ -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); diff --git a/programs/lib/tls/libtls.a b/programs/lib/tls/libtls.a index 00aef42..3143169 100644 Binary files a/programs/lib/tls/libtls.a and b/programs/lib/tls/libtls.a differ diff --git a/programs/lib/tls/obj/tls.o b/programs/lib/tls/obj/tls.o index 1b4ca61..d023fae 100644 Binary files a/programs/lib/tls/obj/tls.o and b/programs/lib/tls/obj/tls.o differ diff --git a/programs/lib/tls/tls.cpp b/programs/lib/tls/tls.cpp index c2c3e4e..9534a2d 100644 --- a/programs/lib/tls/tls.cpp +++ b/programs/lib/tls/tls.cpp @@ -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)); diff --git a/programs/src/fetch/main.cpp b/programs/src/fetch/main.cpp index 072e3b7..1bdace6 100644 --- a/programs/src/fetch/main.cpp +++ b/programs/src/fetch/main.cpp @@ -8,7 +8,7 @@ #include #include -#include +#include extern "C" { #include @@ -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); } diff --git a/programs/src/weather/main.cpp b/programs/src/weather/main.cpp index a89a1c8..7f71fe9 100644 --- a/programs/src/weather/main.cpp +++ b/programs/src/weather/main.cpp @@ -14,7 +14,7 @@ #include #include #include -#include +#include extern "C" { #include @@ -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]; diff --git a/programs/src/wiki/main.cpp b/programs/src/wiki/main.cpp index f379448..13022ea 100644 --- a/programs/src/wiki/main.cpp +++ b/programs/src/wiki/main.cpp @@ -10,7 +10,7 @@ #include #include -#include +#include extern "C" { #include @@ -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'); diff --git a/programs/src/wikipedia/network.cpp b/programs/src/wikipedia/network.cpp index 71d814b..58dc73c 100644 --- a/programs/src/wikipedia/network.cpp +++ b/programs/src/wikipedia/network.cpp @@ -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; } diff --git a/programs/src/wikipedia/text.cpp b/programs/src/wikipedia/text.cpp index e2a0209..70095bb 100644 --- a/programs/src/wikipedia/text.cpp +++ b/programs/src/wikipedia/text.cpp @@ -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; diff --git a/programs/src/wikipedia/wikipedia.h b/programs/src/wikipedia/wikipedia.h index 19fc38b..8ffb7cf 100644 --- a/programs/src/wikipedia/wikipedia.h +++ b/programs/src/wikipedia/wikipedia.h @@ -14,7 +14,7 @@ #include #include #include -#include +#include extern "C" { #include @@ -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(); diff --git a/template/docs/gui-apps.md b/template/docs/gui-apps.md index e388366..292462c 100644 --- a/template/docs/gui-apps.md +++ b/template/docs/gui-apps.md @@ -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); ``` diff --git a/template/docs/syscalls.md b/template/docs/syscalls.md index a0badca..5de2bfd 100644 --- a/template/docs/syscalls.md +++ b/template/docs/syscalls.md @@ -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 @@ -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", diff --git a/template/sysroot/include/http/http.hpp b/template/sysroot/include/http/http.hpp index e84c358..14ee092 100644 --- a/template/sysroot/include/http/http.hpp +++ b/template/sysroot/include/http/http.hpp @@ -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 diff --git a/template/sysroot/include/tls/tls.hpp b/template/sysroot/include/tls/tls.hpp index 9555a49..64fff35 100644 --- a/template/sysroot/include/tls/tls.hpp +++ b/template/sysroot/include/tls/tls.hpp @@ -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); diff --git a/template/sysroot/lib/libtls.a b/template/sysroot/lib/libtls.a index c143167..3143169 100644 Binary files a/template/sysroot/lib/libtls.a and b/template/sysroot/lib/libtls.a differ diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..7536695 --- /dev/null +++ b/tests/Makefile @@ -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) diff --git a/tests/http_test.cpp b/tests/http_test.cpp new file mode 100644 index 0000000..6f46642 --- /dev/null +++ b/tests/http_test.cpp @@ -0,0 +1,85 @@ +#include + +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; +}