// ============================================================================= // Boko — Async Web Framework for Bux (inspired by FastAPI) // ============================================================================= module Boko { import Std::Io::{PrintLine, Print, PrintInt}; import Std::Net::{Net_Create, Net_SetReuse, Net_Bind, Net_Listen, Net_Accept, Net_Send, Net_Recv, Net_Close, Net_LastError}; import Std::String::{String_Len, String_Eq, String_StartsWith, String_Contains, String_Offset}; import Std::Task::{Task_Join, TaskHandle}; extern func bux_alloc(size: uint) -> *void; extern func bux_strlen(s: String) -> uint; extern func bux_strcmp(a: String, b: String) -> int; extern func bux_strstr(haystack: String, needle: String) -> String; extern func bux_str_slice(s: String, start: uint, len: uint) -> String; extern func bux_str_split_count(s: String, delim: String) -> uint; extern func bux_str_split_part(s: String, delim: String, index: uint) -> String; extern func bux_sb_new(initial_cap: uint) -> *void; extern func bux_sb_append(sb: *void, s: String); extern func bux_sb_append_int(sb: *void, n: int64); extern func bux_sb_append_char(sb: *void, c: char8); extern func bux_sb_build(sb: *void) -> String; extern func bux_sb_free(sb: *void); extern func bux_base64url_encode(data: String, len: int) -> String; extern func bux_base64url_decode(data: String, len: int, outlen: *int) -> String; // ============================================================================= // HTTP Methods // ============================================================================= enum HttpVerb { GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, } // ============================================================================= // Request — parsed incoming HTTP request // ============================================================================= struct Request { method: HttpVerb, path: String, body: String, headerKeys: *String, headerValues: *String, headerCount: int, queryKeys: *String, queryValues: *String, queryCount: int, pathParamKeys: *String, pathParamValues: *String, pathParamCount: int, } // ============================================================================= // Response — outgoing HTTP response // ============================================================================= struct Response { statusCode: int, contentType: String, body: String, extraHeaders: String, } // ============================================================================= // App — server instance // ============================================================================= struct App { port: int, threadCount: int, serverName: String, } // ============================================================================= // Request helpers // ============================================================================= func Request_GetHeader(req: *Request, name: String) -> String { var i: int = 0; while i < req.headerCount { if bux_strcmp(req.headerKeys[i], name) == 0 { return req.headerValues[i]; } i = i + 1; } return ""; } func Request_GetQuery(req: *Request, name: String) -> String { var i: int = 0; while i < req.queryCount { if bux_strcmp(req.queryKeys[i], name) == 0 { return req.queryValues[i]; } i = i + 1; } return ""; } func Request_GetPathParam(req: *Request, name: String) -> String { var i: int = 0; while i < req.pathParamCount { if bux_strcmp(req.pathParamKeys[i], name) == 0 { return req.pathParamValues[i]; } i = i + 1; } return ""; } // ============================================================================= // Response constructors // ============================================================================= func Response_New(status: int, contentType: String, body: String) -> Response { var resp: Response; resp.statusCode = status; resp.contentType = contentType; resp.body = body; resp.extraHeaders = ""; return resp; } func Response_Ok(body: String) -> Response { return Response_New(200, "text/html; charset=utf-8", body); } func Response_Html(html: String) -> Response { return Response_New(200, "text/html; charset=utf-8", html); } func Response_Json(json: String) -> Response { return Response_New(200, "application/json; charset=utf-8", json); } func Response_Text(text: String) -> Response { return Response_New(200, "text/plain; charset=utf-8", text); } func Response_Redirect(url: String) -> Response { var resp: Response; resp.statusCode = 302; resp.contentType = ""; resp.body = ""; let sb: *void = bux_sb_new(256); bux_sb_append(sb, "Location: "); bux_sb_append(sb, url); bux_sb_append(sb, "\r\n"); resp.extraHeaders = bux_sb_build(sb); bux_sb_free(sb); return resp; } func Response_NotFound() -> Response { return Response_New(404, "application/json; charset=utf-8", "{\"error\":\"not_found\"}"); } func Response_Error(status: int, message: String) -> Response { return Response_New(status, "application/json; charset=utf-8", message); } func Response_NoContent() -> Response { return Response_New(204, "", ""); } // ============================================================================= // Build HTTP response string from Response struct // ============================================================================= func Response_Build(resp: Response) -> String { let sb: *void = bux_sb_new(2048); // Status line bux_sb_append(sb, "HTTP/1.1 "); bux_sb_append_int(sb, resp.statusCode as int64); bux_sb_append(sb, " "); if resp.statusCode == 200 { bux_sb_append(sb, "OK"); } else if resp.statusCode == 201 { bux_sb_append(sb, "Created"); } else if resp.statusCode == 204 { bux_sb_append(sb, "No Content"); } else if resp.statusCode == 302 { bux_sb_append(sb, "Found"); } else if resp.statusCode == 400 { bux_sb_append(sb, "Bad Request"); } else if resp.statusCode == 404 { bux_sb_append(sb, "Not Found"); } else if resp.statusCode == 500 { bux_sb_append(sb, "Internal Server Error"); } else { bux_sb_append(sb, "OK"); } bux_sb_append(sb, "\r\n"); // Server bux_sb_append(sb, "Server: Boko/0.1.0 (Bux)\r\n"); // Extra headers (redirects, CORS, etc.) if bux_strlen(resp.extraHeaders) > 0 { bux_sb_append(sb, resp.extraHeaders); } // Content-Type if bux_strlen(resp.contentType) > 0 { bux_sb_append(sb, "Content-Type: "); bux_sb_append(sb, resp.contentType); bux_sb_append(sb, "\r\n"); } // Content-Length let bodyLen: uint = bux_strlen(resp.body); bux_sb_append(sb, "Content-Length: "); bux_sb_append_int(sb, bodyLen as int64); bux_sb_append(sb, "\r\n"); // Connection close bux_sb_append(sb, "Connection: close\r\n"); bux_sb_append(sb, "\r\n"); if bodyLen > 0 { bux_sb_append(sb, resp.body); } let result: String = bux_sb_build(sb); bux_sb_free(sb); return result; } // ============================================================================= // Parse query string: ?key=val&key2=val2 → key-value arrays // ============================================================================= func Query_Parse(req: *Request, queryString: String) { if bux_strlen(queryString) == 0 { return; } let pairCount: uint = bux_str_split_count(queryString, "&"); let cap: int = pairCount as int + 1; req.queryKeys = bux_alloc(cap as uint * 8) as *String; req.queryValues = bux_alloc(cap as uint * 8) as *String; req.queryCount = 0; var i: uint = 0; while i < pairCount { let pair: String = bux_str_split_part(queryString, "&", i); let eqPos: String = bux_strstr(pair, "="); var key: String = pair; var value: String = ""; if String_Len(eqPos) > 0 { let keyLen: uint = String_Offset(eqPos, pair); key = bux_str_slice(pair, 0, keyLen); let valStart: uint = keyLen + 1; let pairLen: uint = bux_strlen(pair); if valStart < pairLen { value = bux_str_slice(pair, valStart, pairLen - valStart); } } req.queryKeys[req.queryCount] = key; req.queryValues[req.queryCount] = value; req.queryCount = req.queryCount + 1; i = i + 1; } } // ============================================================================= // Parse incoming HTTP request from raw bytes // ============================================================================= func Request_Parse(raw: String) -> Request { var req: Request; req.method = HttpVerb { tag: HttpVerb_GET }; req.path = "/"; req.body = ""; req.headerCount = 0; req.queryCount = 0; req.pathParamCount = 0; if String_Len(raw) == 0 { return req; } let rawLen: uint = bux_strlen(raw); if rawLen == 0 { return req; } // Find header/body boundary let boundary: String = bux_strstr(raw, "\r\n\r\n"); var headerLen: uint = rawLen; if String_Len(boundary) > 0 { headerLen = String_Offset(boundary, raw); let bodyStart: uint = headerLen + 4; if bodyStart < rawLen { req.body = bux_str_slice(raw, bodyStart, rawLen - bodyStart); } } if headerLen == 0 { return req; } let headerBlock: String = bux_str_slice(raw, 0, headerLen); let lineCount: uint = bux_str_split_count(headerBlock, "\r\n"); if lineCount == 0 { return req; } // Parse request line let requestLine: String = bux_str_split_part(headerBlock, "\r\n", 0); if bux_strlen(requestLine) > 0 { let methodStr: String = bux_str_split_part(requestLine, " ", 0); if String_Eq(methodStr, "GET") { req.method = HttpVerb { tag: HttpVerb_GET }; } else if String_Eq(methodStr, "POST") { req.method = HttpVerb { tag: HttpVerb_POST }; } else if String_Eq(methodStr, "PUT") { req.method = HttpVerb { tag: HttpVerb_PUT }; } else if String_Eq(methodStr, "DELETE") { req.method = HttpVerb { tag: HttpVerb_DELETE }; } let fullPath: String = bux_str_split_part(requestLine, " ", 1); // Split path and query string let qmark: String = bux_strstr(fullPath, "?"); if String_Len(qmark) > 0 { let pathLen: uint = String_Offset(qmark, fullPath); req.path = bux_str_slice(fullPath, 0, pathLen); let qsStart: uint = pathLen + 1; let fullLen: uint = bux_strlen(fullPath); if qsStart < fullLen { let qs: String = bux_str_slice(fullPath, qsStart, fullLen - qsStart); Query_Parse(&req, qs); } } else { req.path = fullPath; } } // Parse headers let cap: int = lineCount as int + 1; req.headerKeys = bux_alloc(cap as uint * 8) as *String; req.headerValues = bux_alloc(cap as uint * 8) as *String; var i: uint = 1; while i < lineCount { let line: String = bux_str_split_part(headerBlock, "\r\n", i); let colonPos: String = bux_strstr(line, ": "); if String_Len(colonPos) > 0 { let keyLen: uint = String_Offset(colonPos, line); let key: String = bux_str_slice(line, 0, keyLen); let valStart: uint = keyLen + 2; let lineLen: uint = bux_strlen(line); var value: String = ""; if valStart < lineLen { value = bux_str_slice(line, valStart, lineLen - valStart); } req.headerKeys[req.headerCount] = key; req.headerValues[req.headerCount] = value; req.headerCount = req.headerCount + 1; } i = i + 1; } return req; } // ============================================================================= // Path pattern matching: /users/{id} against /users/42 → extracts id=42 // ============================================================================= func Path_Match(pattern: String, path: String, req: *Request) -> bool { // Split both by "/" let patParts: uint = bux_str_split_count(pattern, "/"); let pathParts: uint = bux_str_split_count(path, "/"); if patParts != pathParts { return false; } // Count params to allocate var paramCount: int = 0; var pi: uint = 0; while pi < patParts { let patPart: String = bux_str_split_part(pattern, "/", pi); if String_StartsWith(patPart, "{") && String_Contains(patPart, "}") { paramCount = paramCount + 1; } pi = pi + 1; } let cap: int = paramCount + 1; req.pathParamKeys = bux_alloc(cap as uint * 8) as *String; req.pathParamValues = bux_alloc(cap as uint * 8) as *String; req.pathParamCount = 0; // Match and extract var i: uint = 0; while i < patParts { let patPart: String = bux_str_split_part(pattern, "/", i); let pathPart: String = bux_str_split_part(path, "/", i); if String_StartsWith(patPart, "{") && String_Contains(patPart, "}") { // Extract param name from {name} let braceOpen: uint = 1; // skip "{" let braceClose: String = bux_strstr(patPart, "}"); var nameLen: uint = 0; if String_Len(braceClose) > 0 { nameLen = String_Offset(braceClose, patPart) - 1; } let name: String = bux_str_slice(patPart, braceOpen, nameLen); req.pathParamKeys[req.pathParamCount] = name; req.pathParamValues[req.pathParamCount] = pathPart; req.pathParamCount = req.pathParamCount + 1; } else { // Literal match if !String_Eq(patPart, pathPart) { return false; } } i = i + 1; } return true; } // ============================================================================= // App constructor // ============================================================================= func App_New(port: int, threadCount: int) -> App { var app: App; app.port = port; app.threadCount = threadCount; app.serverName = "Boko/0.1.0 (Bux)"; return app; } // ============================================================================= // Forward declaration — user must implement this in their module // ============================================================================= func Boko_Router(req: Request) -> Response; // ============================================================================= // Handle a single connection: parse → dispatch → respond // ============================================================================= func App_HandleConnection(clientFd: int) { let raw: String = Net_Recv(clientFd, 8192); if String_Len(raw) == 0 { return; } if bux_strlen(raw) == 0 { return; } let req: Request = Request_Parse(raw); // Call user-defined dispatch (forward declaration — user must implement) let resp: Response = Boko_Router(req); // Log if req.method.tag == HttpVerb_GET { Print("GET "); } else if req.method.tag == HttpVerb_POST { Print("POST "); } else if req.method.tag == HttpVerb_PUT { Print("PUT "); } else if req.method.tag == HttpVerb_DELETE { Print("DELETE "); } else { Print("? "); } Print(req.path); Print(" → "); PrintInt(resp.statusCode); PrintLine(""); // Send let respStr: String = Response_Build(resp); Net_Send(clientFd, respStr); } // ============================================================================= // Worker thread: accept loop // ============================================================================= func App_Worker(serverFd: int) { while true { let clientFd: int = Net_Accept(serverFd); if clientFd < 0 { continue; } App_HandleConnection(clientFd); Net_Close(clientFd); } } // ============================================================================= // App_Run: start the server (blocking) // ============================================================================= func App_Run(app: *App) { PrintLine("╔══════════════════════════════════════╗"); PrintLine("║ Boko Framework v0.1.0 ║"); PrintLine("║ Async web framework for Bux ║"); PrintLine("╚══════════════════════════════════════╝"); PrintLine(""); let fd: int = Net_Create(); if fd < 0 { PrintLine("FATAL: socket() failed"); return; } Net_SetReuse(fd); if !Net_Bind(fd, "0.0.0.0", app.port) { Print("FATAL: bind(:"); PrintInt(app.port); Print(") failed: "); PrintLine(Net_LastError()); Net_Close(fd); return; } if !Net_Listen(fd, 128) { PrintLine("FATAL: listen() failed"); Net_Close(fd); return; } Print("✓ Boko running on http://0.0.0.0:"); PrintInt(app.port); PrintLine(""); Print("✓ Workers: "); PrintInt(app.threadCount); PrintLine(""); PrintLine(""); // Spawn workers var i: int = 0; while i < app.threadCount - 1 { spawn App_Worker(fd); i = i + 1; } // Main thread is the last worker App_Worker(fd); } } // module Boko