feat: e100e Ethernet driver, ramdisk reorganization, shell rewrite, web server/client, and more

This commit is contained in:
2026-02-19 01:18:11 +01:00
parent 1a5d943649
commit d355d376f9
63 changed files with 5368 additions and 614 deletions
+562
View File
@@ -0,0 +1,562 @@
/*
* main.cpp
* HTTP/1.0 server for ZenithOS
* Usage: run httpd.elf [port] (default: 80)
* Serves a built-in index page and files from the VFS
* Copyright (c) 2025-2026 Daniel Hammer
*/
#include <zenith/syscall.h>
#include <zenith/string.h>
using zenith::slen;
using zenith::streq;
using zenith::starts_with;
using zenith::skip_spaces;
// ---- Minimal snprintf (no libc available) ----
using va_list = __builtin_va_list;
#define va_start __builtin_va_start
#define va_end __builtin_va_end
#define va_arg __builtin_va_arg
struct PfState {
char* buf;
int pos;
int max;
};
static void pf_putc(PfState* st, char c) {
if (st->pos < st->max) st->buf[st->pos] = c;
st->pos++;
}
static void pf_putnum(PfState* st, unsigned long val, int base, int width, char pad, int neg) {
char tmp[24];
int i = 0;
const char* digits = "0123456789abcdef";
if (val == 0) { tmp[i++] = '0'; }
else { while (val > 0) { tmp[i++] = digits[val % base]; val /= base; } }
int total = (neg ? 1 : 0) + i;
if (neg && pad == '0') pf_putc(st, '-');
for (int w = total; w < width; w++) pf_putc(st, pad);
if (neg && pad != '0') pf_putc(st, '-');
while (i > 0) pf_putc(st, tmp[--i]);
}
static int vsnprintf(char* buf, int size, const char* fmt, va_list ap) {
PfState st;
st.buf = buf;
st.pos = 0;
st.max = size > 0 ? size - 1 : 0;
while (*fmt) {
if (*fmt != '%') { pf_putc(&st, *fmt++); continue; }
fmt++;
char pad = ' ';
if (*fmt == '0') { pad = '0'; fmt++; }
int width = 0;
while (*fmt >= '0' && *fmt <= '9') { width = width * 10 + (*fmt - '0'); fmt++; }
if (*fmt == 'l') fmt++;
switch (*fmt) {
case 'd': case 'i': {
long val = va_arg(ap, int);
int neg = 0; unsigned long uval;
if (val < 0) { neg = 1; uval = (unsigned long)(-val); }
else uval = (unsigned long)val;
pf_putnum(&st, uval, 10, width, pad, neg);
break;
}
case 'u': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 10, width, pad, 0); break; }
case 'x': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 16, width, pad, 0); break; }
case 's': {
const char* s = va_arg(ap, const char*);
if (!s) s = "(null)";
int slen = 0; while (s[slen]) slen++;
for (int w = slen; w < width; w++) pf_putc(&st, ' ');
for (int j = 0; j < slen; j++) pf_putc(&st, s[j]);
break;
}
case 'c': { char c = (char)va_arg(ap, int); pf_putc(&st, c); break; }
case '%': pf_putc(&st, '%'); break;
default: pf_putc(&st, '%'); pf_putc(&st, *fmt); break;
}
if (*fmt) fmt++;
}
if (size > 0) {
if (st.pos < size) st.buf[st.pos] = '\0';
else st.buf[size - 1] = '\0';
}
return st.pos;
}
static int snprintf(char* buf, int size, const char* fmt, ...) {
va_list ap;
va_start(ap, fmt);
int ret = vsnprintf(buf, size, fmt, ap);
va_end(ap);
return ret;
}
// ---- IP/port parsing ----
static bool parse_uint16(const char* s, uint16_t* out) {
uint32_t val = 0;
if (*s == '\0') return false;
while (*s) {
if (*s < '0' || *s > '9') return false;
val = val * 10 + (*s - '0');
if (val > 65535) return false;
s++;
}
*out = (uint16_t)val;
return true;
}
// ---- Content type detection ----
static bool ends_with(const char* str, const char* suffix) {
int sl = slen(str);
int xl = slen(suffix);
if (xl > sl) return false;
for (int i = 0; i < xl; i++) {
char a = str[sl - xl + i];
char b = suffix[i];
// Case-insensitive for file extensions
if (a >= 'A' && a <= 'Z') a += 32;
if (b >= 'A' && b <= 'Z') b += 32;
if (a != b) return false;
}
return true;
}
static const char* content_type_for(const char* path) {
if (ends_with(path, ".html") || ends_with(path, ".htm"))
return "text/html";
if (ends_with(path, ".txt"))
return "text/plain";
if (ends_with(path, ".css"))
return "text/css";
if (ends_with(path, ".js"))
return "application/javascript";
return "application/octet-stream";
}
// ---- HTTP response helpers ----
// Send a complete HTTP response with headers and body
static void send_response(int clientFd, int statusCode, const char* statusText,
const char* contentType, const char* body, int bodyLen) {
char header[512];
int hlen = snprintf(header, sizeof(header),
"HTTP/1.0 %d %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %d\r\n"
"Connection: close\r\n"
"Server: ZenithOS/1.0\r\n"
"\r\n",
statusCode, statusText, contentType, bodyLen);
zenith::send(clientFd, header, hlen);
if (bodyLen > 0) {
zenith::send(clientFd, body, bodyLen);
}
}
// Send a file from the VFS
static int send_file_response(int clientFd, const char* vfsPath, const char* urlPath) {
int handle = zenith::open(vfsPath);
if (handle < 0) return -1;
uint64_t size = zenith::getsize(handle);
const char* ctype = content_type_for(urlPath);
// Send header
char header[512];
int hlen = snprintf(header, sizeof(header),
"HTTP/1.0 200 OK\r\n"
"Content-Type: %s\r\n"
"Content-Length: %u\r\n"
"Connection: close\r\n"
"Server: ZenithOS/1.0\r\n"
"\r\n",
ctype, (unsigned)size);
zenith::send(clientFd, header, hlen);
// Send file body in chunks
uint8_t buf[512];
uint64_t offset = 0;
while (offset < size) {
uint64_t chunk = size - offset;
if (chunk > sizeof(buf)) chunk = sizeof(buf);
int bytesRead = zenith::read(handle, buf, offset, chunk);
if (bytesRead <= 0) break;
zenith::send(clientFd, buf, bytesRead);
offset += bytesRead;
}
zenith::close(handle);
return (int)size;
}
// ---- Request parsing ----
// Extract the request path from "GET /path HTTP/1.x\r\n..."
// Returns length of path written, or -1 on parse error
static int parse_request_path(const char* req, int reqLen, char* pathOut, int pathMax) {
// Find "GET "
if (reqLen < 4) return -1;
if (req[0] != 'G' || req[1] != 'E' || req[2] != 'T' || req[3] != ' ')
return -1;
int i = 4;
int j = 0;
while (i < reqLen && req[i] != ' ' && req[i] != '\r' && req[i] != '\n') {
if (j < pathMax - 1) pathOut[j++] = req[i];
i++;
}
pathOut[j] = '\0';
return j;
}
// ---- Logging ----
static void log_request(const char* method, const char* path, int status, int bodyLen) {
// Get timestamp
Zenith::DateTime dt;
zenith::gettime(&dt);
char msg[256];
snprintf(msg, sizeof(msg), "[%02d:%02d:%02d] %s %s -> %d (%d bytes)\n",
(int)dt.Hour, (int)dt.Minute, (int)dt.Second,
method, path, status, bodyLen);
zenith::print(msg);
}
// ---- Page generators ----
static int generate_index_page(char* buf, int bufSize) {
Zenith::SysInfo info;
zenith::get_info(&info);
uint64_t ms = zenith::get_milliseconds();
uint64_t secs = ms / 1000;
uint64_t mins = secs / 60;
uint64_t hours = mins / 60;
secs %= 60;
mins %= 60;
return snprintf(buf, bufSize,
"<!DOCTYPE html>\n"
"<html>\n"
"<head><title>ZenithOS Web Server</title></head>\n"
"<body>\n"
"<h1>ZenithOS Web Server</h1>\n"
"<p>Welcome! This page is being served by <b>httpd</b> running on ZenithOS.</p>\n"
"<h2>System Information</h2>\n"
"<table>\n"
"<tr><td><b>OS:</b></td><td>%s</td></tr>\n"
"<tr><td><b>Version:</b></td><td>%s</td></tr>\n"
"<tr><td><b>Uptime:</b></td><td>%uh %um %us</td></tr>\n"
"</table>\n"
"<h2>Browse Files</h2>\n"
"<p><a href=\"/files/\">Browse VFS files</a></p>\n"
"</body>\n"
"</html>\n",
info.osName, info.osVersion,
(unsigned)hours, (unsigned)mins, (unsigned)secs);
}
static int generate_404_page(char* buf, int bufSize, const char* path) {
return snprintf(buf, bufSize,
"<!DOCTYPE html>\n"
"<html>\n"
"<head><title>404 Not Found</title></head>\n"
"<body>\n"
"<h1>404 Not Found</h1>\n"
"<p>The requested path <code>%s</code> was not found on this server.</p>\n"
"<p><a href=\"/\">Back to home</a></p>\n"
"</body>\n"
"</html>\n",
path);
}
static int generate_dir_listing(char* buf, int bufSize, const char* urlPath, const char* vfsDir) {
int pos = 0;
pos += snprintf(buf + pos, bufSize - pos,
"<!DOCTYPE html>\n"
"<html>\n"
"<head><title>Index of %s</title></head>\n"
"<body>\n"
"<h1>Index of %s</h1>\n"
"<hr>\n"
"<ul>\n",
urlPath, urlPath);
// Add parent directory link if not at /files/
if (!streq(urlPath, "/files/")) {
pos += snprintf(buf + pos, bufSize - pos,
"<li><a href=\"..\">..</a></li>\n");
}
// List directory entries
const char* entries[64];
int count = zenith::readdir(vfsDir, entries, 64);
// Find the prefix to strip from entry names
// vfsDir is like "0:/" or "0:/subdir"
// entries come back as "subdir/file" for "0:/subdir"
// or "file" for "0:/"
// The prefix in entry names is the part after "0:/"
const char* dirRel = vfsDir + 3; // skip "0:/"
int dirRelLen = slen(dirRel);
for (int i = 0; i < count && pos < bufSize - 128; i++) {
// Extract just the filename portion
const char* name = entries[i];
// If directory is not root, entries have "dir/name" format — strip the dir prefix
if (dirRelLen > 0 && starts_with(name, dirRel)) {
name = name + dirRelLen;
if (*name == '/') name++;
}
if (*name == '\0') continue;
// Build the URL for this entry
pos += snprintf(buf + pos, bufSize - pos,
"<li><a href=\"%s%s\">%s</a></li>\n",
urlPath, name, name);
}
pos += snprintf(buf + pos, bufSize - pos,
"</ul>\n"
"<hr>\n"
"<p><i>ZenithOS httpd</i></p>\n"
"</body>\n"
"</html>\n");
return pos;
}
// ---- Request handler ----
static void handle_client(int clientFd) {
// Read request (HTTP requests are small, 4 KB is plenty)
char reqBuf[4096];
int reqLen = 0;
int idleCount = 0;
// Read until we get the full header (ends with \r\n\r\n)
while (reqLen < (int)sizeof(reqBuf) - 1) {
int r = zenith::recv(clientFd, reqBuf + reqLen, sizeof(reqBuf) - 1 - reqLen);
if (r > 0) {
reqLen += r;
idleCount = 0;
// Check if we have the full header
bool done = false;
for (int i = 0; i + 3 < reqLen; i++) {
if (reqBuf[i] == '\r' && reqBuf[i+1] == '\n' &&
reqBuf[i+2] == '\r' && reqBuf[i+3] == '\n') {
done = true;
break;
}
}
if (done) break;
} else if (r == 0) {
break; // Connection closed
} else {
idleCount++;
if (idleCount > 500) break; // Timeout
zenith::yield();
}
}
reqBuf[reqLen] = '\0';
if (reqLen == 0) {
zenith::closesocket(clientFd);
return;
}
// Parse request path
char path[256];
if (parse_request_path(reqBuf, reqLen, path, sizeof(path)) < 0) {
// Bad request
static char body[] = "<!DOCTYPE html><html><body><h1>400 Bad Request</h1></body></html>";
send_response(clientFd, 400, "Bad Request", "text/html", body, slen(body));
log_request("???", "???", 400, slen(body));
zenith::closesocket(clientFd);
return;
}
// Route the request
static char pageBuf[16384];
if (streq(path, "/")) {
// Try to serve 0:/www/index.html from disk first
int handle = zenith::open("0:/www/index.html");
if (handle >= 0) {
zenith::close(handle);
int bodyLen = send_file_response(clientFd, "0:/www/index.html", "/index.html");
log_request("GET", path, 200, bodyLen);
} else {
// Fall back to built-in index page
int bodyLen = generate_index_page(pageBuf, sizeof(pageBuf));
send_response(clientFd, 200, "OK", "text/html", pageBuf, bodyLen);
log_request("GET", path, 200, bodyLen);
}
} else if (streq(path, "/files") || streq(path, "/files/")) {
// Root directory listing
int bodyLen = generate_dir_listing(pageBuf, sizeof(pageBuf), "/files/", "0:/");
send_response(clientFd, 200, "OK", "text/html", pageBuf, bodyLen);
log_request("GET", path, 200, bodyLen);
} else if (starts_with(path, "/files/")) {
// Serve file or directory from VFS
const char* relPath = path + 7; // skip "/files/"
// Build VFS path: "0:/<relPath>"
char vfsPath[256];
int pi = 0;
vfsPath[pi++] = '0'; vfsPath[pi++] = ':'; vfsPath[pi++] = '/';
int ri = 0;
while (relPath[ri] && pi < (int)sizeof(vfsPath) - 1) {
vfsPath[pi++] = relPath[ri++];
}
// Strip trailing slash for VFS lookup
if (pi > 3 && vfsPath[pi-1] == '/') pi--;
vfsPath[pi] = '\0';
// Try to open as file first
int handle = zenith::open(vfsPath);
if (handle >= 0) {
// It's a file — serve it
zenith::close(handle);
int bodyLen = send_file_response(clientFd, vfsPath, path);
if (bodyLen >= 0) {
log_request("GET", path, 200, bodyLen);
} else {
static char body[] = "<!DOCTYPE html><html><body><h1>500 Internal Server Error</h1></body></html>";
send_response(clientFd, 500, "Internal Server Error", "text/html", body, slen(body));
log_request("GET", path, 500, slen(body));
}
} else {
// Try as directory
const char* entries[64];
int count = zenith::readdir(vfsPath, entries, 64);
if (count >= 0) {
// Make sure urlPath ends with /
char urlPath[256];
int up = 0;
int pp = 0;
while (path[pp] && up < (int)sizeof(urlPath) - 2) urlPath[up++] = path[pp++];
if (up > 0 && urlPath[up-1] != '/') urlPath[up++] = '/';
urlPath[up] = '\0';
int bodyLen = generate_dir_listing(pageBuf, sizeof(pageBuf), urlPath, vfsPath);
send_response(clientFd, 200, "OK", "text/html", pageBuf, bodyLen);
log_request("GET", path, 200, bodyLen);
} else {
// Not found
int bodyLen = generate_404_page(pageBuf, sizeof(pageBuf), path);
send_response(clientFd, 404, "Not Found", "text/html", pageBuf, bodyLen);
log_request("GET", path, 404, bodyLen);
}
}
} else {
// 404 for anything else
int bodyLen = generate_404_page(pageBuf, sizeof(pageBuf), path);
send_response(clientFd, 404, "Not Found", "text/html", pageBuf, bodyLen);
log_request("GET", path, 404, bodyLen);
}
zenith::closesocket(clientFd);
}
// ---- Entry point ----
extern "C" void _start() {
// Parse arguments: [port]
char argbuf[64];
zenith::getargs(argbuf, sizeof(argbuf));
const char* arg = skip_spaces(argbuf);
uint16_t port = 80;
if (*arg) {
if (!parse_uint16(arg, &port)) {
zenith::print("Invalid port: ");
zenith::print(arg);
zenith::putchar('\n');
zenith::exit(1);
}
}
// Create server socket
int listenFd = zenith::socket(Zenith::SOCK_TCP);
if (listenFd < 0) {
zenith::print("Error: failed to create socket\n");
zenith::exit(1);
}
// Bind
if (zenith::bind(listenFd, port) < 0) {
zenith::print("Error: failed to bind to port ");
char tmp[8];
snprintf(tmp, sizeof(tmp), "%d", (int)port);
zenith::print(tmp);
zenith::putchar('\n');
zenith::closesocket(listenFd);
zenith::exit(1);
}
// Listen
if (zenith::listen(listenFd) < 0) {
zenith::print("Error: failed to listen\n");
zenith::closesocket(listenFd);
zenith::exit(1);
}
char msg[128];
snprintf(msg, sizeof(msg), "ZenithOS httpd listening on port %d\n", (int)port);
zenith::print(msg);
zenith::print("Press Ctrl+Q between requests to stop.\n\n");
bool running = true;
while (running) {
// Check for Ctrl+Q before blocking on accept
while (zenith::is_key_available()) {
Zenith::KeyEvent ev;
zenith::getkey(&ev);
if (ev.pressed && ev.ctrl && ev.ascii == 'q') {
running = false;
break;
}
}
if (!running) break;
// Accept next client (blocks until a connection arrives)
int clientFd = zenith::accept(listenFd);
if (clientFd < 0) {
zenith::print("Warning: accept failed\n");
zenith::yield();
continue;
}
handle_client(clientFd);
// After serving, check for Ctrl+Q
while (zenith::is_key_available()) {
Zenith::KeyEvent ev;
zenith::getkey(&ev);
if (ev.pressed && ev.ctrl && ev.ascii == 'q') {
running = false;
break;
}
}
}
zenith::print("\nShutting down httpd...\n");
zenith::closesocket(listenFd);
zenith::exit(0);
}