refactor: network - harden TCP/IP and unify HTTP clients
This commit is contained in:
+4
-8
@@ -31,25 +31,23 @@ run-hdd: run-hdd-$(ARCH)
|
|||||||
|
|
||||||
.PHONY: run-x86_64
|
.PHONY: run-x86_64
|
||||||
run-x86_64: $(IMAGE_NAME).iso
|
run-x86_64: $(IMAGE_NAME).iso
|
||||||
sudo ./scripts/net-setup.sh
|
|
||||||
qemu-system-$(ARCH) \
|
qemu-system-$(ARCH) \
|
||||||
-enable-kvm \
|
-enable-kvm \
|
||||||
-M q35 \
|
-M q35 \
|
||||||
-bios /usr/share/ovmf/OVMF.fd \
|
-bios /usr/share/ovmf/OVMF.fd \
|
||||||
-cdrom $(IMAGE_NAME).iso \
|
-cdrom $(IMAGE_NAME).iso \
|
||||||
-device e1000,netdev=net0,mac=52:54:00:68:00:99 \
|
-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)
|
$(QEMUFLAGS)
|
||||||
|
|
||||||
.PHONY: run-hdd-x86_64
|
.PHONY: run-hdd-x86_64
|
||||||
run-hdd-x86_64: $(IMAGE_NAME).hdd
|
run-hdd-x86_64: $(IMAGE_NAME).hdd
|
||||||
sudo ./scripts/net-setup.sh
|
|
||||||
qemu-system-$(ARCH) \
|
qemu-system-$(ARCH) \
|
||||||
-M q35 \
|
-M q35 \
|
||||||
-bios /usr/share/ovmf/OVMF.fd \
|
-bios /usr/share/ovmf/OVMF.fd \
|
||||||
-hda $(IMAGE_NAME).hdd \
|
-hda $(IMAGE_NAME).hdd \
|
||||||
-device e1000,netdev=net0,mac=52:54:00:68:00:99 \
|
-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)
|
$(QEMUFLAGS)
|
||||||
|
|
||||||
.PHONY: run-aarch64
|
.PHONY: run-aarch64
|
||||||
@@ -133,23 +131,21 @@ run-hdd-loongarch64: $(IMAGE_NAME).hdd
|
|||||||
|
|
||||||
.PHONY: run-bios
|
.PHONY: run-bios
|
||||||
run-bios: $(IMAGE_NAME).iso
|
run-bios: $(IMAGE_NAME).iso
|
||||||
sudo ./scripts/net-setup.sh
|
|
||||||
qemu-system-$(ARCH) \
|
qemu-system-$(ARCH) \
|
||||||
-M q35 \
|
-M q35 \
|
||||||
-cdrom $(IMAGE_NAME).iso \
|
-cdrom $(IMAGE_NAME).iso \
|
||||||
-boot d \
|
-boot d \
|
||||||
-device e1000,netdev=net0,mac=52:54:00:68:00:99 \
|
-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)
|
$(QEMUFLAGS)
|
||||||
|
|
||||||
.PHONY: run-hdd-bios
|
.PHONY: run-hdd-bios
|
||||||
run-hdd-bios: $(IMAGE_NAME).hdd
|
run-hdd-bios: $(IMAGE_NAME).hdd
|
||||||
sudo ./scripts/net-setup.sh
|
|
||||||
qemu-system-$(ARCH) \
|
qemu-system-$(ARCH) \
|
||||||
-M q35 \
|
-M q35 \
|
||||||
-hda $(IMAGE_NAME).hdd \
|
-hda $(IMAGE_NAME).hdd \
|
||||||
-device e1000,netdev=net0,mac=52:54:00:68:00:99 \
|
-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)
|
$(QEMUFLAGS)
|
||||||
|
|
||||||
.PHONY: toolchain
|
.PHONY: toolchain
|
||||||
|
|||||||
@@ -12,4 +12,4 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define MONTAUK_BUILD_NUMBER 7
|
#define MONTAUK_BUILD_NUMBER 8
|
||||||
|
|||||||
@@ -1382,7 +1382,8 @@ namespace Ipc {
|
|||||||
Net::Tcp::Connection* conn = socket->tcpConn;
|
Net::Tcp::Connection* conn = socket->tcpConn;
|
||||||
socket->socketLock.Release();
|
socket->socketLock.Release();
|
||||||
if (conn == nullptr) return -1;
|
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) {
|
int SocketRecvHandle(int handle, uint8_t* buffer, uint32_t maxLen) {
|
||||||
@@ -1399,13 +1400,15 @@ namespace Ipc {
|
|||||||
socket->socketLock.Release();
|
socket->socketLock.Release();
|
||||||
if (conn == nullptr) return -1;
|
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);
|
if (result != 0) NotifyObjectChanged((Object*)socket);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
int SocketSendToHandle(int handle, const uint8_t* data, uint32_t len, uint32_t destIp, uint16_t destPort) {
|
int SocketSendToHandle(int handle, const uint8_t* data, uint32_t len, uint32_t destIp, uint16_t destPort) {
|
||||||
if (data == nullptr) return -1;
|
if (data == nullptr) return -1;
|
||||||
|
if (len > 1472) return -1;
|
||||||
if (len > 0 && !montauk::abi::UserMemory::Range((uint64_t)data, len, false)) return -1;
|
if (len > 0 && !montauk::abi::UserMemory::Range((uint64_t)data, len, false)) return -1;
|
||||||
|
|
||||||
Socket* socket = nullptr;
|
Socket* socket = nullptr;
|
||||||
|
|||||||
+72
-11
@@ -15,6 +15,7 @@
|
|||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
#include <CppLib/Stream.hpp>
|
#include <CppLib/Stream.hpp>
|
||||||
#include <Timekeeping/ApicTimer.hpp>
|
#include <Timekeeping/ApicTimer.hpp>
|
||||||
|
#include <CppLib/Spinlock.hpp>
|
||||||
|
|
||||||
using namespace Kt;
|
using namespace Kt;
|
||||||
|
|
||||||
@@ -38,6 +39,16 @@ namespace Net::Arp {
|
|||||||
static constexpr uint64_t ARP_CACHE_TIMEOUT_MS = 60000; // 60 seconds
|
static constexpr uint64_t ARP_CACHE_TIMEOUT_MS = 60000; // 60 seconds
|
||||||
|
|
||||||
static CacheEntry g_cache[ARP_CACHE_SIZE] = {};
|
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() {
|
void Initialize() {
|
||||||
for (uint32_t i = 0; i < ARP_CACHE_SIZE; i++) {
|
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) {
|
static void CacheInsert(uint32_t ip, const uint8_t* mac) {
|
||||||
|
g_cacheLock.Acquire();
|
||||||
// Look for existing entry or empty slot
|
// Look for existing entry or empty slot
|
||||||
uint32_t emptySlot = ARP_CACHE_SIZE;
|
uint32_t emptySlot = ARP_CACHE_SIZE;
|
||||||
|
uint32_t oldestSlot = 0;
|
||||||
|
uint64_t oldestTimestamp = ~0ULL;
|
||||||
for (uint32_t i = 0; i < ARP_CACHE_SIZE; i++) {
|
for (uint32_t i = 0; i < ARP_CACHE_SIZE; i++) {
|
||||||
if (g_cache[i].Valid && g_cache[i].Ip == ip) {
|
if (g_cache[i].Valid && g_cache[i].Ip == ip) {
|
||||||
// Update existing entry
|
// Update existing entry
|
||||||
memcpy(g_cache[i].Mac, mac, 6);
|
memcpy(g_cache[i].Mac, mac, 6);
|
||||||
g_cache[i].Timestamp = Timekeeping::GetMilliseconds();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (!g_cache[i].Valid && emptySlot == ARP_CACHE_SIZE) {
|
if (!g_cache[i].Valid && emptySlot == ARP_CACHE_SIZE) {
|
||||||
emptySlot = i;
|
emptySlot = i;
|
||||||
}
|
}
|
||||||
|
if (g_cache[i].Valid && g_cache[i].Timestamp < oldestTimestamp) {
|
||||||
|
oldestTimestamp = g_cache[i].Timestamp;
|
||||||
|
oldestSlot = i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (emptySlot < ARP_CACHE_SIZE) {
|
uint32_t slot = emptySlot < ARP_CACHE_SIZE ? emptySlot : oldestSlot;
|
||||||
g_cache[emptySlot].Ip = ip;
|
g_cache[slot].Ip = ip;
|
||||||
memcpy(g_cache[emptySlot].Mac, mac, 6);
|
memcpy(g_cache[slot].Mac, mac, 6);
|
||||||
g_cache[emptySlot].Timestamp = Timekeeping::GetMilliseconds();
|
g_cache[slot].Timestamp = Timekeeping::GetMilliseconds();
|
||||||
g_cache[emptySlot].Valid = true;
|
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) {
|
static bool CacheLookup(uint32_t ip, uint8_t* outMac) {
|
||||||
|
g_cacheLock.Acquire();
|
||||||
uint64_t now = Timekeeping::GetMilliseconds();
|
uint64_t now = Timekeeping::GetMilliseconds();
|
||||||
for (uint32_t i = 0; i < ARP_CACHE_SIZE; i++) {
|
for (uint32_t i = 0; i < ARP_CACHE_SIZE; i++) {
|
||||||
if (g_cache[i].Valid && g_cache[i].Ip == ip) {
|
if (g_cache[i].Valid && g_cache[i].Ip == ip) {
|
||||||
if ((now - g_cache[i].Timestamp) > ARP_CACHE_TIMEOUT_MS) {
|
if ((now - g_cache[i].Timestamp) > ARP_CACHE_TIMEOUT_MS) {
|
||||||
g_cache[i].Valid = false;
|
g_cache[i].Valid = false;
|
||||||
|
g_cacheLock.Release();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
memcpy(outMac, g_cache[i].Mac, 6);
|
memcpy(outMac, g_cache[i].Mac, 6);
|
||||||
|
g_cacheLock.Release();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
g_cacheLock.Release();
|
||||||
return false;
|
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) {
|
void OnPacketReceived(const uint8_t* data, uint16_t length) {
|
||||||
if (length < sizeof(Packet)) {
|
if (length < sizeof(Packet)) {
|
||||||
return;
|
return;
|
||||||
@@ -92,10 +151,14 @@ namespace Net::Arp {
|
|||||||
const Packet* pkt = (const Packet*)data;
|
const Packet* pkt = (const Packet*)data;
|
||||||
|
|
||||||
if (Ntohs(pkt->HardwareType) != HW_TYPE_ETHERNET ||
|
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;
|
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 senderIp = pkt->SenderIp; // Already in network byte order in struct
|
||||||
uint32_t targetIp = pkt->TargetIp;
|
uint32_t targetIp = pkt->TargetIp;
|
||||||
|
|
||||||
@@ -103,8 +166,6 @@ namespace Net::Arp {
|
|||||||
CacheInsert(senderIp, pkt->SenderMac);
|
CacheInsert(senderIp, pkt->SenderMac);
|
||||||
Ipv4::FlushPending();
|
Ipv4::FlushPending();
|
||||||
|
|
||||||
uint16_t op = Ntohs(pkt->Operation);
|
|
||||||
|
|
||||||
if (op == OP_REQUEST && targetIp == GetIpAddress()) {
|
if (op == OP_REQUEST && targetIp == GetIpAddress()) {
|
||||||
// Someone is asking for our MAC address -- send a reply
|
// Someone is asking for our MAC address -- send a reply
|
||||||
Packet reply;
|
Packet reply;
|
||||||
@@ -135,8 +196,8 @@ namespace Net::Arp {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not in cache, send a request
|
// Coalesce bursts of packets awaiting the same next hop.
|
||||||
SendRequest(ip);
|
if (ShouldSendRequest(ip)) SendRequest(ip);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+113
-54
@@ -13,6 +13,7 @@
|
|||||||
#include <Timekeeping/ApicTimer.hpp>
|
#include <Timekeeping/ApicTimer.hpp>
|
||||||
#include <Sched/Scheduler.hpp>
|
#include <Sched/Scheduler.hpp>
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
|
#include <CppLib/Spinlock.hpp>
|
||||||
|
|
||||||
namespace Net::Dns {
|
namespace Net::Dns {
|
||||||
|
|
||||||
@@ -36,16 +37,22 @@ namespace Net::Dns {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static CacheEntry g_cache[CACHE_SIZE] = {};
|
static CacheEntry g_cache[CACHE_SIZE] = {};
|
||||||
|
static kcp::Spinlock g_cacheLock;
|
||||||
|
|
||||||
static bool streq(const char* a, const char* b) {
|
static bool streq(const char* a, const char* b) {
|
||||||
while (*a && *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++;
|
a++; b++;
|
||||||
}
|
}
|
||||||
return *a == *b;
|
return *a == *b;
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint32_t CacheLookup(const char* hostname) {
|
static uint32_t CacheLookup(const char* hostname) {
|
||||||
|
g_cacheLock.Acquire();
|
||||||
uint64_t now = Timekeeping::GetMilliseconds();
|
uint64_t now = Timekeeping::GetMilliseconds();
|
||||||
for (int i = 0; i < CACHE_SIZE; i++) {
|
for (int i = 0; i < CACHE_SIZE; i++) {
|
||||||
if (!g_cache[i].valid) continue;
|
if (!g_cache[i].valid) continue;
|
||||||
@@ -53,17 +60,22 @@ namespace Net::Dns {
|
|||||||
// Check TTL
|
// Check TTL
|
||||||
uint64_t elapsed = (now - g_cache[i].timestamp) / 1000;
|
uint64_t elapsed = (now - g_cache[i].timestamp) / 1000;
|
||||||
if (elapsed < g_cache[i].ttl) {
|
if (elapsed < g_cache[i].ttl) {
|
||||||
return g_cache[i].ip;
|
uint32_t ip = g_cache[i].ip;
|
||||||
|
g_cacheLock.Release();
|
||||||
|
return ip;
|
||||||
}
|
}
|
||||||
// Expired
|
// Expired
|
||||||
g_cache[i].valid = false;
|
g_cache[i].valid = false;
|
||||||
|
g_cacheLock.Release();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
g_cacheLock.Release();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void CacheStore(const char* hostname, uint32_t ip, uint32_t ttl) {
|
static void CacheStore(const char* hostname, uint32_t ip, uint32_t ttl) {
|
||||||
if (ttl == 0) ttl = 60; // Minimum 60s TTL
|
if (ttl == 0) ttl = 60; // Minimum 60s TTL
|
||||||
|
g_cacheLock.Acquire();
|
||||||
|
|
||||||
// Find free or oldest slot
|
// Find free or oldest slot
|
||||||
int slot = 0;
|
int slot = 0;
|
||||||
@@ -85,6 +97,7 @@ namespace Net::Dns {
|
|||||||
e.ttl = ttl;
|
e.ttl = ttl;
|
||||||
e.timestamp = Timekeeping::GetMilliseconds();
|
e.timestamp = Timekeeping::GetMilliseconds();
|
||||||
e.valid = true;
|
e.valid = true;
|
||||||
|
g_cacheLock.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- DNS query building ----
|
// ---- DNS query building ----
|
||||||
@@ -178,8 +191,10 @@ namespace Net::Dns {
|
|||||||
maxJumps--;
|
maxJumps--;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if ((len & 0xC0) != 0) return -1;
|
||||||
|
|
||||||
// Regular label
|
// Regular label
|
||||||
|
if (offset + 1 + len > packetLen) return -1;
|
||||||
offset += 1 + len;
|
offset += 1 + len;
|
||||||
maxJumps--;
|
maxJumps--;
|
||||||
}
|
}
|
||||||
@@ -209,6 +224,7 @@ namespace Net::Dns {
|
|||||||
// Check RCODE (must be 0 = no error)
|
// Check RCODE (must be 0 = no error)
|
||||||
uint8_t rcode = packet[3] & 0x0F;
|
uint8_t rcode = packet[3] & 0x0F;
|
||||||
if (rcode != 0) return result;
|
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 qdcount = ((uint16_t)packet[4] << 8) | packet[5];
|
||||||
uint16_t ancount = ((uint16_t)packet[6] << 8) | packet[7];
|
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;
|
if (offset < 0 || offset + 10 > packetLen) return result;
|
||||||
|
|
||||||
uint16_t atype = ((uint16_t)packet[offset] << 8) | packet[offset + 1];
|
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 attl = ((uint32_t)packet[offset + 4] << 24) |
|
||||||
((uint32_t)packet[offset + 5] << 16) |
|
((uint32_t)packet[offset + 5] << 16) |
|
||||||
((uint32_t)packet[offset + 6] << 8) |
|
((uint32_t)packet[offset + 6] << 8) |
|
||||||
@@ -238,7 +254,7 @@ namespace Net::Dns {
|
|||||||
|
|
||||||
if (offset + rdlen > packetLen) return result;
|
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)
|
// A record: 4-byte IPv4 address (already in network byte order)
|
||||||
result.ip = ((uint32_t)packet[offset])
|
result.ip = ((uint32_t)packet[offset])
|
||||||
| ((uint32_t)packet[offset + 1] << 8)
|
| ((uint32_t)packet[offset + 1] << 8)
|
||||||
@@ -255,26 +271,38 @@ namespace Net::Dns {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Resolve state (shared with UDP callback) ----
|
// ---- Concurrent resolve state (shared with UDP callback) ----
|
||||||
|
|
||||||
static volatile bool g_gotResponse = false;
|
static constexpr int MAX_QUERIES = 8;
|
||||||
static volatile uint16_t g_currentId = 0;
|
struct Query {
|
||||||
static uint8_t g_responseBuffer[512];
|
volatile bool active;
|
||||||
static volatile int g_responseLen = 0;
|
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,
|
static void DnsRecvCallback(uint32_t srcIp, uint16_t srcPort,
|
||||||
uint16_t dstPort,
|
uint16_t dstPort,
|
||||||
const uint8_t* data, uint16_t length) {
|
const uint8_t* data, uint16_t length) {
|
||||||
(void)srcIp;
|
if (srcPort != DNS_PORT || length < 2) return;
|
||||||
(void)srcPort;
|
uint16_t responseId = ((uint16_t)data[0] << 8) | data[1];
|
||||||
(void)dstPort;
|
for (int i = 0; i < MAX_QUERIES; ++i) {
|
||||||
|
Query& query = g_queries[i];
|
||||||
if (g_gotResponse) return; // Already got a response
|
if (!query.active || query.gotResponse ||
|
||||||
if (length > sizeof(g_responseBuffer)) length = sizeof(g_responseBuffer);
|
query.localPort != dstPort || query.serverIp != srcIp ||
|
||||||
|
query.id != responseId) continue;
|
||||||
memcpy(g_responseBuffer, data, length);
|
if (length > sizeof(query.response)) length = sizeof(query.response);
|
||||||
g_responseLen = length;
|
memcpy(query.response, data, length);
|
||||||
g_gotResponse = true;
|
query.responseLen = length;
|
||||||
|
asm volatile("" ::: "memory");
|
||||||
|
query.gotResponse = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Simple PRNG for transaction IDs ----
|
// ---- Simple PRNG for transaction IDs ----
|
||||||
@@ -288,21 +316,26 @@ namespace Net::Dns {
|
|||||||
|
|
||||||
// ---- Check if string is already an IP address ----
|
// ---- Check if string is already an IP address ----
|
||||||
|
|
||||||
static bool IsIpAddress(const char* s) {
|
static bool ParseIpAddress(const char* s, uint32_t* out) {
|
||||||
int dotCount = 0;
|
uint32_t ip = 0;
|
||||||
bool hasDigit = false;
|
for (int octet = 0; octet < 4; ++octet) {
|
||||||
for (int i = 0; s[i]; i++) {
|
if (*s < '0' || *s > '9') return false;
|
||||||
if (s[i] >= '0' && s[i] <= '9') {
|
uint32_t value = 0;
|
||||||
hasDigit = true;
|
int digits = 0;
|
||||||
} else if (s[i] == '.') {
|
while (*s >= '0' && *s <= '9') {
|
||||||
if (!hasDigit) return false;
|
value = value * 10 + (uint32_t)(*s - '0');
|
||||||
dotCount++;
|
if (value > 255 || ++digits > 3) return false;
|
||||||
hasDigit = false;
|
++s;
|
||||||
} else {
|
}
|
||||||
|
ip |= value << (octet * 8);
|
||||||
|
if (octet < 3) {
|
||||||
|
if (*s++ != '.') return false;
|
||||||
|
} else if (*s != '\0') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hasDigit && dotCount == 3;
|
*out = ip;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Public API ----
|
// ---- Public API ----
|
||||||
@@ -310,8 +343,8 @@ namespace Net::Dns {
|
|||||||
uint32_t Resolve(const char* hostname, uint32_t timeoutMs) {
|
uint32_t Resolve(const char* hostname, uint32_t timeoutMs) {
|
||||||
if (hostname == nullptr || hostname[0] == '\0') return 0;
|
if (hostname == nullptr || hostname[0] == '\0') return 0;
|
||||||
|
|
||||||
// Don't try to resolve IP addresses
|
uint32_t literalIp = 0;
|
||||||
if (IsIpAddress(hostname)) return 0;
|
if (ParseIpAddress(hostname, &literalIp)) return literalIp;
|
||||||
|
|
||||||
// Check cache first
|
// Check cache first
|
||||||
uint32_t cached = CacheLookup(hostname);
|
uint32_t cached = CacheLookup(hostname);
|
||||||
@@ -321,51 +354,77 @@ namespace Net::Dns {
|
|||||||
uint32_t dnsServer = Net::GetDnsServer();
|
uint32_t dnsServer = Net::GetDnsServer();
|
||||||
if (dnsServer == 0) return 0;
|
if (dnsServer == 0) return 0;
|
||||||
|
|
||||||
// Pick a local port for receiving the response (ephemeral range)
|
g_queriesLock.Acquire();
|
||||||
uint16_t localPort = 10000 + (NextId() % 50000);
|
Query* query = nullptr;
|
||||||
uint16_t txId = NextId();
|
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
|
// Build DNS query
|
||||||
uint8_t queryPacket[512];
|
uint8_t queryPacket[512];
|
||||||
int queryLen = BuildQuery(txId, hostname, queryPacket, sizeof(queryPacket));
|
int queryLen = BuildQuery(txId, hostname, queryPacket, sizeof(queryPacket));
|
||||||
if (queryLen == 0) return 0;
|
if (queryLen == 0) {
|
||||||
|
query->active = false;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Reset response state
|
bool bound = false;
|
||||||
g_gotResponse = false;
|
for (int attempt = 0; attempt < 16; ++attempt) {
|
||||||
g_responseLen = 0;
|
g_queriesLock.Acquire();
|
||||||
g_currentId = txId;
|
uint16_t localPort = (uint16_t)(10000 + (NextId() % 50000));
|
||||||
|
g_queriesLock.Release();
|
||||||
// Bind our receive port
|
if (Net::Udp::Bind(localPort, DnsRecvCallback)) {
|
||||||
if (!Net::Udp::Bind(localPort, DnsRecvCallback)) {
|
query->localPort = localPort;
|
||||||
// Port might be in use, try another
|
bound = true;
|
||||||
localPort = 10000 + (NextId() % 50000);
|
break;
|
||||||
if (!Net::Udp::Bind(localPort, DnsRecvCallback)) {
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!bound) {
|
||||||
|
query->active = false;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Send the query to DNS server port 53
|
// 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) {
|
if (!sent) {
|
||||||
Net::Udp::Unbind(localPort);
|
Net::Udp::Unbind(query->localPort);
|
||||||
|
query->active = false;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for response with timeout
|
// Wait for response with timeout
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (!g_gotResponse) {
|
while (!query->gotResponse) {
|
||||||
if (Timekeeping::GetMilliseconds() - start >= timeoutMs) {
|
if (Timekeeping::GetMilliseconds() - start >= timeoutMs) {
|
||||||
Net::Udp::Unbind(localPort);
|
Net::Udp::Unbind(query->localPort);
|
||||||
|
query->active = false;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
Sched::Schedule();
|
Sched::Schedule();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unbind the port
|
// Unbind the port
|
||||||
Net::Udp::Unbind(localPort);
|
Net::Udp::Unbind(query->localPort);
|
||||||
|
|
||||||
// Parse the response
|
// 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;
|
if (!answer.found) return 0;
|
||||||
|
|
||||||
// Cache the result
|
// Cache the result
|
||||||
|
|||||||
+53
-6
@@ -15,6 +15,8 @@
|
|||||||
#include <Libraries/Memory.hpp>
|
#include <Libraries/Memory.hpp>
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
#include <CppLib/Stream.hpp>
|
#include <CppLib/Stream.hpp>
|
||||||
|
#include <CppLib/Spinlock.hpp>
|
||||||
|
#include <Timekeeping/ApicTimer.hpp>
|
||||||
|
|
||||||
using namespace Kt;
|
using namespace Kt;
|
||||||
|
|
||||||
@@ -28,11 +30,15 @@ namespace Net::Ipv4 {
|
|||||||
uint8_t Protocol;
|
uint8_t Protocol;
|
||||||
uint8_t Data[Ethernet::MAX_PAYLOAD_SIZE - HEADER_SIZE];
|
uint8_t Data[Ethernet::MAX_PAYLOAD_SIZE - HEADER_SIZE];
|
||||||
uint16_t Length;
|
uint16_t Length;
|
||||||
|
uint64_t Timestamp;
|
||||||
bool Active;
|
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 PendingPacket g_pendingQueue[PENDING_QUEUE_SIZE] = {};
|
||||||
|
static kcp::Spinlock g_pendingLock;
|
||||||
|
|
||||||
void Initialize() {
|
void Initialize() {
|
||||||
g_identification = 0;
|
g_identification = 0;
|
||||||
@@ -125,6 +131,11 @@ namespace Net::Ipv4 {
|
|||||||
return;
|
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
|
// Check destination: accept packets addressed to us or broadcast
|
||||||
uint32_t ourIp = GetIpAddress();
|
uint32_t ourIp = GetIpAddress();
|
||||||
if (hdr->DstIp != ourIp && hdr->DstIp != 0xFFFFFFFF) {
|
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->VersionIhl = (4 << 4) | 5; // IPv4, 5 dwords (20 bytes)
|
||||||
hdr->Tos = 0;
|
hdr->Tos = 0;
|
||||||
hdr->TotalLength = Htons(HEADER_SIZE + payloadLen);
|
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->FlagsFragment = 0;
|
||||||
hdr->Ttl = DEFAULT_TTL;
|
hdr->Ttl = DEFAULT_TTL;
|
||||||
hdr->Protocol = protocol;
|
hdr->Protocol = protocol;
|
||||||
@@ -187,16 +200,25 @@ namespace Net::Ipv4 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ARP request already sent by Resolve(), queue the packet for later
|
// 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++) {
|
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) {
|
if (!g_pendingQueue[i].Active) {
|
||||||
g_pendingQueue[i].DestIp = destIp;
|
g_pendingQueue[i].DestIp = destIp;
|
||||||
g_pendingQueue[i].Protocol = protocol;
|
g_pendingQueue[i].Protocol = protocol;
|
||||||
g_pendingQueue[i].Length = payloadLen;
|
g_pendingQueue[i].Length = payloadLen;
|
||||||
|
g_pendingQueue[i].Timestamp = now;
|
||||||
memcpy(g_pendingQueue[i].Data, payload, payloadLen);
|
memcpy(g_pendingQueue[i].Data, payload, payloadLen);
|
||||||
g_pendingQueue[i].Active = true;
|
g_pendingQueue[i].Active = true;
|
||||||
|
g_pendingQueue[i].Processing = false;
|
||||||
|
g_pendingLock.Release();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
g_pendingLock.Release();
|
||||||
|
|
||||||
// Queue full, drop the packet
|
// Queue full, drop the packet
|
||||||
return false;
|
return false;
|
||||||
@@ -204,17 +226,42 @@ namespace Net::Ipv4 {
|
|||||||
|
|
||||||
void FlushPending() {
|
void FlushPending() {
|
||||||
for (uint32_t i = 0; i < PENDING_QUEUE_SIZE; i++) {
|
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;
|
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];
|
uint8_t destMac[6];
|
||||||
|
|
||||||
if (Arp::Resolve(nextHop, destMac)) {
|
if (Arp::Resolve(nextHop, destMac)) {
|
||||||
SendDirect(g_pendingQueue[i].DestIp, g_pendingQueue[i].Protocol,
|
g_pendingLock.Acquire();
|
||||||
destMac, g_pendingQueue[i].Data, g_pendingQueue[i].Length);
|
|
||||||
g_pendingQueue[i].Active = false;
|
g_pendingQueue[i].Active = false;
|
||||||
|
g_pendingQueue[i].Processing = false;
|
||||||
|
g_pendingLock.Release();
|
||||||
|
SendDirect(destIp, protocol, destMac, data, length);
|
||||||
|
} else {
|
||||||
|
g_pendingLock.Acquire();
|
||||||
|
g_pendingQueue[i].Processing = false;
|
||||||
|
g_pendingLock.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+141
-45
@@ -23,7 +23,8 @@ namespace Net::Tcp {
|
|||||||
// Receive buffer size per connection
|
// Receive buffer size per connection
|
||||||
static constexpr uint16_t RECV_BUFFER_SIZE = 32768;
|
static constexpr uint16_t RECV_BUFFER_SIZE = 32768;
|
||||||
static constexpr uint16_t WINDOW_SIZE = RECV_BUFFER_SIZE;
|
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 uint64_t RETRANSMIT_TIMEOUT_MS = 1000;
|
||||||
static constexpr int MAX_RETRANSMITS = 5;
|
static constexpr int MAX_RETRANSMITS = 5;
|
||||||
static constexpr uint64_t TIME_WAIT_MS = 2000;
|
static constexpr uint64_t TIME_WAIT_MS = 2000;
|
||||||
@@ -51,12 +52,18 @@ namespace Net::Tcp {
|
|||||||
uint16_t RetransmitLen;
|
uint16_t RetransmitLen;
|
||||||
uint64_t RetransmitTime;
|
uint64_t RetransmitTime;
|
||||||
int RetransmitCount;
|
int RetransmitCount;
|
||||||
|
bool SendBusy;
|
||||||
|
|
||||||
// For Listen/Accept
|
// For Listen/Accept
|
||||||
bool PendingAccept;
|
struct PendingSyn {
|
||||||
uint32_t PendingRemoteIp;
|
uint32_t RemoteIp;
|
||||||
uint16_t PendingRemotePort;
|
uint16_t RemotePort;
|
||||||
uint32_t PendingSeq;
|
uint32_t Seq;
|
||||||
|
};
|
||||||
|
PendingSyn Pending[ACCEPT_BACKLOG_SIZE];
|
||||||
|
uint8_t PendingHead;
|
||||||
|
uint8_t PendingTail;
|
||||||
|
uint8_t PendingCount;
|
||||||
|
|
||||||
bool Active;
|
bool Active;
|
||||||
|
|
||||||
@@ -71,6 +78,17 @@ namespace Net::Tcp {
|
|||||||
return (uint32_t)(Timekeeping::GetMilliseconds() * 2654435761u);
|
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,
|
static Connection* FindConnection(uint32_t remoteIp, uint16_t remotePort,
|
||||||
uint16_t localPort) {
|
uint16_t localPort) {
|
||||||
for (uint32_t i = 0; i < MAX_CONNECTIONS; i++) {
|
for (uint32_t i = 0; i < MAX_CONNECTIONS; i++) {
|
||||||
@@ -109,6 +127,13 @@ namespace Net::Tcp {
|
|||||||
return nullptr;
|
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,
|
static bool SendSegment(Connection* conn, uint8_t flags,
|
||||||
const uint8_t* payload, uint16_t payloadLen) {
|
const uint8_t* payload, uint16_t payloadLen) {
|
||||||
uint8_t packet[1500];
|
uint8_t packet[1500];
|
||||||
@@ -213,15 +238,35 @@ namespace Net::Tcp {
|
|||||||
if (flags & FLAG_SYN) {
|
if (flags & FLAG_SYN) {
|
||||||
Connection* listener = FindListener(dstPort);
|
Connection* listener = FindListener(dstPort);
|
||||||
if (listener != nullptr) {
|
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->Lock.Acquire();
|
||||||
listener->PendingAccept = true;
|
bool duplicate = false;
|
||||||
listener->PendingRemoteIp = srcIp;
|
for (uint8_t i = 0; i < listener->PendingCount; ++i) {
|
||||||
listener->PendingRemotePort = srcPort;
|
uint8_t index = (uint8_t)((listener->PendingHead + i) %
|
||||||
listener->PendingSeq = seqNum;
|
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();
|
listener->Lock.Release();
|
||||||
Sched::WakeObjectWaiters(listener);
|
if (duplicate || listener->PendingCount > 0) {
|
||||||
Ipc::NotifyTcpConnectionChanged(listener);
|
Sched::WakeObjectWaiters(listener);
|
||||||
|
Ipc::NotifyTcpConnectionChanged(listener);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,7 +291,9 @@ namespace Net::Tcp {
|
|||||||
// RST handling
|
// RST handling
|
||||||
if (flags & FLAG_RST) {
|
if (flags & FLAG_RST) {
|
||||||
conn->CurrentState = State::Closed;
|
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();
|
conn->Lock.Release();
|
||||||
Sched::WakeObjectWaiters(conn);
|
Sched::WakeObjectWaiters(conn);
|
||||||
Ipc::NotifyTcpConnectionChanged(conn);
|
Ipc::NotifyTcpConnectionChanged(conn);
|
||||||
@@ -285,8 +332,10 @@ namespace Net::Tcp {
|
|||||||
case State::Established: {
|
case State::Established: {
|
||||||
// Handle incoming data
|
// Handle incoming data
|
||||||
if (flags & FLAG_ACK) {
|
if (flags & FLAG_ACK) {
|
||||||
conn->SendUnack = ackNum;
|
if (AckIsValid(conn, ackNum)) {
|
||||||
notify = true;
|
conn->SendUnack = ackNum;
|
||||||
|
notify = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t accepted = 0;
|
uint16_t accepted = 0;
|
||||||
@@ -365,9 +414,9 @@ namespace Net::Tcp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case State::LastAck: {
|
case State::LastAck: {
|
||||||
if (flags & FLAG_ACK) {
|
if ((flags & FLAG_ACK) && ackNum == conn->SendNext) {
|
||||||
|
conn->SendUnack = ackNum;
|
||||||
conn->CurrentState = State::Closed;
|
conn->CurrentState = State::Closed;
|
||||||
conn->Active = false;
|
|
||||||
notify = true;
|
notify = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -401,7 +450,9 @@ namespace Net::Tcp {
|
|||||||
conn->LocalIp = Net::GetIpAddress();
|
conn->LocalIp = Net::GetIpAddress();
|
||||||
conn->LocalPort = port;
|
conn->LocalPort = port;
|
||||||
conn->CurrentState = State::Listen;
|
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;
|
KernelLogStream(INFO, "Net") << "TCP listening on port " << base::dec << (uint64_t)port;
|
||||||
return conn;
|
return conn;
|
||||||
@@ -415,12 +466,15 @@ namespace Net::Tcp {
|
|||||||
// Block until a SYN arrives
|
// Block until a SYN arrives
|
||||||
while (true) {
|
while (true) {
|
||||||
listener->Lock.Acquire();
|
listener->Lock.Acquire();
|
||||||
if (listener->PendingAccept) {
|
if (listener->PendingCount > 0) {
|
||||||
listener->PendingAccept = false;
|
Connection::PendingSyn pending = listener->Pending[listener->PendingHead];
|
||||||
|
listener->PendingHead =
|
||||||
|
(uint8_t)((listener->PendingHead + 1) % ACCEPT_BACKLOG_SIZE);
|
||||||
|
listener->PendingCount--;
|
||||||
|
|
||||||
uint32_t remoteIp = listener->PendingRemoteIp;
|
uint32_t remoteIp = pending.RemoteIp;
|
||||||
uint16_t remotePort = listener->PendingRemotePort;
|
uint16_t remotePort = pending.RemotePort;
|
||||||
uint32_t remoteSeq = listener->PendingSeq;
|
uint32_t remoteSeq = pending.Seq;
|
||||||
listener->Lock.Release();
|
listener->Lock.Release();
|
||||||
|
|
||||||
// Allocate a new connection for this client
|
// Allocate a new connection for this client
|
||||||
@@ -480,9 +534,9 @@ namespace Net::Tcp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Timed out waiting for ACK
|
// Timed out waiting for ACK
|
||||||
conn->Active = false;
|
|
||||||
Sched::WakeObjectWaiters(conn);
|
Sched::WakeObjectWaiters(conn);
|
||||||
Ipc::NotifyTcpConnectionChanged(conn);
|
Ipc::NotifyTcpConnectionChanged(conn);
|
||||||
|
ReleaseConnection(conn);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
listener->Lock.Release();
|
listener->Lock.Release();
|
||||||
@@ -568,25 +622,56 @@ namespace Net::Tcp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Failed to connect
|
// Failed to connect
|
||||||
conn->Active = false;
|
|
||||||
Sched::WakeObjectWaiters(conn);
|
Sched::WakeObjectWaiters(conn);
|
||||||
Ipc::NotifyTcpConnectionChanged(conn);
|
Ipc::NotifyTcpConnectionChanged(conn);
|
||||||
|
ReleaseConnection(conn);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
int Send(Connection* conn, const uint8_t* data, uint16_t length) {
|
int Send(Connection* conn, const uint8_t* data, uint32_t length) {
|
||||||
if (conn == nullptr || conn->CurrentState != State::Established) {
|
if (conn == nullptr || data == nullptr) return -1;
|
||||||
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;
|
constexpr uint16_t MSS = 1460;
|
||||||
uint16_t sent = 0;
|
uint32_t sent = 0;
|
||||||
|
|
||||||
while (sent < length) {
|
while (sent < length) {
|
||||||
uint16_t segLen = length - sent;
|
uint32_t remaining = length - sent;
|
||||||
if (segLen > MSS) {
|
uint16_t segLen = remaining > MSS ? MSS : (uint16_t)remaining;
|
||||||
segLen = MSS;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t segSeq = 0;
|
uint32_t segSeq = 0;
|
||||||
uint32_t expectedAck = 0;
|
uint32_t expectedAck = 0;
|
||||||
@@ -597,7 +682,9 @@ namespace Net::Tcp {
|
|||||||
if (conn->CurrentState != State::Established) {
|
if (conn->CurrentState != State::Established) {
|
||||||
conn->Lock.Release();
|
conn->Lock.Release();
|
||||||
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
|
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;
|
segSeq = conn->SendNext;
|
||||||
@@ -607,7 +694,9 @@ namespace Net::Tcp {
|
|||||||
if (!ok) {
|
if (!ok) {
|
||||||
conn->Lock.Release();
|
conn->Lock.Release();
|
||||||
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
|
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;
|
conn->SendNext = expectedAck;
|
||||||
@@ -626,7 +715,7 @@ namespace Net::Tcp {
|
|||||||
asm volatile("pushfq; pop %0; cli" : "=r"(flags) :: "memory");
|
asm volatile("pushfq; pop %0; cli" : "=r"(flags) :: "memory");
|
||||||
conn->Lock.Acquire();
|
conn->Lock.Acquire();
|
||||||
|
|
||||||
if (conn->SendUnack >= expectedAck) {
|
if (SeqLessOrEqual(expectedAck, conn->SendUnack)) {
|
||||||
conn->RetransmitLen = 0;
|
conn->RetransmitLen = 0;
|
||||||
conn->Lock.Release();
|
conn->Lock.Release();
|
||||||
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
|
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
|
||||||
@@ -639,7 +728,9 @@ namespace Net::Tcp {
|
|||||||
conn->RetransmitLen = 0;
|
conn->RetransmitLen = 0;
|
||||||
conn->Lock.Release();
|
conn->Lock.Release();
|
||||||
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
|
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();
|
uint64_t now = Timekeeping::GetMilliseconds();
|
||||||
@@ -653,7 +744,9 @@ namespace Net::Tcp {
|
|||||||
conn->RetransmitLen = 0;
|
conn->RetransmitLen = 0;
|
||||||
conn->Lock.Release();
|
conn->Lock.Release();
|
||||||
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
|
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;
|
uint32_t savedNext = conn->SendNext;
|
||||||
@@ -667,7 +760,9 @@ namespace Net::Tcp {
|
|||||||
conn->RetransmitLen = 0;
|
conn->RetransmitLen = 0;
|
||||||
conn->Lock.Release();
|
conn->Lock.Release();
|
||||||
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
|
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) {
|
int Receive(Connection* conn, uint8_t* buffer, uint16_t bufferSize) {
|
||||||
@@ -815,9 +911,9 @@ namespace Net::Tcp {
|
|||||||
}
|
}
|
||||||
Sched::BlockOnObject(conn, 50);
|
Sched::BlockOnObject(conn, 50);
|
||||||
}
|
}
|
||||||
conn->Active = false;
|
|
||||||
Sched::WakeObjectWaiters(conn);
|
Sched::WakeObjectWaiters(conn);
|
||||||
Ipc::NotifyTcpConnectionChanged(conn);
|
Ipc::NotifyTcpConnectionChanged(conn);
|
||||||
|
ReleaseConnection(conn);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -835,29 +931,29 @@ namespace Net::Tcp {
|
|||||||
}
|
}
|
||||||
Sched::BlockOnObject(conn, 50);
|
Sched::BlockOnObject(conn, 50);
|
||||||
}
|
}
|
||||||
conn->Active = false;
|
|
||||||
Sched::WakeObjectWaiters(conn);
|
Sched::WakeObjectWaiters(conn);
|
||||||
Ipc::NotifyTcpConnectionChanged(conn);
|
Ipc::NotifyTcpConnectionChanged(conn);
|
||||||
|
ReleaseConnection(conn);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
case State::Listen:
|
case State::Listen:
|
||||||
case State::SynSent: {
|
case State::SynSent: {
|
||||||
conn->CurrentState = State::Closed;
|
conn->CurrentState = State::Closed;
|
||||||
conn->Active = false;
|
|
||||||
conn->Lock.Release();
|
conn->Lock.Release();
|
||||||
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
|
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
|
||||||
Sched::WakeObjectWaiters(conn);
|
Sched::WakeObjectWaiters(conn);
|
||||||
Ipc::NotifyTcpConnectionChanged(conn);
|
Ipc::NotifyTcpConnectionChanged(conn);
|
||||||
|
ReleaseConnection(conn);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
conn->Lock.Release();
|
conn->Lock.Release();
|
||||||
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
|
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
|
||||||
conn->Active = false;
|
|
||||||
Sched::WakeObjectWaiters(conn);
|
Sched::WakeObjectWaiters(conn);
|
||||||
Ipc::NotifyTcpConnectionChanged(conn);
|
Ipc::NotifyTcpConnectionChanged(conn);
|
||||||
|
ReleaseConnection(conn);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -875,7 +971,7 @@ namespace Net::Tcp {
|
|||||||
bool HasPendingAccept(Connection* conn) {
|
bool HasPendingAccept(Connection* conn) {
|
||||||
if (conn == nullptr) return false;
|
if (conn == nullptr) return false;
|
||||||
conn->Lock.Acquire();
|
conn->Lock.Acquire();
|
||||||
bool pending = conn->PendingAccept;
|
bool pending = conn->PendingCount > 0;
|
||||||
conn->Lock.Release();
|
conn->Lock.Release();
|
||||||
return pending;
|
return pending;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ namespace Net::Tcp {
|
|||||||
Connection* Connect(uint32_t destIp, uint16_t destPort, uint16_t srcPort);
|
Connection* Connect(uint32_t destIp, uint16_t destPort, uint16_t srcPort);
|
||||||
|
|
||||||
// Send data on an established connection. Returns number of bytes sent.
|
// 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.
|
// Receive data from an established connection. Returns number of bytes received.
|
||||||
// Blocks until data is available or connection is closed.
|
// Blocks until data is available or connection is closed.
|
||||||
|
|||||||
+20
-4
@@ -11,6 +11,7 @@
|
|||||||
#include <Libraries/Memory.hpp>
|
#include <Libraries/Memory.hpp>
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
#include <CppLib/Stream.hpp>
|
#include <CppLib/Stream.hpp>
|
||||||
|
#include <CppLib/Spinlock.hpp>
|
||||||
|
|
||||||
using namespace Kt;
|
using namespace Kt;
|
||||||
|
|
||||||
@@ -22,8 +23,9 @@ namespace Net::Udp {
|
|||||||
bool Active;
|
bool Active;
|
||||||
};
|
};
|
||||||
|
|
||||||
static constexpr uint32_t MAX_BINDINGS = 16;
|
static constexpr uint32_t MAX_BINDINGS = 64;
|
||||||
static PortBinding g_bindings[MAX_BINDINGS] = {};
|
static PortBinding g_bindings[MAX_BINDINGS] = {};
|
||||||
|
static kcp::Spinlock g_bindingsLock;
|
||||||
|
|
||||||
void Initialize() {
|
void Initialize() {
|
||||||
for (uint32_t i = 0; i < MAX_BINDINGS; i++) {
|
for (uint32_t i = 0; i < MAX_BINDINGS; i++) {
|
||||||
@@ -58,13 +60,18 @@ namespace Net::Udp {
|
|||||||
const uint8_t* payload = data + HEADER_SIZE;
|
const uint8_t* payload = data + HEADER_SIZE;
|
||||||
uint16_t payloadLen = udpLen - 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++) {
|
for (uint32_t i = 0; i < MAX_BINDINGS; i++) {
|
||||||
if (g_bindings[i].Active && g_bindings[i].Port == dstPort) {
|
if (g_bindings[i].Active && g_bindings[i].Port == dstPort) {
|
||||||
g_bindings[i].Callback(srcIp, srcPort, dstPort, payload, payloadLen);
|
callback = g_bindings[i].Callback;
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
g_bindingsLock.Release();
|
||||||
|
if (callback) callback(srcIp, srcPort, dstPort, payload, payloadLen);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Send(uint32_t destIp, uint16_t srcPort, uint16_t destPort,
|
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) {
|
bool Bind(uint16_t port, RecvCallback callback) {
|
||||||
|
if (port == 0 || callback == nullptr) return false;
|
||||||
|
g_bindingsLock.Acquire();
|
||||||
// Check for duplicate
|
// Check for duplicate
|
||||||
for (uint32_t i = 0; i < MAX_BINDINGS; i++) {
|
for (uint32_t i = 0; i < MAX_BINDINGS; i++) {
|
||||||
if (g_bindings[i].Active && g_bindings[i].Port == port) {
|
if (g_bindings[i].Active && g_bindings[i].Port == port) {
|
||||||
|
g_bindingsLock.Release();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,19 +119,25 @@ namespace Net::Udp {
|
|||||||
g_bindings[i].Port = port;
|
g_bindings[i].Port = port;
|
||||||
g_bindings[i].Callback = callback;
|
g_bindings[i].Callback = callback;
|
||||||
g_bindings[i].Active = true;
|
g_bindings[i].Active = true;
|
||||||
|
g_bindingsLock.Release();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
g_bindingsLock.Release();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Unbind(uint16_t port) {
|
void Unbind(uint16_t port) {
|
||||||
|
g_bindingsLock.Acquire();
|
||||||
for (uint32_t i = 0; i < MAX_BINDINGS; i++) {
|
for (uint32_t i = 0; i < MAX_BINDINGS; i++) {
|
||||||
if (g_bindings[i].Active && g_bindings[i].Port == port) {
|
if (g_bindings[i].Active && g_bindings[i].Port == port) {
|
||||||
g_bindings[i].Active = false;
|
g_bindings[i].Active = false;
|
||||||
|
g_bindings[i].Callback = nullptr;
|
||||||
|
g_bindingsLock.Release();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
g_bindingsLock.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+536
-277
@@ -1,7 +1,10 @@
|
|||||||
/*
|
/*
|
||||||
* http.hpp
|
* http.hpp
|
||||||
* Simple HTTP request builder and response parser for MontaukOS
|
* Shared HTTP/1.1 client for MontaukOS.
|
||||||
* Wraps tls::https_fetch() and raw sockets for ergonomic HTTP usage.
|
*
|
||||||
|
* 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
|
#pragma once
|
||||||
@@ -13,67 +16,113 @@
|
|||||||
|
|
||||||
namespace http {
|
namespace http {
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
enum class Error {
|
||||||
// Response
|
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 {
|
struct Response {
|
||||||
int status; // HTTP status code (200, 404, etc.) or -1 on error
|
int status; // HTTP status code, or -1 before a response is parsed
|
||||||
const char* headers; // Pointer into raw buffer (header block)
|
const char* headers; // Pointers into raw
|
||||||
int headers_len;
|
int headers_len;
|
||||||
const char* body; // Pointer into raw buffer (body)
|
const char* body;
|
||||||
int body_len;
|
int body_len;
|
||||||
char* raw; // Owned buffer — caller must free with montauk::mfree()
|
char* raw;
|
||||||
int raw_len;
|
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) {
|
inline const char* find_header_block_end(const char* start, const char* end) {
|
||||||
if (!start || !end || start >= end) return nullptr;
|
if (!start || !end || start >= end) return nullptr;
|
||||||
|
for (const char* p = start; p + 3 < end; ++p) {
|
||||||
for (const char* s = start; s < end - 3; s++) {
|
if (p[0] == '\r' && p[1] == '\n' && p[2] == '\r' && p[3] == '\n')
|
||||||
if (s[0] == '\r' && s[1] == '\n' && s[2] == '\r' && s[3] == '\n')
|
return p + 4;
|
||||||
return s + 4;
|
|
||||||
}
|
}
|
||||||
for (const char* s = start; s < end - 1; s++) {
|
for (const char* p = start; p + 1 < end; ++p) {
|
||||||
if (s[0] == '\n' && s[1] == '\n')
|
if (p[0] == '\n' && p[1] == '\n') return p + 2;
|
||||||
return s + 2;
|
|
||||||
}
|
}
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline int parse_status_code(const char* start, const char* end) {
|
inline int parse_status_code(const char* start, const char* end) {
|
||||||
if (!start || !end || end - start < 12) return -1;
|
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;
|
const char* p = start;
|
||||||
while (p < end && *p && *p != ' ') p++;
|
while (p < end && *p != ' ' && *p != '\r' && *p != '\n') ++p;
|
||||||
if (p >= end || *p != ' ') return -1;
|
if (p >= end || *p++ != ' ') return -1;
|
||||||
p++;
|
if (end - p < 3 || p[0] < '0' || p[0] > '9' ||
|
||||||
|
p[1] < '0' || p[1] > '9' || p[2] < '0' || p[2] > '9') return -1;
|
||||||
int code = 0;
|
return (p[0] - '0') * 100 + (p[1] - '0') * 10 + (p[2] - '0');
|
||||||
int digits = 0;
|
|
||||||
while (digits < 3 && p < end && *p >= '0' && *p <= '9') {
|
|
||||||
code = code * 10 + (*p - '0');
|
|
||||||
p++;
|
|
||||||
digits++;
|
|
||||||
}
|
|
||||||
return digits == 3 ? code : -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline const char* find_final_response_start(const char* buf, int len) {
|
inline const char* find_final_response_start(const char* buf, int len) {
|
||||||
if (!buf || len <= 0) return nullptr;
|
if (!buf || len <= 0) return nullptr;
|
||||||
|
|
||||||
const char* start = buf;
|
const char* start = buf;
|
||||||
const char* end = buf + len;
|
const char* end = buf + len;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
int code = parse_status_code(start, end);
|
int code = parse_status_code(start, end);
|
||||||
if (code < 0) return nullptr;
|
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);
|
const char* next = find_header_block_end(start, end);
|
||||||
if (!next || next >= end) return nullptr;
|
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;
|
start = next;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,295 +132,505 @@ inline const char* skip_informational_responses(const char* buf, int len) {
|
|||||||
return start ? start : buf;
|
return start ? start : buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse raw HTTP response in-place. Sets pointers into buf (does not copy).
|
inline bool ascii_equal_ci(char a, char b) {
|
||||||
// Skips leading informational 1xx responses such as "100 Continue".
|
if (a >= 'A' && a <= 'Z') a += 'a' - 'A';
|
||||||
// Returns status code, or -1 if unparseable.
|
if (b >= 'A' && b <= 'Z') b += 'a' - 'A';
|
||||||
inline int parse_response(char* buf, int len, Response* out) {
|
return a == b;
|
||||||
out->raw = buf;
|
|
||||||
out->raw_len = len;
|
|
||||||
out->status = -1;
|
|
||||||
out->headers = nullptr;
|
|
||||||
out->headers_len = 0;
|
|
||||||
out->body = nullptr;
|
|
||||||
out->body_len = 0;
|
|
||||||
|
|
||||||
const char* start = find_final_response_start(buf, len);
|
|
||||||
const char* end = buf + len;
|
|
||||||
if (!start || end - start < 12) return -1; // "HTTP/1.x NNN"
|
|
||||||
|
|
||||||
// Parse status code from "HTTP/1.x NNN"
|
|
||||||
int code = parse_status_code(start, end);
|
|
||||||
if (code < 0) return -1;
|
|
||||||
out->status = code;
|
|
||||||
|
|
||||||
// Headers start after the status line
|
|
||||||
const char* hdr_start = start;
|
|
||||||
while (hdr_start < end - 1) {
|
|
||||||
if (*hdr_start == '\r' && *(hdr_start + 1) == '\n') { hdr_start += 2; break; }
|
|
||||||
if (*hdr_start == '\n') { hdr_start++; break; }
|
|
||||||
hdr_start++;
|
|
||||||
}
|
|
||||||
out->headers = hdr_start;
|
|
||||||
|
|
||||||
// Find \r\n\r\n boundary between headers and body
|
|
||||||
const char* body = find_header_block_end(hdr_start, end);
|
|
||||||
if (body) {
|
|
||||||
if (body >= hdr_start + 4 &&
|
|
||||||
body[-4] == '\r' && body[-3] == '\n' && body[-2] == '\r' && body[-1] == '\n')
|
|
||||||
out->headers_len = (int)((body - 4) - hdr_start);
|
|
||||||
else if (body >= hdr_start + 2 &&
|
|
||||||
body[-2] == '\n' && body[-1] == '\n')
|
|
||||||
out->headers_len = (int)((body - 2) - hdr_start);
|
|
||||||
else
|
|
||||||
out->headers_len = (int)(body - hdr_start);
|
|
||||||
out->body = body;
|
|
||||||
out->body_len = len - (int)(out->body - buf);
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No body separator found — entire remainder is headers
|
|
||||||
out->headers_len = len - (int)(hdr_start - buf);
|
|
||||||
return code;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find a header value by name (case-insensitive match on the name).
|
inline bool get_header(const Response* resp, const char* name,
|
||||||
// Writes value into out_val (up to max_len), returns true if found.
|
char* out_val, int max_len) {
|
||||||
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 ||
|
||||||
if (!resp->headers || resp->headers_len == 0) return false;
|
!out_val || max_len <= 0) return false;
|
||||||
|
|
||||||
int name_len = montauk::slen(name);
|
int name_len = montauk::slen(name);
|
||||||
const char* p = resp->headers;
|
const char* p = resp->headers;
|
||||||
const char* end = resp->headers + resp->headers_len;
|
const char* end = p + resp->headers_len;
|
||||||
|
|
||||||
while (p < end) {
|
while (p < end) {
|
||||||
// Case-insensitive prefix match
|
const char* line_end = p;
|
||||||
bool match = true;
|
while (line_end < end && *line_end != '\r' && *line_end != '\n') ++line_end;
|
||||||
if (p + name_len >= end) { match = false; }
|
bool match = line_end - p > name_len && p[name_len] == ':';
|
||||||
else {
|
for (int i = 0; match && i < name_len; ++i)
|
||||||
for (int i = 0; i < name_len; i++) {
|
if (!ascii_equal_ci(p[i], name[i])) match = false;
|
||||||
char a = p[i], b = name[i];
|
|
||||||
if (a >= 'A' && a <= 'Z') a += 32;
|
|
||||||
if (b >= 'A' && b <= 'Z') b += 32;
|
|
||||||
if (a != b) { match = false; break; }
|
|
||||||
}
|
|
||||||
if (match && p[name_len] != ':') match = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const char* v = p + name_len + 1;
|
const char* value = p + name_len + 1;
|
||||||
while (v < end && *v == ' ') v++; // skip OWS
|
while (value < line_end && (*value == ' ' || *value == '\t')) ++value;
|
||||||
int i = 0;
|
while (line_end > value &&
|
||||||
while (v < end && *v != '\r' && *v != '\n' && i < max_len - 1)
|
(line_end[-1] == ' ' || line_end[-1] == '\t')) --line_end;
|
||||||
out_val[i++] = *v++;
|
int n = (int)(line_end - value);
|
||||||
out_val[i] = 0;
|
if (n >= max_len) n = max_len - 1;
|
||||||
|
if (n > 0) montauk::memcpy(out_val, value, n);
|
||||||
|
out_val[n] = '\0';
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip to next line
|
p = line_end;
|
||||||
while (p < end && *p != '\n') p++;
|
while (p < end && (*p == '\r' || *p == '\n')) ++p;
|
||||||
if (p < end) p++;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Free a response's raw buffer.
|
inline bool header_has_token(const Response* resp, const char* name,
|
||||||
inline void free_response(Response* resp) {
|
const char* token) {
|
||||||
if (resp->raw) { montauk::mfree(resp->raw); resp->raw = nullptr; }
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
inline int parse_decimal(const char* value) {
|
||||||
// Request builder (internal)
|
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,
|
inline int build_request(char* buf, int buf_size,
|
||||||
const char* method, const char* host,
|
const char* method, const char* host,
|
||||||
const char* path, const char* content_type,
|
const char* path, const char* content_type,
|
||||||
const char* body_data, int body_len,
|
const char* body_data, int body_len,
|
||||||
const char* extra_headers) {
|
const char* extra_headers) {
|
||||||
char* p = buf;
|
if (!buf || buf_size <= 0 || !method || !*method || !host || !*host ||
|
||||||
char* end = buf + buf_size - 1;
|
!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) {
|
RequestWriter w = {buf, buf + buf_size, false};
|
||||||
while (*s && p < end) *p++ = *s++;
|
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");
|
||||||
auto append_int = [&](int n) {
|
if (body_data || body_len > 0) {
|
||||||
char tmp[16]; int ti = 0;
|
|
||||||
if (n == 0) { if (p < end) *p++ = '0'; return; }
|
|
||||||
while (n > 0) { tmp[ti++] = '0' + (n % 10); n /= 10; }
|
|
||||||
for (int j = ti - 1; j >= 0 && p < end; j--) *p++ = tmp[j];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Request line
|
|
||||||
append(method); append(" "); append(path); append(" HTTP/1.1\r\n");
|
|
||||||
|
|
||||||
// Host
|
|
||||||
append("Host: "); append(host); append("\r\n");
|
|
||||||
|
|
||||||
// Content headers (for POST/PUT/PATCH)
|
|
||||||
if (body_data && body_len > 0) {
|
|
||||||
if (content_type) {
|
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");
|
||||||
}
|
}
|
||||||
|
if (extra_headers) {
|
||||||
// Extra headers (caller-supplied, must include \r\n terminators)
|
w.text(extra_headers);
|
||||||
if (extra_headers) append(extra_headers);
|
int n = montauk::slen(extra_headers);
|
||||||
|
if (n > 0 && extra_headers[n - 1] != '\n') w.text("\r\n");
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
w.text("Connection: close\r\n\r\n");
|
||||||
return (int)(p - buf);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
inline bool plain_send_all(int fd, const char* data, int len,
|
||||||
// Public API
|
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,
|
inline Response get(const char* host, const char* path,
|
||||||
const tls::TrustAnchors& tas,
|
const tls::TrustAnchors& tas, int response_buffer_size = 32768,
|
||||||
int resp_buf_size = 32768,
|
|
||||||
const char* extra_headers = nullptr,
|
const char* extra_headers = nullptr,
|
||||||
tls::AbortCheckFn abort_check = nullptr) {
|
tls::AbortCheckFn abort_check = nullptr) {
|
||||||
Response resp = {};
|
RequestOptions options;
|
||||||
resp.status = -1;
|
options.response_buffer_size = response_buffer_size;
|
||||||
|
options.extra_headers = extra_headers;
|
||||||
uint32_t ip = montauk::resolve(host);
|
options.abort_check = abort_check;
|
||||||
if (!ip) return resp;
|
return request("GET", host, path, nullptr, nullptr, 0, &tas, options);
|
||||||
|
|
||||||
char req[1024];
|
|
||||||
int reqLen = build_request(req, sizeof(req), "GET", host, path,
|
|
||||||
nullptr, nullptr, 0, extra_headers);
|
|
||||||
|
|
||||||
char* buf = (char*)montauk::malloc(resp_buf_size);
|
|
||||||
if (!buf) return resp;
|
|
||||||
|
|
||||||
int n = tls::https_fetch(host, ip, 443, req, reqLen, tas,
|
|
||||||
buf, resp_buf_size - 1, abort_check);
|
|
||||||
if (n <= 0) { montauk::mfree(buf); return resp; }
|
|
||||||
buf[n] = 0;
|
|
||||||
|
|
||||||
parse_response(buf, n, &resp);
|
|
||||||
return resp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST request over HTTPS. Returns parsed response. Caller must free_response().
|
inline Response post(const char* host, const char* path, const char* content_type,
|
||||||
inline Response post(const char* host, const char* path,
|
|
||||||
const char* content_type,
|
|
||||||
const char* body_data, int body_len,
|
const char* body_data, int body_len,
|
||||||
const tls::TrustAnchors& tas,
|
const tls::TrustAnchors& tas, int response_buffer_size = 32768,
|
||||||
int resp_buf_size = 32768,
|
|
||||||
const char* extra_headers = nullptr,
|
const char* extra_headers = nullptr,
|
||||||
tls::AbortCheckFn abort_check = nullptr) {
|
tls::AbortCheckFn abort_check = nullptr) {
|
||||||
Response resp = {};
|
RequestOptions options;
|
||||||
resp.status = -1;
|
options.response_buffer_size = response_buffer_size;
|
||||||
|
options.extra_headers = extra_headers;
|
||||||
uint32_t ip = montauk::resolve(host);
|
options.abort_check = abort_check;
|
||||||
if (!ip) return resp;
|
return request("POST", host, path, content_type, body_data, body_len, &tas, options);
|
||||||
|
|
||||||
int req_size = 1024 + body_len;
|
|
||||||
char* req = (char*)montauk::malloc(req_size);
|
|
||||||
if (!req) return resp;
|
|
||||||
|
|
||||||
int reqLen = build_request(req, req_size, "POST", host, path,
|
|
||||||
content_type, body_data, body_len,
|
|
||||||
extra_headers);
|
|
||||||
|
|
||||||
char* buf = (char*)montauk::malloc(resp_buf_size);
|
|
||||||
if (!buf) { montauk::mfree(req); return resp; }
|
|
||||||
|
|
||||||
int n = tls::https_fetch(host, ip, 443, req, reqLen, tas,
|
|
||||||
buf, resp_buf_size - 1, abort_check);
|
|
||||||
montauk::mfree(req);
|
|
||||||
|
|
||||||
if (n <= 0) { montauk::mfree(buf); return resp; }
|
|
||||||
buf[n] = 0;
|
|
||||||
|
|
||||||
parse_response(buf, n, &resp);
|
|
||||||
return resp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic request over HTTPS (PUT, PATCH, DELETE, etc.).
|
inline Response request(const char* method, const char* host, const char* path,
|
||||||
inline Response request(const char* method,
|
const char* content_type, const char* body_data, int body_len,
|
||||||
const char* host, const char* path,
|
const tls::TrustAnchors& tas, int response_buffer_size = 32768,
|
||||||
const char* content_type,
|
|
||||||
const char* body_data, int body_len,
|
|
||||||
const tls::TrustAnchors& tas,
|
|
||||||
int resp_buf_size = 32768,
|
|
||||||
const char* extra_headers = nullptr,
|
const char* extra_headers = nullptr,
|
||||||
tls::AbortCheckFn abort_check = nullptr) {
|
tls::AbortCheckFn abort_check = nullptr) {
|
||||||
Response resp = {};
|
RequestOptions options;
|
||||||
resp.status = -1;
|
options.response_buffer_size = response_buffer_size;
|
||||||
|
options.extra_headers = extra_headers;
|
||||||
uint32_t ip = montauk::resolve(host);
|
options.abort_check = abort_check;
|
||||||
if (!ip) return resp;
|
return request(method, host, path, content_type, body_data, body_len, &tas, options);
|
||||||
|
|
||||||
int req_size = 1024 + (body_len > 0 ? body_len : 0);
|
|
||||||
char* req = (char*)montauk::malloc(req_size);
|
|
||||||
if (!req) return resp;
|
|
||||||
|
|
||||||
int reqLen = build_request(req, req_size, method, host, path,
|
|
||||||
content_type, body_data, body_len,
|
|
||||||
extra_headers);
|
|
||||||
|
|
||||||
char* buf = (char*)montauk::malloc(resp_buf_size);
|
|
||||||
if (!buf) { montauk::mfree(req); return resp; }
|
|
||||||
|
|
||||||
int n = tls::https_fetch(host, ip, 443, req, reqLen, tas,
|
|
||||||
buf, resp_buf_size - 1, abort_check);
|
|
||||||
montauk::mfree(req);
|
|
||||||
|
|
||||||
if (n <= 0) { montauk::mfree(buf); return resp; }
|
|
||||||
buf[n] = 0;
|
|
||||||
|
|
||||||
parse_response(buf, n, &resp);
|
|
||||||
return resp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plain HTTP (no TLS) GET over port 80.
|
|
||||||
inline Response get_plain(const char* host, const char* path,
|
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) {
|
const char* extra_headers = nullptr) {
|
||||||
Response resp = {};
|
RequestOptions options;
|
||||||
resp.status = -1;
|
options.secure = false;
|
||||||
|
options.response_buffer_size = response_buffer_size;
|
||||||
uint32_t ip = montauk::resolve(host);
|
options.extra_headers = extra_headers;
|
||||||
if (!ip) return resp;
|
return request("GET", host, path, nullptr, nullptr, 0, nullptr, options);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace http
|
} // namespace http
|
||||||
|
|||||||
@@ -1272,197 +1272,17 @@ inline uint32_t next_request_id() {
|
|||||||
return 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) {
|
inline bool extract_http_body(const http::Response* resp, uint8_t** out_body, int* out_len) {
|
||||||
if (out_body) *out_body = nullptr;
|
if (out_body) *out_body = nullptr;
|
||||||
if (out_len) *out_len = 0;
|
if (out_len) *out_len = 0;
|
||||||
if (resp == nullptr || resp->body == nullptr || resp->body_len < 0) return false;
|
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));
|
||||||
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);
|
|
||||||
if (!body) return false;
|
if (!body) return false;
|
||||||
|
if (resp->body_len > 0) memcpy(body, resp->body, (size_t)resp->body_len);
|
||||||
while (pos < src_len) {
|
if (out_body) *out_body = body;
|
||||||
int line_start = pos;
|
if (out_len) *out_len = resp->body_len;
|
||||||
while (pos < src_len && src[pos] != '\n') pos++;
|
return true;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool ipp_http_post(const IppUri* uri,
|
inline bool ipp_http_post(const IppUri* uri,
|
||||||
@@ -1487,86 +1307,39 @@ inline bool ipp_http_post(const IppUri* uri,
|
|||||||
else
|
else
|
||||||
snprintf(host_header, sizeof(host_header), "%s:%u", uri->host, (unsigned)uri->port);
|
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);
|
char* raw = (char*)malloc(HTTP_RESPONSE_MAX);
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
free(req);
|
|
||||||
safe_copy(err, err_len, "out of memory");
|
safe_copy(err, err_len, "out of memory");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int raw_len = -1;
|
tls::TrustAnchors tas = {};
|
||||||
if (uri->use_tls) {
|
if (uri->use_tls) {
|
||||||
tls::TrustAnchors tas = tls::load_trust_anchors();
|
tas = tls::load_trust_anchors();
|
||||||
raw_len = tls::https_fetch(uri->host, uri->ip, uri->port, req, req_len,
|
if (tas.count == 0) {
|
||||||
tas, raw, HTTP_RESPONSE_MAX - 1);
|
|
||||||
if (tas.anchors) free(tas.anchors);
|
|
||||||
} else {
|
|
||||||
int fd = montauk::socket(montauk::abi::SOCK_TCP);
|
|
||||||
if (fd < 0) {
|
|
||||||
free(raw);
|
free(raw);
|
||||||
free(req);
|
safe_copy(err, err_len, "no CA certificates loaded");
|
||||||
snprintf(err, (size_t)err_len, "failed to create socket for %s:%u", uri->host, (unsigned)uri->port);
|
|
||||||
return false;
|
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);
|
http::RequestOptions options;
|
||||||
|
options.secure = uri->use_tls;
|
||||||
if (raw_len <= 0) {
|
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);
|
free(raw);
|
||||||
snprintf(err, (size_t)err_len, "printer returned no response from %s (%s):%u",
|
snprintf(err, (size_t)err_len, "%s from %s (%s):%u",
|
||||||
uri->host, ip_text, (unsigned)uri->port);
|
http::error_string(resp.error), uri->host, ip_text,
|
||||||
return false;
|
(unsigned)uri->port);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (out_http_status) *out_http_status = resp.status;
|
if (out_http_status) *out_http_status = resp.status;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ struct TrustAnchors {
|
|||||||
};
|
};
|
||||||
|
|
||||||
TrustAnchors load_trust_anchors();
|
TrustAnchors load_trust_anchors();
|
||||||
|
void free_trust_anchors(TrustAnchors* tas);
|
||||||
void get_bearssl_time(uint32_t* days, uint32_t* seconds);
|
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_send_all(int fd, const unsigned char* data, size_t len);
|
||||||
int tls_recv_some(int fd, unsigned char* buf, size_t maxlen);
|
int tls_recv_some(int fd, unsigned char* buf, size_t maxlen);
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
+105
-38
@@ -19,8 +19,8 @@ extern "C" {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
struct DerAccum { 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; };
|
struct DnAccum { unsigned char* data; size_t len, cap; bool failed; };
|
||||||
|
|
||||||
void der_append(void* ctx, const void* buf, size_t len) {
|
void der_append(void* ctx, const void* buf, size_t len) {
|
||||||
DerAccum* a = (DerAccum*)ctx;
|
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;
|
size_t nc = a->cap * 2;
|
||||||
if (nc < a->len + len) nc = a->len + len + 4096;
|
if (nc < a->len + len) nc = a->len + len + 4096;
|
||||||
unsigned char* nb = (unsigned char*)malloc(nc);
|
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); }
|
if (a->data) { memcpy(nb, a->data, a->len); free(a->data); }
|
||||||
a->data = nb; a->cap = nc;
|
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;
|
size_t nc = a->cap * 2;
|
||||||
if (nc < a->len + len) nc = a->len + len + 256;
|
if (nc < a->len + len) nc = a->len + len + 256;
|
||||||
unsigned char* nb = (unsigned char*)malloc(nc);
|
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); }
|
if (a->data) { memcpy(nb, a->data, a->len); free(a->data); }
|
||||||
a->data = nb; a->cap = nc;
|
a->data = nb; a->cap = nc;
|
||||||
}
|
}
|
||||||
@@ -50,53 +50,73 @@ void dn_append(void* ctx, const void* buf, size_t len) {
|
|||||||
a->len += 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) {
|
if (tas->count >= tas->capacity) {
|
||||||
size_t nc = tas->capacity == 0 ? 64 : tas->capacity * 2;
|
size_t nc = tas->capacity == 0 ? 64 : tas->capacity * 2;
|
||||||
br_x509_trust_anchor* na = (br_x509_trust_anchor*)malloc(nc * sizeof(*na));
|
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); }
|
if (tas->anchors) { memcpy(na, tas->anchors, tas->count * sizeof(*na)); free(tas->anchors); }
|
||||||
tas->anchors = na; tas->capacity = nc;
|
tas->anchors = na; tas->capacity = nc;
|
||||||
}
|
}
|
||||||
tas->anchors[tas->count++] = *ta;
|
tas->anchors[tas->count++] = *ta;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool process_cert_der(tls::TrustAnchors* tas, const unsigned char* der, size_t der_len) {
|
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
|
br_x509_decoder_context* dc =
|
||||||
DnAccum dn = {nullptr, 0, 0};
|
(br_x509_decoder_context*)malloc(sizeof(br_x509_decoder_context));
|
||||||
br_x509_decoder_init(&dc, dn_append, &dn);
|
if (!dc) return false;
|
||||||
br_x509_decoder_push(&dc, der, der_len);
|
DnAccum dn = {nullptr, 0, 0, false};
|
||||||
br_x509_pkey* pk = br_x509_decoder_get_pkey(&dc);
|
br_x509_decoder_init(dc, dn_append, &dn);
|
||||||
if (!pk) { if (dn.data) free(dn.data); return false; }
|
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;
|
br_x509_trust_anchor ta;
|
||||||
memset(&ta, 0, sizeof(ta));
|
memset(&ta, 0, sizeof(ta));
|
||||||
ta.dn.data = dn.data; ta.dn.len = dn.len; ta.flags = 0;
|
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) {
|
switch (pk->key_type) {
|
||||||
case BR_KEYTYPE_RSA:
|
case BR_KEYTYPE_RSA:
|
||||||
ta.pkey.key_type = BR_KEYTYPE_RSA;
|
ta.pkey.key_type = BR_KEYTYPE_RSA;
|
||||||
ta.pkey.key.rsa.nlen = pk->key.rsa.nlen;
|
ta.pkey.key.rsa.nlen = pk->key.rsa.nlen;
|
||||||
ta.pkey.key.rsa.n = (unsigned char*)malloc(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.elen = pk->key.rsa.elen;
|
||||||
ta.pkey.key.rsa.e = (unsigned char*)malloc(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;
|
break;
|
||||||
case BR_KEYTYPE_EC:
|
case BR_KEYTYPE_EC:
|
||||||
ta.pkey.key_type = BR_KEYTYPE_EC;
|
ta.pkey.key_type = BR_KEYTYPE_EC;
|
||||||
ta.pkey.key.ec.curve = pk->key.ec.curve;
|
ta.pkey.key.ec.curve = pk->key.ec.curve;
|
||||||
ta.pkey.key.ec.qlen = pk->key.ec.qlen;
|
ta.pkey.key.ec.qlen = pk->key.ec.qlen;
|
||||||
ta.pkey.key.ec.q = (unsigned char*)malloc(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;
|
break;
|
||||||
default:
|
default:
|
||||||
|
free(dc);
|
||||||
if (dn.data) free(dn.data);
|
if (dn.data) free(dn.data);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ta_add(tas, &ta);
|
free(dc);
|
||||||
return true;
|
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
|
} // anonymous namespace
|
||||||
@@ -116,36 +136,65 @@ TrustAnchors load_trust_anchors() {
|
|||||||
|
|
||||||
unsigned char* pem = (unsigned char*)malloc(fsize + 1);
|
unsigned char* pem = (unsigned char*)malloc(fsize + 1);
|
||||||
if (!pem) { montauk::close(fh); return tas; }
|
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);
|
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_context* pc =
|
||||||
br_pem_decoder_init(&pc);
|
(br_pem_decoder_context*)malloc(sizeof(br_pem_decoder_context));
|
||||||
DerAccum der = {nullptr, 0, 0};
|
if (!pc) { free(pem); return tas; }
|
||||||
|
br_pem_decoder_init(pc);
|
||||||
|
DerAccum der = {nullptr, 0, 0, false};
|
||||||
bool inCert = false;
|
bool inCert = false;
|
||||||
size_t offset = 0;
|
size_t offset = 0;
|
||||||
|
|
||||||
while (offset < fsize) {
|
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;
|
offset += pushed;
|
||||||
int ev = br_pem_decoder_event(&pc);
|
int ev = br_pem_decoder_event(pc);
|
||||||
if (ev == BR_PEM_BEGIN_OBJ) {
|
if (ev == BR_PEM_BEGIN_OBJ) {
|
||||||
inCert = (strcmp(br_pem_decoder_name(&pc), "CERTIFICATE") == 0);
|
inCert = (strcmp(br_pem_decoder_name(pc), "CERTIFICATE") == 0);
|
||||||
br_pem_decoder_setdest(&pc, inCert ? der_append : nullptr, inCert ? &der : nullptr);
|
br_pem_decoder_setdest(pc, inCert ? der_append : nullptr, inCert ? &der : nullptr);
|
||||||
if (inCert) der.len = 0;
|
if (inCert) { der.len = 0; der.failed = false; }
|
||||||
} else if (ev == BR_PEM_END_OBJ) {
|
} else if (ev == BR_PEM_END_OBJ) {
|
||||||
if (inCert && der.len > 0) process_cert_der(&tas, der.data, der.len);
|
if (inCert && der.len > 0) process_cert_der(&tas, der.data, der.len);
|
||||||
inCert = false;
|
inCert = false;
|
||||||
} else if (ev == BR_PEM_ERROR) {
|
} else if (ev == BR_PEM_ERROR) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (der.failed) break;
|
||||||
|
if (pushed == 0 && ev == 0) break;
|
||||||
}
|
}
|
||||||
if (der.data) free(der.data);
|
if (der.data) free(der.data);
|
||||||
|
free(pc);
|
||||||
free(pem);
|
free(pem);
|
||||||
return tas;
|
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) {
|
void get_bearssl_time(uint32_t* days, uint32_t* seconds) {
|
||||||
montauk::abi::DateTime dt;
|
montauk::abi::DateTime dt;
|
||||||
montauk::gettime(&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));
|
int r = montauk::send(fd, data + sent, (uint32_t)(len - sent));
|
||||||
if (r > 0) { sent += r; deadline = montauk::get_milliseconds() + 15000; }
|
if (r > 0) { sent += r; deadline = montauk::get_milliseconds() + 15000; }
|
||||||
else if (r < 0) return -1;
|
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;
|
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);
|
int r = montauk::recv(fd, buf, (uint32_t)maxlen);
|
||||||
if (r > 0) return r;
|
if (r > 0) return r;
|
||||||
if (r < 0) return -1;
|
if (r < 0) return -1;
|
||||||
if (montauk::get_milliseconds() >= deadline) return -1;
|
uint64_t now = montauk::get_milliseconds();
|
||||||
montauk::sleep_ms(1);
|
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,
|
const char* request, int reqLen,
|
||||||
char* respBuf, int respMax,
|
char* respBuf, int respMax,
|
||||||
AbortCheckFn abort_check) {
|
AbortCheckFn abort_check) {
|
||||||
bool requestSent = false;
|
int requestOffset = 0;
|
||||||
int respLen = 0;
|
int respLen = 0;
|
||||||
uint64_t deadline = montauk::get_milliseconds() + 30000;
|
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);
|
br_ssl_engine_recvapp_ack(eng, len);
|
||||||
deadline = montauk::get_milliseconds() + 30000; continue;
|
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 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;
|
if (toWrite > len) toWrite = len;
|
||||||
memcpy(buf, request, toWrite);
|
memcpy(buf, request + requestOffset, toWrite);
|
||||||
br_ssl_engine_sendapp_ack(eng, toWrite);
|
br_ssl_engine_sendapp_ack(eng, toWrite);
|
||||||
br_ssl_engine_flush(eng, 0);
|
requestOffset += (int)toWrite;
|
||||||
requestSent = true;
|
if (requestOffset == reqLen) br_ssl_engine_flush(eng, 0);
|
||||||
deadline = montauk::get_milliseconds() + 30000; continue;
|
deadline = montauk::get_milliseconds() + 30000; continue;
|
||||||
}
|
}
|
||||||
if (state & BR_SSL_RECVREC) {
|
if (state & BR_SSL_RECVREC) {
|
||||||
@@ -244,6 +307,8 @@ int https_fetch(const char* host, uint32_t ip, uint16_t port,
|
|||||||
const TrustAnchors& tas,
|
const TrustAnchors& tas,
|
||||||
char* respBuf, int respMax,
|
char* respBuf, int respMax,
|
||||||
AbortCheckFn abort_check) {
|
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);
|
int fd = montauk::socket(montauk::abi::SOCK_TCP);
|
||||||
if (fd < 0) return -1;
|
if (fd < 0) return -1;
|
||||||
if (montauk::connect(fd, ip, port) < 0) { montauk::closesocket(fd); 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);
|
br_x509_minimal_set_time(xc, days, secs);
|
||||||
|
|
||||||
unsigned char seed[32];
|
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_set_buffer(&cc->eng, iobuf, BR_SSL_BUFSIZE_BIDI, 1);
|
||||||
br_ssl_engine_inject_entropy(&cc->eng, seed, sizeof(seed));
|
br_ssl_engine_inject_entropy(&cc->eng, seed, sizeof(seed));
|
||||||
|
|
||||||
|
|||||||
+29
-179
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
#include <tls/tls.hpp>
|
#include <http/http.hpp>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -56,11 +56,6 @@ static bool parse_uint16(const char* s, uint16_t* out) {
|
|||||||
return true;
|
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 ----
|
// ---- URL parser ----
|
||||||
|
|
||||||
struct ParsedUrl {
|
struct ParsedUrl {
|
||||||
@@ -124,38 +119,6 @@ static ParsedUrl parse_url(const char* url) {
|
|||||||
return u;
|
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 ----
|
// ---- Keyboard abort check for TLS ----
|
||||||
|
|
||||||
static bool check_keyboard_abort() {
|
static bool check_keyboard_abort() {
|
||||||
@@ -167,95 +130,23 @@ static bool check_keyboard_abort() {
|
|||||||
return false;
|
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 ----
|
// ---- Print response body ----
|
||||||
|
|
||||||
static void print_response(const char* respBuf, int respLen, bool verbose) {
|
static void print_response(const http::Response& response, 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;
|
|
||||||
|
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
char msg[256];
|
char msg[128];
|
||||||
snprintf(msg, sizeof(msg), "HTTP %d %s (%d bytes)\n\n", statusCode, statusText, bodyLen);
|
snprintf(msg, sizeof(msg), "HTTP %d (%d bytes)\n\n",
|
||||||
|
response.status, response.body_len);
|
||||||
montauk::print(msg);
|
montauk::print(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bodyLen > 0) {
|
if (response.body_len > 0) {
|
||||||
const char* body = respBuf + headerEnd;
|
|
||||||
char chunk[512];
|
char chunk[512];
|
||||||
int printed = 0;
|
int printed = 0;
|
||||||
while (printed < bodyLen) {
|
while (printed < response.body_len) {
|
||||||
int n = bodyLen - printed;
|
int n = response.body_len - printed;
|
||||||
if (n > 511) n = 511;
|
if (n > 511) n = 511;
|
||||||
memcpy(chunk, body + printed, n);
|
memcpy(chunk, response.body + printed, n);
|
||||||
chunk[n] = '\0';
|
chunk[n] = '\0';
|
||||||
montauk::print(chunk);
|
montauk::print(chunk);
|
||||||
printed += n;
|
printed += n;
|
||||||
@@ -339,20 +230,8 @@ extern "C" void _start() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve host to IP
|
uint32_t numericIp = 0;
|
||||||
uint32_t serverIp;
|
parse_ip(hostStr, &numericIp);
|
||||||
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);
|
|
||||||
|
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
char msg[256];
|
char msg[256];
|
||||||
@@ -361,16 +240,6 @@ extern "C" void _start() {
|
|||||||
montauk::print(msg);
|
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) {
|
if (verbose) {
|
||||||
char msg[128];
|
char msg[128];
|
||||||
snprintf(msg, sizeof(msg), "GET %s\n", path);
|
snprintf(msg, sizeof(msg), "GET %s\n", path);
|
||||||
@@ -385,11 +254,9 @@ extern "C" void _start() {
|
|||||||
montauk::exit(1);
|
montauk::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
int respLen;
|
tls::TrustAnchors tas = {};
|
||||||
|
|
||||||
if (useHttps) {
|
if (useHttps) {
|
||||||
// ---- TLS handshake and exchange ----
|
tas = tls::load_trust_anchors();
|
||||||
tls::TrustAnchors tas = tls::load_trust_anchors();
|
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
char msg[64];
|
char msg[64];
|
||||||
snprintf(msg, sizeof(msg), "Loaded %u trust anchors\n", (unsigned)tas.count);
|
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");
|
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) {
|
http::RequestOptions options;
|
||||||
montauk::print("Error: no response received\n");
|
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);
|
montauk::exit(1);
|
||||||
}
|
}
|
||||||
|
if (verbose && useHttps) montauk::print("TLS connection established\n");
|
||||||
respBuf[respLen] = '\0';
|
print_response(response, verbose);
|
||||||
print_response(respBuf, respLen, verbose);
|
|
||||||
|
|
||||||
montauk::exit(0);
|
montauk::exit(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
#include <gui/svg.hpp>
|
#include <gui/svg.hpp>
|
||||||
#include <gui/truetype.hpp>
|
#include <gui/truetype.hpp>
|
||||||
#include <gui/mtk.hpp>
|
#include <gui/mtk.hpp>
|
||||||
#include <tls/tls.hpp>
|
#include <http/http.hpp>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -77,26 +77,6 @@ static bool g_tls_ready = false;
|
|||||||
static uint32_t g_server_ip = 0;
|
static uint32_t g_server_ip = 0;
|
||||||
static tls::TrustAnchors g_tas = {nullptr, 0, 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
|
// JSON parsing
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -237,39 +217,27 @@ static void do_fetch() {
|
|||||||
g_tls_ready = true;
|
g_tls_ready = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static char request[512];
|
http::RequestOptions options;
|
||||||
int reqLen = snprintf(request, sizeof(request),
|
options.resolved_ip = g_server_ip;
|
||||||
"GET /?format=j1 HTTP/1.0\r\n"
|
options.extra_headers =
|
||||||
"Host: %s\r\n"
|
|
||||||
"User-Agent: MontaukOS/1.0 weather\r\n"
|
"User-Agent: MontaukOS/1.0 weather\r\n"
|
||||||
"Accept: application/json\r\n"
|
"Accept: application/json\r\n";
|
||||||
"Connection: close\r\n"
|
http::Response response = http::request_into(
|
||||||
"\r\n",
|
"GET", WTTR_HOST, "/?format=j1", nullptr, nullptr, 0, &g_tas,
|
||||||
WTTR_HOST);
|
g_resp_buf, RESP_MAX, options);
|
||||||
|
if (response.error != http::Error::NONE) {
|
||||||
int respLen = tls::https_fetch(WTTR_HOST, g_server_ip, 443,
|
snprintf(g_status, sizeof(g_status), "Error: %s",
|
||||||
request, reqLen, g_tas,
|
http::error_string(response.error));
|
||||||
g_resp_buf, RESP_MAX);
|
|
||||||
if (respLen <= 0) {
|
|
||||||
snprintf(g_status, sizeof(g_status), "Error: no response from server");
|
|
||||||
g_phase = AppPhase::ERR; return;
|
g_phase = AppPhase::ERR; return;
|
||||||
}
|
}
|
||||||
g_resp_buf[respLen] = '\0';
|
if (response.status != 200) {
|
||||||
|
snprintf(g_status, sizeof(g_status), "Error: HTTP %d from server",
|
||||||
int headerEnd = find_header_end(g_resp_buf, respLen);
|
response.status);
|
||||||
if (headerEnd < 0) {
|
|
||||||
snprintf(g_status, sizeof(g_status), "Error: malformed HTTP response");
|
|
||||||
g_phase = AppPhase::ERR; return;
|
g_phase = AppPhase::ERR; return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int status = parse_status_code(g_resp_buf, headerEnd);
|
const char* body = response.body;
|
||||||
if (status != 200) {
|
int bodyLen = response.body_len;
|
||||||
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;
|
|
||||||
|
|
||||||
// Extract core weather fields
|
// Extract core weather fields
|
||||||
static char temp_raw[16], feels_raw[16], code_raw[8];
|
static char temp_raw[16], feels_raw[16], code_raw[8];
|
||||||
|
|||||||
+28
-85
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
#include <tls/tls.hpp>
|
#include <http/http.hpp>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -82,39 +82,15 @@ static bool check_keyboard_abort() {
|
|||||||
|
|
||||||
// ---- HTTPS fetch wrapper ----
|
// ---- HTTPS fetch wrapper ----
|
||||||
|
|
||||||
static int wiki_fetch(const char* path, char* respBuf, int respMax) {
|
static http::Response wiki_fetch(const char* path, char* respBuf, int respMax) {
|
||||||
static char request[2560]; // keep off stack
|
http::RequestOptions options;
|
||||||
int reqLen = snprintf(request, sizeof(request),
|
options.resolved_ip = g_serverIp;
|
||||||
"GET %s HTTP/1.0\r\n"
|
options.extra_headers =
|
||||||
"Host: %s\r\n"
|
|
||||||
"User-Agent: MontaukOS/1.0 wiki\r\n"
|
"User-Agent: MontaukOS/1.0 wiki\r\n"
|
||||||
"Accept: application/json\r\n"
|
"Accept: application/json\r\n";
|
||||||
"Connection: close\r\n"
|
options.abort_check = check_keyboard_abort;
|
||||||
"\r\n",
|
return http::request_into("GET", WIKI_HOST, path, nullptr, nullptr, 0,
|
||||||
path, WIKI_HOST);
|
&g_tas, respBuf, respMax, options);
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- URL encoding ----
|
// ---- URL encoding ----
|
||||||
@@ -725,23 +701,13 @@ extern "C" void _start() {
|
|||||||
"/w/api.php?action=query&format=json&formatversion=2"
|
"/w/api.php?action=query&format=json&formatversion=2"
|
||||||
"&prop=extracts&explaintext=1&titles=%s", encoded);
|
"&prop=extracts&explaintext=1&titles=%s", encoded);
|
||||||
|
|
||||||
int respLen = wiki_fetch(path, respBuf, RESP_MAX);
|
http::Response response = wiki_fetch(path, respBuf, RESP_MAX);
|
||||||
if (respLen <= 0) {
|
if (response.error != http::Error::NONE) {
|
||||||
montauk::print("\x01"); // error sentinel
|
montauk::print("\x01"); // error sentinel
|
||||||
montauk::sleep_ms(100);
|
montauk::sleep_ms(100);
|
||||||
montauk::exit(1);
|
montauk::exit(1);
|
||||||
}
|
}
|
||||||
respBuf[respLen] = '\0';
|
if (response.status == 404) {
|
||||||
|
|
||||||
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) {
|
|
||||||
montauk::print("\x01");
|
montauk::print("\x01");
|
||||||
montauk::sleep_ms(100);
|
montauk::sleep_ms(100);
|
||||||
montauk::exit(1);
|
montauk::exit(1);
|
||||||
@@ -749,8 +715,8 @@ extern "C" void _start() {
|
|||||||
|
|
||||||
// Output raw JSON body in chunks to avoid overflowing
|
// Output raw JSON body in chunks to avoid overflowing
|
||||||
// the 4KB kernel ring buffer (parent polls at ~60fps)
|
// the 4KB kernel ring buffer (parent polls at ~60fps)
|
||||||
const char* body = respBuf + headerEnd;
|
const char* body = response.body;
|
||||||
int bodyLen = respLen - headerEnd;
|
int bodyLen = response.body_len;
|
||||||
static char chunk[2049];
|
static char chunk[2049];
|
||||||
int sent = 0;
|
int sent = 0;
|
||||||
while (sent < bodyLen) {
|
while (sent < bodyLen) {
|
||||||
@@ -774,21 +740,13 @@ extern "C" void _start() {
|
|||||||
"/w/api.php?action=opensearch&search=%s&limit=10&format=json",
|
"/w/api.php?action=opensearch&search=%s&limit=10&format=json",
|
||||||
encoded);
|
encoded);
|
||||||
|
|
||||||
int respLen = wiki_fetch(path, respBuf, RESP_MAX);
|
http::Response response = wiki_fetch(path, respBuf, RESP_MAX);
|
||||||
if (respLen <= 0) {
|
if (response.error != http::Error::NONE) {
|
||||||
montauk::print("\033[1;31mError:\033[0m no response from Wikipedia\n");
|
montauk::print("\033[1;31mError:\033[0m no response from Wikipedia\n");
|
||||||
montauk::exit(1);
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* body = respBuf + headerEnd;
|
|
||||||
int bodyLen = respLen - headerEnd;
|
|
||||||
|
|
||||||
static char titles[MAX_SEARCH_RESULTS][256];
|
static char titles[MAX_SEARCH_RESULTS][256];
|
||||||
int titleCount = parse_search_titles(body, bodyLen, titles, MAX_SEARCH_RESULTS);
|
int titleCount = parse_search_titles(body, bodyLen, titles, MAX_SEARCH_RESULTS);
|
||||||
@@ -831,8 +789,8 @@ extern "C" void _start() {
|
|||||||
snprintf(articlePath, sizeof(articlePath),
|
snprintf(articlePath, sizeof(articlePath),
|
||||||
"/api/rest_v1/page/summary/%s", articleEncoded);
|
"/api/rest_v1/page/summary/%s", articleEncoded);
|
||||||
|
|
||||||
respLen = wiki_fetch(articlePath, respBuf, RESP_MAX);
|
response = wiki_fetch(articlePath, respBuf, RESP_MAX);
|
||||||
if (respLen <= 0) {
|
if (response.error != http::Error::NONE) {
|
||||||
sb_reset();
|
sb_reset();
|
||||||
sb_cursor_to(infoRow, 3);
|
sb_cursor_to(infoRow, 3);
|
||||||
sb_puts("\033[2K\033[1;31mFetch failed. Press any key.\033[0m");
|
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);
|
montauk::abi::KeyEvent ev; montauk::getkey(&ev);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
respBuf[respLen] = '\0';
|
body = response.body;
|
||||||
|
bodyLen = response.body_len;
|
||||||
|
|
||||||
headerEnd = find_header_end(respBuf, respLen);
|
if (response.status == 404) {
|
||||||
if (headerEnd < 0) continue;
|
|
||||||
|
|
||||||
int statusCode = parse_status_code(respBuf, headerEnd);
|
|
||||||
body = respBuf + headerEnd;
|
|
||||||
bodyLen = respLen - headerEnd;
|
|
||||||
|
|
||||||
if (statusCode == 404) {
|
|
||||||
sb_reset();
|
sb_reset();
|
||||||
sb_cursor_to(infoRow, 3);
|
sb_cursor_to(infoRow, 3);
|
||||||
sb_puts("\033[2K\033[1;31mArticle not found. Press any key.\033[0m");
|
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);
|
"&prop=extracts&explaintext=1&titles=%s", encoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
int respLen = wiki_fetch(path, respBuf, RESP_MAX);
|
http::Response response = wiki_fetch(path, respBuf, RESP_MAX);
|
||||||
if (respLen <= 0) {
|
if (response.error != http::Error::NONE) {
|
||||||
montauk::print("\033[1;31mError:\033[0m no response from Wikipedia\n");
|
montauk::print("\033[1;31mError:\033[0m no response from Wikipedia\n");
|
||||||
montauk::exit(1);
|
montauk::exit(1);
|
||||||
}
|
}
|
||||||
respBuf[respLen] = '\0';
|
const char* body = response.body;
|
||||||
|
int bodyLen = response.body_len;
|
||||||
|
|
||||||
int headerEnd = find_header_end(respBuf, respLen);
|
if (response.status == 404) {
|
||||||
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) {
|
|
||||||
montauk::print("\033[1;31mArticle not found:\033[0m ");
|
montauk::print("\033[1;31mArticle not found:\033[0m ");
|
||||||
montauk::print(query);
|
montauk::print(query);
|
||||||
montauk::putchar('\n');
|
montauk::putchar('\n');
|
||||||
|
|||||||
@@ -6,19 +6,14 @@
|
|||||||
|
|
||||||
#include "wikipedia.h"
|
#include "wikipedia.h"
|
||||||
|
|
||||||
int wiki_fetch(const char* path, char* respBuf, int respMax) {
|
http::Response wiki_fetch(const char* path, char* respBuf, int respMax) {
|
||||||
static char request[2560];
|
http::RequestOptions options;
|
||||||
int reqLen = snprintf(request, sizeof(request),
|
options.resolved_ip = g_server_ip;
|
||||||
"GET %s HTTP/1.0\r\n"
|
options.extra_headers =
|
||||||
"Host: %s\r\n"
|
|
||||||
"User-Agent: MontaukOS/1.0 wikipedia\r\n"
|
"User-Agent: MontaukOS/1.0 wikipedia\r\n"
|
||||||
"Accept: application/json\r\n"
|
"Accept: application/json\r\n";
|
||||||
"Connection: close\r\n"
|
return http::request_into("GET", WIKI_HOST, path, nullptr, nullptr, 0,
|
||||||
"\r\n",
|
&g_tas, respBuf, respMax, options);
|
||||||
path, WIKI_HOST);
|
|
||||||
return tls::https_fetch(WIKI_HOST, g_server_ip, 443,
|
|
||||||
request, reqLen, g_tas,
|
|
||||||
respBuf, respMax);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ensure_wiki_tls_ready(char* err, int err_len) {
|
bool ensure_wiki_tls_ready(char* err, int err_len) {
|
||||||
@@ -74,27 +69,16 @@ void do_welcome_fetch() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int respLen = wiki_fetch(path, g_resp_buf, RESP_MAX);
|
http::Response response = wiki_fetch(path, g_resp_buf, RESP_MAX);
|
||||||
if (respLen <= 0) {
|
if (response.error != http::Error::NONE) {
|
||||||
snprintf(g_welcome_status, sizeof(g_welcome_status),
|
snprintf(g_welcome_status, sizeof(g_welcome_status),
|
||||||
"Daily featured article unavailable; search is ready.");
|
"Daily featured article unavailable; search is ready.");
|
||||||
build_welcome_lines(true);
|
build_welcome_lines(true);
|
||||||
return;
|
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 (response.status < 200 || response.status >= 300) {
|
||||||
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) {
|
|
||||||
snprintf(g_welcome_status, sizeof(g_welcome_status),
|
snprintf(g_welcome_status, sizeof(g_welcome_status),
|
||||||
"Daily featured article unavailable; search is ready.");
|
"Daily featured article unavailable; search is ready.");
|
||||||
build_welcome_lines(true);
|
build_welcome_lines(true);
|
||||||
@@ -148,24 +132,16 @@ void do_search(const char* query) {
|
|||||||
"/w/api.php?action=query&format=json&formatversion=2"
|
"/w/api.php?action=query&format=json&formatversion=2"
|
||||||
"&prop=extracts&explaintext=1&titles=%s", encoded);
|
"&prop=extracts&explaintext=1&titles=%s", encoded);
|
||||||
|
|
||||||
int respLen = wiki_fetch(path, g_resp_buf, RESP_MAX);
|
http::Response response = wiki_fetch(path, g_resp_buf, RESP_MAX);
|
||||||
if (respLen <= 0) {
|
if (response.error != http::Error::NONE) {
|
||||||
snprintf(g_status, sizeof(g_status), "Error: no response from Wikipedia");
|
snprintf(g_status, sizeof(g_status), "Error: %s",
|
||||||
|
http::error_string(response.error));
|
||||||
g_phase = AppPhase::ERR; return;
|
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 (response.status == 404) {
|
||||||
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) {
|
|
||||||
snprintf(g_status, sizeof(g_status), "Article not found: %s", query);
|
snprintf(g_status, sizeof(g_status), "Article not found: %s", query);
|
||||||
g_phase = AppPhase::ERR; return;
|
g_phase = AppPhase::ERR; return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,26 +140,6 @@ unsigned decode_utf8_codepoint(const char* buf, int len, int* consumed) {
|
|||||||
return '?';
|
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 find_substr(const char* buf, int len, const char* needle) {
|
||||||
int nlen = (int)strlen(needle);
|
int nlen = (int)strlen(needle);
|
||||||
if (!buf || !needle || nlen <= 0 || nlen > len) return -1;
|
if (!buf || !needle || nlen <= 0 || nlen > len) return -1;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
#include <gui/standalone.hpp>
|
#include <gui/standalone.hpp>
|
||||||
#include <gui/svg.hpp>
|
#include <gui/svg.hpp>
|
||||||
#include <gui/truetype.hpp>
|
#include <gui/truetype.hpp>
|
||||||
#include <tls/tls.hpp>
|
#include <http/http.hpp>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -190,8 +190,6 @@ void underscores_to_spaces(char* text);
|
|||||||
bool is_main_page_query(const char* query);
|
bool is_main_page_query(const char* query);
|
||||||
unsigned decode_utf8_codepoint(const char* buf, int len, int* consumed);
|
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 find_substr(const char* buf, int len, const char* needle);
|
||||||
|
|
||||||
int url_encode_title(const char* in, char* out, int maxLen);
|
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
|
// 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 ensure_wiki_tls_ready(char* err, int err_len);
|
||||||
bool welcome_feed_path(char* path, int path_len);
|
bool welcome_feed_path(char* path, int path_len);
|
||||||
void do_welcome_fetch();
|
void do_welcome_fetch();
|
||||||
|
|||||||
@@ -643,11 +643,16 @@ The allocator uses size-class buckets (32 to 4096 bytes) with an overflow list f
|
|||||||
|
|
||||||
## Networking and HTTPS
|
## 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`)
|
### 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
|
#### 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)
|
// Load CA certificates once at startup (required for HTTPS)
|
||||||
tls::TrustAnchors tas = tls::load_trust_anchors();
|
tls::TrustAnchors tas = tls::load_trust_anchors();
|
||||||
|
// Call tls::free_trust_anchors(&tas) during application shutdown.
|
||||||
```
|
```
|
||||||
|
|
||||||
#### GET
|
#### GET
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
auto resp = http::get("api.example.com", "/v1/data", tas);
|
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 is a pointer to the response body
|
||||||
// resp.body_len is its length
|
// 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.
|
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
|
#### Response Struct Reference
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
@@ -754,6 +782,8 @@ struct http::Response {
|
|||||||
int body_len;
|
int body_len;
|
||||||
char* raw; // Owned buffer — freed by free_response()
|
char* raw; // Owned buffer — freed by free_response()
|
||||||
int raw_len;
|
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,
|
bool http::get_header(const http::Response* resp, const char* name,
|
||||||
char* out_val, int max_len);
|
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);
|
void http::free_response(http::Response* resp);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -376,7 +376,10 @@ The optional `AbortCheckFn` callback (e.g., `bool check_quit()`) lets terminal/G
|
|||||||
|
|
||||||
### HTTP Wrapper (`http/http.hpp`)
|
### 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
|
```cpp
|
||||||
#include <http/http.hpp>
|
#include <http/http.hpp>
|
||||||
@@ -384,7 +387,9 @@ The MontaukAI dev environment includes a higher-level HTTP wrapper built on top
|
|||||||
tls::TrustAnchors tas = tls::load_trust_anchors();
|
tls::TrustAnchors tas = tls::load_trust_anchors();
|
||||||
|
|
||||||
auto resp = http::get("api.example.com", "/v1/data", tas);
|
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);
|
http::free_response(&resp);
|
||||||
|
|
||||||
auto resp2 = http::post("api.example.com", "/v1/submit",
|
auto resp2 = http::post("api.example.com", "/v1/submit",
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
/*
|
/*
|
||||||
* http.hpp
|
* http.hpp
|
||||||
* Simple HTTP request builder and response parser for MontaukOS
|
* Shared HTTP/1.1 client for MontaukOS.
|
||||||
* Wraps tls::https_fetch() and raw sockets for ergonomic HTTP usage.
|
*
|
||||||
|
* 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
|
#pragma once
|
||||||
@@ -13,67 +16,113 @@
|
|||||||
|
|
||||||
namespace http {
|
namespace http {
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
enum class Error {
|
||||||
// Response
|
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 {
|
struct Response {
|
||||||
int status; // HTTP status code (200, 404, etc.) or -1 on error
|
int status; // HTTP status code, or -1 before a response is parsed
|
||||||
const char* headers; // Pointer into raw buffer (header block)
|
const char* headers; // Pointers into raw
|
||||||
int headers_len;
|
int headers_len;
|
||||||
const char* body; // Pointer into raw buffer (body)
|
const char* body;
|
||||||
int body_len;
|
int body_len;
|
||||||
char* raw; // Owned buffer — caller must free with montauk::mfree()
|
char* raw;
|
||||||
int raw_len;
|
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) {
|
inline const char* find_header_block_end(const char* start, const char* end) {
|
||||||
if (!start || !end || start >= end) return nullptr;
|
if (!start || !end || start >= end) return nullptr;
|
||||||
|
for (const char* p = start; p + 3 < end; ++p) {
|
||||||
for (const char* s = start; s < end - 3; s++) {
|
if (p[0] == '\r' && p[1] == '\n' && p[2] == '\r' && p[3] == '\n')
|
||||||
if (s[0] == '\r' && s[1] == '\n' && s[2] == '\r' && s[3] == '\n')
|
return p + 4;
|
||||||
return s + 4;
|
|
||||||
}
|
}
|
||||||
for (const char* s = start; s < end - 1; s++) {
|
for (const char* p = start; p + 1 < end; ++p) {
|
||||||
if (s[0] == '\n' && s[1] == '\n')
|
if (p[0] == '\n' && p[1] == '\n') return p + 2;
|
||||||
return s + 2;
|
|
||||||
}
|
}
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline int parse_status_code(const char* start, const char* end) {
|
inline int parse_status_code(const char* start, const char* end) {
|
||||||
if (!start || !end || end - start < 12) return -1;
|
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;
|
const char* p = start;
|
||||||
while (p < end && *p && *p != ' ') p++;
|
while (p < end && *p != ' ' && *p != '\r' && *p != '\n') ++p;
|
||||||
if (p >= end || *p != ' ') return -1;
|
if (p >= end || *p++ != ' ') return -1;
|
||||||
p++;
|
if (end - p < 3 || p[0] < '0' || p[0] > '9' ||
|
||||||
|
p[1] < '0' || p[1] > '9' || p[2] < '0' || p[2] > '9') return -1;
|
||||||
int code = 0;
|
return (p[0] - '0') * 100 + (p[1] - '0') * 10 + (p[2] - '0');
|
||||||
int digits = 0;
|
|
||||||
while (digits < 3 && p < end && *p >= '0' && *p <= '9') {
|
|
||||||
code = code * 10 + (*p - '0');
|
|
||||||
p++;
|
|
||||||
digits++;
|
|
||||||
}
|
|
||||||
return digits == 3 ? code : -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline const char* find_final_response_start(const char* buf, int len) {
|
inline const char* find_final_response_start(const char* buf, int len) {
|
||||||
if (!buf || len <= 0) return nullptr;
|
if (!buf || len <= 0) return nullptr;
|
||||||
|
|
||||||
const char* start = buf;
|
const char* start = buf;
|
||||||
const char* end = buf + len;
|
const char* end = buf + len;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
int code = parse_status_code(start, end);
|
int code = parse_status_code(start, end);
|
||||||
if (code < 0) return nullptr;
|
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);
|
const char* next = find_header_block_end(start, end);
|
||||||
if (!next || next >= end) return nullptr;
|
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;
|
start = next;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,295 +132,505 @@ inline const char* skip_informational_responses(const char* buf, int len) {
|
|||||||
return start ? start : buf;
|
return start ? start : buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse raw HTTP response in-place. Sets pointers into buf (does not copy).
|
inline bool ascii_equal_ci(char a, char b) {
|
||||||
// Skips leading informational 1xx responses such as "100 Continue".
|
if (a >= 'A' && a <= 'Z') a += 'a' - 'A';
|
||||||
// Returns status code, or -1 if unparseable.
|
if (b >= 'A' && b <= 'Z') b += 'a' - 'A';
|
||||||
inline int parse_response(char* buf, int len, Response* out) {
|
return a == b;
|
||||||
out->raw = buf;
|
|
||||||
out->raw_len = len;
|
|
||||||
out->status = -1;
|
|
||||||
out->headers = nullptr;
|
|
||||||
out->headers_len = 0;
|
|
||||||
out->body = nullptr;
|
|
||||||
out->body_len = 0;
|
|
||||||
|
|
||||||
const char* start = find_final_response_start(buf, len);
|
|
||||||
const char* end = buf + len;
|
|
||||||
if (!start || end - start < 12) return -1; // "HTTP/1.x NNN"
|
|
||||||
|
|
||||||
// Parse status code from "HTTP/1.x NNN"
|
|
||||||
int code = parse_status_code(start, end);
|
|
||||||
if (code < 0) return -1;
|
|
||||||
out->status = code;
|
|
||||||
|
|
||||||
// Headers start after the status line
|
|
||||||
const char* hdr_start = start;
|
|
||||||
while (hdr_start < end - 1) {
|
|
||||||
if (*hdr_start == '\r' && *(hdr_start + 1) == '\n') { hdr_start += 2; break; }
|
|
||||||
if (*hdr_start == '\n') { hdr_start++; break; }
|
|
||||||
hdr_start++;
|
|
||||||
}
|
|
||||||
out->headers = hdr_start;
|
|
||||||
|
|
||||||
// Find \r\n\r\n boundary between headers and body
|
|
||||||
const char* body = find_header_block_end(hdr_start, end);
|
|
||||||
if (body) {
|
|
||||||
if (body >= hdr_start + 4 &&
|
|
||||||
body[-4] == '\r' && body[-3] == '\n' && body[-2] == '\r' && body[-1] == '\n')
|
|
||||||
out->headers_len = (int)((body - 4) - hdr_start);
|
|
||||||
else if (body >= hdr_start + 2 &&
|
|
||||||
body[-2] == '\n' && body[-1] == '\n')
|
|
||||||
out->headers_len = (int)((body - 2) - hdr_start);
|
|
||||||
else
|
|
||||||
out->headers_len = (int)(body - hdr_start);
|
|
||||||
out->body = body;
|
|
||||||
out->body_len = len - (int)(out->body - buf);
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No body separator found — entire remainder is headers
|
|
||||||
out->headers_len = len - (int)(hdr_start - buf);
|
|
||||||
return code;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find a header value by name (case-insensitive match on the name).
|
inline bool get_header(const Response* resp, const char* name,
|
||||||
// Writes value into out_val (up to max_len), returns true if found.
|
char* out_val, int max_len) {
|
||||||
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 ||
|
||||||
if (!resp->headers || resp->headers_len == 0) return false;
|
!out_val || max_len <= 0) return false;
|
||||||
|
|
||||||
int name_len = montauk::slen(name);
|
int name_len = montauk::slen(name);
|
||||||
const char* p = resp->headers;
|
const char* p = resp->headers;
|
||||||
const char* end = resp->headers + resp->headers_len;
|
const char* end = p + resp->headers_len;
|
||||||
|
|
||||||
while (p < end) {
|
while (p < end) {
|
||||||
// Case-insensitive prefix match
|
const char* line_end = p;
|
||||||
bool match = true;
|
while (line_end < end && *line_end != '\r' && *line_end != '\n') ++line_end;
|
||||||
if (p + name_len >= end) { match = false; }
|
bool match = line_end - p > name_len && p[name_len] == ':';
|
||||||
else {
|
for (int i = 0; match && i < name_len; ++i)
|
||||||
for (int i = 0; i < name_len; i++) {
|
if (!ascii_equal_ci(p[i], name[i])) match = false;
|
||||||
char a = p[i], b = name[i];
|
|
||||||
if (a >= 'A' && a <= 'Z') a += 32;
|
|
||||||
if (b >= 'A' && b <= 'Z') b += 32;
|
|
||||||
if (a != b) { match = false; break; }
|
|
||||||
}
|
|
||||||
if (match && p[name_len] != ':') match = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const char* v = p + name_len + 1;
|
const char* value = p + name_len + 1;
|
||||||
while (v < end && *v == ' ') v++; // skip OWS
|
while (value < line_end && (*value == ' ' || *value == '\t')) ++value;
|
||||||
int i = 0;
|
while (line_end > value &&
|
||||||
while (v < end && *v != '\r' && *v != '\n' && i < max_len - 1)
|
(line_end[-1] == ' ' || line_end[-1] == '\t')) --line_end;
|
||||||
out_val[i++] = *v++;
|
int n = (int)(line_end - value);
|
||||||
out_val[i] = 0;
|
if (n >= max_len) n = max_len - 1;
|
||||||
|
if (n > 0) montauk::memcpy(out_val, value, n);
|
||||||
|
out_val[n] = '\0';
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip to next line
|
p = line_end;
|
||||||
while (p < end && *p != '\n') p++;
|
while (p < end && (*p == '\r' || *p == '\n')) ++p;
|
||||||
if (p < end) p++;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Free a response's raw buffer.
|
inline bool header_has_token(const Response* resp, const char* name,
|
||||||
inline void free_response(Response* resp) {
|
const char* token) {
|
||||||
if (resp->raw) { montauk::mfree(resp->raw); resp->raw = nullptr; }
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
inline int parse_decimal(const char* value) {
|
||||||
// Request builder (internal)
|
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,
|
inline int build_request(char* buf, int buf_size,
|
||||||
const char* method, const char* host,
|
const char* method, const char* host,
|
||||||
const char* path, const char* content_type,
|
const char* path, const char* content_type,
|
||||||
const char* body_data, int body_len,
|
const char* body_data, int body_len,
|
||||||
const char* extra_headers) {
|
const char* extra_headers) {
|
||||||
char* p = buf;
|
if (!buf || buf_size <= 0 || !method || !*method || !host || !*host ||
|
||||||
char* end = buf + buf_size - 1;
|
!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) {
|
RequestWriter w = {buf, buf + buf_size, false};
|
||||||
while (*s && p < end) *p++ = *s++;
|
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");
|
||||||
auto append_int = [&](int n) {
|
if (body_data || body_len > 0) {
|
||||||
char tmp[16]; int ti = 0;
|
|
||||||
if (n == 0) { if (p < end) *p++ = '0'; return; }
|
|
||||||
while (n > 0) { tmp[ti++] = '0' + (n % 10); n /= 10; }
|
|
||||||
for (int j = ti - 1; j >= 0 && p < end; j--) *p++ = tmp[j];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Request line
|
|
||||||
append(method); append(" "); append(path); append(" HTTP/1.1\r\n");
|
|
||||||
|
|
||||||
// Host
|
|
||||||
append("Host: "); append(host); append("\r\n");
|
|
||||||
|
|
||||||
// Content headers (for POST/PUT/PATCH)
|
|
||||||
if (body_data && body_len > 0) {
|
|
||||||
if (content_type) {
|
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");
|
||||||
}
|
}
|
||||||
|
if (extra_headers) {
|
||||||
// Extra headers (caller-supplied, must include \r\n terminators)
|
w.text(extra_headers);
|
||||||
if (extra_headers) append(extra_headers);
|
int n = montauk::slen(extra_headers);
|
||||||
|
if (n > 0 && extra_headers[n - 1] != '\n') w.text("\r\n");
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
w.text("Connection: close\r\n\r\n");
|
||||||
return (int)(p - buf);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
inline bool plain_send_all(int fd, const char* data, int len,
|
||||||
// Public API
|
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,
|
inline Response get(const char* host, const char* path,
|
||||||
const tls::TrustAnchors& tas,
|
const tls::TrustAnchors& tas, int response_buffer_size = 32768,
|
||||||
int resp_buf_size = 32768,
|
|
||||||
const char* extra_headers = nullptr,
|
const char* extra_headers = nullptr,
|
||||||
tls::AbortCheckFn abort_check = nullptr) {
|
tls::AbortCheckFn abort_check = nullptr) {
|
||||||
Response resp = {};
|
RequestOptions options;
|
||||||
resp.status = -1;
|
options.response_buffer_size = response_buffer_size;
|
||||||
|
options.extra_headers = extra_headers;
|
||||||
uint32_t ip = montauk::resolve(host);
|
options.abort_check = abort_check;
|
||||||
if (!ip) return resp;
|
return request("GET", host, path, nullptr, nullptr, 0, &tas, options);
|
||||||
|
|
||||||
char req[1024];
|
|
||||||
int reqLen = build_request(req, sizeof(req), "GET", host, path,
|
|
||||||
nullptr, nullptr, 0, extra_headers);
|
|
||||||
|
|
||||||
char* buf = (char*)montauk::malloc(resp_buf_size);
|
|
||||||
if (!buf) return resp;
|
|
||||||
|
|
||||||
int n = tls::https_fetch(host, ip, 443, req, reqLen, tas,
|
|
||||||
buf, resp_buf_size - 1, abort_check);
|
|
||||||
if (n <= 0) { montauk::mfree(buf); return resp; }
|
|
||||||
buf[n] = 0;
|
|
||||||
|
|
||||||
parse_response(buf, n, &resp);
|
|
||||||
return resp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST request over HTTPS. Returns parsed response. Caller must free_response().
|
inline Response post(const char* host, const char* path, const char* content_type,
|
||||||
inline Response post(const char* host, const char* path,
|
|
||||||
const char* content_type,
|
|
||||||
const char* body_data, int body_len,
|
const char* body_data, int body_len,
|
||||||
const tls::TrustAnchors& tas,
|
const tls::TrustAnchors& tas, int response_buffer_size = 32768,
|
||||||
int resp_buf_size = 32768,
|
|
||||||
const char* extra_headers = nullptr,
|
const char* extra_headers = nullptr,
|
||||||
tls::AbortCheckFn abort_check = nullptr) {
|
tls::AbortCheckFn abort_check = nullptr) {
|
||||||
Response resp = {};
|
RequestOptions options;
|
||||||
resp.status = -1;
|
options.response_buffer_size = response_buffer_size;
|
||||||
|
options.extra_headers = extra_headers;
|
||||||
uint32_t ip = montauk::resolve(host);
|
options.abort_check = abort_check;
|
||||||
if (!ip) return resp;
|
return request("POST", host, path, content_type, body_data, body_len, &tas, options);
|
||||||
|
|
||||||
int req_size = 1024 + body_len;
|
|
||||||
char* req = (char*)montauk::malloc(req_size);
|
|
||||||
if (!req) return resp;
|
|
||||||
|
|
||||||
int reqLen = build_request(req, req_size, "POST", host, path,
|
|
||||||
content_type, body_data, body_len,
|
|
||||||
extra_headers);
|
|
||||||
|
|
||||||
char* buf = (char*)montauk::malloc(resp_buf_size);
|
|
||||||
if (!buf) { montauk::mfree(req); return resp; }
|
|
||||||
|
|
||||||
int n = tls::https_fetch(host, ip, 443, req, reqLen, tas,
|
|
||||||
buf, resp_buf_size - 1, abort_check);
|
|
||||||
montauk::mfree(req);
|
|
||||||
|
|
||||||
if (n <= 0) { montauk::mfree(buf); return resp; }
|
|
||||||
buf[n] = 0;
|
|
||||||
|
|
||||||
parse_response(buf, n, &resp);
|
|
||||||
return resp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic request over HTTPS (PUT, PATCH, DELETE, etc.).
|
inline Response request(const char* method, const char* host, const char* path,
|
||||||
inline Response request(const char* method,
|
const char* content_type, const char* body_data, int body_len,
|
||||||
const char* host, const char* path,
|
const tls::TrustAnchors& tas, int response_buffer_size = 32768,
|
||||||
const char* content_type,
|
|
||||||
const char* body_data, int body_len,
|
|
||||||
const tls::TrustAnchors& tas,
|
|
||||||
int resp_buf_size = 32768,
|
|
||||||
const char* extra_headers = nullptr,
|
const char* extra_headers = nullptr,
|
||||||
tls::AbortCheckFn abort_check = nullptr) {
|
tls::AbortCheckFn abort_check = nullptr) {
|
||||||
Response resp = {};
|
RequestOptions options;
|
||||||
resp.status = -1;
|
options.response_buffer_size = response_buffer_size;
|
||||||
|
options.extra_headers = extra_headers;
|
||||||
uint32_t ip = montauk::resolve(host);
|
options.abort_check = abort_check;
|
||||||
if (!ip) return resp;
|
return request(method, host, path, content_type, body_data, body_len, &tas, options);
|
||||||
|
|
||||||
int req_size = 1024 + (body_len > 0 ? body_len : 0);
|
|
||||||
char* req = (char*)montauk::malloc(req_size);
|
|
||||||
if (!req) return resp;
|
|
||||||
|
|
||||||
int reqLen = build_request(req, req_size, method, host, path,
|
|
||||||
content_type, body_data, body_len,
|
|
||||||
extra_headers);
|
|
||||||
|
|
||||||
char* buf = (char*)montauk::malloc(resp_buf_size);
|
|
||||||
if (!buf) { montauk::mfree(req); return resp; }
|
|
||||||
|
|
||||||
int n = tls::https_fetch(host, ip, 443, req, reqLen, tas,
|
|
||||||
buf, resp_buf_size - 1, abort_check);
|
|
||||||
montauk::mfree(req);
|
|
||||||
|
|
||||||
if (n <= 0) { montauk::mfree(buf); return resp; }
|
|
||||||
buf[n] = 0;
|
|
||||||
|
|
||||||
parse_response(buf, n, &resp);
|
|
||||||
return resp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plain HTTP (no TLS) GET over port 80.
|
|
||||||
inline Response get_plain(const char* host, const char* path,
|
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) {
|
const char* extra_headers = nullptr) {
|
||||||
Response resp = {};
|
RequestOptions options;
|
||||||
resp.status = -1;
|
options.secure = false;
|
||||||
|
options.response_buffer_size = response_buffer_size;
|
||||||
uint32_t ip = montauk::resolve(host);
|
options.extra_headers = extra_headers;
|
||||||
if (!ip) return resp;
|
return request("GET", host, path, nullptr, nullptr, 0, nullptr, options);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace http
|
} // namespace http
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ struct TrustAnchors {
|
|||||||
};
|
};
|
||||||
|
|
||||||
TrustAnchors load_trust_anchors();
|
TrustAnchors load_trust_anchors();
|
||||||
|
void free_trust_anchors(TrustAnchors* tas);
|
||||||
void get_bearssl_time(uint32_t* days, uint32_t* seconds);
|
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_send_all(int fd, const unsigned char* data, size_t len);
|
||||||
int tls_recv_some(int fd, unsigned char* buf, size_t maxlen);
|
int tls_recv_some(int fd, unsigned char* buf, size_t maxlen);
|
||||||
|
|||||||
Binary file not shown.
@@ -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)
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#include <http/http.hpp>
|
||||||
|
|
||||||
|
static bool bytes_equal(const char* a, const char* b, int len) {
|
||||||
|
for (int i = 0; i < len; ++i)
|
||||||
|
if (a[i] != b[i]) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_content_length() {
|
||||||
|
char raw[] =
|
||||||
|
"HTTP/1.1 200 OK\r\n"
|
||||||
|
"Content-Type: text/plain\r\n"
|
||||||
|
"Content-Length: 5\r\n\r\n"
|
||||||
|
"helloignored";
|
||||||
|
http::Response response = {};
|
||||||
|
if (http::parse_response(raw, sizeof(raw) - 1, &response) != 200) return 1;
|
||||||
|
if (response.error != http::Error::NONE || response.body_len != 5) return 2;
|
||||||
|
if (!bytes_equal(response.body, "hello", 5)) return 3;
|
||||||
|
char type[32];
|
||||||
|
if (!http::get_header(&response, "content-type", type, sizeof(type))) return 4;
|
||||||
|
if (!bytes_equal(type, "text/plain", 10) || type[10] != '\0') return 5;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_chunked() {
|
||||||
|
char raw[] =
|
||||||
|
"HTTP/1.1 200 OK\r\n"
|
||||||
|
"Transfer-Encoding: Chunked\r\n\r\n"
|
||||||
|
"4\r\nWiki\r\n5;extension=yes\r\npedia\r\n0\r\nX-Trailer: yes\r\n\r\n";
|
||||||
|
http::Response response = {};
|
||||||
|
if (http::parse_response(raw, sizeof(raw) - 1, &response) != 200) return 1;
|
||||||
|
if (response.error != http::Error::NONE || response.body_len != 9) return 2;
|
||||||
|
if (!bytes_equal(response.body, "Wikipedia", 9)) return 3;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_informational() {
|
||||||
|
char raw[] =
|
||||||
|
"HTTP/1.1 100 Continue\r\nHeader: value\r\n\r\n"
|
||||||
|
"HTTP/1.1 201 Created\r\nContent-Length: 2\r\n\r\nok";
|
||||||
|
http::Response response = {};
|
||||||
|
if (http::parse_response(raw, sizeof(raw) - 1, &response) != 201) return 1;
|
||||||
|
if (response.status != 201 || response.body_len != 2) return 2;
|
||||||
|
if (!bytes_equal(response.body, "ok", 2)) return 3;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_rejections() {
|
||||||
|
char truncated[] = "HTTP/1.1 200 OK\r\nContent-Length: 8\r\n\r\nshort";
|
||||||
|
http::Response response = {};
|
||||||
|
if (http::parse_response(truncated, sizeof(truncated) - 1, &response) != 200) return 1;
|
||||||
|
if (response.error != http::Error::TRUNCATED_RESPONSE) return 2;
|
||||||
|
|
||||||
|
char malformed[] = "not http\r\n\r\n";
|
||||||
|
response = {};
|
||||||
|
if (http::parse_response(malformed, sizeof(malformed) - 1, &response) >= 0) return 3;
|
||||||
|
if (response.error != http::Error::INVALID_RESPONSE) return 4;
|
||||||
|
|
||||||
|
char request[128];
|
||||||
|
if (http::build_request(request, sizeof(request), "GET", "safe\r\nInjected: yes",
|
||||||
|
"/", nullptr, nullptr, 0, nullptr) >= 0) return 5;
|
||||||
|
if (http::build_request(request, 16, "GET", "example.com", "/",
|
||||||
|
nullptr, nullptr, 0, nullptr) >= 0) return 6;
|
||||||
|
char bad_chunk[] =
|
||||||
|
"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n";
|
||||||
|
response = {};
|
||||||
|
http::parse_response(bad_chunk, sizeof(bad_chunk) - 1, &response);
|
||||||
|
if (response.error != http::Error::TRUNCATED_RESPONSE) return 7;
|
||||||
|
if (http::build_request(request, sizeof(request), "GET", "example.com", "/",
|
||||||
|
nullptr, nullptr, 0, "Safe: yes\r\n\r\nGET /evil") >= 0)
|
||||||
|
return 8;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
int result = test_content_length();
|
||||||
|
if (result) return 10 + result;
|
||||||
|
result = test_chunked();
|
||||||
|
if (result) return 20 + result;
|
||||||
|
result = test_informational();
|
||||||
|
if (result) return 30 + result;
|
||||||
|
result = test_rejections();
|
||||||
|
if (result) return 40 + result;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user