feat: crypto library, Nexus HTTP server, JWT CLI, Boko web framework
- lib/crypto/: 9-module crypto library (Hash, HMAC, Base64/URL, Random, AES, RSA, ECDSA, Ed25519, JWT) - rt/runtime.c: +346 lines OpenSSL crypto primitives (SHA-1/384/512, HMAC, Base64URL, AES-CBC/GCM, RSA, ECDSA, Ed25519) - apps/nexus/: multi-threaded HTTP/1.1 + HTTP/2 detect + WebSocket server - apps/jwt-pitbul/: JWT CLI tool (sign, verify, decode, keygen) - apps/boko-framework/: FastAPI-inspired async web framework - test_crypto/: crypto library test suite (23 tests)
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
# Nexus
|
||||
|
||||
**High-performance, multi-threaded HTTP/1.1, HTTP/2 & WebSocket server — built with the [Bux](https://github.com/bux-lang/bux) programming language.**
|
||||
|
||||
Nexus is a from-scratch web server that demonstrates Bux's systems-programming capabilities: raw TCP sockets, pthread-based concurrency, manual memory management, and zero-dependency C ABI interop — all from a clean, modern syntax.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
| Area | What's Implemented |
|
||||
|------|-------------------|
|
||||
| **HTTP/1.1** | Full request parsing (method, path, headers, body), response building with status codes, content negotiation |
|
||||
| **Multi-threaded** | Configurable worker pool using the multi-accept pattern — each worker calls `accept()` directly on the shared listen socket |
|
||||
| **HTTP/2** | Connection preface detection (`PRI * HTTP/2.0`), upgrade-aware routing |
|
||||
| **WebSocket** | RFC 6455 upgrade handshake detection, `Sec-WebSocket-Key` extraction |
|
||||
| **Static files** | Serves from `public/` with MIME-type detection for 20+ file types, directory-traversal protection |
|
||||
| **JSON API** | Built-in `/api/health` and `/api/info` endpoints |
|
||||
| **Logging** | Per-request structured logging (method, path, status code) |
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# From the project root
|
||||
cd apps/nexus
|
||||
|
||||
# Build with the bootstrap compiler (Nim)
|
||||
../../buxc build
|
||||
|
||||
# Or with the self-hosted compiler
|
||||
../../buxc_lir build
|
||||
|
||||
# Run
|
||||
./nexus
|
||||
```
|
||||
|
||||
Server starts on `http://0.0.0.0:8080`:
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════╗
|
||||
║ Nexus HTTP Server v0.1.0 ║
|
||||
║ High-performance multi-threaded HTTP/1.1 ║
|
||||
║ HTTP/2 & WebSocket detection included ║
|
||||
║ Built with Bux ║
|
||||
╚══════════════════════════════════════════════╝
|
||||
|
||||
✓ Server listening on http://0.0.0.0:8080
|
||||
✓ Worker threads: 4
|
||||
✓ Static files: ./public/
|
||||
|
||||
Endpoints:
|
||||
GET / — Static files (public/)
|
||||
GET /api/health — Health check (JSON)
|
||||
GET /api/info — Server info (JSON)
|
||||
GET /ws — WebSocket upgrade
|
||||
ANY /* — Static file serving
|
||||
```
|
||||
|
||||
## Testing It
|
||||
|
||||
```bash
|
||||
# Home page (HTML)
|
||||
curl http://localhost:8080/
|
||||
|
||||
# Health check
|
||||
curl http://localhost:8080/api/health
|
||||
# → {"status":"ok","server":"Nexus","version":"0.1.0"}
|
||||
|
||||
# Server info
|
||||
curl http://localhost:8080/api/info
|
||||
# → {"name":"Nexus","language":"Bux","features":[...]}
|
||||
|
||||
# Static file (404 page)
|
||||
curl http://localhost:8080/404.html
|
||||
|
||||
# Nonexistent file
|
||||
curl http://localhost:8080/nope
|
||||
# → 404 Not Found
|
||||
|
||||
# WebSocket upgrade attempt
|
||||
curl -H "Upgrade: websocket" \
|
||||
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
|
||||
http://localhost:8080/ws
|
||||
# → 101 Switching Protocols
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Thread Model — Multi-Accept
|
||||
|
||||
Nexus uses the **multi-accept** pattern rather than a traditional thread pool with a work queue:
|
||||
|
||||
```
|
||||
Main Thread Worker 1 Worker 2 Worker 3
|
||||
│ │ │ │
|
||||
├─ socket()/bind()/listen() │ │ │
|
||||
├─ spawn Worker() ──────────► │ │
|
||||
├─ spawn Worker() ─────────────────────────► │
|
||||
├─ spawn Worker() ────────────────────────────────────────►
|
||||
│ │ │ │
|
||||
▼ Worker() ▼ accept() ▼ accept() ▼ accept()
|
||||
accept() loop │ │ │
|
||||
```
|
||||
|
||||
Each worker thread calls `accept()` on the **same** listening socket. The Linux kernel distributes incoming connections across the blocked accept calls — the same mechanism used by nginx and Apache prefork. No mutex, no queue, no context switching between a dispatcher and workers.
|
||||
|
||||
### Request Lifecycle
|
||||
|
||||
```
|
||||
Client connects
|
||||
│
|
||||
▼
|
||||
Net_Accept() → client fd
|
||||
│
|
||||
▼
|
||||
Net_Recv(fd, 8192) → raw bytes
|
||||
│
|
||||
▼
|
||||
ParseRequest()
|
||||
├─ Split headers on \r\n\r\n
|
||||
├─ Parse request line → method, path, version
|
||||
└─ Parse header lines → key-value array
|
||||
│
|
||||
▼
|
||||
Router_Dispatch()
|
||||
├─ /api/* → JSON handlers
|
||||
├─ /ws → WebSocket upgrade
|
||||
├─ Upgrade header → HTTP/2 or WS detection
|
||||
└─ /* → Static file serving
|
||||
│
|
||||
▼
|
||||
BuildResponse() → HTTP/1.1 status line + headers + body
|
||||
│
|
||||
▼
|
||||
Net_Send(fd, response) → bytes to client
|
||||
│
|
||||
▼
|
||||
Net_Close(fd)
|
||||
```
|
||||
|
||||
### Design Decisions
|
||||
|
||||
- **No keep-alive by default.** Each connection is closed after one response. This avoids blocking worker threads on idle clients (Bux doesn't yet expose `SO_RCVTIMEO`).
|
||||
- **Linear header array instead of hash map.** HTTP requests typically carry 5–15 headers. A linear scan over a key-value array is faster than hashing for this N, and avoids the complexity of iterating over Bux's generic `StringMap`.
|
||||
- **Raw extern calls for the string builder.** The stdlib `StringBuilder` wrapper adds a struct indirection. Calling `bux_sb_new` / `bux_sb_append` / `bux_sb_build` directly is simpler and equally safe.
|
||||
- **WebSocket accept key is a placeholder.** A full implementation needs SHA-1 hashing and Base64 encoding. These aren't in Bux's stdlib yet; they can be added as extern C functions when needed.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
apps/nexus/
|
||||
├── bux.toml # Package manifest
|
||||
├── README.md # This file
|
||||
├── src/
|
||||
│ └── Main.bux # The entire server (~640 lines)
|
||||
└── public/
|
||||
├── index.html # Landing page
|
||||
└── 404.html # Error page
|
||||
```
|
||||
|
||||
Everything lives in one file (`src/Main.bux`) by design — it keeps the module graph flat and the build fast. As the server grows, the HTTP parser, router, and handlers can be split into separate modules.
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit the constants at the top of `src/Main.bux`:
|
||||
|
||||
```bux
|
||||
const SERVER_PORT: int = 8080; // Listen port
|
||||
const THREAD_COUNT: int = 4; // Number of worker threads
|
||||
const RECV_BUF_SIZE: int = 8192; // Receive buffer per request
|
||||
const SERVER_NAME: String = "Nexus/0.1.0 (Bux)";
|
||||
const PUBLIC_DIR: String = "public"; // Static files directory
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Keep-alive with configurable socket timeout
|
||||
- [ ] Full WebSocket frame read/write (requires SHA-1 + Base64)
|
||||
- [ ] HTTP/2 binary framing layer (HPACK, stream multiplexing)
|
||||
- [ ] SSL/TLS via OpenSSL extern bindings
|
||||
- [ ] Middleware / filter chain
|
||||
- [ ] Request body parsing (JSON, form-encoded, multipart)
|
||||
- [ ] Virtual hosts
|
||||
- [ ] Access logging to file
|
||||
- [ ] Rate limiting
|
||||
|
||||
## License
|
||||
|
||||
Nexus is part of the Bux project. See the root [LICENSE](../../LICENSE) for terms.
|
||||
@@ -0,0 +1 @@
|
||||
EXIT:137
|
||||
@@ -0,0 +1 @@
|
||||
EXIT:124
|
||||
@@ -0,0 +1 @@
|
||||
EXIT:124
|
||||
@@ -0,0 +1 @@
|
||||
EXIT:124
|
||||
@@ -0,0 +1 @@
|
||||
EXIT:124
|
||||
@@ -0,0 +1,2 @@
|
||||
timeout: командата „„../../buxc““ не може да бъде изпълнена: Няма такъв файл или директория
|
||||
EXIT:127
|
||||
@@ -0,0 +1 @@
|
||||
EXIT:137
|
||||
@@ -0,0 +1,8 @@
|
||||
[Package]
|
||||
Name = "nexus"
|
||||
Version = "0.1.0"
|
||||
Type = "bin"
|
||||
Description = "High-performance multi-threaded HTTP/1.1, HTTP/2 and WebSocket server"
|
||||
|
||||
[Build]
|
||||
Output = "Bin"
|
||||
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>404 — Not Found | Nexus</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #0d1117;
|
||||
color: #c9d1d9;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
h1 { font-size: 4rem; color: #f85149; margin-bottom: 0.5rem; }
|
||||
p { color: #8b949e; font-size: 1.2rem; }
|
||||
.back { margin-top: 2rem; }
|
||||
.back a { color: #58a6ff; text-decoration: none; }
|
||||
.back a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>404</h1>
|
||||
<p>The requested resource was not found on this server.</p>
|
||||
<p class="back"><a href="/">← Back to Home</a></p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,129 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Nexus — Bux HTTP Server</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #0d1117;
|
||||
color: #c9d1d9;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: #161b22;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 8px;
|
||||
padding: 3rem;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
color: #58a6ff;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.subtitle {
|
||||
color: #8b949e;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.features {
|
||||
text-align: left;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
.features h3 {
|
||||
color: #58a6ff;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.features ul {
|
||||
list-style: none;
|
||||
}
|
||||
.features li {
|
||||
padding: 0.4rem 0;
|
||||
color: #8b949e;
|
||||
}
|
||||
.features li::before {
|
||||
content: "▸ ";
|
||||
color: #3fb950;
|
||||
}
|
||||
.endpoints {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid #30363d;
|
||||
}
|
||||
.endpoints h3 {
|
||||
color: #d2a8ff;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.endpoint {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.3rem 0;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.endpoint .method {
|
||||
color: #3fb950;
|
||||
font-weight: bold;
|
||||
min-width: 60px;
|
||||
}
|
||||
.endpoint .path {
|
||||
color: #c9d1d9;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid #30363d;
|
||||
color: #484f58;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>⚡ Nexus</h1>
|
||||
<p class="subtitle">High-performance HTTP Server — Built with Bux</p>
|
||||
|
||||
<div class="features">
|
||||
<h3>Features</h3>
|
||||
<ul>
|
||||
<li>HTTP/1.1 — Full request parsing & response building</li>
|
||||
<li>Multi-threaded — Configurable worker pool</li>
|
||||
<li>HTTP/2 Detection — Upgrade-aware</li>
|
||||
<li>WebSocket Upgrade — Handshake support</li>
|
||||
<li>Static File Serving — With MIME type detection</li>
|
||||
<li>JSON API Endpoints</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="endpoints">
|
||||
<h3>API Endpoints</h3>
|
||||
<div class="endpoint">
|
||||
<span class="method">GET</span>
|
||||
<span class="path">/api/health</span>
|
||||
</div>
|
||||
<div class="endpoint">
|
||||
<span class="method">GET</span>
|
||||
<span class="path">/api/info</span>
|
||||
</div>
|
||||
<div class="endpoint">
|
||||
<span class="method">GET</span>
|
||||
<span class="path">/ws</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
Nexus v0.1.0 · Bux Programming Language
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,550 @@
|
||||
// =============================================================================
|
||||
// Nexus — High-performance multi-threaded HTTP/1.1, HTTP/2 & WebSocket server
|
||||
// Built with the Bux programming language
|
||||
// =============================================================================
|
||||
module Main {
|
||||
|
||||
// =============================================================================
|
||||
// Imports — only what we actually use
|
||||
// =============================================================================
|
||||
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_EndsWith, String_Contains, String_Trim, String_FromInt};
|
||||
import Std::Task::{Task_Join, TaskHandle};
|
||||
|
||||
// =============================================================================
|
||||
// Extern Functions (from C runtime — used directly for performance)
|
||||
// =============================================================================
|
||||
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_path_join(a: String, b: String) -> String;
|
||||
extern func bux_read_file(path: String) -> String;
|
||||
extern func bux_file_exists(path: String) -> int;
|
||||
|
||||
// =============================================================================
|
||||
// HTTP Method Enum (simple enum — no data payload)
|
||||
// =============================================================================
|
||||
enum HttpMethod {
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
DELETE,
|
||||
PATCH,
|
||||
HEAD,
|
||||
OPTIONS,
|
||||
UNKNOWN,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Struct: HTTP Request
|
||||
// =============================================================================
|
||||
struct HttpRequest {
|
||||
method: HttpMethod,
|
||||
path: String,
|
||||
body: String,
|
||||
headerKeys: *String,
|
||||
headerValues: *String,
|
||||
headerCount: int,
|
||||
headerCap: int,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Struct: HTTP Response
|
||||
// =============================================================================
|
||||
struct HttpResponse {
|
||||
statusCode: int,
|
||||
statusText: String,
|
||||
contentType: String,
|
||||
body: String,
|
||||
extraHeaders: String, // pre-formatted extra headers (for WS upgrade, etc.)
|
||||
connectionClose: bool,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Utility: Status text lookup
|
||||
// =============================================================================
|
||||
func Http_StatusText(code: int) -> String {
|
||||
if code == 200 { return "OK"; }
|
||||
if code == 201 { return "Created"; }
|
||||
if code == 204 { return "No Content"; }
|
||||
if code == 301 { return "Moved Permanently"; }
|
||||
if code == 302 { return "Found"; }
|
||||
if code == 304 { return "Not Modified"; }
|
||||
if code == 400 { return "Bad Request"; }
|
||||
if code == 401 { return "Unauthorized"; }
|
||||
if code == 403 { return "Forbidden"; }
|
||||
if code == 404 { return "Not Found"; }
|
||||
if code == 405 { return "Method Not Allowed"; }
|
||||
if code == 413 { return "Payload Too Large"; }
|
||||
if code == 414 { return "URI Too Long"; }
|
||||
if code == 500 { return "Internal Server Error"; }
|
||||
if code == 501 { return "Not Implemented"; }
|
||||
if code == 503 { return "Service Unavailable"; }
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Utility: Parse HTTP method string → HttpMethod enum
|
||||
// =============================================================================
|
||||
func Http_ParseMethod(s: String) -> HttpMethod {
|
||||
if String_Eq(s, "GET") { return HttpMethod { tag: HttpMethod_GET }; }
|
||||
if String_Eq(s, "POST") { return HttpMethod { tag: HttpMethod_POST }; }
|
||||
if String_Eq(s, "PUT") { return HttpMethod { tag: HttpMethod_PUT }; }
|
||||
if String_Eq(s, "DELETE") { return HttpMethod { tag: HttpMethod_DELETE }; }
|
||||
if String_Eq(s, "PATCH") { return HttpMethod { tag: HttpMethod_PATCH }; }
|
||||
if String_Eq(s, "HEAD") { return HttpMethod { tag: HttpMethod_HEAD }; }
|
||||
if String_Eq(s, "OPTIONS") { return HttpMethod { tag: HttpMethod_OPTIONS }; }
|
||||
return HttpMethod { tag: HttpMethod_UNKNOWN };
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Utility: MIME type from file extension
|
||||
// =============================================================================
|
||||
func Http_MimeType(path: String) -> String {
|
||||
if String_EndsWith(path, ".html") || String_EndsWith(path, ".htm") { return "text/html; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".css") { return "text/css; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".js") { return "application/javascript; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".json") { return "application/json; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".xml") { return "application/xml; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".txt") { return "text/plain; charset=utf-8"; }
|
||||
if String_EndsWith(path, ".png") { return "image/png"; }
|
||||
if String_EndsWith(path, ".jpg") || String_EndsWith(path, ".jpeg") { return "image/jpeg"; }
|
||||
if String_EndsWith(path, ".gif") { return "image/gif"; }
|
||||
if String_EndsWith(path, ".svg") { return "image/svg+xml"; }
|
||||
if String_EndsWith(path, ".ico") { return "image/x-icon"; }
|
||||
if String_EndsWith(path, ".webp") { return "image/webp"; }
|
||||
if String_EndsWith(path, ".woff2") { return "font/woff2"; }
|
||||
if String_EndsWith(path, ".woff") { return "font/woff"; }
|
||||
if String_EndsWith(path, ".wasm") { return "application/wasm"; }
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Header Storage: dynamic key-value array (faster than hash map for 5-15 headers)
|
||||
// =============================================================================
|
||||
func RequestHeader_Init(req: *HttpRequest) {
|
||||
let cap: int = 16;
|
||||
req.headerCap = cap;
|
||||
req.headerKeys = bux_alloc(cap as uint * 8) as *String;
|
||||
req.headerValues = bux_alloc(cap as uint * 8) as *String;
|
||||
req.headerCount = 0;
|
||||
}
|
||||
|
||||
func RequestHeader_Add(req: *HttpRequest, key: String, value: String) {
|
||||
if req.headerCount >= req.headerCap { return; }
|
||||
req.headerKeys[req.headerCount] = key;
|
||||
req.headerValues[req.headerCount] = value;
|
||||
req.headerCount = req.headerCount + 1;
|
||||
}
|
||||
|
||||
func RequestHeader_Get(req: *HttpRequest, key: String) -> String {
|
||||
var i: int = 0;
|
||||
while i < req.headerCount {
|
||||
if bux_strcmp(req.headerKeys[i], key) == 0 {
|
||||
return req.headerValues[i];
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HTTP Request Parser
|
||||
// =============================================================================
|
||||
func Http_ParseRequest(raw: String) -> HttpRequest {
|
||||
var req: HttpRequest;
|
||||
req.method = HttpMethod { tag: HttpMethod_UNKNOWN };
|
||||
req.path = "/";
|
||||
req.body = "";
|
||||
RequestHeader_Init(&req);
|
||||
|
||||
// Guard: null/empty input
|
||||
if raw as uint == 0 { return req; }
|
||||
let rawLen: uint = bux_strlen(raw);
|
||||
if rawLen == 0 { return req; }
|
||||
|
||||
// Find \r\n\r\n separating headers from body
|
||||
let boundary: String = bux_strstr(raw, "\r\n\r\n");
|
||||
var headerLen: uint = rawLen;
|
||||
if boundary as uint != 0 {
|
||||
headerLen = boundary as uint - raw as uint;
|
||||
let bodyStart: uint = headerLen + 4;
|
||||
if bodyStart < rawLen {
|
||||
req.body = bux_str_slice(raw, bodyStart, rawLen - bodyStart);
|
||||
}
|
||||
}
|
||||
|
||||
// Slice out header block
|
||||
if headerLen == 0 { return req; }
|
||||
let headerBlock: String = bux_str_slice(raw, 0, headerLen);
|
||||
|
||||
// Count header lines
|
||||
let lineCount: uint = bux_str_split_count(headerBlock, "\r\n");
|
||||
if lineCount == 0 { return req; }
|
||||
|
||||
// --- Parse request line: "METHOD /path HTTP/1.1" ---
|
||||
let requestLine: String = bux_str_split_part(headerBlock, "\r\n", 0);
|
||||
if bux_strlen(requestLine) > 0 {
|
||||
req.method = Http_ParseMethod(bux_str_split_part(requestLine, " ", 0));
|
||||
req.path = bux_str_split_part(requestLine, " ", 1);
|
||||
if String_Eq(req.path, "") || req.path as uint == 0 {
|
||||
req.path = "/";
|
||||
}
|
||||
}
|
||||
|
||||
// --- Parse headers (lines 1..N-1) ---
|
||||
var i: uint = 1;
|
||||
while i < lineCount {
|
||||
let line: String = bux_str_split_part(headerBlock, "\r\n", i);
|
||||
let lineLen: uint = bux_strlen(line);
|
||||
if lineLen == 0 { i = i + 1; continue; }
|
||||
|
||||
// Find ": " separator
|
||||
let colonPos: String = bux_strstr(line, ": ");
|
||||
var key: String = "";
|
||||
var value: String = "";
|
||||
|
||||
if colonPos as uint != 0 {
|
||||
let keyLen: uint = colonPos as uint - line as uint;
|
||||
key = bux_str_slice(line, 0, keyLen);
|
||||
let valStart: uint = keyLen + 2;
|
||||
if valStart < lineLen {
|
||||
value = bux_str_slice(line, valStart, lineLen - valStart);
|
||||
}
|
||||
} else {
|
||||
// Try ":" without space
|
||||
let colonPos2: String = bux_strstr(line, ":");
|
||||
if colonPos2 as uint != 0 {
|
||||
let keyLen: uint = colonPos2 as uint - line as uint;
|
||||
key = bux_str_slice(line, 0, keyLen);
|
||||
let valStart: uint = keyLen + 1;
|
||||
if valStart < lineLen {
|
||||
value = bux_str_slice(line, valStart, lineLen - valStart);
|
||||
}
|
||||
value = String_Trim(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Store if valid key found
|
||||
if bux_strlen(key) > 0 {
|
||||
key = String_Trim(key);
|
||||
RequestHeader_Add(&req, key, value);
|
||||
}
|
||||
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HTTP Response Builder
|
||||
// =============================================================================
|
||||
func Http_BuildResponse(resp: HttpResponse) -> String {
|
||||
let sb: *void = bux_sb_new(4096);
|
||||
|
||||
// Status line
|
||||
bux_sb_append(sb, "HTTP/1.1 ");
|
||||
bux_sb_append_int(sb, resp.statusCode as int64);
|
||||
bux_sb_append_char(sb, ' ');
|
||||
bux_sb_append(sb, resp.statusText);
|
||||
bux_sb_append(sb, "\r\n");
|
||||
|
||||
// Server
|
||||
bux_sb_append(sb, "Server: Nexus/0.1.0 (Bux)\r\n");
|
||||
|
||||
// Extra headers (for WS upgrade 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
|
||||
if resp.connectionClose {
|
||||
bux_sb_append(sb, "Connection: close\r\n");
|
||||
} else {
|
||||
bux_sb_append(sb, "Connection: keep-alive\r\n");
|
||||
}
|
||||
|
||||
// End headers
|
||||
bux_sb_append(sb, "\r\n");
|
||||
|
||||
// Body
|
||||
if bodyLen > 0 {
|
||||
bux_sb_append(sb, resp.body);
|
||||
}
|
||||
|
||||
let result: String = bux_sb_build(sb);
|
||||
bux_sb_free(sb);
|
||||
return result;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Helper: Create a simple response
|
||||
// =============================================================================
|
||||
func Http_NewResponse(code: int, contentType: String, body: String) -> HttpResponse {
|
||||
var resp: HttpResponse;
|
||||
resp.statusCode = code;
|
||||
resp.statusText = Http_StatusText(code);
|
||||
resp.contentType = contentType;
|
||||
resp.body = body;
|
||||
resp.extraHeaders = "";
|
||||
resp.connectionClose = true;
|
||||
return resp;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Static File Server
|
||||
// =============================================================================
|
||||
func ServeStaticFile(requestPath: String) -> HttpResponse {
|
||||
// Block directory traversal
|
||||
if String_Contains(requestPath, "..") {
|
||||
return Http_NewResponse(403, "text/plain; charset=utf-8", "Forbidden");
|
||||
}
|
||||
|
||||
// Default to index.html
|
||||
var filePath: String = requestPath;
|
||||
if String_Eq(filePath, "/") {
|
||||
filePath = "/index.html";
|
||||
}
|
||||
|
||||
// Resolve full path
|
||||
let fullPath: String = bux_path_join("public", filePath);
|
||||
|
||||
// Check existence
|
||||
if bux_file_exists(fullPath) == 0 {
|
||||
return Http_NewResponse(404, "text/plain; charset=utf-8", "Not Found");
|
||||
}
|
||||
|
||||
// Read and serve
|
||||
let content: String = bux_read_file(fullPath);
|
||||
if content as uint == 0 {
|
||||
return Http_NewResponse(500, "text/plain; charset=utf-8", "Internal Server Error");
|
||||
}
|
||||
|
||||
let mime: String = Http_MimeType(filePath);
|
||||
return Http_NewResponse(200, mime, content);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// API Handlers
|
||||
// =============================================================================
|
||||
func HandleApiHealth() -> HttpResponse {
|
||||
return Http_NewResponse(200, "application/json; charset=utf-8",
|
||||
"{\"status\":\"ok\",\"server\":\"Nexus\",\"version\":\"0.1.0\"}");
|
||||
}
|
||||
|
||||
func HandleApiInfo() -> HttpResponse {
|
||||
return Http_NewResponse(200, "application/json; charset=utf-8",
|
||||
"{\"name\":\"Nexus\",\"language\":\"Bux\",\"threads\":4,\"features\":[\"HTTP/1.1\",\"HTTP/2-detect\",\"WebSocket-upgrade\"]}");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// WebSocket Upgrade Handshake
|
||||
// =============================================================================
|
||||
func HandleWebSocketUpgrade(req: HttpRequest) -> HttpResponse {
|
||||
let wsKey: String = RequestHeader_Get(&req, "Sec-WebSocket-Key");
|
||||
|
||||
var resp: HttpResponse;
|
||||
resp.statusCode = 101;
|
||||
resp.statusText = "Switching Protocols";
|
||||
resp.contentType = "";
|
||||
resp.body = "";
|
||||
resp.connectionClose = false;
|
||||
|
||||
// Build upgrade headers as extraHeaders
|
||||
// TODO: SHA-1 + Base64 for proper accept key
|
||||
let sb: *void = bux_sb_new(256);
|
||||
bux_sb_append(sb, "Upgrade: websocket\r\n");
|
||||
bux_sb_append(sb, "Connection: Upgrade\r\n");
|
||||
bux_sb_append(sb, "Sec-WebSocket-Accept: ");
|
||||
bux_sb_append(sb, wsKey); // placeholder — real impl needs crypto
|
||||
bux_sb_append(sb, "\r\n");
|
||||
resp.extraHeaders = bux_sb_build(sb);
|
||||
bux_sb_free(sb);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Router / Dispatcher
|
||||
// =============================================================================
|
||||
func Router_Dispatch(req: HttpRequest) -> HttpResponse {
|
||||
// Check for upgrade headers
|
||||
let upgrade: String = RequestHeader_Get(&req, "Upgrade");
|
||||
if bux_strlen(upgrade) > 0 {
|
||||
if String_Contains(upgrade, "websocket") {
|
||||
return HandleWebSocketUpgrade(req);
|
||||
}
|
||||
}
|
||||
|
||||
// API routes
|
||||
if String_StartsWith(req.path, "/api/") {
|
||||
if String_Eq(req.path, "/api/health") { return HandleApiHealth(); }
|
||||
if String_Eq(req.path, "/api/info") { return HandleApiInfo(); }
|
||||
return Http_NewResponse(404, "application/json; charset=utf-8",
|
||||
"{\"error\":\"not_found\"}");
|
||||
}
|
||||
|
||||
// WebSocket endpoint
|
||||
if String_Eq(req.path, "/ws") {
|
||||
return HandleWebSocketUpgrade(req);
|
||||
}
|
||||
|
||||
// Static files — GET/HEAD only
|
||||
if req.method.tag == HttpMethod_GET || req.method.tag == HttpMethod_HEAD {
|
||||
return ServeStaticFile(req.path);
|
||||
}
|
||||
|
||||
// Method not allowed
|
||||
return Http_NewResponse(405, "text/plain; charset=utf-8", "Method Not Allowed");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Method name for logging (avoids 8 if-checks in hot path)
|
||||
// =============================================================================
|
||||
func Http_MethodName(m: HttpMethod) -> String {
|
||||
if m.tag == HttpMethod_GET { return "GET"; }
|
||||
if m.tag == HttpMethod_POST { return "POST"; }
|
||||
if m.tag == HttpMethod_PUT { return "PUT"; }
|
||||
if m.tag == HttpMethod_DELETE { return "DELETE"; }
|
||||
if m.tag == HttpMethod_PATCH { return "PATCH"; }
|
||||
if m.tag == HttpMethod_HEAD { return "HEAD"; }
|
||||
if m.tag == HttpMethod_OPTIONS { return "OPTIONS"; }
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Connection Handler — read, parse, route, respond
|
||||
// =============================================================================
|
||||
func HandleConnection(clientFd: int) {
|
||||
let raw: String = Net_Recv(clientFd, 8192);
|
||||
|
||||
// Empty read → client disconnected
|
||||
if raw as uint == 0 { return; }
|
||||
if bux_strlen(raw) == 0 { return; }
|
||||
|
||||
// HTTP/2 connection preface detection
|
||||
if String_StartsWith(raw, "PRI * HTTP/2.0") {
|
||||
let resp: HttpResponse = Http_NewResponse(200, "text/plain; charset=utf-8",
|
||||
"HTTP/2 detected — full support planned for future release.\r\n");
|
||||
let respStr: String = Http_BuildResponse(resp);
|
||||
Net_Send(clientFd, respStr);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse HTTP/1.1
|
||||
let req: HttpRequest = Http_ParseRequest(raw);
|
||||
|
||||
// Log
|
||||
Print(Http_MethodName(req.method));
|
||||
Print(" ");
|
||||
Print(req.path);
|
||||
Print(" -> ");
|
||||
|
||||
// Dispatch
|
||||
let resp: HttpResponse = Router_Dispatch(req);
|
||||
|
||||
// Log result
|
||||
PrintInt(resp.statusCode);
|
||||
Print(" ");
|
||||
PrintLine(resp.statusText);
|
||||
|
||||
// Send
|
||||
let respStr: String = Http_BuildResponse(resp);
|
||||
Net_Send(clientFd, respStr);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Worker Thread — accept loop
|
||||
// =============================================================================
|
||||
func Worker(serverFd: int) {
|
||||
while true {
|
||||
let clientFd: int = Net_Accept(serverFd);
|
||||
if clientFd < 0 {
|
||||
continue;
|
||||
}
|
||||
HandleConnection(clientFd);
|
||||
Net_Close(clientFd);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Main Entry Point
|
||||
// =============================================================================
|
||||
func Main() -> int {
|
||||
PrintLine("================================================");
|
||||
PrintLine(" Nexus HTTP Server v0.1.0");
|
||||
PrintLine(" Multi-threaded HTTP/1.1 + HTTP/2 & WS detect");
|
||||
PrintLine(" Built with Bux");
|
||||
PrintLine("================================================");
|
||||
PrintLine("");
|
||||
|
||||
// Create socket
|
||||
let serverFd: int = Net_Create();
|
||||
if serverFd < 0 {
|
||||
PrintLine("FATAL: socket() failed");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// SO_REUSEADDR
|
||||
if !Net_SetReuse(serverFd) {
|
||||
PrintLine("WARN: SO_REUSEADDR failed");
|
||||
}
|
||||
|
||||
// Bind
|
||||
if !Net_Bind(serverFd, "0.0.0.0", 8080) {
|
||||
Print("FATAL: bind(8080) failed: ");
|
||||
PrintLine(Net_LastError());
|
||||
Net_Close(serverFd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Listen
|
||||
if !Net_Listen(serverFd, 128) {
|
||||
PrintLine("FATAL: listen() failed");
|
||||
Net_Close(serverFd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
PrintLine("Listening on http://0.0.0.0:8080");
|
||||
PrintLine("4 worker threads | static: ./public/");
|
||||
PrintLine("Endpoints: / /api/health /api/info /ws");
|
||||
PrintLine("Press Ctrl+C to stop.");
|
||||
PrintLine("");
|
||||
|
||||
// Spawn 3 workers (main thread is the 4th)
|
||||
spawn Worker(serverFd);
|
||||
spawn Worker(serverFd);
|
||||
spawn Worker(serverFd);
|
||||
|
||||
// Main thread becomes worker #4 (blocking call)
|
||||
Worker(serverFd);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // module Main
|
||||
@@ -0,0 +1,2 @@
|
||||
bux 0.1.0 (bootstrap)
|
||||
EXIT:0
|
||||
Reference in New Issue
Block a user