diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index ac63bcb..6cda082 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -789,6 +789,16 @@ namespace Zenith { return count; } + // ---- Window scale syscalls ---- + + static int Sys_WinSetScale(int scale) { + return WinServer::SetScale(scale); + } + + static int Sys_WinGetScale() { + return WinServer::GetScale(); + } + // ---- Window server syscalls ---- static int Sys_WinCreate(const char* title, int w, int h, WinCreateResult* result) { @@ -1003,6 +1013,10 @@ namespace Zenith { return (int64_t)Sys_Kill((int)frame->arg1); case SYS_DEVLIST: return (int64_t)Sys_DevList((DevInfo*)frame->arg1, (int)frame->arg2); + case SYS_WINSETSCALE: + return (int64_t)Sys_WinSetScale((int)frame->arg1); + case SYS_WINGETSCALE: + return (int64_t)Sys_WinGetScale(); default: return -1; } diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index f128d6b..33e947b 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -75,6 +75,8 @@ namespace Zenith { static constexpr uint64_t SYS_WINMAP = 59; static constexpr uint64_t SYS_WINSENDEVENT = 60; static constexpr uint64_t SYS_WINRESIZE = 64; + static constexpr uint64_t SYS_WINSETSCALE = 65; + static constexpr uint64_t SYS_WINGETSCALE = 66; // Process management syscalls static constexpr uint64_t SYS_PROCLIST = 61; @@ -135,12 +137,13 @@ namespace Zenith { // Window server shared types struct WinEvent { - uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close + uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale uint8_t _pad[3]; union { KeyEvent key; struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse; struct { int32_t w, h; } resize; + struct { int32_t scale; } scale; }; }; diff --git a/kernel/src/Api/WinServer.cpp b/kernel/src/Api/WinServer.cpp index 88f9259..116ecf6 100644 --- a/kernel/src/Api/WinServer.cpp +++ b/kernel/src/Api/WinServer.cpp @@ -14,6 +14,7 @@ namespace WinServer { static WindowSlot g_slots[MaxWindows]; + static int g_uiScale = 1; int Create(int ownerPid, uint64_t ownerPml4, const char* title, int w, int h, uint64_t& heapNext, uint64_t& outVa) { @@ -201,6 +202,28 @@ namespace WinServer { return 0; } + int SetScale(int scale) { + if (scale < 0) scale = 0; + if (scale > 2) scale = 2; + g_uiScale = scale; + + // Broadcast scale event to all active windows + Zenith::WinEvent ev; + memset(&ev, 0, sizeof(ev)); + ev.type = 4; + ev.scale.scale = scale; + for (int i = 0; i < MaxWindows; i++) { + if (g_slots[i].used) { + SendEvent(i, &ev); + } + } + return 0; + } + + int GetScale() { + return g_uiScale; + } + void CleanupProcess(int pid) { for (int i = 0; i < MaxWindows; i++) { if (g_slots[i].used && g_slots[i].ownerPid == pid) { diff --git a/kernel/src/Api/WinServer.hpp b/kernel/src/Api/WinServer.hpp index da8091b..a63ff7c 100644 --- a/kernel/src/Api/WinServer.hpp +++ b/kernel/src/Api/WinServer.hpp @@ -40,5 +40,7 @@ namespace WinServer { int Resize(int windowId, int callerPid, uint64_t ownerPml4, int newW, int newH, uint64_t& heapNext, uint64_t& outVa); void CleanupProcess(int pid); + int SetScale(int scale); + int GetScale(); } diff --git a/programs/GNUmakefile b/programs/GNUmakefile index 35d52b7..5f5b225 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -57,7 +57,7 @@ BINDIR := bin PROGRAMS := $(notdir $(wildcard src/*)) # Programs with custom Makefiles (built separately). -CUSTOM_BUILDS := doom fetch wiki wikipedia desktop +CUSTOM_BUILDS := doom fetch wiki wikipedia weather desktop SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS)) # Build targets: system programs go to bin/os/, games are handled separately. @@ -82,9 +82,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt # Home directory placeholder. HOMEKEEP := $(BINDIR)/home/.keep -.PHONY: all clean doom fetch wiki wikipedia desktop icons fonts bearssl libc +.PHONY: all clean doom fetch wiki wikipedia weather desktop icons fonts bearssl libc tls -all: bearssl libc $(TARGETS) fetch wiki wikipedia doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP) +all: bearssl libc tls $(TARGETS) fetch wiki wikipedia weather doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP) # Build BearSSL static library. bearssl: @@ -94,18 +94,26 @@ bearssl: libc: $(MAKE) -C lib/libc -# Build fetch via its own Makefile (depends on bearssl and libc). -fetch: bearssl libc +# Build shared TLS helper library. +tls: bearssl libc + $(MAKE) -C lib/tls + +# Build fetch via its own Makefile (depends on bearssl, libc, and tls). +fetch: bearssl libc tls $(MAKE) -C src/fetch -# Build wiki via its own Makefile (depends on bearssl and libc). -wiki: bearssl libc +# Build wiki via its own Makefile (depends on bearssl, libc, and tls). +wiki: bearssl libc tls $(MAKE) -C src/wiki -# Build wikipedia standalone GUI client (depends on bearssl and libc). -wikipedia: bearssl libc +# Build wikipedia standalone GUI client (depends on bearssl, libc, and tls). +wikipedia: bearssl libc tls $(MAKE) -C src/wikipedia +# Build weather standalone GUI client (depends on bearssl, libc, and tls). +weather: bearssl libc tls + $(MAKE) -C src/weather + # Build desktop via its own Makefile. desktop: $(MAKE) -C src/desktop @@ -157,7 +165,9 @@ clean: rm -rf $(BINDIR) obj $(MAKE) -C lib/bearssl clean $(MAKE) -C lib/libc clean + $(MAKE) -C lib/tls clean $(MAKE) -C src/fetch clean $(MAKE) -C src/doom clean $(MAKE) -C src/desktop clean $(MAKE) -C src/wikipedia clean + $(MAKE) -C src/weather clean diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index f5520eb..b96c454 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -75,6 +75,8 @@ namespace Zenith { static constexpr uint64_t SYS_WINMAP = 59; static constexpr uint64_t SYS_WINSENDEVENT = 60; static constexpr uint64_t SYS_WINRESIZE = 64; + static constexpr uint64_t SYS_WINSETSCALE = 65; + static constexpr uint64_t SYS_WINGETSCALE = 66; // Process management syscalls static constexpr uint64_t SYS_PROCLIST = 61; @@ -135,12 +137,13 @@ namespace Zenith { // Window server shared types struct WinEvent { - uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close + uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale uint8_t _pad[3]; union { KeyEvent key; struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse; struct { int32_t w, h; } resize; + struct { int32_t scale; } scale; }; }; diff --git a/programs/include/gui/desktop.hpp b/programs/include/gui/desktop.hpp index d46f693..25b439b 100644 --- a/programs/include/gui/desktop.hpp +++ b/programs/include/gui/desktop.hpp @@ -73,6 +73,8 @@ struct DesktopState { SvgIcon icon_settings; SvgIcon icon_reboot; + SvgIcon icon_weather; + SvgIcon icon_doom; SvgIcon icon_procmgr; SvgIcon icon_mandelbrot; diff --git a/programs/include/gui/svg.hpp b/programs/include/gui/svg.hpp index 522a8be..1516cee 100644 --- a/programs/include/gui/svg.hpp +++ b/programs/include/gui/svg.hpp @@ -77,6 +77,52 @@ struct SvgGradientTable { } }; +// --------------------------------------------------------------------------- +// CSS class color table (.ClassName { color: #rrggbb; }) +// Used to resolve fill:currentColor via the element's class= attribute. +// --------------------------------------------------------------------------- +static constexpr int SVG_MAX_CSS_CLASSES = 8; + +struct SvgCssClass { + char name[48]; + Color color; +}; + +struct SvgCssTable { + SvgCssClass entries[SVG_MAX_CSS_CLASSES]; + int count; + + void clear() { count = 0; } + + void add(const char* name, Color c) { + if (count >= SVG_MAX_CSS_CLASSES) return; + int i = 0; + while (name[i] && i < 47) { entries[count].name[i] = name[i]; i++; } + entries[count].name[i] = '\0'; + entries[count].color = c; + count++; + } + + // Look up class by name (first token if multiple classes are listed). + // Returns true if found. + bool lookup(const char* cls, Color* out) const { + for (int i = 0; i < count; i++) { + const char* a = entries[i].name; + const char* b = cls; + bool match = true; + while (*a && *b) { + if (*a != *b) { match = false; break; } + a++; b++; + } + if (match && *a == '\0' && (*b == '\0' || *b == ' ')) { + *out = entries[i].color; + return true; + } + } + return false; + } +}; + // --------------------------------------------------------------------------- // Fixed-point number parser (NO floating point) // Parses strings like "3.25", "-0.5", ".1115", "16" @@ -815,6 +861,52 @@ inline void svg_rasterize(const SvgEdgeList& el, uint32_t* pixels, int w, int h, zenith::free(isect); } +// --------------------------------------------------------------------------- +// Parse CSS class color mappings out of a "); + if (se) + svg_parse_css_table(sp, (int)(se - sp), &css); + } + } + // Shared edge list (cleared per element for multi-color support) SvgEdgeList el; el.init(SVG_MAX_EDGES); @@ -1113,7 +1243,7 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t // Determine fill color for this element Color elem_color = fill_color; - int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads); + int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css); if (fillResult == -1) { p = elem_end; continue; } // fill="none" int alpha = svg_get_element_opacity(elem_start, elem_len); @@ -1146,7 +1276,7 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t } Color elem_color = fill_color; - int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads); + int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css); if (fillResult == -1) { p = elem_end; continue; } int alpha = svg_get_element_opacity(elem_start, elem_len); @@ -1189,7 +1319,7 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t } Color elem_color = fill_color; - int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads); + int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css); if (fillResult == -1) { p = elem_end; continue; } int alpha = svg_get_element_opacity(elem_start, elem_len); diff --git a/programs/include/gui/truetype.hpp b/programs/include/gui/truetype.hpp index ef0192c..6d53ef3 100644 --- a/programs/include/gui/truetype.hpp +++ b/programs/include/gui/truetype.hpp @@ -50,7 +50,7 @@ struct CachedGlyph { }; struct GlyphCache { - CachedGlyph glyphs[128]; + CachedGlyph glyphs[256]; // Latin-1 Supplement (U+0080..U+00FF) included int pixel_size; float scale; int ascent, descent, line_gap; @@ -127,7 +127,7 @@ struct TrueTypeFont { } CachedGlyph* get_glyph(GlyphCache* gc, int codepoint) { - if (codepoint < 0 || codepoint >= 128) return nullptr; + if (codepoint < 0 || codepoint >= 256) return nullptr; CachedGlyph* g = &gc->glyphs[codepoint]; if (g->loaded) return g; diff --git a/programs/include/tls/tls.hpp b/programs/include/tls/tls.hpp new file mode 100644 index 0000000..7281314 --- /dev/null +++ b/programs/include/tls/tls.hpp @@ -0,0 +1,45 @@ +/* + * tls.hpp + * Shared TLS helper library for ZenithOS + * Trust anchor loading, BearSSL time, TLS I/O, HTTPS fetch + * Copyright (c) 2026 Daniel Hammer + */ + +#pragma once + +extern "C" { +#include +} + +#include +#include + +namespace tls { + +struct TrustAnchors { + br_x509_trust_anchor* anchors; + size_t count; + size_t capacity; +}; + +TrustAnchors load_trust_anchors(); +void get_bearssl_time(uint32_t* days, uint32_t* seconds); +int tls_send_all(int fd, const unsigned char* data, size_t len); +int tls_recv_some(int fd, unsigned char* buf, size_t maxlen); + +// Optional abort callback (for Ctrl+Q in terminal apps). nullptr = no abort. +using AbortCheckFn = bool (*)(); + +int tls_exchange(int fd, br_ssl_engine_context* eng, + const char* request, int reqLen, + char* respBuf, int respMax, + AbortCheckFn abort_check = nullptr); + +// High-level: socket -> TLS setup -> exchange -> cleanup, all in one call. +int https_fetch(const char* host, uint32_t ip, uint16_t port, + const char* request, int reqLen, + const TrustAnchors& tas, + char* respBuf, int respMax, + AbortCheckFn abort_check = nullptr); + +} // namespace tls diff --git a/programs/include/zenith/syscall.h b/programs/include/zenith/syscall.h index 44ca103..5c2b9d3 100644 --- a/programs/include/zenith/syscall.h +++ b/programs/include/zenith/syscall.h @@ -320,5 +320,11 @@ namespace zenith { inline uint64_t win_resize(int id, int w, int h) { return (uint64_t)syscall3(Zenith::SYS_WINRESIZE, (uint64_t)id, (uint64_t)w, (uint64_t)h); } + inline int win_setscale(int scale) { + return (int)syscall1(Zenith::SYS_WINSETSCALE, (uint64_t)scale); + } + inline int win_getscale() { + return (int)syscall0(Zenith::SYS_WINGETSCALE); + } } diff --git a/programs/lib/tls/Makefile b/programs/lib/tls/Makefile new file mode 100644 index 0000000..9529120 --- /dev/null +++ b/programs/lib/tls/Makefile @@ -0,0 +1,72 @@ +# Makefile for shared TLS helper library on ZenithOS +# Copyright (c) 2026 Daniel Hammer + +MAKEFLAGS += -rR +.SUFFIXES: + +# ---- Toolchain ---- + +TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf- +ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),) + CXX := $(TOOLCHAIN_PREFIX)g++ + AR := $(TOOLCHAIN_PREFIX)ar +else + CXX := g++ + AR := ar +endif + +# ---- Paths ---- + +BEARSSL := ../bearssl +LIBC_INC := ../../include/libc +PROG_INC := ../../include +OBJDIR := obj + +# ---- Compiler flags ---- + +CXXFLAGS := \ + -std=gnu++20 \ + -g -O2 -pipe \ + -Wall \ + -Wextra \ + -Wno-unused-parameter \ + -nostdinc \ + -ffreestanding \ + -fno-stack-protector \ + -fno-stack-check \ + -fno-PIC \ + -fno-rtti \ + -fno-exceptions \ + -ffunction-sections \ + -fdata-sections \ + -m64 \ + -march=x86-64 \ + -mno-80387 \ + -mno-mmx \ + -mno-sse \ + -mno-sse2 \ + -mno-red-zone \ + -mcmodel=small \ + -I $(PROG_INC) \ + -isystem $(LIBC_INC) \ + -I $(BEARSSL)/inc \ + -isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \ + -isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include + +# ---- Target ---- + +TARGET := libtls.a + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJDIR)/tls.o + $(AR) rcs $@ $^ + +$(OBJDIR)/tls.o: tls.cpp Makefile + @mkdir -p $(OBJDIR) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/lib/tls/libtls.a b/programs/lib/tls/libtls.a new file mode 100644 index 0000000..2eb62f0 Binary files /dev/null and b/programs/lib/tls/libtls.a differ diff --git a/programs/lib/tls/obj/tls.o b/programs/lib/tls/obj/tls.o new file mode 100644 index 0000000..89b2fb8 Binary files /dev/null and b/programs/lib/tls/obj/tls.o differ diff --git a/programs/lib/tls/tls.cpp b/programs/lib/tls/tls.cpp new file mode 100644 index 0000000..ffa1552 --- /dev/null +++ b/programs/lib/tls/tls.cpp @@ -0,0 +1,279 @@ +/* + * tls.cpp + * Shared TLS helper library for ZenithOS + * Extracted from fetch, wiki, wikipedia, and weather apps + * Copyright (c) 2025-2026 Daniel Hammer + */ + +#include +#include + +extern "C" { +#include +#include +} + +// ============================================================================ +// File-local: PEM/DER certificate parsing helpers +// ============================================================================ + +namespace { + +struct DerAccum { unsigned char* data; size_t len, cap; }; +struct DnAccum { unsigned char* data; size_t len, cap; }; + +void der_append(void* ctx, const void* buf, size_t len) { + DerAccum* a = (DerAccum*)ctx; + if (a->len + len > a->cap) { + size_t nc = a->cap * 2; + if (nc < a->len + len) nc = a->len + len + 4096; + unsigned char* nb = (unsigned char*)malloc(nc); + if (!nb) return; + if (a->data) { memcpy(nb, a->data, a->len); free(a->data); } + a->data = nb; a->cap = nc; + } + memcpy(a->data + a->len, buf, len); + a->len += len; +} + +void dn_append(void* ctx, const void* buf, size_t len) { + DnAccum* a = (DnAccum*)ctx; + if (a->len + len > a->cap) { + size_t nc = a->cap * 2; + if (nc < a->len + len) nc = a->len + len + 256; + unsigned char* nb = (unsigned char*)malloc(nc); + if (!nb) return; + if (a->data) { memcpy(nb, a->data, a->len); free(a->data); } + a->data = nb; a->cap = nc; + } + memcpy(a->data + a->len, buf, len); + a->len += len; +} + +void ta_add(tls::TrustAnchors* tas, const br_x509_trust_anchor* ta) { + if (tas->count >= tas->capacity) { + size_t nc = tas->capacity == 0 ? 64 : tas->capacity * 2; + br_x509_trust_anchor* na = (br_x509_trust_anchor*)malloc(nc * sizeof(*na)); + if (!na) return; + if (tas->anchors) { memcpy(na, tas->anchors, tas->count * sizeof(*na)); free(tas->anchors); } + tas->anchors = na; tas->capacity = nc; + } + tas->anchors[tas->count++] = *ta; +} + +bool process_cert_der(tls::TrustAnchors* tas, const unsigned char* der, size_t der_len) { + static br_x509_decoder_context dc; // ~2KB+, keep off stack + DnAccum dn = {nullptr, 0, 0}; + br_x509_decoder_init(&dc, dn_append, &dn); + br_x509_decoder_push(&dc, der, der_len); + br_x509_pkey* pk = br_x509_decoder_get_pkey(&dc); + if (!pk) { if (dn.data) free(dn.data); return false; } + + br_x509_trust_anchor ta; + memset(&ta, 0, sizeof(ta)); + ta.dn.data = dn.data; ta.dn.len = dn.len; ta.flags = 0; + if (br_x509_decoder_isCA(&dc)) ta.flags |= BR_X509_TA_CA; + + switch (pk->key_type) { + case BR_KEYTYPE_RSA: + ta.pkey.key_type = BR_KEYTYPE_RSA; + ta.pkey.key.rsa.nlen = pk->key.rsa.nlen; + ta.pkey.key.rsa.n = (unsigned char*)malloc(pk->key.rsa.nlen); + if (ta.pkey.key.rsa.n) memcpy(ta.pkey.key.rsa.n, pk->key.rsa.n, pk->key.rsa.nlen); + ta.pkey.key.rsa.elen = pk->key.rsa.elen; + ta.pkey.key.rsa.e = (unsigned char*)malloc(pk->key.rsa.elen); + if (ta.pkey.key.rsa.e) memcpy(ta.pkey.key.rsa.e, pk->key.rsa.e, pk->key.rsa.elen); + break; + case BR_KEYTYPE_EC: + ta.pkey.key_type = BR_KEYTYPE_EC; + ta.pkey.key.ec.curve = pk->key.ec.curve; + ta.pkey.key.ec.qlen = pk->key.ec.qlen; + ta.pkey.key.ec.q = (unsigned char*)malloc(pk->key.ec.qlen); + if (ta.pkey.key.ec.q) memcpy(ta.pkey.key.ec.q, pk->key.ec.q, pk->key.ec.qlen); + break; + default: + if (dn.data) free(dn.data); + return false; + } + ta_add(tas, &ta); + return true; +} + +} // anonymous namespace + +// ============================================================================ +// Exported functions +// ============================================================================ + +namespace tls { + +TrustAnchors load_trust_anchors() { + TrustAnchors tas = {nullptr, 0, 0}; + int fh = zenith::open("0:/etc/ca-certificates.crt"); + if (fh < 0) return tas; + uint64_t fsize = zenith::getsize(fh); + if (fsize == 0 || fsize > 512 * 1024) { zenith::close(fh); return tas; } + + unsigned char* pem = (unsigned char*)malloc(fsize + 1); + if (!pem) { zenith::close(fh); return tas; } + zenith::read(fh, pem, 0, fsize); + zenith::close(fh); + pem[fsize] = 0; + + static br_pem_decoder_context pc; // keep off stack + br_pem_decoder_init(&pc); + DerAccum der = {nullptr, 0, 0}; + bool inCert = false; + size_t offset = 0; + + while (offset < fsize) { + size_t pushed = br_pem_decoder_push(&pc, pem + offset, fsize - offset); + offset += pushed; + int ev = br_pem_decoder_event(&pc); + if (ev == BR_PEM_BEGIN_OBJ) { + inCert = (strcmp(br_pem_decoder_name(&pc), "CERTIFICATE") == 0); + br_pem_decoder_setdest(&pc, inCert ? der_append : nullptr, inCert ? &der : nullptr); + if (inCert) der.len = 0; + } else if (ev == BR_PEM_END_OBJ) { + if (inCert && der.len > 0) process_cert_der(&tas, der.data, der.len); + inCert = false; + } else if (ev == BR_PEM_ERROR) { + break; + } + } + if (der.data) free(der.data); + free(pem); + return tas; +} + +void get_bearssl_time(uint32_t* days, uint32_t* seconds) { + Zenith::DateTime dt; + zenith::gettime(&dt); + int y = dt.Year, m = dt.Month, d = dt.Day; + uint32_t total = 365u * (uint32_t)y + + (uint32_t)(y/4) - (uint32_t)(y/100) + (uint32_t)(y/400); + const int md[] = {0,31,28,31,30,31,30,31,31,30,31,30,31}; + for (int mo = 1; mo < m && mo <= 12; mo++) total += md[mo]; + if (y%4==0 && (y%100!=0 || y%400==0) && m > 2) total++; + total += d - 1; + *days = total; + *seconds = (uint32_t)(dt.Hour*3600 + dt.Minute*60 + dt.Second); +} + +int tls_send_all(int fd, const unsigned char* data, size_t len) { + size_t sent = 0; + uint64_t deadline = zenith::get_milliseconds() + 15000; + while (sent < len) { + int r = zenith::send(fd, data + sent, (uint32_t)(len - sent)); + if (r > 0) { sent += r; deadline = zenith::get_milliseconds() + 15000; } + else if (r < 0) return -1; + else { if (zenith::get_milliseconds() >= deadline) return -1; zenith::sleep_ms(1); } + } + return (int)sent; +} + +int tls_recv_some(int fd, unsigned char* buf, size_t maxlen) { + uint64_t deadline = zenith::get_milliseconds() + 15000; + while (true) { + int r = zenith::recv(fd, buf, (uint32_t)maxlen); + if (r > 0) return r; + if (r < 0) return -1; + if (zenith::get_milliseconds() >= deadline) return -1; + zenith::sleep_ms(1); + } +} + +int tls_exchange(int fd, br_ssl_engine_context* eng, + const char* request, int reqLen, + char* respBuf, int respMax, + AbortCheckFn abort_check) { + bool requestSent = false; + int respLen = 0; + uint64_t deadline = zenith::get_milliseconds() + 30000; + + while (true) { + unsigned state = br_ssl_engine_current_state(eng); + if (state & BR_SSL_CLOSED) { + int err = br_ssl_engine_last_error(eng); + if (err != BR_ERR_OK && err != BR_ERR_IO && respLen == 0) return -1; + return respLen; + } + if (abort_check && abort_check()) { + br_ssl_engine_close(eng); + return respLen > 0 ? respLen : -1; + } + if (state & BR_SSL_SENDREC) { + size_t len; unsigned char* buf = br_ssl_engine_sendrec_buf(eng, &len); + int sent = tls_send_all(fd, buf, len); + if (sent < 0) { br_ssl_engine_close(eng); return respLen > 0 ? respLen : -1; } + br_ssl_engine_sendrec_ack(eng, len); + deadline = zenith::get_milliseconds() + 30000; continue; + } + if (state & BR_SSL_RECVAPP) { + size_t len; unsigned char* buf = br_ssl_engine_recvapp_buf(eng, &len); + size_t toCopy = len; + if (respLen + (int)toCopy > respMax - 1) toCopy = respMax - 1 - respLen; + if (toCopy > 0) { memcpy(respBuf + respLen, buf, toCopy); respLen += toCopy; } + br_ssl_engine_recvapp_ack(eng, len); + deadline = zenith::get_milliseconds() + 30000; continue; + } + if ((state & BR_SSL_SENDAPP) && !requestSent) { + size_t len; unsigned char* buf = br_ssl_engine_sendapp_buf(eng, &len); + size_t toWrite = (size_t)reqLen; + if (toWrite > len) toWrite = len; + memcpy(buf, request, toWrite); + br_ssl_engine_sendapp_ack(eng, toWrite); + br_ssl_engine_flush(eng, 0); + requestSent = true; + deadline = zenith::get_milliseconds() + 30000; continue; + } + if (state & BR_SSL_RECVREC) { + size_t len; unsigned char* buf = br_ssl_engine_recvrec_buf(eng, &len); + int got = tls_recv_some(fd, buf, len); + if (got < 0) { br_ssl_engine_close(eng); return respLen > 0 ? respLen : -1; } + br_ssl_engine_recvrec_ack(eng, got); + deadline = zenith::get_milliseconds() + 30000; continue; + } + if (zenith::get_milliseconds() >= deadline) return respLen > 0 ? respLen : -1; + zenith::sleep_ms(1); + } +} + +int https_fetch(const char* host, uint32_t ip, uint16_t port, + const char* request, int reqLen, + const TrustAnchors& tas, + char* respBuf, int respMax, + AbortCheckFn abort_check) { + int fd = zenith::socket(Zenith::SOCK_TCP); + if (fd < 0) return -1; + if (zenith::connect(fd, ip, port) < 0) { zenith::closesocket(fd); return -1; } + + br_ssl_client_context* cc = (br_ssl_client_context*)malloc(sizeof(*cc)); + br_x509_minimal_context* xc = (br_x509_minimal_context*)malloc(sizeof(*xc)); + void* iobuf = malloc(BR_SSL_BUFSIZE_BIDI); + if (!cc || !xc || !iobuf) { + free(cc); free(xc); free(iobuf); + zenith::closesocket(fd); return -1; + } + + br_ssl_client_init_full(cc, xc, tas.anchors, tas.count); + uint32_t days, secs; + get_bearssl_time(&days, &secs); + br_x509_minimal_set_time(xc, days, secs); + + unsigned char seed[32]; + zenith::getrandom(seed, sizeof(seed)); + br_ssl_engine_set_buffer(&cc->eng, iobuf, BR_SSL_BUFSIZE_BIDI, 1); + br_ssl_engine_inject_entropy(&cc->eng, seed, sizeof(seed)); + + if (!br_ssl_client_reset(cc, host, 0)) { + zenith::closesocket(fd); free(cc); free(xc); free(iobuf); return -1; + } + + int respLen = tls_exchange(fd, &cc->eng, request, reqLen, respBuf, respMax, abort_check); + zenith::closesocket(fd); + free(cc); free(xc); free(iobuf); + return respLen; +} + +} // namespace tls diff --git a/programs/src/desktop/Makefile b/programs/src/desktop/Makefile index 926befb..6a3b409 100644 --- a/programs/src/desktop/Makefile +++ b/programs/src/desktop/Makefile @@ -61,7 +61,7 @@ LDFLAGS := \ # ---- C++ source files ---- -SRCS := main.cpp font_data.cpp stb_truetype_impl.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_klog.cpp app_wiki.cpp app_procmgr.cpp app_mandelbrot.cpp app_devexplorer.cpp app_settings.cpp app_doom.cpp +SRCS := main.cpp font_data.cpp stb_truetype_impl.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_klog.cpp app_wiki.cpp app_weather.cpp app_procmgr.cpp app_mandelbrot.cpp app_devexplorer.cpp app_settings.cpp app_doom.cpp OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) # ---- Target ---- diff --git a/programs/src/desktop/app_settings.cpp b/programs/src/desktop/app_settings.cpp index 0cddcf7..ce79825 100644 --- a/programs/src/desktop/app_settings.cpp +++ b/programs/src/desktop/app_settings.cpp @@ -458,18 +458,21 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) { if (mx >= bx && mx < bx + sbw && cy >= y && cy < y + btn_h) { s.ui_scale = 0; apply_ui_scale(0); + zenith::win_setscale(0); return; } // Default if (mx >= bx + sbw + 8 && mx < bx + sbw * 2 + 8 && cy >= y && cy < y + btn_h) { s.ui_scale = 1; apply_ui_scale(1); + zenith::win_setscale(1); return; } // Large if (mx >= bx + (sbw + 8) * 2 && mx < bx + sbw * 3 + 16 && cy >= y && cy < y + btn_h) { s.ui_scale = 2; apply_ui_scale(2); + zenith::win_setscale(2); return; } } diff --git a/programs/src/desktop/app_weather.cpp b/programs/src/desktop/app_weather.cpp new file mode 100644 index 0000000..0d3aa6c --- /dev/null +++ b/programs/src/desktop/app_weather.cpp @@ -0,0 +1,13 @@ +/* + * app_weather.cpp + * ZenithOS Desktop - Weather app launcher + * Spawns weather.elf as a standalone Window Server process + * Copyright (c) 2026 Daniel Hammer + */ + +#include "apps_common.hpp" + +void open_weather(DesktopState* ds) { + (void)ds; + zenith::spawn("0:/os/weather.elf"); +} diff --git a/programs/src/desktop/apps_common.hpp b/programs/src/desktop/apps_common.hpp index 4b7cc77..ef493c9 100644 --- a/programs/src/desktop/apps_common.hpp +++ b/programs/src/desktop/apps_common.hpp @@ -167,6 +167,7 @@ void open_texteditor(DesktopState* ds); void open_texteditor_with_file(DesktopState* ds, const char* path); void open_klog(DesktopState* ds); void open_wiki(DesktopState* ds); +void open_weather(DesktopState* ds); void open_procmgr(DesktopState* ds); void open_mandelbrot(DesktopState* ds); void open_devexplorer(DesktopState* ds); diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp index 899aca4..6e8b78a 100644 --- a/programs/src/desktop/main.cpp +++ b/programs/src/desktop/main.cpp @@ -61,6 +61,8 @@ void gui::desktop_init(DesktopState* ds) { ds->icon_settings = svg_load("0:/icons/help-about.svg", 20, 20, defColor); ds->icon_reboot = svg_load("0:/icons/system-reboot.svg", 20, 20, defColor); + ds->icon_weather = svg_load("0:/icons/weather-widget.svg", 20, 20, defColor); + ds->icon_doom = svg_load("0:/icons/doom.svg", 20, 20, defColor); ds->icon_procmgr = svg_load("0:/icons/system-monitor.svg", 20, 20, defColor); ds->icon_mandelbrot = svg_load("0:/icons/applications-science.svg", 20, 20, defColor); @@ -76,6 +78,7 @@ void gui::desktop_init(DesktopState* ds) { ds->settings.show_shadows = true; ds->settings.clock_24h = true; ds->settings.ui_scale = 1; + zenith::win_setscale(1); ds->ctx_menu_open = false; ds->ctx_menu_x = 0; @@ -421,7 +424,7 @@ struct MenuRow { int app_id; // -1 for category headers / dividers }; -static constexpr int MENU_ROW_COUNT = 18; +static constexpr int MENU_ROW_COUNT = 19; static const MenuRow menu_rows[MENU_ROW_COUNT] = { { true, "Applications", -1 }, { false, "Terminal", 0 }, @@ -430,6 +433,7 @@ static const MenuRow menu_rows[MENU_ROW_COUNT] = { { false, "Calculator", 3 }, { true, "Internet", -1 }, { false, "Wikipedia", 9 }, + { false, "Weather", 13 }, { true, "System", -1 }, { false, "System Info", 2 }, { false, "Kernel Log", 5 }, @@ -470,7 +474,7 @@ static void desktop_draw_app_menu(DesktopState* ds) { draw_rect(fb, menu_x, menu_y, MENU_W, menu_h, colors::BORDER); // Icon lookup by app_id - SvgIcon* icons[13] = { + SvgIcon* icons[14] = { &ds->icon_terminal, // 0 &ds->icon_filemanager, // 1 &ds->icon_sysinfo, // 2 @@ -484,6 +488,7 @@ static void desktop_draw_app_menu(DesktopState* ds) { &ds->icon_doom, // 10 &ds->icon_settings, // 11 &ds->icon_reboot, // 12 + &ds->icon_weather, // 13 }; int mx = ds->mouse.x; @@ -519,7 +524,7 @@ static void desktop_draw_app_menu(DesktopState* ds) { // Icon int icon_x = item_rect.x + 8; int icon_y = item_rect.y + (row_h - 20) / 2; - if (row.app_id >= 0 && row.app_id < 13) { + if (row.app_id >= 0 && row.app_id < 14) { SvgIcon* icon = icons[row.app_id]; if (icon && icon->pixels) { fb.blit_alpha(icon_x, icon_y, icon->width, icon->height, icon->pixels); @@ -1099,6 +1104,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { case 10: open_doom(ds); break; case 11: open_settings(ds); break; case 12: open_reboot_dialog(ds); break; + case 13: open_weather(ds); break; } ds->app_menu_open = false; } diff --git a/programs/src/fetch/Makefile b/programs/src/fetch/Makefile index 901874d..2a2f7b7 100644 --- a/programs/src/fetch/Makefile +++ b/programs/src/fetch/Makefile @@ -16,6 +16,7 @@ endif # ---- Paths ---- BEARSSL := ../../lib/bearssl +TLS_LIB := ../../lib/tls LIBC_LIB := ../../lib/libc LIBC_INC := ../../include/libc PROG_INC := ../../include @@ -67,7 +68,7 @@ LDFLAGS := \ # ---- Libraries ---- -LIBS := $(BEARSSL)/libbearssl.a $(LIBC_LIB)/liblibc.a +LIBS := $(TLS_LIB)/libtls.a $(BEARSSL)/libbearssl.a $(LIBC_LIB)/liblibc.a # ---- Target ---- diff --git a/programs/src/fetch/main.cpp b/programs/src/fetch/main.cpp index c587f19..ba19e08 100644 --- a/programs/src/fetch/main.cpp +++ b/programs/src/fetch/main.cpp @@ -8,9 +8,9 @@ #include #include +#include extern "C" { -#include #include #include #include @@ -156,366 +156,15 @@ static void parse_status_text(const char* buf, int len, char* out, int outMax) { out[j] = '\0'; } -// ---- Trust anchor loading ---- +// ---- Keyboard abort check for TLS ---- -struct TrustAnchors { - br_x509_trust_anchor* anchors; - size_t count; - size_t capacity; -}; - -// Accumulate DER-decoded certificate data -struct DerAccum { - unsigned char* data; - size_t len; - size_t cap; -}; - -static void der_append(void* ctx, const void* buf, size_t len) { - DerAccum* a = (DerAccum*)ctx; - if (a->len + len > a->cap) { - size_t newcap = a->cap * 2; - if (newcap < a->len + len) newcap = a->len + len + 4096; - unsigned char* nb = (unsigned char*)malloc(newcap); - if (!nb) return; - if (a->data) { - memcpy(nb, a->data, a->len); - free(a->data); - } - a->data = nb; - a->cap = newcap; - } - memcpy(a->data + a->len, buf, len); - a->len += len; -} - -// Accumulate DN data from X.509 decoder -struct DnAccum { - unsigned char* data; - size_t len; - size_t cap; -}; - -static void dn_append(void* ctx, const void* buf, size_t len) { - DnAccum* a = (DnAccum*)ctx; - if (a->len + len > a->cap) { - size_t newcap = a->cap * 2; - if (newcap < a->len + len) newcap = a->len + len + 256; - unsigned char* nb = (unsigned char*)malloc(newcap); - if (!nb) return; - if (a->data) { - memcpy(nb, a->data, a->len); - free(a->data); - } - a->data = nb; - a->cap = newcap; - } - memcpy(a->data + a->len, buf, len); - a->len += len; -} - -static void ta_add(TrustAnchors* tas, const br_x509_trust_anchor* ta) { - if (tas->count >= tas->capacity) { - size_t newcap = tas->capacity == 0 ? 64 : tas->capacity * 2; - br_x509_trust_anchor* na = (br_x509_trust_anchor*)malloc( - newcap * sizeof(br_x509_trust_anchor)); - if (!na) return; - if (tas->anchors) { - memcpy(na, tas->anchors, tas->count * sizeof(br_x509_trust_anchor)); - free(tas->anchors); - } - tas->anchors = na; - tas->capacity = newcap; - } - tas->anchors[tas->count++] = *ta; -} - -// Process a single DER certificate into a trust anchor -static bool process_cert_der(TrustAnchors* tas, const unsigned char* der, size_t der_len) { - br_x509_decoder_context dc; - DnAccum dn = {nullptr, 0, 0}; - - br_x509_decoder_init(&dc, dn_append, &dn); - br_x509_decoder_push(&dc, der, der_len); - - br_x509_pkey* pk = br_x509_decoder_get_pkey(&dc); - if (!pk) { - if (dn.data) free(dn.data); - return false; - } - - br_x509_trust_anchor ta; - memset(&ta, 0, sizeof(ta)); - - // Copy DN - 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; - - // Deep-copy public key data - switch (pk->key_type) { - case BR_KEYTYPE_RSA: { - ta.pkey.key_type = BR_KEYTYPE_RSA; - ta.pkey.key.rsa.nlen = pk->key.rsa.nlen; - ta.pkey.key.rsa.n = (unsigned char*)malloc(pk->key.rsa.nlen); - if (ta.pkey.key.rsa.n) memcpy(ta.pkey.key.rsa.n, pk->key.rsa.n, pk->key.rsa.nlen); - ta.pkey.key.rsa.elen = pk->key.rsa.elen; - ta.pkey.key.rsa.e = (unsigned char*)malloc(pk->key.rsa.elen); - if (ta.pkey.key.rsa.e) memcpy(ta.pkey.key.rsa.e, pk->key.rsa.e, pk->key.rsa.elen); - break; - } - case BR_KEYTYPE_EC: { - ta.pkey.key_type = BR_KEYTYPE_EC; - ta.pkey.key.ec.curve = pk->key.ec.curve; - ta.pkey.key.ec.qlen = pk->key.ec.qlen; - ta.pkey.key.ec.q = (unsigned char*)malloc(pk->key.ec.qlen); - if (ta.pkey.key.ec.q) memcpy(ta.pkey.key.ec.q, pk->key.ec.q, pk->key.ec.qlen); - break; - } - default: - if (dn.data) free(dn.data); - return false; - } - - ta_add(tas, &ta); - return true; -} - -static TrustAnchors load_trust_anchors(bool verbose) { - TrustAnchors tas = {nullptr, 0, 0}; - - int fh = zenith::open("0:/etc/ca-certificates.crt"); - if (fh < 0) { - printf("Warning: could not open CA certificate bundle\n"); - return tas; - } - - uint64_t fsize = zenith::getsize(fh); - if (fsize == 0 || fsize > 512 * 1024) { - zenith::close(fh); - printf("Warning: CA cert file invalid size\n"); - return tas; - } - - unsigned char* pem = (unsigned char*)malloc(fsize + 1); - if (!pem) { - zenith::close(fh); - printf("Warning: out of memory loading CA certs\n"); - return tas; - } - - zenith::read(fh, pem, 0, fsize); - zenith::close(fh); - pem[fsize] = 0; - - // Parse PEM -> DER certificates -> trust anchors - br_pem_decoder_context pc; - br_pem_decoder_init(&pc); - - DerAccum der = {nullptr, 0, 0}; - bool inCert = false; - - size_t offset = 0; - while (offset < fsize) { - size_t pushed = br_pem_decoder_push(&pc, pem + offset, fsize - offset); - offset += pushed; - - int event = br_pem_decoder_event(&pc); - if (event == BR_PEM_BEGIN_OBJ) { - const char* name = br_pem_decoder_name(&pc); - inCert = (strcmp(name, "CERTIFICATE") == 0); - if (inCert) { - der.len = 0; - br_pem_decoder_setdest(&pc, der_append, &der); - } else { - br_pem_decoder_setdest(&pc, nullptr, nullptr); - } - } else if (event == BR_PEM_END_OBJ) { - if (inCert && der.len > 0) { - process_cert_der(&tas, der.data, der.len); - } - inCert = false; - } else if (event == BR_PEM_ERROR) { - break; - } - } - - if (der.data) free(der.data); - free(pem); - - if (verbose) { - char msg[64]; - snprintf(msg, sizeof(msg), "Loaded %u trust anchors\n", (unsigned)tas.count); - zenith::print(msg); - } - - return tas; -} - -// ---- Time conversion for certificate validation ---- - -// Returns days since January 1, 0 AD (Gregorian) and seconds within the day. -// This is the format BearSSL's br_x509_minimal_set_time() expects. -static void get_bearssl_time(uint32_t* days, uint32_t* seconds) { - Zenith::DateTime dt; - zenith::gettime(&dt); - - int y = dt.Year; - int m = dt.Month; - int d = dt.Day; - - // Days from year 0 to start of year y (Gregorian proleptic calendar) - uint32_t total_days = 365 * (uint32_t)y - + (uint32_t)(y / 4) - - (uint32_t)(y / 100) - + (uint32_t)(y / 400); - - // Add days for completed months in this year - const int mdays[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - for (int mo = 1; mo < m && mo <= 12; mo++) { - total_days += mdays[mo]; - } - // Leap day for this year if we're past February - bool leap = (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)); - if (leap && m > 2) total_days++; - - total_days += d - 1; - - *days = total_days; - *seconds = (uint32_t)(dt.Hour * 3600 + dt.Minute * 60 + dt.Second); -} - -// ---- TLS I/O loop ---- - -static int tls_send_all(int fd, const unsigned char* data, size_t len) { - size_t sent = 0; - uint64_t deadline = zenith::get_milliseconds() + 15000; - while (sent < len) { - int r = zenith::send(fd, data + sent, (uint32_t)(len - sent)); - if (r > 0) { - sent += r; - deadline = zenith::get_milliseconds() + 15000; - } else if (r < 0) { - return -1; - } else { - if (zenith::get_milliseconds() >= deadline) return -1; - zenith::sleep_ms(1); - } - } - return (int)sent; -} - -static int tls_recv_some(int fd, unsigned char* buf, size_t maxlen) { - uint64_t deadline = zenith::get_milliseconds() + 15000; - while (true) { - int r = zenith::recv(fd, buf, (uint32_t)maxlen); - if (r > 0) return r; - if (r < 0) return -1; - if (zenith::get_milliseconds() >= deadline) return -1; - zenith::sleep_ms(1); - } -} - -// Run BearSSL I/O loop until handshake + app data exchange is done -// Returns: number of response bytes in respBuf, or -1 on error -static int tls_exchange(int fd, br_ssl_engine_context* eng, - const char* request, int reqLen, - char* respBuf, int respMax, bool verbose) { - bool requestSent = false; - int respLen = 0; - uint64_t deadline = zenith::get_milliseconds() + 30000; - - while (true) { - unsigned state = br_ssl_engine_current_state(eng); - - if (state & BR_SSL_CLOSED) { - int err = br_ssl_engine_last_error(eng); - if (err != BR_ERR_OK && err != BR_ERR_IO) { - char msg[64]; - snprintf(msg, sizeof(msg), "TLS error: %d\n", err); - zenith::print(msg); - if (respLen == 0) return -1; - } - return respLen; - } - - // Check for keyboard abort - if (zenith::is_key_available()) { - Zenith::KeyEvent ev; - zenith::getkey(&ev); - if (ev.pressed && ev.ctrl && ev.ascii == 'q') { - br_ssl_engine_close(eng); - return respLen > 0 ? respLen : -1; - } - } - - // Send record data to network - if (state & BR_SSL_SENDREC) { - size_t len; - unsigned char* buf = br_ssl_engine_sendrec_buf(eng, &len); - int sent = tls_send_all(fd, buf, len); - if (sent < 0) { - br_ssl_engine_close(eng); - return respLen > 0 ? respLen : -1; - } - br_ssl_engine_sendrec_ack(eng, len); - deadline = zenith::get_milliseconds() + 30000; - continue; - } - - // Read application data from TLS - if (state & BR_SSL_RECVAPP) { - size_t len; - unsigned char* buf = br_ssl_engine_recvapp_buf(eng, &len); - size_t toCopy = len; - if (respLen + (int)toCopy > respMax - 1) - toCopy = respMax - 1 - respLen; - if (toCopy > 0) { - memcpy(respBuf + respLen, buf, toCopy); - respLen += toCopy; - } - br_ssl_engine_recvapp_ack(eng, len); - deadline = zenith::get_milliseconds() + 30000; - continue; - } - - // Send application data (HTTP request) into TLS - if ((state & BR_SSL_SENDAPP) && !requestSent) { - size_t len; - unsigned char* buf = br_ssl_engine_sendapp_buf(eng, &len); - size_t toWrite = (size_t)reqLen; - if (toWrite > len) toWrite = len; - memcpy(buf, request, toWrite); - br_ssl_engine_sendapp_ack(eng, toWrite); - br_ssl_engine_flush(eng, 0); - requestSent = true; - deadline = zenith::get_milliseconds() + 30000; - continue; - } - - // Receive record data from network - if (state & BR_SSL_RECVREC) { - size_t len; - unsigned char* buf = br_ssl_engine_recvrec_buf(eng, &len); - int got = tls_recv_some(fd, buf, len); - if (got < 0) { - br_ssl_engine_close(eng); - return respLen > 0 ? respLen : -1; - } - br_ssl_engine_recvrec_ack(eng, got); - deadline = zenith::get_milliseconds() + 30000; - continue; - } - - // Nothing actionable — wait - if (zenith::get_milliseconds() >= deadline) { - return respLen > 0 ? respLen : -1; - } - zenith::sleep_ms(1); +static bool check_keyboard_abort() { + if (zenith::is_key_available()) { + Zenith::KeyEvent ev; + zenith::getkey(&ev); + if (ev.pressed && ev.ctrl && ev.ascii == 'q') return true; } + return false; } // ---- Plain HTTP exchange (no TLS) ---- @@ -706,19 +355,6 @@ extern "C" void _start() { zenith::print(msg); } - // Create and connect socket - int fd = zenith::socket(Zenith::SOCK_TCP); - if (fd < 0) { - zenith::print("Error: failed to create socket\n"); - zenith::exit(1); - } - - if (zenith::connect(fd, serverIp, port) < 0) { - zenith::print("Error: connection failed\n"); - zenith::closesocket(fd); - zenith::exit(1); - } - // Build HTTP request char request[1024]; int reqLen = snprintf(request, sizeof(request), @@ -740,7 +376,6 @@ extern "C" void _start() { char* respBuf = (char*)malloc(RESP_MAX); if (!respBuf) { zenith::print("Error: out of memory\n"); - zenith::closesocket(fd); zenith::exit(1); } @@ -748,35 +383,21 @@ extern "C" void _start() { if (useHttps) { // ---- TLS handshake and exchange ---- - - // Load trust anchors - TrustAnchors tas = load_trust_anchors(verbose); + tls::TrustAnchors tas = tls::load_trust_anchors(); + if (verbose) { + char msg[64]; + snprintf(msg, sizeof(msg), "Loaded %u trust anchors\n", (unsigned)tas.count); + zenith::print(msg); + } if (tas.count == 0) { zenith::print("Error: no trust anchors loaded\n"); free(respBuf); - zenith::closesocket(fd); zenith::exit(1); } - // Initialize BearSSL client - // Allocate contexts on heap to avoid stack overflow - br_ssl_client_context* cc = (br_ssl_client_context*)malloc(sizeof(br_ssl_client_context)); - br_x509_minimal_context* xc = (br_x509_minimal_context*)malloc(sizeof(br_x509_minimal_context)); - if (!cc || !xc) { - zenith::print("Error: out of memory for TLS context\n"); - free(respBuf); - zenith::closesocket(fd); - zenith::exit(1); - } - - br_ssl_client_init_full(cc, xc, tas.anchors, tas.count); - - // Set time for certificate validation - uint32_t days, secs; - get_bearssl_time(&days, &secs); - br_x509_minimal_set_time(xc, days, secs); - if (verbose) { + uint32_t days, secs; + tls::get_bearssl_time(&days, &secs); Zenith::DateTime dt; zenith::gettime(&dt); char tmsg[128]; @@ -785,54 +406,39 @@ extern "C" void _start() { (unsigned)dt.Hour, (unsigned)dt.Minute, (unsigned)dt.Second, (unsigned)days, (unsigned)secs); zenith::print(tmsg); - } - - // Seed the PRNG with RDRAND entropy - unsigned char seed[32]; - zenith::getrandom(seed, sizeof(seed)); - br_ssl_engine_set_buffer(&cc->eng, malloc(BR_SSL_BUFSIZE_BIDI), - BR_SSL_BUFSIZE_BIDI, 1); - - // Inject entropy - br_ssl_engine_inject_entropy(&cc->eng, seed, sizeof(seed)); - - // Reset client with server name for SNI - if (!br_ssl_client_reset(cc, hostStr, 0)) { - int err = br_ssl_engine_last_error(&cc->eng); - char msg[64]; - snprintf(msg, sizeof(msg), "Error: TLS reset failed (err=%d)\n", err); - zenith::print(msg); - free(respBuf); - zenith::closesocket(fd); - zenith::exit(1); - } - - if (verbose) { zenith::print("TLS handshake...\n"); } - // Run TLS I/O loop - respLen = tls_exchange(fd, &cc->eng, request, reqLen, - respBuf, RESP_MAX, verbose); + respLen = tls::https_fetch(hostStr, serverIp, port, + request, reqLen, tas, + respBuf, RESP_MAX, check_keyboard_abort); if (verbose && respLen > 0) { zenith::print("TLS connection established\n"); } - - // Cleanup (we don't bother freeing everything since we're exiting) } else { // ---- Plain HTTP ---- + int fd = zenith::socket(Zenith::SOCK_TCP); + if (fd < 0) { + zenith::print("Error: failed to create socket\n"); + zenith::exit(1); + } + + if (zenith::connect(fd, serverIp, port) < 0) { + zenith::print("Error: connection failed\n"); + zenith::closesocket(fd); + zenith::exit(1); + } + respLen = plain_http_exchange(fd, request, reqLen, respBuf, RESP_MAX); + zenith::closesocket(fd); if (respLen == -2) { zenith::print("\nAborted.\n"); - zenith::closesocket(fd); zenith::exit(0); } } - zenith::closesocket(fd); - if (respLen <= 0) { zenith::print("Error: no response received\n"); zenith::exit(1); diff --git a/programs/src/weather/Makefile b/programs/src/weather/Makefile new file mode 100644 index 0000000..6b98f19 --- /dev/null +++ b/programs/src/weather/Makefile @@ -0,0 +1,94 @@ +# Makefile for weather (standalone Weather GUI client) on ZenithOS +# Copyright (c) 2026 Daniel Hammer + +MAKEFLAGS += -rR +.SUFFIXES: + +# ---- Toolchain ---- + +TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf- +ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),) + CXX := $(TOOLCHAIN_PREFIX)g++ +else + CXX := g++ +endif + +# ---- Paths ---- + +BEARSSL := ../../lib/bearssl +TLS_LIB := ../../lib/tls +LIBC_LIB := ../../lib/libc +LIBC_INC := ../../include/libc +PROG_INC := ../../include +LINK_LD := ../../link.ld +BINDIR := ../../bin +OBJDIR := obj + +# ---- Compiler flags ---- + +CXXFLAGS := \ + -std=gnu++20 \ + -g -O2 -pipe \ + -Wall \ + -Wextra \ + -Wno-unused-parameter \ + -Wno-unused-function \ + -nostdinc \ + -ffreestanding \ + -fno-stack-protector \ + -fno-stack-check \ + -fno-PIC \ + -fno-rtti \ + -fno-exceptions \ + -ffunction-sections \ + -fdata-sections \ + -m64 \ + -march=x86-64 \ + -msse \ + -msse2 \ + -mno-red-zone \ + -mcmodel=small \ + -I $(PROG_INC) \ + -isystem $(LIBC_INC) \ + -I $(BEARSSL)/inc \ + -isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \ + -isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include + +# ---- Linker flags ---- + +LDFLAGS := \ + -nostdlib \ + -static \ + -Wl,--build-id=none \ + -Wl,--gc-sections \ + -Wl,-m,elf_x86_64 \ + -z max-page-size=0x1000 \ + -T $(LINK_LD) + +# ---- Libraries ---- + +LIBS := $(TLS_LIB)/libtls.a $(BEARSSL)/libbearssl.a $(LIBC_LIB)/liblibc.a + +# ---- Source files ---- + +SRCS := main.cpp stb_truetype_impl.cpp +OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) + +# ---- Target ---- + +TARGET := $(BINDIR)/os/weather.elf + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJS) $(LIBS) $(LINK_LD) Makefile + mkdir -p $(BINDIR)/os + $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@ + +$(OBJDIR)/%.o: %.cpp Makefile + mkdir -p $(OBJDIR) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/src/weather/main.cpp b/programs/src/weather/main.cpp new file mode 100644 index 0000000..60ce7bf --- /dev/null +++ b/programs/src/weather/main.cpp @@ -0,0 +1,575 @@ +/* + * main.cpp + * ZenithOS Weather app - standalone Window Server process + * Fetches current weather from wttr.in via HTTPS (BearSSL) + * Displays temperature, description, feels like, and location + * Copyright (c) 2026 Daniel Hammer + */ + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +} + +using namespace gui; +using namespace gui::colors; + +// ============================================================================ +// Constants +// ============================================================================ + +static constexpr int INIT_W = 380; +static constexpr int INIT_H = 280; +static constexpr int HEADER_H = 160; +static constexpr int FOOTER_H = 50; +static constexpr int ICON_SIZE = 80; +static constexpr int ICON_X = 28; +static constexpr int ICON_Y = 40; +static constexpr int INFO_X = ICON_X + ICON_SIZE + 20; // 128 +static constexpr int TEMP_Y = 40; +static int TEMP_SIZE = 40; +static constexpr int DESC_Y = 92; +static int DESC_SIZE = 17; +static constexpr int FEELS_Y = 116; +static int LABEL_SIZE = 15; +static constexpr int RESP_MAX = 65536; + +static const char WTTR_HOST[] = "wttr.in"; + +// ============================================================================ +// App state +// ============================================================================ + +enum class AppPhase { IDLE, LOADING, DONE, ERR }; + +static AppPhase g_phase = AppPhase::IDLE; +static char g_temp[32] = {}; +static char g_desc[128] = {}; +static char g_feels[48] = {}; +static char g_location[128] = {}; +static char g_status[256] = {}; +static int g_win_w = INIT_W; +static int g_win_h = INIT_H; + +static char* g_resp_buf = nullptr; + +// Fonts +static TrueTypeFont* g_font = nullptr; // Roboto Medium +static TrueTypeFont* g_font_bold = nullptr; // Roboto Bold + +// Weather icon (loaded per-fetch based on weather code) +static SvgIcon g_icon = {nullptr, 0, 0}; +static char g_icon_name[64] = {}; + +// TLS state (lazy-init on first fetch) +static bool g_tls_ready = false; +static uint32_t g_server_ip = 0; +static tls::TrustAnchors g_tas = {nullptr, 0, 0}; + +// ============================================================================ +// Pixel buffer helpers +// ============================================================================ + +static void px_fill(uint32_t* px, int bw, int bh, + int x, int y, int w, int h, Color c) { + uint32_t v = c.to_pixel(); + int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y; + int x1 = x + w > bw ? bw : x + w; + int y1 = y + h > bh ? bh : y + h; + for (int row = y0; row < y1; row++) + for (int col = x0; col < x1; col++) + px[row * bw + col] = v; +} + +static void px_hline(uint32_t* px, int bw, int bh, int x, int y, int len, Color c) { + if (y < 0 || y >= bh) return; + uint32_t v = c.to_pixel(); + int x1 = x + len > bw ? bw : x + len; + for (int col = x < 0 ? 0 : x; col < x1; col++) + px[y * bw + col] = v; +} + +// Integer square root (Newton's method). +static int isqrt(int n) { + if (n <= 0) return 0; + int x = n, y = (x + 1) / 2; + while (y < x) { x = y; y = (x + n / x) / 2; } + return x; +} + +// Fill a rounded rectangle directly onto the pixel buffer. +static void px_fill_rounded(uint32_t* px, int bw, int bh, + int x, int y, int w, int h, int r, Color c) { + if (r <= 0) { px_fill(px, bw, bh, x, y, w, h, c); return; } + if (r > w / 2) r = w / 2; + if (r > h / 2) r = h / 2; + + uint32_t v = c.to_pixel(); + // Corner arc centers + int cx_l = x + r; + int cx_r = x + w - 1 - r; + + for (int row = y; row < y + h; row++) { + if (row < 0 || row >= bh) continue; + + int left, right; + if (row < y + r) { + int dy = y + r - row; // 1..r + int dx = isqrt(r * r - dy * dy); + left = cx_l - dx; + right = cx_r + dx + 1; + } else if (row >= y + h - r) { + int dy = row - (y + h - 1 - r); // 1..r + int dx = isqrt(r * r - dy * dy); + left = cx_l - dx; + right = cx_r + dx + 1; + } else { + left = x; + right = x + w; + } + + if (left < 0) left = 0; + if (right > bw) right = bw; + for (int col = left; col < right; col++) + px[row * bw + col] = v; + } +} + +// Alpha-composite an SvgIcon onto the pixel buffer. +static void draw_icon(uint32_t* px, int bw, int bh, + int dx, int dy, const SvgIcon& icon) { + if (!icon.pixels) return; + for (int row = 0; row < icon.height; row++) { + int ty = dy + row; + if (ty < 0 || ty >= bh) continue; + for (int col = 0; col < icon.width; col++) { + int tx = dx + col; + if (tx < 0 || tx >= bw) continue; + uint32_t s = icon.pixels[row * icon.width + col]; + uint8_t sa = (s >> 24) & 0xFF; + if (sa == 0) continue; + if (sa == 255) { px[ty * bw + tx] = s; continue; } + uint8_t sr = (s >> 16) & 0xFF, sg = (s >> 8) & 0xFF, sb = s & 0xFF; + uint32_t d = px[ty * bw + tx]; + uint8_t dr = (d >> 16) & 0xFF, dg = (d >> 8) & 0xFF, db = d & 0xFF; + uint32_t inv = 255 - sa; + px[ty * bw + tx] = 0xFF000000u + | (((sr * sa + dr * inv) / 255u) << 16) + | (((sg * sa + dg * inv) / 255u) << 8) + | ((sb * sa + db * inv) / 255u); + } + } +} + +// ============================================================================ +// 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 +// ============================================================================ + +// Extract value of a simple JSON string field: "key":"value" +static int extract_json_string(const char* buf, int len, const char* key, + char* out, int maxOut) { + int klen = (int)strlen(key); + for (int i = 0; i < len - klen - 3; i++) { + if (buf[i] != '"') continue; + if (memcmp(buf + i + 1, key, klen) != 0) continue; + if (buf[i + 1 + klen] != '"') continue; + if (buf[i + 2 + klen] != ':') continue; + + int p = i + 3 + klen; + while (p < len && (buf[p]==' ' || buf[p]=='\t')) p++; + if (p >= len || buf[p] != '"') continue; + p++; + + int j = 0; + while (p < len && j < maxOut - 1 && buf[p] != '"') { + if (buf[p] == '\\' && p + 1 < len) { p++; out[j++] = buf[p]; } + else out[j++] = buf[p]; + p++; + } + out[j] = '\0'; + return j; + } + out[0] = '\0'; + return 0; +} + +// Find the first occurrence of needle in buf[0..len-1]. Returns offset or -1. +static int find_substr(const char* buf, int len, const char* needle) { + int nlen = (int)strlen(needle); + if (nlen > len) return -1; + for (int i = 0; i <= len - nlen; i++) { + if (memcmp(buf + i, needle, nlen) == 0) return i; + } + return -1; +} + +// Extract the "value" string within a named JSON array-of-objects field. +// e.g., "weatherDesc":[{"value":"Partly cloudy"}] → "Partly cloudy" +// Works by finding section_key first, then the first "value" after it. +static int extract_array_value(const char* buf, int len, const char* section_key, + char* out, int maxOut) { + int pos = find_substr(buf, len, section_key); + if (pos < 0) { out[0] = '\0'; return 0; } + return extract_json_string(buf + pos, len - pos, "value", out, maxOut); +} + +// Parse a signed integer from a decimal string. +static int parse_int(const char* s) { + int sign = 1, val = 0, i = 0; + if (s[0] == '-') { sign = -1; i = 1; } + while (s[i] >= '0' && s[i] <= '9') val = val * 10 + (s[i++] - '0'); + return sign * val; +} + +// ============================================================================ +// Weather code → icon filename +// Maps wttr.in WMO-style weather codes to Flat-Remix panel icon names. +// ============================================================================ + +static const char* weather_code_to_icon(int code) { + switch (code) { + case 113: return "weather-clear.svg"; + case 116: return "weather-few-clouds.svg"; + case 119: return "weather-clouds.svg"; + case 122: return "weather-overcast.svg"; + case 143: return "weather-mist.svg"; + case 248: case 260: return "weather-fog.svg"; + case 176: case 263: case 266: case 353: return "weather-showers-scattered.svg"; + case 293: case 296: case 299: case 302: + case 305: case 308: case 356: case 359: return "weather-showers.svg"; + case 179: case 362: case 365: case 368: return "weather-snow-scattered.svg"; + case 323: case 326: case 329: case 332: + case 335: case 338: case 371: case 374: return "weather-snow.svg"; + case 227: case 230: return "weather-snow.svg"; + case 182: case 311: case 314: case 317: case 320: return "weather-snow-rain.svg"; + case 185: case 281: case 284: return "weather-freezing-rain.svg"; + case 350: case 377: return "weather-hail.svg"; + case 200: case 386: case 389: case 392: case 395: return "weather-storm.svg"; + default: return "weather-none-available.svg"; + } +} + +// Load the weather icon for the given icon filename (caches the last result). +static void load_weather_icon(const char* icon_name) { + if (strcmp(g_icon_name, icon_name) == 0 && g_icon.pixels) return; + + if (g_icon.pixels) svg_free(g_icon); + + static char path[80]; + snprintf(path, sizeof(path), "0:/icons/%s", icon_name); + // ColorScheme-Text paths (main shape) use this fill_color; accent classes + // (ColorScheme-Highlight=blue, ColorScheme-NeutralText=yellow, etc.) use their + // CSS-defined colors as parsed from the SVG's