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,177 @@
|
|||||||
|
# Boko Framework
|
||||||
|
|
||||||
|
**Async web framework for Bux — inspired by FastAPI.**
|
||||||
|
|
||||||
|
Boko is a lightweight, multi-threaded web framework that brings FastAPI-style routing to the Bux programming language. It handles HTTP parsing, path pattern matching, query parameter extraction, and response building so you can focus on your application logic.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/boko-framework
|
||||||
|
../../buxc build
|
||||||
|
./boko-framework
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `http://localhost:8080` — you'll see the demo landing page.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
Boko follows a **single-dispatch-function** pattern. You define one function — `Boko_Router` — and the framework calls it for every incoming request:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
import Boko::{Request, Response, Response_Json, Response_Html, Response_NotFound,
|
||||||
|
Path_Match, Request_GetQuery, Request_GetPathParam,
|
||||||
|
App, App_New, App_Run};
|
||||||
|
|
||||||
|
func Boko_Router(req: Request) -> Response {
|
||||||
|
// Route: GET /
|
||||||
|
if String_Eq(req.path, "/") {
|
||||||
|
return Response_Html("<h1>Hello Boko!</h1>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route: GET /api/health
|
||||||
|
if String_Eq(req.path, "/api/health") {
|
||||||
|
return Response_Json("{\"status\":\"ok\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route: GET /users/{id} — path parameter
|
||||||
|
if Path_Match("/users/{id}", req.path, &req) {
|
||||||
|
let id: String = Request_GetPathParam(&req, "id");
|
||||||
|
// Build JSON response with id
|
||||||
|
return Response_Json(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route: GET /search?q=... — query parameter
|
||||||
|
if String_Eq(req.path, "/search") {
|
||||||
|
let q: String = Request_GetQuery(&req, "q");
|
||||||
|
return Response_Json(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response_NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
let app: App = App_New(8080, 4); // port, threads
|
||||||
|
App_Run(&app);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `method` | `HttpVerb` | GET, POST, PUT, DELETE |
|
||||||
|
| `path` | `String` | Request path (without query string) |
|
||||||
|
| `body` | `String` | Request body (for POST/PUT) |
|
||||||
|
| `headerCount` | `int` | Number of headers |
|
||||||
|
|
||||||
|
| Function | Returns | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `Request_GetHeader(req, name)` | `String` | Get header value by name |
|
||||||
|
| `Request_GetQuery(req, name)` | `String` | Get query parameter by name |
|
||||||
|
| `Request_GetPathParam(req, name)` | `String` | Get extracted path parameter |
|
||||||
|
|
||||||
|
### Response
|
||||||
|
|
||||||
|
| Constructor | Content-Type | Status |
|
||||||
|
|-------------|-------------|--------|
|
||||||
|
| `Response_Html(html)` | `text/html` | 200 |
|
||||||
|
| `Response_Json(json)` | `application/json` | 200 |
|
||||||
|
| `Response_Text(text)` | `text/plain` | 200 |
|
||||||
|
| `Response_Redirect(url)` | — | 302 |
|
||||||
|
| `Response_NotFound()` | `application/json` | 404 |
|
||||||
|
| `Response_Error(code, msg)` | `application/json` | custom |
|
||||||
|
| `Response_NoContent()` | — | 204 |
|
||||||
|
|
||||||
|
### Path Matching
|
||||||
|
|
||||||
|
`Path_Match(pattern, actualPath, req)` matches a pattern with `{param}` placeholders:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
// Pattern: /users/{id}/posts/{postId}
|
||||||
|
// Actual: /users/42/posts/7
|
||||||
|
// Extracts: id=42, postId=7
|
||||||
|
|
||||||
|
if Path_Match("/users/{id}/posts/{postId}", req.path, &req) {
|
||||||
|
let userId: String = Request_GetPathParam(&req, "id"); // "42"
|
||||||
|
let postId: String = Request_GetPathParam(&req, "postId"); // "7"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The function returns `true` if the pattern matches and populates `req.pathParamKeys` / `req.pathParamValues`.
|
||||||
|
|
||||||
|
### App
|
||||||
|
|
||||||
|
```bux
|
||||||
|
let app: App = App_New(8080, 4); // port 8080, 4 worker threads
|
||||||
|
App_Run(&app); // blocks, handles requests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Incoming connection
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Net_Accept() ─── worker thread (1 of N)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Net_Recv() → raw HTTP bytes
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Request_Parse() → method, path, query, headers, body
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Boko_Router(req) ←── YOUR CODE
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Response_Build() → HTTP/1.1 response string
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Net_Send() → bytes to client
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Net_Close()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Endpoints in Demo App
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| GET | `/` | Landing page (HTML) |
|
||||||
|
| GET | `/api/health` | Health check (JSON) |
|
||||||
|
| GET | `/api/info` | Framework info (JSON) |
|
||||||
|
| GET | `/hello?name=X` | Query param demo |
|
||||||
|
| GET | `/users/{id}` | Path param demo |
|
||||||
|
| GET | `/posts/{id}/comments/{cid}` | Multi path param demo |
|
||||||
|
| GET | `/redirect` | 302 redirect to `/` |
|
||||||
|
| POST | `/api/echo` | Echo request body |
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/boko-framework/
|
||||||
|
├── bux.toml # Package manifest
|
||||||
|
├── README.md # This file
|
||||||
|
└── src/
|
||||||
|
├── Boko.bux # Framework core (~500 lines)
|
||||||
|
└── Main.bux # Example app (~150 lines)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design Philosophy
|
||||||
|
|
||||||
|
Boko is intentionally simple. Instead of complex DSLs or code generation, it gives you:
|
||||||
|
|
||||||
|
- **One function to write** — `Boko_Router(req) -> Response`
|
||||||
|
- **Direct control** — you write plain if/else or match for routing
|
||||||
|
- **No magic** — path params, query params, headers are explicit function calls
|
||||||
|
- **Fast** — multi-threaded accept loop, zero allocations where possible
|
||||||
|
|
||||||
|
This is the same philosophy as Go's `net/http` — simple, explicit, composable.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Part of the Bux project. See root [LICENSE](../../LICENSE).
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
EXIT:137
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[Package]
|
||||||
|
Name = "boko-framework"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
Description = "Boko — async web framework for Bux, inspired by FastAPI"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
EXIT:137
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
EXIT:143
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
EXIT:137
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
bash: ред 1: ../../buxc_lir: Няма такъв файл или директория
|
||||||
|
EXIT:127
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
EXIT:143
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
EXIT:143
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
bux 0.1.0 (bootstrap)
|
||||||
|
EXIT:0
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
nim c -o:buxc -d:release --opt:size bootstrap/main.nim
|
||||||
|
Hint: used config file '/etc/nim/nim.cfg' [Conf]
|
||||||
|
Hint: used config file '/etc/nim/config.nims' [Conf]
|
||||||
|
...........................................................................................................................................
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lexer.nim(340, 12) Hint: 'check3' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lexer.nim(350, 12) Hint: 'checkEq' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lexer.nim(64, 6) Hint: 'match' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lexer.nim(84, 6) Hint: 'emitWarning' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
..
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/parser.nim(663, 7) Hint: 'loc' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/parser.nim(1001, 9) Hint: 'isVar' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/parser.nim(59, 6) Hint: 'match' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
.......
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/sema.nim(1150, 9) Hint: 'operandType' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/sema.nim(1155, 9) Hint: 'operandType' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/sema.nim(1165, 9) Hint: 'subjectType' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/sema.nim(1200, 9) Hint: 'operand' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/sema.nim(94, 6) Hint: 'emitWarning' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
.
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/manifest.nim(93, 11) Hint: 'val' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
..
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/hir_lower.nim(1057, 9) Hint: 'loweredIter' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
.
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lir.nim(5, 8) Warning: imported and not used: 'types' [UnusedImport]
|
||||||
|
.
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lir_lower.nim(419, 9) Hint: 't' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lir_lower.nim(595, 9) Hint: 'bodyLbl' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lir_lower.nim(734, 11) Hint: 'exprVal' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lir_lower.nim(746, 9) Hint: 'exprVal' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lir_lower.nim(6, 8) Warning: imported and not used: 'ast' [UnusedImport]
|
||||||
|
.
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lir_c_backend.nim(20, 6) Hint: 'emit' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lir_c_backend.nim(46, 6) Hint: 'typeFromValue' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lir_c_backend.nim(58, 6) Hint: 'setTempType' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/cli.nim(2, 55) Warning: imported and not used: 'lir' [UnusedImport]
|
||||||
|
Hint: [Link]
|
||||||
|
Hint: mm: orc; threads: on; opt: size; options: -d:release
|
||||||
|
76982 lines; 1.552s; 174.824MiB peakmem; proj: /home/ziko/z-git/bux/bux/bootstrap/main.nim; out: /home/ziko/z-git/bux/bux/buxc [SuccessX]
|
||||||
|
# strip buxc
|
||||||
|
EXIT:0
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd /home/ziko/z-git/bux/bux/apps/boko-framework
|
||||||
|
|
||||||
|
echo "=== CHECK ==="
|
||||||
|
../../buxc check 2>&1
|
||||||
|
echo "CHECK_EXIT=$?"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== BUILD ==="
|
||||||
|
../../buxc build 2>&1
|
||||||
|
echo "BUILD_EXIT=$?"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== FILES ==="
|
||||||
|
find . -type f -newer bux.toml 2>/dev/null | head -30
|
||||||
|
echo ""
|
||||||
|
echo "=== BINARY SEARCH ==="
|
||||||
|
file boko-framework 2>/dev/null || echo "no boko-framework"
|
||||||
|
ls -la build/ 2>/dev/null || echo "no build dir"
|
||||||
@@ -0,0 +1,507 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// 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};
|
||||||
|
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 eqPos as uint != 0 {
|
||||||
|
let keyLen: uint = eqPos as uint - pair as uint;
|
||||||
|
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 raw as uint == 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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 qmark as uint != 0 {
|
||||||
|
let pathLen: uint = qmark as uint - fullPath as uint;
|
||||||
|
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 colonPos as uint != 0 {
|
||||||
|
let keyLen: uint = colonPos as uint - line as uint;
|
||||||
|
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 braceClose as uint != 0 {
|
||||||
|
nameLen = braceClose as uint - patPart as uint - 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 raw as uint == 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
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// Boko Example App — demonstrates the framework API
|
||||||
|
// =============================================================================
|
||||||
|
module Main {
|
||||||
|
|
||||||
|
import Std::Io::{PrintLine, Print};
|
||||||
|
import Std::String::{String_Eq};
|
||||||
|
import Boko::{
|
||||||
|
App, App_New, App_Run,
|
||||||
|
Request, Response,
|
||||||
|
Request_GetQuery, Request_GetPathParam,
|
||||||
|
Response_Html, Response_Json, Response_NotFound, Response_Redirect,
|
||||||
|
Path_Match,
|
||||||
|
HttpVerb
|
||||||
|
};
|
||||||
|
|
||||||
|
extern func bux_strlen(s: String) -> uint;
|
||||||
|
extern func bux_sb_new(initial_cap: uint) -> *void;
|
||||||
|
extern func bux_sb_append(sb: *void, s: String);
|
||||||
|
extern func bux_sb_build(sb: *void) -> String;
|
||||||
|
extern func bux_sb_free(sb: *void);
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Boko_Router — user-defined dispatch (called by the framework)
|
||||||
|
// =============================================================================
|
||||||
|
func Boko_Router(req: Request) -> Response {
|
||||||
|
// --- GET / ---
|
||||||
|
if String_Eq(req.path, "/") && req.method.tag == HttpVerb_GET {
|
||||||
|
return Response_Html(PageHome());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GET /api/health ---
|
||||||
|
if String_Eq(req.path, "/api/health") {
|
||||||
|
return Response_Json("{\"status\":\"ok\",\"framework\":\"Boko\",\"version\":\"0.1.0\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GET /api/info ---
|
||||||
|
if String_Eq(req.path, "/api/info") {
|
||||||
|
return Response_Json("{\"name\":\"Boko\",\"language\":\"Bux\",\"inspiration\":\"FastAPI\",\"features\":[\"routing\",\"path-params\",\"query-params\",\"json\"]}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GET /hello?name=World ---
|
||||||
|
if String_Eq(req.path, "/hello") {
|
||||||
|
let name: String = Request_GetQuery(&req, "name");
|
||||||
|
if bux_strlen(name) == 0 { name = "World"; }
|
||||||
|
let sb: *void = bux_sb_new(256);
|
||||||
|
bux_sb_append(sb, "<h1>Hello, ");
|
||||||
|
bux_sb_append(sb, name);
|
||||||
|
bux_sb_append(sb, "!</h1>");
|
||||||
|
let html: String = bux_sb_build(sb);
|
||||||
|
bux_sb_free(sb);
|
||||||
|
return Response_Html(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GET /users/{id} ---
|
||||||
|
if Path_Match("/users/{id}", req.path, &req) {
|
||||||
|
let id: String = Request_GetPathParam(&req, "id");
|
||||||
|
let sb: *void = bux_sb_new(128);
|
||||||
|
bux_sb_append(sb, "{\"id\":");
|
||||||
|
bux_sb_append(sb, id);
|
||||||
|
bux_sb_append(sb, ",\"name\":\"User ");
|
||||||
|
bux_sb_append(sb, id);
|
||||||
|
bux_sb_append(sb, "\"}");
|
||||||
|
let json: String = bux_sb_build(sb);
|
||||||
|
bux_sb_free(sb);
|
||||||
|
return Response_Json(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GET /posts/{postId}/comments/{commentId} ---
|
||||||
|
if Path_Match("/posts/{postId}/comments/{commentId}", req.path, &req) {
|
||||||
|
let pid: String = Request_GetPathParam(&req, "postId");
|
||||||
|
let cid: String = Request_GetPathParam(&req, "commentId");
|
||||||
|
let sb: *void = bux_sb_new(128);
|
||||||
|
bux_sb_append(sb, "{\"postId\":");
|
||||||
|
bux_sb_append(sb, pid);
|
||||||
|
bux_sb_append(sb, ",\"commentId\":");
|
||||||
|
bux_sb_append(sb, cid);
|
||||||
|
bux_sb_append(sb, ",\"text\":\"Great post!\"}");
|
||||||
|
let json: String = bux_sb_build(sb);
|
||||||
|
bux_sb_free(sb);
|
||||||
|
return Response_Json(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GET /redirect → 302 to / ---
|
||||||
|
if String_Eq(req.path, "/redirect") {
|
||||||
|
return Response_Redirect("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- POST /api/echo ---
|
||||||
|
if String_Eq(req.path, "/api/echo") && req.method.tag == HttpVerb_POST {
|
||||||
|
let sb: *void = bux_sb_new(256);
|
||||||
|
bux_sb_append(sb, "{\"echo\":");
|
||||||
|
bux_sb_append(sb, req.body);
|
||||||
|
bux_sb_append(sb, "}");
|
||||||
|
let json: String = bux_sb_build(sb);
|
||||||
|
bux_sb_free(sb);
|
||||||
|
return Response_Json(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 404 ---
|
||||||
|
return Response_NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// HTML landing page (raw multi-line string via backticks)
|
||||||
|
// =============================================================================
|
||||||
|
func PageHome() -> String {
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Boko Framework</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:system-ui,sans-serif;background:#0d1117;color:#c9d1d9;min-height:100vh;display:flex;align-items:center;justify-content:center}
|
||||||
|
.card{background:#161b22;border:1px solid #30363d;border-radius:12px;padding:3rem;max-width:560px;width:100%}
|
||||||
|
h1{color:#58a6ff;font-size:2rem;margin-bottom:.5rem}
|
||||||
|
.tag{color:#8b949e;font-size:.9rem;margin-bottom:2rem}
|
||||||
|
h3{color:#d2a8ff;margin:1.5rem 0 .75rem}
|
||||||
|
.ep{display:flex;gap:1rem;padding:.35rem 0;font-family:monospace;font-size:.9rem}
|
||||||
|
.ep .m{color:#3fb950;font-weight:bold;min-width:52px}
|
||||||
|
.ep .p{color:#c9d1d9}
|
||||||
|
.ep .d{color:#484f58;margin-left:auto}
|
||||||
|
a{color:#58a6ff}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>⚡ Boko</h1>
|
||||||
|
<p class="tag">Async web framework for Bux — inspired by FastAPI</p>
|
||||||
|
<h3>Try it</h3>
|
||||||
|
<div class="ep"><span class="m">GET</span><span class="p"><a href="/hello?name=Bux">/hello?name=Bux</a></span><span class="d">query param</span></div>
|
||||||
|
<div class="ep"><span class="m">GET</span><span class="p"><a href="/users/42">/users/42</a></span><span class="d">path param</span></div>
|
||||||
|
<div class="ep"><span class="m">GET</span><span class="p"><a href="/posts/7/comments/3">/posts/7/comments/3</a></span><span class="d">multi params</span></div>
|
||||||
|
<div class="ep"><span class="m">GET</span><span class="p"><a href="/redirect">/redirect</a></span><span class="d">302 → /</span></div>
|
||||||
|
<div class="ep"><span class="m">GET</span><span class="p"><a href="/api/health">/api/health</a></span><span class="d">JSON</span></div>
|
||||||
|
<div class="ep"><span class="m">GET</span><span class="p"><a href="/api/info">/api/info</a></span><span class="d">JSON</span></div>
|
||||||
|
<h3>Features</h3>
|
||||||
|
<div class="ep"><span class="m">✓</span><span class="p">Path routing with {params}</span></div>
|
||||||
|
<div class="ep"><span class="m">✓</span><span class="p">Query parameter extraction</span></div>
|
||||||
|
<div class="ep"><span class="m">✓</span><span class="p">JSON / HTML / Text responses</span></div>
|
||||||
|
<div class="ep"><span class="m">✓</span><span class="p">Multi-threaded (configurable)</span></div>
|
||||||
|
<div class="ep"><span class="m">✓</span><span class="p">Redirects (302)</span></div>
|
||||||
|
<div class="ep"><span class="m">✓</span><span class="p">POST body access</span></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Main — start the server
|
||||||
|
// =============================================================================
|
||||||
|
func Main() -> int {
|
||||||
|
let app: App = App_New(8080, 4);
|
||||||
|
App_Run(&app);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // module Main
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
# jwt-pitbul
|
||||||
|
|
||||||
|
**JWT CLI tool for the Bux ecosystem — sign, verify, decode, and key generation.**
|
||||||
|
|
||||||
|
A standalone command-line utility for working with JSON Web Tokens (RFC 7519). Built on `Std::Crypto`, backed by OpenSSL.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/jwt-pitbul
|
||||||
|
../../buxc build
|
||||||
|
./jwt-pitbul
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### `sign` — Create a JWT
|
||||||
|
|
||||||
|
```
|
||||||
|
jwt-pitbul sign <alg> <key> <claims_json>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Argument | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `<alg>` | Algorithm: `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `EdDSA` |
|
||||||
|
| `<key>` | HMAC secret string, or path to PEM file (RSA/ECDSA), or base64 raw key (Ed25519) |
|
||||||
|
| `<claims_json>` | JSON payload string (e.g. `{"sub":"123","exp":1735689600}`) |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# HMAC
|
||||||
|
jwt-pitbul sign HS256 'my-secret-key' '{"sub":"123","role":"admin"}'
|
||||||
|
|
||||||
|
# RSA (private key from file)
|
||||||
|
jwt-pitbul sign RS256 private.pem '{"sub":"456"}'
|
||||||
|
|
||||||
|
# ECDSA
|
||||||
|
jwt-pitbul sign ES256 ec_private.pem '{"sub":"789"}'
|
||||||
|
|
||||||
|
# Ed25519 (base64-encoded 32-byte private key)
|
||||||
|
jwt-pitbul sign EdDSA 'MC4CAQAwBQYDK2VwBCIEIJ...' '{"sub":"000"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### `verify` — Verify and decode
|
||||||
|
|
||||||
|
```
|
||||||
|
jwt-pitbul verify <token> <alg> <key>
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jwt-pitbul verify eyJhbGciOiJ... HS256 'my-secret-key'
|
||||||
|
# ✓ Signature valid
|
||||||
|
#
|
||||||
|
# Header:
|
||||||
|
# {"alg":"HS256","typ":"JWT"}
|
||||||
|
#
|
||||||
|
# Payload:
|
||||||
|
# {"sub":"123","role":"admin"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `decode` — Decode without verification
|
||||||
|
|
||||||
|
```
|
||||||
|
jwt-pitbul decode <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jwt-pitbul decode eyJhbGciOiJ...
|
||||||
|
# Decoded (no verification):
|
||||||
|
#
|
||||||
|
# Header:
|
||||||
|
# {"alg":"HS256","typ":"JWT"}
|
||||||
|
#
|
||||||
|
# Payload:
|
||||||
|
# {"sub":"123"}
|
||||||
|
#
|
||||||
|
# Signature (base64url): xxxxxxxxx...
|
||||||
|
```
|
||||||
|
|
||||||
|
### `keygen` — Generate keys
|
||||||
|
|
||||||
|
```
|
||||||
|
jwt-pitbul keygen <type>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Type | Output |
|
||||||
|
|------|--------|
|
||||||
|
| `ed25519` | Generates an Ed25519 keypair (prints base64 public/private keys) |
|
||||||
|
| `rsa` | Prints OpenSSL CLI commands to generate RSA 2048-bit keys |
|
||||||
|
| `ecdsa` | Prints OpenSSL CLI commands to generate ECDSA P-256 keys |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jwt-pitbul keygen ed25519
|
||||||
|
# Ed25519 keypair (base64):
|
||||||
|
# Public: MCowBQYDK2VwAyEA...
|
||||||
|
# Private: MC4CAQAwBQYDK2VwBCIEIJ...
|
||||||
|
|
||||||
|
jwt-pitbul keygen rsa
|
||||||
|
# RSA key generation requires OpenSSL CLI:
|
||||||
|
# openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
|
||||||
|
# openssl rsa -in private.pem -pubout -out public.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Algorithms
|
||||||
|
|
||||||
|
| Algorithm | Type | Key Format |
|
||||||
|
|-----------|------|------------|
|
||||||
|
| HS256 | HMAC-SHA256 | Raw secret string |
|
||||||
|
| HS384 | HMAC-SHA384 | Raw secret string |
|
||||||
|
| HS512 | HMAC-SHA512 | Raw secret string |
|
||||||
|
| RS256 | RSA PKCS#1 v1.5 SHA-256 | PEM file path |
|
||||||
|
| RS384 | RSA PKCS#1 v1.5 SHA-384 | PEM file path |
|
||||||
|
| RS512 | RSA PKCS#1 v1.5 SHA-512 | PEM file path |
|
||||||
|
| ES256 | ECDSA P-256 | PEM file path |
|
||||||
|
| ES384 | ECDSA P-384 | PEM file path |
|
||||||
|
| EdDSA | Ed25519 | Base64 32-byte raw key |
|
||||||
|
|
||||||
|
## Generating Keys with OpenSSL
|
||||||
|
|
||||||
|
### RSA 2048-bit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
|
||||||
|
openssl rsa -in private.pem -pubout -out public.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
### ECDSA P-256
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem
|
||||||
|
openssl ec -in ec_private.pem -pubout -out ec_public.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
### ECDSA P-384
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl ecparam -genkey -name secp384r1 -noout -out ec_private.pem
|
||||||
|
openssl ec -in ec_private.pem -pubout -out ec_public.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ed25519
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In-app generation:
|
||||||
|
jwt-pitbul keygen ed25519
|
||||||
|
|
||||||
|
# Or via OpenSSL:
|
||||||
|
openssl genpkey -algorithm Ed25519 -out ed25519_private.pem
|
||||||
|
openssl pkey -in ed25519_private.pem -pubout -out ed25519_public.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Codes
|
||||||
|
|
||||||
|
| Exit | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| 0 | Success |
|
||||||
|
| 1 | Error (invalid arguments, bad signature, missing file, etc.) |
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/jwt-pitbul
|
||||||
|
../../buxc build # Bootstrap compiler (Nim)
|
||||||
|
# or
|
||||||
|
../../buxc_lir build # Self-hosted compiler
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Bux standard library (`Std::Crypto`, `Std::Io`, `Std::String`)
|
||||||
|
- OpenSSL 1.1.1+ (linked via runtime)
|
||||||
|
- Bux compiler (`buxc` or `buxc_lir`)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Part of the Bux project. See root [LICENSE](../../LICENSE).
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[Package]
|
||||||
|
Name = "jwt-pitbul"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
Description = "JWT CLI tool — encode, decode, verify, and key generation"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
@@ -0,0 +1,326 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// jwt-pitbul — JWT CLI Tool (encode, decode, verify, keygen)
|
||||||
|
// Built with Bux + Std::Crypto
|
||||||
|
// =============================================================================
|
||||||
|
module Main {
|
||||||
|
|
||||||
|
import Std::Io::{PrintLine, Print, PrintInt};
|
||||||
|
import Std::String::{String_Len, String_Eq, String_Contains};
|
||||||
|
import Std::Crypto::Jwt::{
|
||||||
|
JwtAlg,
|
||||||
|
Jwt_MakeHeader,
|
||||||
|
Jwt_Encode,
|
||||||
|
Jwt_Decode,
|
||||||
|
Jwt_EncodeHS256, Jwt_EncodeHS384, Jwt_EncodeHS512,
|
||||||
|
Jwt_EncodeRS256, Jwt_EncodeES256, Jwt_EncodeEdDSA
|
||||||
|
};
|
||||||
|
import Std::Crypto::Base64::{Base64URL_Decode, Base64_Encode};
|
||||||
|
import Std::Crypto::Ed25519::{Ed25519_Keypair};
|
||||||
|
|
||||||
|
extern func bux_argc() -> int;
|
||||||
|
extern func bux_argv(index: int) -> String;
|
||||||
|
extern func bux_alloc(size: uint) -> *void;
|
||||||
|
extern func bux_read_file(path: String) -> String;
|
||||||
|
extern func bux_file_exists(path: String) -> int;
|
||||||
|
extern func bux_str_split_part(s: String, delim: String, index: uint) -> String;
|
||||||
|
extern func bux_str_split_count(s: String, delim: String) -> uint;
|
||||||
|
extern func bux_base64_encode(data: String, len: int) -> String;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Help / Usage
|
||||||
|
// =============================================================================
|
||||||
|
func PrintUsage() {
|
||||||
|
PrintLine("╔══════════════════════════════════════════════════════╗");
|
||||||
|
PrintLine("║ jwt-pitbul — JWT CLI Tool v0.1.0 ║");
|
||||||
|
PrintLine("║ Sign, verify, decode JSON Web Tokens ║");
|
||||||
|
PrintLine("╚══════════════════════════════════════════════════════╝");
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("Usage:");
|
||||||
|
PrintLine(" jwt-pitbul sign <alg> <key> <claims>");
|
||||||
|
PrintLine(" jwt-pitbul verify <token> <alg> <key>");
|
||||||
|
PrintLine(" jwt-pitbul decode <token>");
|
||||||
|
PrintLine(" jwt-pitbul keygen <type>");
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("Commands:");
|
||||||
|
PrintLine(" sign Create a signed JWT from claims JSON");
|
||||||
|
PrintLine(" verify Verify a JWT signature and print payload");
|
||||||
|
PrintLine(" decode Decode a JWT without verification");
|
||||||
|
PrintLine(" keygen Generate cryptographic keys");
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("Algorithms:");
|
||||||
|
PrintLine(" HS256, HS384, HS512 — HMAC (symmetric)");
|
||||||
|
PrintLine(" RS256, RS384, RS512 — RSA PKCS#1 v1.5");
|
||||||
|
PrintLine(" ES256, ES384 — ECDSA P-256 / P-384");
|
||||||
|
PrintLine(" EdDSA — Ed25519");
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("Key formats:");
|
||||||
|
PrintLine(" HMAC: raw secret string");
|
||||||
|
PrintLine(" RSA: path to PEM private/public key file");
|
||||||
|
PrintLine(" ECDSA: path to PEM private/public key file");
|
||||||
|
PrintLine(" EdDSA: base64-encoded 32-byte raw key");
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("Key generation:");
|
||||||
|
PrintLine(" jwt-pitbul keygen rsa # RSA 2048-bit (PEM)");
|
||||||
|
PrintLine(" jwt-pitbul keygen ecdsa # ECDSA P-256 (PEM)");
|
||||||
|
PrintLine(" jwt-pitbul keygen ed25519 # Ed25519 (raw base64)");
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("Examples:");
|
||||||
|
PrintLine(" jwt-pitbul sign HS256 'my-secret' '{\"sub\":\"123\"}'");
|
||||||
|
PrintLine(" jwt-pitbul verify eyJh... HS256 'my-secret'");
|
||||||
|
PrintLine(" jwt-pitbul decode eyJh...");
|
||||||
|
PrintLine(" jwt-pitbul keygen ed25519");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Algorithm name → JwtAlg enum
|
||||||
|
// =============================================================================
|
||||||
|
func ParseAlg(name: String) -> JwtAlg {
|
||||||
|
if String_Eq(name, "HS256") { return JwtAlg { tag: JwtAlg_HS256 }; }
|
||||||
|
if String_Eq(name, "HS384") { return JwtAlg { tag: JwtAlg_HS384 }; }
|
||||||
|
if String_Eq(name, "HS512") { return JwtAlg { tag: JwtAlg_HS512 }; }
|
||||||
|
if String_Eq(name, "RS256") { return JwtAlg { tag: JwtAlg_RS256 }; }
|
||||||
|
if String_Eq(name, "RS384") { return JwtAlg { tag: JwtAlg_RS384 }; }
|
||||||
|
if String_Eq(name, "RS512") { return JwtAlg { tag: JwtAlg_RS512 }; }
|
||||||
|
if String_Eq(name, "ES256") { return JwtAlg { tag: JwtAlg_ES256 }; }
|
||||||
|
if String_Eq(name, "ES384") { return JwtAlg { tag: JwtAlg_ES384 }; }
|
||||||
|
if String_Eq(name, "EdDSA") { return JwtAlg { tag: JwtAlg_EdDSA }; }
|
||||||
|
return JwtAlg { tag: JwtAlg_HS256 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Resolve key — for RSA/ECDSA, read PEM file; for HMAC/EdDSA, pass through
|
||||||
|
// =============================================================================
|
||||||
|
func ResolveKey(alg: JwtAlg, keyArg: String) -> String {
|
||||||
|
let algTag: int = alg.tag;
|
||||||
|
|
||||||
|
// RSA and ECDSA: key is a path to PEM file
|
||||||
|
if algTag == JwtAlg_RS256 || algTag == JwtAlg_RS384 || algTag == JwtAlg_RS512 ||
|
||||||
|
algTag == JwtAlg_ES256 || algTag == JwtAlg_ES384 {
|
||||||
|
if bux_file_exists(keyArg) != 0 {
|
||||||
|
let pem: String = bux_read_file(keyArg);
|
||||||
|
if pem as uint != 0 && String_Len(pem) > 0 {
|
||||||
|
return pem;
|
||||||
|
}
|
||||||
|
Print("ERROR: could not read PEM file: ");
|
||||||
|
PrintLine(keyArg);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
Print("ERROR: PEM file not found: ");
|
||||||
|
PrintLine(keyArg);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// HMAC and EdDSA: key is used directly
|
||||||
|
return keyArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Check if algorithm is symmetric (HMAC) or asymmetric
|
||||||
|
// =============================================================================
|
||||||
|
func IsSymmetric(alg: JwtAlg) -> bool {
|
||||||
|
let t: int = alg.tag;
|
||||||
|
return t == JwtAlg_HS256 || t == JwtAlg_HS384 || t == JwtAlg_HS512;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Command: sign
|
||||||
|
// =============================================================================
|
||||||
|
func CmdSign(algName: String, keyArg: String, claimsJson: String) -> int {
|
||||||
|
let alg: JwtAlg = ParseAlg(algName);
|
||||||
|
let key: String = ResolveKey(alg, keyArg);
|
||||||
|
if String_Len(key) == 0 {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let header: String = Jwt_MakeHeader(alg);
|
||||||
|
let token: String = Jwt_Encode(header, claimsJson, alg, key);
|
||||||
|
|
||||||
|
if String_Len(token) == 0 {
|
||||||
|
PrintLine("ERROR: signing failed");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintLine(token);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Command: verify
|
||||||
|
// =============================================================================
|
||||||
|
func CmdVerify(token: String, algName: String, keyArg: String) -> int {
|
||||||
|
let alg: JwtAlg = ParseAlg(algName);
|
||||||
|
let key: String = ResolveKey(alg, keyArg);
|
||||||
|
if String_Len(key) == 0 {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var header: String = "";
|
||||||
|
var payload: String = "";
|
||||||
|
|
||||||
|
let ok: bool = Jwt_Decode(token, alg, key, &header, &payload);
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
PrintLine("✓ Signature valid");
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("Header:");
|
||||||
|
PrintLine(header);
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("Payload:");
|
||||||
|
PrintLine(payload);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintLine("✗ Signature INVALID (or malformed token)");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Command: decode (no verification)
|
||||||
|
// =============================================================================
|
||||||
|
func CmdDecode(token: String) -> int {
|
||||||
|
let partCount: uint = bux_str_split_count(token, ".");
|
||||||
|
if partCount != 3 {
|
||||||
|
PrintLine("ERROR: not a valid JWT (expected 3 parts)");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let headerB64: String = bux_str_split_part(token, ".", 0);
|
||||||
|
let payloadB64: String = bux_str_split_part(token, ".", 1);
|
||||||
|
let sigB64: String = bux_str_split_part(token, ".", 2);
|
||||||
|
|
||||||
|
let headerJson: String = Base64URL_Decode(headerB64);
|
||||||
|
let payloadJson: String = Base64URL_Decode(payloadB64);
|
||||||
|
|
||||||
|
PrintLine("Decoded (no verification):");
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("Header:");
|
||||||
|
PrintLine(headerJson);
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("Payload:");
|
||||||
|
PrintLine(payloadJson);
|
||||||
|
PrintLine("");
|
||||||
|
Print("Signature (base64url): ");
|
||||||
|
PrintLine(sigB64);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Command: keygen
|
||||||
|
// =============================================================================
|
||||||
|
func CmdKeygen(keyType: String) -> int {
|
||||||
|
if String_Eq(keyType, "ed25519") {
|
||||||
|
let pub: *void = bux_alloc(32);
|
||||||
|
let priv: *void = bux_alloc(32);
|
||||||
|
if !Ed25519_Keypair(pub, priv) {
|
||||||
|
PrintLine("ERROR: Ed25519 key generation failed (OpenSSL 1.1.1+ required)");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
let pubB64: String = bux_base64_encode(pub as String, 32);
|
||||||
|
let privB64: String = bux_base64_encode(priv as String, 32);
|
||||||
|
PrintLine("Ed25519 keypair (base64):");
|
||||||
|
Print(" Public: ");
|
||||||
|
PrintLine(pubB64);
|
||||||
|
Print(" Private: ");
|
||||||
|
PrintLine(privB64);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if String_Eq(keyType, "rsa") {
|
||||||
|
PrintLine("RSA key generation requires OpenSSL CLI:");
|
||||||
|
PrintLine(" openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048");
|
||||||
|
PrintLine(" openssl rsa -in private.pem -pubout -out public.pem");
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("(Key generation from within Bux requires PEM write support — coming soon)");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if String_Eq(keyType, "ecdsa") {
|
||||||
|
PrintLine("ECDSA key generation requires OpenSSL CLI:");
|
||||||
|
PrintLine(" openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem");
|
||||||
|
PrintLine(" openssl ec -in ec_private.pem -pubout -out ec_public.pem");
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("(Key generation from within Bux requires PEM write support — coming soon)");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Print("ERROR: unknown key type '");
|
||||||
|
Print(keyType);
|
||||||
|
PrintLine("'. Use: rsa, ecdsa, ed25519");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Main Entry Point
|
||||||
|
// =============================================================================
|
||||||
|
func Main() -> int {
|
||||||
|
let argc: int = bux_argc();
|
||||||
|
|
||||||
|
if argc < 2 {
|
||||||
|
PrintUsage();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect arguments
|
||||||
|
let args: *String = bux_alloc(argc as uint * 8) as *String;
|
||||||
|
var i: int = 0;
|
||||||
|
while i < argc {
|
||||||
|
args[i] = bux_argv(i);
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let command: String = args[1];
|
||||||
|
|
||||||
|
if String_Eq(command, "help") || String_Eq(command, "--help") || String_Eq(command, "-h") {
|
||||||
|
PrintUsage();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- sign <alg> <key> <claims> ---
|
||||||
|
if String_Eq(command, "sign") {
|
||||||
|
if argc < 5 {
|
||||||
|
PrintLine("ERROR: 'sign' requires: <alg> <key> <claims_json>");
|
||||||
|
PrintLine(" jwt-pitbul sign HS256 'secret' '{\"sub\":\"123\"}'");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return CmdSign(args[2], args[3], args[4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- verify <token> <alg> <key> ---
|
||||||
|
if String_Eq(command, "verify") {
|
||||||
|
if argc < 5 {
|
||||||
|
PrintLine("ERROR: 'verify' requires: <token> <alg> <key>");
|
||||||
|
PrintLine(" jwt-pitbul verify eyJh... HS256 'secret'");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return CmdVerify(args[2], args[3], args[4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- decode <token> ---
|
||||||
|
if String_Eq(command, "decode") {
|
||||||
|
if argc < 3 {
|
||||||
|
PrintLine("ERROR: 'decode' requires: <token>");
|
||||||
|
PrintLine(" jwt-pitbul decode eyJh...");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return CmdDecode(args[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- keygen <type> ---
|
||||||
|
if String_Eq(command, "keygen") {
|
||||||
|
if argc < 3 {
|
||||||
|
PrintLine("ERROR: 'keygen' requires: <type>");
|
||||||
|
PrintLine(" jwt-pitbul keygen ed25519");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return CmdKeygen(args[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Print("ERROR: unknown command '");
|
||||||
|
Print(command);
|
||||||
|
PrintLine("'");
|
||||||
|
PrintLine("Run 'jwt-pitbul help' for usage.");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // module Main
|
||||||
@@ -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
|
||||||
+21
-6
@@ -1,6 +1,19 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// Std::Crypto — backward-compatible wrapper (delegates to submodules)
|
||||||
|
//
|
||||||
|
// Migration note: new code should use the submodules directly:
|
||||||
|
// Std::Crypto::Hash::{Hash_Sha256, ...}
|
||||||
|
// Std::Crypto::Hmac::{Hmac_Sha256, ...}
|
||||||
|
// Std::Crypto::Base64::{Base64_Encode, Base64URL_Encode, ...}
|
||||||
|
// Std::Crypto::Random::{Random_Bytes, ...}
|
||||||
|
// Std::Crypto::Jwt::{Jwt_EncodeHS256, ...}
|
||||||
|
// =============================================================================
|
||||||
module Std::Crypto {
|
module Std::Crypto {
|
||||||
|
|
||||||
import Std::Mem::{Alloc, Free};
|
import Std::Mem::{Alloc, Free};
|
||||||
import Std::String::{String_Len};
|
import Std::String::{String_Len};
|
||||||
|
|
||||||
|
// Re-use the same externs from submodules (merged by compiler)
|
||||||
extern func bux_sha256(data: String, len: int, out: *void);
|
extern func bux_sha256(data: String, len: int, out: *void);
|
||||||
extern func bux_hmac_sha256(key: String, keylen: int, msg: String, msglen: int, out: *void);
|
extern func bux_hmac_sha256(key: String, keylen: int, msg: String, msglen: int, out: *void);
|
||||||
extern func bux_random_bytes(buf: *void, len: int) -> int;
|
extern func bux_random_bytes(buf: *void, len: int) -> int;
|
||||||
@@ -8,7 +21,9 @@ extern func bux_base64_encode(data: String, len: int) -> String;
|
|||||||
extern func bux_base64_decode(data: String, len: int, outlen: *int) -> String;
|
extern func bux_base64_decode(data: String, len: int, outlen: *int) -> String;
|
||||||
extern func bux_bytes_to_hex(data: *void, len: int) -> String;
|
extern func bux_bytes_to_hex(data: *void, len: int) -> String;
|
||||||
|
|
||||||
/* SHA-256: returns lowercase hex string of the hash */
|
// --- Legacy function names (delegate to new submodule functions) ---
|
||||||
|
|
||||||
|
// SHA-256 → hex
|
||||||
func Crypto_Sha256(data: String) -> String {
|
func Crypto_Sha256(data: String) -> String {
|
||||||
let len: int = String_Len(data) as int;
|
let len: int = String_Len(data) as int;
|
||||||
let hashBuf: *void = Alloc(32);
|
let hashBuf: *void = Alloc(32);
|
||||||
@@ -18,7 +33,7 @@ func Crypto_Sha256(data: String) -> String {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* HMAC-SHA256: returns lowercase hex string */
|
// HMAC-SHA256 → hex
|
||||||
func Crypto_HmacSha256(key: String, message: String) -> String {
|
func Crypto_HmacSha256(key: String, message: String) -> String {
|
||||||
let keylen: int = String_Len(key) as int;
|
let keylen: int = String_Len(key) as int;
|
||||||
let msglen: int = String_Len(message) as int;
|
let msglen: int = String_Len(message) as int;
|
||||||
@@ -29,7 +44,7 @@ func Crypto_HmacSha256(key: String, message: String) -> String {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Random bytes: returns a string of n random bytes */
|
// Random bytes → base64
|
||||||
func Crypto_RandomBytes(n: int) -> String {
|
func Crypto_RandomBytes(n: int) -> String {
|
||||||
if n <= 0 { return ""; }
|
if n <= 0 { return ""; }
|
||||||
let buf: *void = Alloc(n as uint);
|
let buf: *void = Alloc(n as uint);
|
||||||
@@ -42,12 +57,12 @@ func Crypto_RandomBytes(n: int) -> String {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Base64 encode */
|
// Base64 encode
|
||||||
func Crypto_Base64Encode(s: String) -> String {
|
func Crypto_Base64Encode(s: String) -> String {
|
||||||
return bux_base64_encode(s, String_Len(s) as int);
|
return bux_base64_encode(s, String_Len(s) as int);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* HMAC-SHA256 raw: returns base64 of raw binary hmac */
|
// HMAC-SHA256 raw → base64
|
||||||
func Crypto_HmacSha256Raw(key: String, message: String) -> String {
|
func Crypto_HmacSha256Raw(key: String, message: String) -> String {
|
||||||
let keylen: int = String_Len(key) as int;
|
let keylen: int = String_Len(key) as int;
|
||||||
let msglen: int = String_Len(message) as int;
|
let msglen: int = String_Len(message) as int;
|
||||||
@@ -58,7 +73,7 @@ func Crypto_HmacSha256Raw(key: String, message: String) -> String {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Base64 decode */
|
// Base64 decode
|
||||||
func Crypto_Base64Decode(s: String) -> String {
|
func Crypto_Base64Decode(s: String) -> String {
|
||||||
let outlen: int = 0;
|
let outlen: int = 0;
|
||||||
return bux_base64_decode(s, String_Len(s) as int, &outlen);
|
return bux_base64_decode(s, String_Len(s) as int, &outlen);
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
# Bux Crypto Library (`Std::Crypto`)
|
||||||
|
|
||||||
|
Cryptographic primitives for the Bux programming language. Backed by OpenSSL via the C runtime (`rt/runtime.c`).
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
| Module | Import Path | Provides |
|
||||||
|
|--------|------------|----------|
|
||||||
|
| **Base64** | `Std::Crypto::Base64` | Base64 and Base64URL (RFC 4648 §5) encode/decode |
|
||||||
|
| **Hash** | `Std::Crypto::Hash` | SHA-1, SHA-256, SHA-384, SHA-512 (hex + raw) |
|
||||||
|
| **HMAC** | `Std::Crypto::Hmac` | HMAC-SHA256, HMAC-SHA384, HMAC-SHA512 (hex + raw + base64) |
|
||||||
|
| **Random** | `Std::Crypto::Random` | Cryptographically secure random bytes, hex, base64, uint32 |
|
||||||
|
| **AES** | `Std::Crypto::Aes` | AES-256-CBC and AES-256-GCM encrypt/decrypt |
|
||||||
|
| **RSA** | `Std::Crypto::Rsa` | RSA PKCS#1 v1.5 sign/verify (SHA-256/384/512) |
|
||||||
|
| **ECDSA** | `Std::Crypto::Ecdsa` | ECDSA P-256 and P-384 sign/verify |
|
||||||
|
| **Ed25519** | `Std::Crypto::Ed25519` | Ed25519 key generation, signing, verification |
|
||||||
|
| **JWT** | `Std::Crypto::Jwt` | JSON Web Tokens (HS256/384/512, RS256/384/512, ES256/384, EdDSA) |
|
||||||
|
|
||||||
|
The legacy `Std::Crypto` module (the old single-file API) is preserved as a backward-compatible wrapper — existing code continues to work.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Hash
|
||||||
|
|
||||||
|
```bux
|
||||||
|
import Std::Crypto::Hash::{Hash_Sha256, Hash_Sha384, Hash_Sha512};
|
||||||
|
|
||||||
|
let hex: String = Hash_Sha256("hello");
|
||||||
|
// → "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
|
||||||
|
|
||||||
|
let size: int = Hash_Sha256Size(); // → 32 (bytes)
|
||||||
|
```
|
||||||
|
|
||||||
|
### HMAC
|
||||||
|
|
||||||
|
```bux
|
||||||
|
import Std::Crypto::Hmac::{Hmac_Sha256, Hmac_Sha256Raw, Hmac_Sha256Base64};
|
||||||
|
|
||||||
|
let hex: String = Hmac_Sha256("secret-key", "message");
|
||||||
|
// → hex string
|
||||||
|
|
||||||
|
// Raw binary output (caller allocates 32-byte buffer)
|
||||||
|
let buf: *void = Alloc(32);
|
||||||
|
Hmac_Sha256Raw("secret-key", "message", buf);
|
||||||
|
// buf now contains 32 bytes of raw HMAC
|
||||||
|
|
||||||
|
let b64: String = Hmac_Sha256Base64("secret-key", "message");
|
||||||
|
// → base64-encoded HMAC
|
||||||
|
```
|
||||||
|
|
||||||
|
### Base64 & Base64URL
|
||||||
|
|
||||||
|
```bux
|
||||||
|
import Std::Crypto::Base64::{Base64_Encode, Base64_Decode,
|
||||||
|
Base64URL_Encode, Base64URL_Decode};
|
||||||
|
|
||||||
|
let std: String = Base64_Encode("hello"); // → "aGVsbG8="
|
||||||
|
let orig: String = Base64_Decode(std); // → "hello"
|
||||||
|
|
||||||
|
let url: String = Base64URL_Encode("hello"); // → "aGVsbG8" (no padding)
|
||||||
|
let orig2: String = Base64URL_Decode(url); // → "hello"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Random
|
||||||
|
|
||||||
|
```bux
|
||||||
|
import Std::Crypto::Random::{Random_Bytes, Random_Hex, Random_Base64, Random_Uint32};
|
||||||
|
|
||||||
|
let raw: String = Random_Bytes(32); // 32 random bytes (binary-safe string)
|
||||||
|
let hex: String = Random_Hex(16); // 16 random bytes as hex (32 chars)
|
||||||
|
let b64: String = Random_Base64(16); // 16 random bytes as base64
|
||||||
|
let u32: uint = Random_Uint32(); // random 32-bit unsigned integer
|
||||||
|
```
|
||||||
|
|
||||||
|
### AES-256
|
||||||
|
|
||||||
|
```bux
|
||||||
|
import Std::Crypto::Aes::{Aes_GenerateKey, Aes_GenerateIV,
|
||||||
|
Aes_CbcEncrypt, Aes_CbcDecrypt,
|
||||||
|
Aes_GcmEncrypt, Aes_GcmDecrypt};
|
||||||
|
|
||||||
|
// Generate random key and IV
|
||||||
|
let key: String = Aes_GenerateKey(); // 32 raw bytes
|
||||||
|
let iv: String = Aes_GenerateIV(); // 16 raw bytes
|
||||||
|
|
||||||
|
// CBC mode
|
||||||
|
let cipher: String = Aes_CbcEncrypt("secret message", key, iv);
|
||||||
|
let plain: String = Aes_CbcDecrypt(cipher, key, iv);
|
||||||
|
|
||||||
|
// GCM mode (authenticated encryption)
|
||||||
|
let tag: *void = Alloc(16);
|
||||||
|
let gcmCipher: String = Aes_GcmEncrypt("secret", key, iv, tag);
|
||||||
|
let gcmPlain: String = Aes_GcmDecrypt(gcmCipher, key, iv, tag as String);
|
||||||
|
```
|
||||||
|
|
||||||
|
### RSA
|
||||||
|
|
||||||
|
```bux
|
||||||
|
import Std::Crypto::Rsa::{Rsa_SignSha256, Rsa_VerifySha256,
|
||||||
|
Rsa_SignSha256Base64, Rsa_VerifySha256Base64};
|
||||||
|
|
||||||
|
// Keys are PEM-encoded strings
|
||||||
|
let pemPriv: String = ReadFile("private.pem");
|
||||||
|
let pemPub: String = ReadFile("public.pem");
|
||||||
|
|
||||||
|
// Sign — returns raw signature
|
||||||
|
let sig: String = Rsa_SignSha256(pemPriv, "data to sign");
|
||||||
|
|
||||||
|
// Or sign and get base64
|
||||||
|
let sigB64: String = Rsa_SignSha256Base64(pemPriv, "data to sign");
|
||||||
|
|
||||||
|
// Verify raw signature
|
||||||
|
let valid: bool = Rsa_VerifySha256(pemPub, "data to sign", sig);
|
||||||
|
|
||||||
|
// Verify base64 signature
|
||||||
|
let validB64: bool = Rsa_VerifySha256Base64(pemPub, "data to sign", sigB64);
|
||||||
|
```
|
||||||
|
|
||||||
|
### ECDSA
|
||||||
|
|
||||||
|
```bux
|
||||||
|
import Std::Crypto::Ecdsa::{Ecdsa_SignP256, Ecdsa_VerifyP256,
|
||||||
|
Ecdsa_SignP384, Ecdsa_VerifyP384};
|
||||||
|
|
||||||
|
// P-256 (ES256)
|
||||||
|
let sig: String = Ecdsa_SignP256(pemPriv, "data");
|
||||||
|
let ok: bool = Ecdsa_VerifyP256(pemPub, "data", sig);
|
||||||
|
|
||||||
|
// P-384 (ES384)
|
||||||
|
let sig384: String = Ecdsa_SignP384(pemPriv, "data");
|
||||||
|
let ok384: bool = Ecdsa_VerifyP384(pemPub, "data", sig384);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ed25519
|
||||||
|
|
||||||
|
```bux
|
||||||
|
import Std::Crypto::Ed25519::{Ed25519_Keypair, Ed25519_Sign, Ed25519_Verify};
|
||||||
|
|
||||||
|
// Generate keypair — keys are 32 raw bytes each
|
||||||
|
let pub: *void = Alloc(32);
|
||||||
|
let priv: *void = Alloc(32);
|
||||||
|
Ed25519_Keypair(pub, priv);
|
||||||
|
|
||||||
|
// Sign
|
||||||
|
let sig: String = Ed25519_Sign(priv as String, "message");
|
||||||
|
// sig is 64 raw bytes
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
let valid: bool = Ed25519_Verify(pub as String, sig, "message");
|
||||||
|
```
|
||||||
|
|
||||||
|
### JWT
|
||||||
|
|
||||||
|
```bux
|
||||||
|
import Std::Crypto::Jwt::{JwtAlg, Jwt_MakeHeader, Jwt_Encode,
|
||||||
|
Jwt_Decode, Jwt_EncodeHS256};
|
||||||
|
|
||||||
|
// --- Symmetric (HS256) ---
|
||||||
|
let token: String = Jwt_EncodeHS256("{\"sub\":\"123\",\"role\":\"admin\"}", "my-secret");
|
||||||
|
|
||||||
|
var header: String;
|
||||||
|
var payload: String;
|
||||||
|
let alg: JwtAlg = JwtAlg { tag: JwtAlg_HS256 };
|
||||||
|
if Jwt_Decode(token, alg, "my-secret", &header, &payload) {
|
||||||
|
PrintLine(payload); // {"sub":"123","role":"admin"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Asymmetric (RS256) ---
|
||||||
|
let rsToken: String = Jwt_Encode(
|
||||||
|
"{\"alg\":\"RS256\",\"typ\":\"JWT\"}",
|
||||||
|
"{\"sub\":\"456\"}",
|
||||||
|
JwtAlg { tag: JwtAlg_RS256 },
|
||||||
|
pemPrivateKey
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Convenience helpers ---
|
||||||
|
Jwt_EncodeHS256(payload, secret);
|
||||||
|
Jwt_EncodeHS384(payload, secret);
|
||||||
|
Jwt_EncodeHS512(payload, secret);
|
||||||
|
Jwt_EncodeRS256(payload, pemPrivKey);
|
||||||
|
Jwt_EncodeES256(payload, pemPrivKey);
|
||||||
|
Jwt_EncodeEdDSA(payload, rawPrivKey32);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported JWT Algorithms
|
||||||
|
|
||||||
|
| Algorithm | JWT `alg` | Key Type | Key Format |
|
||||||
|
|-----------|-----------|----------|------------|
|
||||||
|
| HS256 | `HS256` | HMAC secret | Raw string |
|
||||||
|
| HS384 | `HS384` | HMAC secret | Raw string |
|
||||||
|
| HS512 | `HS512` | HMAC secret | Raw string |
|
||||||
|
| RS256 | `RS256` | RSA private/public key | PEM string |
|
||||||
|
| RS384 | `RS384` | RSA private/public key | PEM string |
|
||||||
|
| RS512 | `RS512` | RSA private/public key | PEM string |
|
||||||
|
| ES256 | `ES256` | ECDSA P-256 key | PEM string |
|
||||||
|
| ES384 | `ES384` | ECDSA P-384 key | PEM string |
|
||||||
|
| EdDSA | `EdDSA` | Ed25519 key | 32-byte raw |
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
|
||||||
|
All primitives are implemented in C using OpenSSL and linked via the Bux runtime (`rt/runtime.c`). The C functions are declared as `extern func` in each Bux module.
|
||||||
|
|
||||||
|
Requires OpenSSL 1.1.1+ (for Ed25519 support). Link with `-lssl -lcrypto`.
|
||||||
|
|
||||||
|
## File Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
├── Crypto.bux # Backward-compat wrapper (old API)
|
||||||
|
└── crypto/
|
||||||
|
├── base64.bux # Base64 + Base64URL
|
||||||
|
├── hash.bux # SHA-1/256/384/512
|
||||||
|
├── hmac.bux # HMAC-SHA256/384/512
|
||||||
|
├── random.bux # Secure random
|
||||||
|
├── aes.bux # AES-256-CBC/GCM
|
||||||
|
├── rsa.bux # RSA PKCS#1 v1.5
|
||||||
|
├── ecdsa.bux # ECDSA P-256/P-384
|
||||||
|
├── ed25519.bux # Ed25519
|
||||||
|
└── jwt.bux # JSON Web Tokens
|
||||||
|
|
||||||
|
lib/crypto_test/ # Test project (exercises all modules)
|
||||||
|
test_crypto/ # Standalone test project
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration from old `Std::Crypto`
|
||||||
|
|
||||||
|
The old single-file API is still available under `Std::Crypto`:
|
||||||
|
|
||||||
|
| Old Function | New Equivalent | Module |
|
||||||
|
|-------------|----------------|--------|
|
||||||
|
| `Crypto_Sha256(s)` | `Hash_Sha256(s)` | `Std::Crypto::Hash` |
|
||||||
|
| `Crypto_HmacSha256(k, m)` | `Hmac_Sha256(k, m)` | `Std::Crypto::Hmac` |
|
||||||
|
| `Crypto_RandomBytes(n)` | `Random_Base64(n)` | `Std::Crypto::Random` |
|
||||||
|
| `Crypto_Base64Encode(s)` | `Base64_Encode(s)` | `Std::Crypto::Base64` |
|
||||||
|
| `Crypto_Base64Decode(s)` | `Base64_Decode(s)` | `Std::Crypto::Base64` |
|
||||||
|
| `Crypto_HmacSha256Raw(k, m)` | `Hmac_Sha256Base64(k, m)` | `Std::Crypto::Hmac` |
|
||||||
|
|
||||||
|
New code should prefer the submodule imports for clarity and to avoid pulling in unused declarations.
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd test_crypto
|
||||||
|
../buxc build
|
||||||
|
./test_crypto
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
```
|
||||||
|
================================================
|
||||||
|
Bux Crypto Library — Test Suite
|
||||||
|
================================================
|
||||||
|
|
||||||
|
--- Base64 ---
|
||||||
|
PASS Base64_Encode('hello')
|
||||||
|
PASS Base64_Decode('aGVsbG8=')
|
||||||
|
...
|
||||||
|
--- Hash ---
|
||||||
|
PASS Hash_Sha256('hello')
|
||||||
|
...
|
||||||
|
================================================
|
||||||
|
Passed: 23
|
||||||
|
Failed: 0
|
||||||
|
================================================
|
||||||
|
```
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// Std::Crypto::Aes — AES-256-CBC and AES-256-GCM encryption/decryption
|
||||||
|
// =============================================================================
|
||||||
|
module Std::Crypto::Aes {
|
||||||
|
|
||||||
|
import Std::Mem::{Alloc, Free};
|
||||||
|
import Std::String::{String_Len};
|
||||||
|
|
||||||
|
extern func bux_random_bytes(buf: *void, len: int) -> int;
|
||||||
|
extern func bux_aes_256_cbc_encrypt(plain: String, plainlen: int, key: String, iv: String, outlen: *int) -> String;
|
||||||
|
extern func bux_aes_256_cbc_decrypt(cipher: String, cipherlen: int, key: String, iv: String, outlen: *int) -> String;
|
||||||
|
extern func bux_aes_256_gcm_encrypt(plain: String, plainlen: int, key: String, iv: String, tag: *void, outlen: *int) -> String;
|
||||||
|
extern func bux_aes_256_gcm_decrypt(cipher: String, cipherlen: int, key: String, iv: String, tag: String, outlen: *int) -> String;
|
||||||
|
|
||||||
|
// --- AES-256-CBC ---
|
||||||
|
|
||||||
|
const AES_KEY_SIZE: int = 32; // 256 bits
|
||||||
|
const AES_IV_SIZE: int = 16; // 128 bits
|
||||||
|
const AES_GCM_TAG_SIZE: int = 16;
|
||||||
|
|
||||||
|
// Generate a random 256-bit AES key (returns raw 32 bytes)
|
||||||
|
func Aes_GenerateKey() -> String {
|
||||||
|
let buf: *void = Alloc(AES_KEY_SIZE as uint);
|
||||||
|
if bux_random_bytes(buf, AES_KEY_SIZE) != 1 {
|
||||||
|
Free(buf);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return buf as String;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a random 128-bit IV (returns raw 16 bytes)
|
||||||
|
func Aes_GenerateIV() -> String {
|
||||||
|
let buf: *void = Alloc(AES_IV_SIZE as uint);
|
||||||
|
if bux_random_bytes(buf, AES_IV_SIZE) != 1 {
|
||||||
|
Free(buf);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return buf as String;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AES-256-CBC encrypt. plain and key are binary strings, iv is 16 bytes.
|
||||||
|
// Returns ciphertext (may be longer than plain due to PKCS#7 padding).
|
||||||
|
func Aes_CbcEncrypt(plain: String, key: String, iv: String) -> String {
|
||||||
|
let outlen: int = 0;
|
||||||
|
return bux_aes_256_cbc_encrypt(plain, String_Len(plain) as int, key, iv, &outlen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AES-256-CBC decrypt. Returns plaintext.
|
||||||
|
func Aes_CbcDecrypt(cipher: String, key: String, iv: String) -> String {
|
||||||
|
let outlen: int = 0;
|
||||||
|
return bux_aes_256_cbc_decrypt(cipher, String_Len(cipher) as int, key, iv, &outlen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- AES-256-GCM (Authenticated Encryption) ---
|
||||||
|
|
||||||
|
// AES-256-GCM encrypt. Returns ciphertext. tag receives 16-byte authentication tag.
|
||||||
|
func Aes_GcmEncrypt(plain: String, key: String, iv: String, tag: *void) -> String {
|
||||||
|
let outlen: int = 0;
|
||||||
|
return bux_aes_256_gcm_encrypt(plain, String_Len(plain) as int, key, iv, tag, &outlen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AES-256-GCM decrypt. Returns plaintext. tag must be the 16-byte auth tag from encryption.
|
||||||
|
func Aes_GcmDecrypt(cipher: String, key: String, iv: String, tag: String) -> String {
|
||||||
|
let outlen: int = 0;
|
||||||
|
return bux_aes_256_gcm_decrypt(cipher, String_Len(cipher) as int, key, iv, tag, &outlen);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// Std::Crypto::Base64 — Base64 and Base64URL encode/decode
|
||||||
|
// =============================================================================
|
||||||
|
module Std::Crypto::Base64 {
|
||||||
|
|
||||||
|
import Std::String::{String_Len};
|
||||||
|
|
||||||
|
extern func bux_base64_encode(data: String, len: int) -> String;
|
||||||
|
extern func bux_base64_decode(data: String, len: int, outlen: *int) -> String;
|
||||||
|
extern func bux_base64url_encode(data: String, len: int) -> String;
|
||||||
|
extern func bux_base64url_decode(data: String, len: int, outlen: *int) -> String;
|
||||||
|
|
||||||
|
// --- Standard Base64 ---
|
||||||
|
|
||||||
|
func Base64_Encode(s: String) -> String {
|
||||||
|
return bux_base64_encode(s, String_Len(s) as int);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Base64_Decode(s: String) -> String {
|
||||||
|
let outlen: int = 0;
|
||||||
|
return bux_base64_decode(s, String_Len(s) as int, &outlen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Base64URL (RFC 4648 §5, uses - and _ instead of + and /, no padding) ---
|
||||||
|
|
||||||
|
func Base64URL_Encode(s: String) -> String {
|
||||||
|
return bux_base64url_encode(s, String_Len(s) as int);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Base64URL_Decode(s: String) -> String {
|
||||||
|
let outlen: int = 0;
|
||||||
|
return bux_base64url_decode(s, String_Len(s) as int, &outlen);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// Std::Crypto::Ecdsa — ECDSA P-256 and P-384 sign/verify
|
||||||
|
// =============================================================================
|
||||||
|
module Std::Crypto::Ecdsa {
|
||||||
|
|
||||||
|
import Std::Mem::{Alloc, Free};
|
||||||
|
import Std::String::{String_Len};
|
||||||
|
|
||||||
|
extern func bux_ecdsa_sign_p256(pemKey: String, keylen: int, data: String, datalen: int, siglen: *int) -> String;
|
||||||
|
extern func bux_ecdsa_sign_p384(pemKey: String, keylen: int, data: String, datalen: int, siglen: *int) -> String;
|
||||||
|
extern func bux_ecdsa_verify_p256(pemKey: String, keylen: int, data: String, datalen: int, sig: String, siglen: int) -> int;
|
||||||
|
extern func bux_ecdsa_verify_p384(pemKey: String, keylen: int, data: String, datalen: int, sig: String, siglen: int) -> int;
|
||||||
|
extern func bux_base64_encode(data: String, len: int) -> String;
|
||||||
|
extern func bux_base64_decode(data: String, len: int, outlen: *int) -> String;
|
||||||
|
|
||||||
|
// --- ECDSA P-256 (ES256) ---
|
||||||
|
|
||||||
|
func Ecdsa_SignP256(pemPrivateKey: String, data: String) -> String {
|
||||||
|
let siglen: int = 0;
|
||||||
|
return bux_ecdsa_sign_p256(pemPrivateKey, String_Len(pemPrivateKey) as int,
|
||||||
|
data, String_Len(data) as int, &siglen);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Ecdsa_SignP256Base64(pemPrivateKey: String, data: String) -> String {
|
||||||
|
let sig: String = Ecdsa_SignP256(pemPrivateKey, data);
|
||||||
|
return bux_base64_encode(sig, String_Len(sig) as int);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Ecdsa_VerifyP256(pemPublicKey: String, data: String, signature: String) -> bool {
|
||||||
|
let r: int = bux_ecdsa_verify_p256(pemPublicKey, String_Len(pemPublicKey) as int,
|
||||||
|
data, String_Len(data) as int,
|
||||||
|
signature, String_Len(signature) as int);
|
||||||
|
return r == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Ecdsa_VerifyP256Base64(pemPublicKey: String, data: String, signatureB64: String) -> bool {
|
||||||
|
let outlen: int = 0;
|
||||||
|
let sig: String = bux_base64_decode(signatureB64, String_Len(signatureB64) as int, &outlen);
|
||||||
|
return Ecdsa_VerifyP256(pemPublicKey, data, sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ECDSA P-384 (ES384) ---
|
||||||
|
|
||||||
|
func Ecdsa_SignP384(pemPrivateKey: String, data: String) -> String {
|
||||||
|
let siglen: int = 0;
|
||||||
|
return bux_ecdsa_sign_p384(pemPrivateKey, String_Len(pemPrivateKey) as int,
|
||||||
|
data, String_Len(data) as int, &siglen);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Ecdsa_SignP384Base64(pemPrivateKey: String, data: String) -> String {
|
||||||
|
let sig: String = Ecdsa_SignP384(pemPrivateKey, data);
|
||||||
|
return bux_base64_encode(sig, String_Len(sig) as int);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Ecdsa_VerifyP384(pemPublicKey: String, data: String, signature: String) -> bool {
|
||||||
|
let r: int = bux_ecdsa_verify_p384(pemPublicKey, String_Len(pemPublicKey) as int,
|
||||||
|
data, String_Len(data) as int,
|
||||||
|
signature, String_Len(signature) as int);
|
||||||
|
return r == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Ecdsa_VerifyP384Base64(pemPublicKey: String, data: String, signatureB64: String) -> bool {
|
||||||
|
let outlen: int = 0;
|
||||||
|
let sig: String = bux_base64_decode(signatureB64, String_Len(signatureB64) as int, &outlen);
|
||||||
|
return Ecdsa_VerifyP384(pemPublicKey, data, sig);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// Std::Crypto::Ed25519 — Ed25519 key generation, signing, and verification
|
||||||
|
// =============================================================================
|
||||||
|
module Std::Crypto::Ed25519 {
|
||||||
|
|
||||||
|
import Std::Mem::{Alloc, Free};
|
||||||
|
import Std::String::{String_Len, String_Concat};
|
||||||
|
|
||||||
|
extern func bux_ed25519_keypair(pubKey: *void, privKey: *void) -> int;
|
||||||
|
extern func bux_ed25519_sign(privKey: String, data: String, datalen: int, sig: *void) -> int;
|
||||||
|
extern func bux_ed25519_verify(pubKey: String, sig: String, data: String, datalen: int) -> int;
|
||||||
|
extern func bux_base64_encode(data: String, len: int) -> String;
|
||||||
|
extern func bux_base64_decode(data: String, len: int, outlen: *int) -> String;
|
||||||
|
|
||||||
|
const ED25519_PUBKEY_SIZE: int = 32;
|
||||||
|
const ED25519_PRIVKEY_SIZE: int = 32;
|
||||||
|
const ED25519_SIG_SIZE: int = 64;
|
||||||
|
|
||||||
|
// --- Key Generation ---
|
||||||
|
|
||||||
|
// Ed25519_Keypair: generates a new keypair.
|
||||||
|
// Returns true on success. pubKey and privKey receive 32-byte raw keys.
|
||||||
|
func Ed25519_Keypair(pubKey: *void, privKey: *void) -> bool {
|
||||||
|
let r: int = bux_ed25519_keypair(pubKey, privKey);
|
||||||
|
return r == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience: generate and return base64-encoded keypair
|
||||||
|
func Ed25519_KeypairBase64() -> String {
|
||||||
|
let pub: *void = Alloc(ED25519_PUBKEY_SIZE as uint);
|
||||||
|
let priv: *void = Alloc(ED25519_PRIVKEY_SIZE as uint);
|
||||||
|
if bux_ed25519_keypair(pub, priv) != 1 {
|
||||||
|
Free(pub);
|
||||||
|
Free(priv);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
// Return "pub_b64:priv_b64"
|
||||||
|
let pubB64: String = bux_base64_encode(pub as String, ED25519_PUBKEY_SIZE);
|
||||||
|
let privB64: String = bux_base64_encode(priv as String, ED25519_PRIVKEY_SIZE);
|
||||||
|
Free(pub);
|
||||||
|
Free(priv);
|
||||||
|
let pair: String = String_Concat(pubB64, ":");
|
||||||
|
return String_Concat(pair, privB64);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sign ---
|
||||||
|
|
||||||
|
// Ed25519_Sign: sign data with 32-byte raw private key. Returns 64-byte raw signature.
|
||||||
|
func Ed25519_Sign(privKey: String, data: String) -> String {
|
||||||
|
let sig: *void = Alloc(ED25519_SIG_SIZE as uint);
|
||||||
|
if bux_ed25519_sign(privKey, data, String_Len(data) as int, sig) != 1 {
|
||||||
|
Free(sig);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return sig as String;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience: sign and return base64-encoded signature
|
||||||
|
func Ed25519_SignBase64(privKey: String, data: String) -> String {
|
||||||
|
let sig: String = Ed25519_Sign(privKey, data);
|
||||||
|
if String_Len(sig) == 0 { return ""; }
|
||||||
|
return bux_base64_encode(sig, ED25519_SIG_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Verify ---
|
||||||
|
|
||||||
|
// Ed25519_Verify: verify a 64-byte raw signature against data with 32-byte public key.
|
||||||
|
func Ed25519_Verify(pubKey: String, signature: String, data: String) -> bool {
|
||||||
|
let r: int = bux_ed25519_verify(pubKey, signature, data, String_Len(data) as int);
|
||||||
|
return r == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience: verify a base64-encoded signature
|
||||||
|
func Ed25519_VerifyBase64(pubKey: String, signatureB64: String, data: String) -> bool {
|
||||||
|
let outlen: int = 0;
|
||||||
|
let sig: String = bux_base64_decode(signatureB64, String_Len(signatureB64) as int, &outlen);
|
||||||
|
if outlen != ED25519_SIG_SIZE { return false; }
|
||||||
|
return Ed25519_Verify(pubKey, sig, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// Std::Crypto::Hash — SHA-1, SHA-256, SHA-384, SHA-512
|
||||||
|
// =============================================================================
|
||||||
|
module Std::Crypto::Hash {
|
||||||
|
|
||||||
|
import Std::Mem::{Alloc, Free};
|
||||||
|
import Std::String::{String_Len};
|
||||||
|
|
||||||
|
extern func bux_sha1(data: String, len: int, out: *void);
|
||||||
|
extern func bux_sha256(data: String, len: int, out: *void);
|
||||||
|
extern func bux_sha384(data: String, len: int, out: *void);
|
||||||
|
extern func bux_sha512(data: String, len: int, out: *void);
|
||||||
|
extern func bux_bytes_to_hex(data: *void, len: int) -> String;
|
||||||
|
|
||||||
|
// --- Convenience wrappers: hex output ---
|
||||||
|
|
||||||
|
func Hash_Sha1(data: String) -> String {
|
||||||
|
let len: int = String_Len(data) as int;
|
||||||
|
let buf: *void = Alloc(20);
|
||||||
|
bux_sha1(data, len, buf);
|
||||||
|
let result: String = bux_bytes_to_hex(buf, 20);
|
||||||
|
Free(buf);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Hash_Sha256(data: String) -> String {
|
||||||
|
let len: int = String_Len(data) as int;
|
||||||
|
let buf: *void = Alloc(32);
|
||||||
|
bux_sha256(data, len, buf);
|
||||||
|
let result: String = bux_bytes_to_hex(buf, 32);
|
||||||
|
Free(buf);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Hash_Sha384(data: String) -> String {
|
||||||
|
let len: int = String_Len(data) as int;
|
||||||
|
let buf: *void = Alloc(48);
|
||||||
|
bux_sha384(data, len, buf);
|
||||||
|
let result: String = bux_bytes_to_hex(buf, 48);
|
||||||
|
Free(buf);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Hash_Sha512(data: String) -> String {
|
||||||
|
let len: int = String_Len(data) as int;
|
||||||
|
let buf: *void = Alloc(64);
|
||||||
|
bux_sha512(data, len, buf);
|
||||||
|
let result: String = bux_bytes_to_hex(buf, 64);
|
||||||
|
Free(buf);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Raw binary output (caller must Alloc/Free) ---
|
||||||
|
|
||||||
|
func Hash_Sha256Raw(data: String, out: *void) {
|
||||||
|
bux_sha256(data, String_Len(data) as int, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Hash_Sha384Raw(data: String, out: *void) {
|
||||||
|
bux_sha384(data, String_Len(data) as int, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Hash_Sha512Raw(data: String, out: *void) {
|
||||||
|
bux_sha512(data, String_Len(data) as int, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Digest sizes ---
|
||||||
|
|
||||||
|
func Hash_Sha1Size() -> int { return 20; }
|
||||||
|
func Hash_Sha256Size() -> int { return 32; }
|
||||||
|
func Hash_Sha384Size() -> int { return 48; }
|
||||||
|
func Hash_Sha512Size() -> int { return 64; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// Std::Crypto::Hmac — HMAC-SHA256, HMAC-SHA384, HMAC-SHA512
|
||||||
|
// =============================================================================
|
||||||
|
module Std::Crypto::Hmac {
|
||||||
|
|
||||||
|
import Std::Mem::{Alloc, Free};
|
||||||
|
import Std::String::{String_Len};
|
||||||
|
|
||||||
|
extern func bux_hmac_sha256(key: String, keylen: int, msg: String, msglen: int, out: *void);
|
||||||
|
extern func bux_hmac_sha384(key: String, keylen: int, msg: String, msglen: int, out: *void);
|
||||||
|
extern func bux_hmac_sha512(key: String, keylen: int, msg: String, msglen: int, out: *void);
|
||||||
|
extern func bux_bytes_to_hex(data: *void, len: int) -> String;
|
||||||
|
extern func bux_base64_encode(data: String, len: int) -> String;
|
||||||
|
|
||||||
|
// --- HMAC-SHA256 ---
|
||||||
|
|
||||||
|
func Hmac_Sha256(key: String, message: String) -> String {
|
||||||
|
let kl: int = String_Len(key) as int;
|
||||||
|
let ml: int = String_Len(message) as int;
|
||||||
|
let buf: *void = Alloc(32);
|
||||||
|
bux_hmac_sha256(key, kl, message, ml, buf);
|
||||||
|
let result: String = bux_bytes_to_hex(buf, 32);
|
||||||
|
Free(buf);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Hmac_Sha256Raw(key: String, message: String, out: *void) {
|
||||||
|
bux_hmac_sha256(key, String_Len(key) as int, message, String_Len(message) as int, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Hmac_Sha256Base64(key: String, message: String) -> String {
|
||||||
|
let kl: int = String_Len(key) as int;
|
||||||
|
let ml: int = String_Len(message) as int;
|
||||||
|
let buf: *void = Alloc(32);
|
||||||
|
bux_hmac_sha256(key, kl, message, ml, buf);
|
||||||
|
let result: String = bux_base64_encode(buf as String, 32);
|
||||||
|
Free(buf);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- HMAC-SHA384 ---
|
||||||
|
|
||||||
|
func Hmac_Sha384(key: String, message: String) -> String {
|
||||||
|
let kl: int = String_Len(key) as int;
|
||||||
|
let ml: int = String_Len(message) as int;
|
||||||
|
let buf: *void = Alloc(48);
|
||||||
|
bux_hmac_sha384(key, kl, message, ml, buf);
|
||||||
|
let result: String = bux_bytes_to_hex(buf, 48);
|
||||||
|
Free(buf);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Hmac_Sha384Raw(key: String, message: String, out: *void) {
|
||||||
|
bux_hmac_sha384(key, String_Len(key) as int, message, String_Len(message) as int, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Hmac_Sha384Base64(key: String, message: String) -> String {
|
||||||
|
let kl: int = String_Len(key) as int;
|
||||||
|
let ml: int = String_Len(message) as int;
|
||||||
|
let buf: *void = Alloc(48);
|
||||||
|
bux_hmac_sha384(key, kl, message, ml, buf);
|
||||||
|
let result: String = bux_base64_encode(buf as String, 48);
|
||||||
|
Free(buf);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- HMAC-SHA512 ---
|
||||||
|
|
||||||
|
func Hmac_Sha512(key: String, message: String) -> String {
|
||||||
|
let kl: int = String_Len(key) as int;
|
||||||
|
let ml: int = String_Len(message) as int;
|
||||||
|
let buf: *void = Alloc(64);
|
||||||
|
bux_hmac_sha512(key, kl, message, ml, buf);
|
||||||
|
let result: String = bux_bytes_to_hex(buf, 64);
|
||||||
|
Free(buf);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Hmac_Sha512Raw(key: String, message: String, out: *void) {
|
||||||
|
bux_hmac_sha512(key, String_Len(key) as int, message, String_Len(message) as int, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Hmac_Sha512Base64(key: String, message: String) -> String {
|
||||||
|
let kl: int = String_Len(key) as int;
|
||||||
|
let ml: int = String_Len(message) as int;
|
||||||
|
let buf: *void = Alloc(64);
|
||||||
|
bux_hmac_sha512(key, kl, message, ml, buf);
|
||||||
|
let result: String = bux_base64_encode(buf as String, 64);
|
||||||
|
Free(buf);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// Std::Crypto::Jwt — JSON Web Token (RFC 7519) encode/decode
|
||||||
|
// Supports HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, EdDSA
|
||||||
|
// =============================================================================
|
||||||
|
module Std::Crypto::Jwt {
|
||||||
|
|
||||||
|
import Std::Mem::{Alloc, Free};
|
||||||
|
import Std::String::{String_Len, String_Eq, String_StartsWith, String_Concat};
|
||||||
|
import Std::Crypto::Base64::{Base64URL_Encode, Base64URL_Decode};
|
||||||
|
import Std::Crypto::Hash::{Hash_Sha256Raw, Hash_Sha384Raw, Hash_Sha512Raw};
|
||||||
|
import Std::Crypto::Hmac::{Hmac_Sha256Raw, Hmac_Sha384Raw, Hmac_Sha512Raw};
|
||||||
|
import Std::Crypto::Rsa::{Rsa_SignSha256, Rsa_SignSha384, Rsa_SignSha512,
|
||||||
|
Rsa_VerifySha256, Rsa_VerifySha384, Rsa_VerifySha512};
|
||||||
|
import Std::Crypto::Ecdsa::{Ecdsa_SignP256, Ecdsa_SignP384, Ecdsa_VerifyP256, Ecdsa_VerifyP384};
|
||||||
|
import Std::Crypto::Ed25519::{Ed25519_Sign, Ed25519_Verify};
|
||||||
|
|
||||||
|
extern func bux_base64_encode(data: String, len: int) -> 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;
|
||||||
|
|
||||||
|
// --- JWT Algorithm enum ---
|
||||||
|
enum JwtAlg {
|
||||||
|
HS256,
|
||||||
|
HS384,
|
||||||
|
HS512,
|
||||||
|
RS256,
|
||||||
|
RS384,
|
||||||
|
RS512,
|
||||||
|
ES256,
|
||||||
|
ES384,
|
||||||
|
EdDSA,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Header ---
|
||||||
|
|
||||||
|
// Jwt_MakeHeader: build the JWT header JSON string for the given algorithm
|
||||||
|
func Jwt_MakeHeader(alg: JwtAlg) -> String {
|
||||||
|
if alg.tag == JwtAlg_HS256 { return "{\"alg\":\"HS256\",\"typ\":\"JWT\"}"; }
|
||||||
|
if alg.tag == JwtAlg_HS384 { return "{\"alg\":\"HS384\",\"typ\":\"JWT\"}"; }
|
||||||
|
if alg.tag == JwtAlg_HS512 { return "{\"alg\":\"HS512\",\"typ\":\"JWT\"}"; }
|
||||||
|
if alg.tag == JwtAlg_RS256 { return "{\"alg\":\"RS256\",\"typ\":\"JWT\"}"; }
|
||||||
|
if alg.tag == JwtAlg_RS384 { return "{\"alg\":\"RS384\",\"typ\":\"JWT\"}"; }
|
||||||
|
if alg.tag == JwtAlg_RS512 { return "{\"alg\":\"RS512\",\"typ\":\"JWT\"}"; }
|
||||||
|
if alg.tag == JwtAlg_ES256 { return "{\"alg\":\"ES256\",\"typ\":\"JWT\"}"; }
|
||||||
|
if alg.tag == JwtAlg_ES384 { return "{\"alg\":\"ES384\",\"typ\":\"JWT\"}"; }
|
||||||
|
if alg.tag == JwtAlg_EdDSA { return "{\"alg\":\"EdDSA\",\"typ\":\"JWT\"}"; }
|
||||||
|
return "{\"alg\":\"none\",\"typ\":\"JWT\"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Signing ---
|
||||||
|
|
||||||
|
// Sign the JWT signing input with the given algorithm
|
||||||
|
func Jwt_Sign(alg: JwtAlg, signingInput: String, key: String) -> String {
|
||||||
|
let algTag: int = alg.tag;
|
||||||
|
let inputLen: int = String_Len(signingInput) as int;
|
||||||
|
|
||||||
|
// --- HMAC algorithms ---
|
||||||
|
if algTag == JwtAlg_HS256 {
|
||||||
|
let buf: *void = Alloc(32);
|
||||||
|
Hmac_Sha256Raw(key, signingInput, buf);
|
||||||
|
let result: String = bux_base64_encode(buf as String, 32);
|
||||||
|
Free(buf);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if algTag == JwtAlg_HS384 {
|
||||||
|
let buf: *void = Alloc(48);
|
||||||
|
Hmac_Sha384Raw(key, signingInput, buf);
|
||||||
|
let result: String = bux_base64_encode(buf as String, 48);
|
||||||
|
Free(buf);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if algTag == JwtAlg_HS512 {
|
||||||
|
let buf: *void = Alloc(64);
|
||||||
|
Hmac_Sha512Raw(key, signingInput, buf);
|
||||||
|
let result: String = bux_base64_encode(buf as String, 64);
|
||||||
|
Free(buf);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- RSA algorithms (key is PEM private key) ---
|
||||||
|
if algTag == JwtAlg_RS256 {
|
||||||
|
let raw: String = Rsa_SignSha256(key, signingInput);
|
||||||
|
return bux_base64_encode(raw, String_Len(raw) as int);
|
||||||
|
}
|
||||||
|
if algTag == JwtAlg_RS384 {
|
||||||
|
let raw: String = Rsa_SignSha384(key, signingInput);
|
||||||
|
return bux_base64_encode(raw, String_Len(raw) as int);
|
||||||
|
}
|
||||||
|
if algTag == JwtAlg_RS512 {
|
||||||
|
let raw: String = Rsa_SignSha512(key, signingInput);
|
||||||
|
return bux_base64_encode(raw, String_Len(raw) as int);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ECDSA algorithms (key is PEM private key) ---
|
||||||
|
if algTag == JwtAlg_ES256 {
|
||||||
|
let raw: String = Ecdsa_SignP256(key, signingInput);
|
||||||
|
return bux_base64_encode(raw, String_Len(raw) as int);
|
||||||
|
}
|
||||||
|
if algTag == JwtAlg_ES384 {
|
||||||
|
let raw: String = Ecdsa_SignP384(key, signingInput);
|
||||||
|
return bux_base64_encode(raw, String_Len(raw) as int);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- EdDSA (key is 32-byte raw private key) ---
|
||||||
|
if algTag == JwtAlg_EdDSA {
|
||||||
|
return Ed25519_Sign(key, signingInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Verify ---
|
||||||
|
|
||||||
|
// Verify a JWT signature
|
||||||
|
func Jwt_Verify(alg: JwtAlg, signingInput: String, signatureB64: String, key: String) -> bool {
|
||||||
|
let algTag: int = alg.tag;
|
||||||
|
let inputLen: int = String_Len(signingInput) as int;
|
||||||
|
|
||||||
|
// --- HMAC algorithms ---
|
||||||
|
if algTag == JwtAlg_HS256 {
|
||||||
|
let expectBuf: *void = Alloc(32);
|
||||||
|
Hmac_Sha256Raw(key, signingInput, expectBuf);
|
||||||
|
let expectB64: String = bux_base64_encode(expectBuf as String, 32);
|
||||||
|
Free(expectBuf);
|
||||||
|
return String_Eq(expectB64, signatureB64);
|
||||||
|
}
|
||||||
|
if algTag == JwtAlg_HS384 {
|
||||||
|
let expectBuf: *void = Alloc(48);
|
||||||
|
Hmac_Sha384Raw(key, signingInput, expectBuf);
|
||||||
|
let expectB64: String = bux_base64_encode(expectBuf as String, 48);
|
||||||
|
Free(expectBuf);
|
||||||
|
return String_Eq(expectB64, signatureB64);
|
||||||
|
}
|
||||||
|
if algTag == JwtAlg_HS512 {
|
||||||
|
let expectBuf: *void = Alloc(64);
|
||||||
|
Hmac_Sha512Raw(key, signingInput, expectBuf);
|
||||||
|
let expectB64: String = bux_base64_encode(expectBuf as String, 64);
|
||||||
|
Free(expectBuf);
|
||||||
|
return String_Eq(expectB64, signatureB64);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- RSA algorithms ---
|
||||||
|
if algTag == JwtAlg_RS256 { return Rsa_VerifySha256(key, signingInput, signatureB64); }
|
||||||
|
if algTag == JwtAlg_RS384 { return Rsa_VerifySha384(key, signingInput, signatureB64); }
|
||||||
|
if algTag == JwtAlg_RS512 { return Rsa_VerifySha512(key, signingInput, signatureB64); }
|
||||||
|
|
||||||
|
// --- ECDSA algorithms ---
|
||||||
|
if algTag == JwtAlg_ES256 { return Ecdsa_VerifyP256(key, signingInput, signatureB64); }
|
||||||
|
if algTag == JwtAlg_ES384 { return Ecdsa_VerifyP384(key, signingInput, signatureB64); }
|
||||||
|
|
||||||
|
// --- EdDSA ---
|
||||||
|
if algTag == JwtAlg_EdDSA { return Ed25519_Verify(key, signatureB64, signingInput); }
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Encode ---
|
||||||
|
|
||||||
|
// Jwt_Encode: create a signed JWT
|
||||||
|
// headerJson — JSON header string (use Jwt_MakeHeader or custom)
|
||||||
|
// payloadJson — JSON payload/claims string
|
||||||
|
// alg — signing algorithm
|
||||||
|
// key — signing key (HMAC secret, RSA PEM, ECDSA PEM, or Ed25519 raw privkey)
|
||||||
|
// Returns the complete "header.payload.signature" JWT string
|
||||||
|
func Jwt_Encode(headerJson: String, payloadJson: String, alg: JwtAlg, key: String) -> String {
|
||||||
|
let headerB64: String = Base64URL_Encode(headerJson);
|
||||||
|
let payloadB64: String = Base64URL_Encode(payloadJson);
|
||||||
|
let signingInput: String = String_Concat(headerB64, ".");
|
||||||
|
let signingInputFull: String = String_Concat(signingInput, payloadB64);
|
||||||
|
|
||||||
|
let sigB64: String = Jwt_Sign(alg, signingInputFull, key);
|
||||||
|
|
||||||
|
let part1: String = String_Concat(signingInputFull, ".");
|
||||||
|
return String_Concat(part1, sigB64);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Decode ---
|
||||||
|
|
||||||
|
// Jwt_Decode: decode and verify a JWT.
|
||||||
|
// token — the full "header.payload.signature" string
|
||||||
|
// alg — expected algorithm
|
||||||
|
// key — verification key
|
||||||
|
// headerOut — receives decoded header JSON
|
||||||
|
// payloadOut — receives decoded payload JSON
|
||||||
|
// Returns true if signature is valid.
|
||||||
|
func Jwt_Decode(token: String, alg: JwtAlg, key: String,
|
||||||
|
headerOut: *String, payloadOut: *String) -> bool {
|
||||||
|
// Split by "."
|
||||||
|
let partCount: uint = bux_str_split_count(token, ".");
|
||||||
|
if partCount != 3 { return false; }
|
||||||
|
|
||||||
|
let headerB64: String = bux_str_split_part(token, ".", 0);
|
||||||
|
let payloadB64: String = bux_str_split_part(token, ".", 1);
|
||||||
|
let sigB64: String = bux_str_split_part(token, ".", 2);
|
||||||
|
|
||||||
|
// Build signing input
|
||||||
|
let input: String = String_Concat(headerB64, ".");
|
||||||
|
let signingInput: String = String_Concat(input, payloadB64);
|
||||||
|
|
||||||
|
// Verify signature
|
||||||
|
if !Jwt_Verify(alg, signingInput, sigB64, key) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode
|
||||||
|
headerOut[0] = Base64URL_Decode(headerB64);
|
||||||
|
payloadOut[0] = Base64URL_Decode(payloadB64);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Convenience: Encode with standard header ---
|
||||||
|
|
||||||
|
func Jwt_EncodeHS256(payloadJson: String, secret: String) -> String {
|
||||||
|
let header: String = Jwt_MakeHeader(JwtAlg { tag: JwtAlg_HS256 });
|
||||||
|
return Jwt_Encode(header, payloadJson, JwtAlg { tag: JwtAlg_HS256 }, secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Jwt_EncodeHS384(payloadJson: String, secret: String) -> String {
|
||||||
|
let header: String = Jwt_MakeHeader(JwtAlg { tag: JwtAlg_HS384 });
|
||||||
|
return Jwt_Encode(header, payloadJson, JwtAlg { tag: JwtAlg_HS384 }, secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Jwt_EncodeHS512(payloadJson: String, secret: String) -> String {
|
||||||
|
let header: String = Jwt_MakeHeader(JwtAlg { tag: JwtAlg_HS512 });
|
||||||
|
return Jwt_Encode(header, payloadJson, JwtAlg { tag: JwtAlg_HS512 }, secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Jwt_EncodeRS256(payloadJson: String, pemPrivateKey: String) -> String {
|
||||||
|
let header: String = Jwt_MakeHeader(JwtAlg { tag: JwtAlg_RS256 });
|
||||||
|
return Jwt_Encode(header, payloadJson, JwtAlg { tag: JwtAlg_RS256 }, pemPrivateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Jwt_EncodeES256(payloadJson: String, pemPrivateKey: String) -> String {
|
||||||
|
let header: String = Jwt_MakeHeader(JwtAlg { tag: JwtAlg_ES256 });
|
||||||
|
return Jwt_Encode(header, payloadJson, JwtAlg { tag: JwtAlg_ES256 }, pemPrivateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Jwt_EncodeEdDSA(payloadJson: String, rawPrivKey: String) -> String {
|
||||||
|
let header: String = Jwt_MakeHeader(JwtAlg { tag: JwtAlg_EdDSA });
|
||||||
|
return Jwt_Encode(header, payloadJson, JwtAlg { tag: JwtAlg_EdDSA }, rawPrivKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// Std::Crypto::Random — cryptographically secure random bytes
|
||||||
|
// =============================================================================
|
||||||
|
module Std::Crypto::Random {
|
||||||
|
|
||||||
|
import Std::Mem::{Alloc, Free};
|
||||||
|
|
||||||
|
extern func bux_random_bytes(buf: *void, len: int) -> int;
|
||||||
|
extern func bux_base64_encode(data: String, len: int) -> String;
|
||||||
|
extern func bux_bytes_to_hex(data: *void, len: int) -> String;
|
||||||
|
|
||||||
|
// RandomBytes: returns n cryptographically secure random bytes as a raw string
|
||||||
|
func Random_Bytes(n: int) -> String {
|
||||||
|
if n <= 0 { return ""; }
|
||||||
|
let buf: *void = Alloc(n as uint);
|
||||||
|
if bux_random_bytes(buf, n) != 1 {
|
||||||
|
Free(buf);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
// Return raw buffer as string (binary-safe)
|
||||||
|
return buf as String;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomHex: returns n random bytes as lowercase hex
|
||||||
|
func Random_Hex(n: int) -> String {
|
||||||
|
if n <= 0 { return ""; }
|
||||||
|
let buf: *void = Alloc(n as uint);
|
||||||
|
if bux_random_bytes(buf, n) != 1 {
|
||||||
|
Free(buf);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
let result: String = bux_bytes_to_hex(buf, n);
|
||||||
|
Free(buf);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomBase64: returns n random bytes as base64-encoded string
|
||||||
|
func Random_Base64(n: int) -> String {
|
||||||
|
if n <= 0 { return ""; }
|
||||||
|
let buf: *void = Alloc(n as uint);
|
||||||
|
if bux_random_bytes(buf, n) != 1 {
|
||||||
|
Free(buf);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
let result: String = bux_base64_encode(buf as String, n);
|
||||||
|
Free(buf);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomUint32: returns a random 32-bit unsigned integer
|
||||||
|
func Random_Uint32() -> uint {
|
||||||
|
let buf: *void = Alloc(4);
|
||||||
|
if bux_random_bytes(buf, 4) != 1 {
|
||||||
|
Free(buf);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// Interpret first 4 bytes as uint (native endian)
|
||||||
|
let ptr: *uint = buf as *uint;
|
||||||
|
let val: uint = *ptr;
|
||||||
|
Free(buf);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// Std::Crypto::Rsa — RSA PKCS#1 v1.5 sign/verify (SHA-256, SHA-384, SHA-512)
|
||||||
|
// =============================================================================
|
||||||
|
module Std::Crypto::Rsa {
|
||||||
|
|
||||||
|
import Std::Mem::{Alloc, Free};
|
||||||
|
import Std::String::{String_Len};
|
||||||
|
|
||||||
|
extern func bux_rsa_sign_sha256(pemKey: String, keylen: int, data: String, datalen: int, siglen: *int) -> String;
|
||||||
|
extern func bux_rsa_sign_sha384(pemKey: String, keylen: int, data: String, datalen: int, siglen: *int) -> String;
|
||||||
|
extern func bux_rsa_sign_sha512(pemKey: String, keylen: int, data: String, datalen: int, siglen: *int) -> String;
|
||||||
|
extern func bux_rsa_verify_sha256(pemKey: String, keylen: int, data: String, datalen: int, sig: String, siglen: int) -> int;
|
||||||
|
extern func bux_rsa_verify_sha384(pemKey: String, keylen: int, data: String, datalen: int, sig: String, siglen: int) -> int;
|
||||||
|
extern func bux_rsa_verify_sha512(pemKey: String, keylen: int, data: String, datalen: int, sig: String, siglen: int) -> int;
|
||||||
|
extern func bux_base64_encode(data: String, len: int) -> String;
|
||||||
|
extern func bux_base64_decode(data: String, len: int, outlen: *int) -> String;
|
||||||
|
|
||||||
|
// --- RSA Sign ---
|
||||||
|
|
||||||
|
// Rsa_SignSha256: sign data with RSA private key (PEM format), returns raw signature
|
||||||
|
func Rsa_SignSha256(pemPrivateKey: String, data: String) -> String {
|
||||||
|
let siglen: int = 0;
|
||||||
|
return bux_rsa_sign_sha256(pemPrivateKey, String_Len(pemPrivateKey) as int,
|
||||||
|
data, String_Len(data) as int, &siglen);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Rsa_SignSha384(pemPrivateKey: String, data: String) -> String {
|
||||||
|
let siglen: int = 0;
|
||||||
|
return bux_rsa_sign_sha384(pemPrivateKey, String_Len(pemPrivateKey) as int,
|
||||||
|
data, String_Len(data) as int, &siglen);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Rsa_SignSha512(pemPrivateKey: String, data: String) -> String {
|
||||||
|
let siglen: int = 0;
|
||||||
|
return bux_rsa_sign_sha512(pemPrivateKey, String_Len(pemPrivateKey) as int,
|
||||||
|
data, String_Len(data) as int, &siglen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience: sign and return base64-encoded signature
|
||||||
|
func Rsa_SignSha256Base64(pemPrivateKey: String, data: String) -> String {
|
||||||
|
let sig: String = Rsa_SignSha256(pemPrivateKey, data);
|
||||||
|
return bux_base64_encode(sig, String_Len(sig) as int);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Rsa_SignSha384Base64(pemPrivateKey: String, data: String) -> String {
|
||||||
|
let sig: String = Rsa_SignSha384(pemPrivateKey, data);
|
||||||
|
return bux_base64_encode(sig, String_Len(sig) as int);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Rsa_SignSha512Base64(pemPrivateKey: String, data: String) -> String {
|
||||||
|
let sig: String = Rsa_SignSha512(pemPrivateKey, data);
|
||||||
|
return bux_base64_encode(sig, String_Len(sig) as int);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- RSA Verify ---
|
||||||
|
|
||||||
|
// Rsa_VerifySha256: verify raw signature against data with RSA public key (PEM)
|
||||||
|
// Returns true if signature is valid.
|
||||||
|
func Rsa_VerifySha256(pemPublicKey: String, data: String, signature: String) -> bool {
|
||||||
|
let r: int = bux_rsa_verify_sha256(pemPublicKey, String_Len(pemPublicKey) as int,
|
||||||
|
data, String_Len(data) as int,
|
||||||
|
signature, String_Len(signature) as int);
|
||||||
|
return r == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Rsa_VerifySha384(pemPublicKey: String, data: String, signature: String) -> bool {
|
||||||
|
let r: int = bux_rsa_verify_sha384(pemPublicKey, String_Len(pemPublicKey) as int,
|
||||||
|
data, String_Len(data) as int,
|
||||||
|
signature, String_Len(signature) as int);
|
||||||
|
return r == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Rsa_VerifySha512(pemPublicKey: String, data: String, signature: String) -> bool {
|
||||||
|
let r: int = bux_rsa_verify_sha512(pemPublicKey, String_Len(pemPublicKey) as int,
|
||||||
|
data, String_Len(data) as int,
|
||||||
|
signature, String_Len(signature) as int);
|
||||||
|
return r == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience: verify base64-encoded signature
|
||||||
|
func Rsa_VerifySha256Base64(pemPublicKey: String, data: String, signatureB64: String) -> bool {
|
||||||
|
let outlen: int = 0;
|
||||||
|
let sig: String = bux_base64_decode(signatureB64, String_Len(signatureB64) as int, &outlen);
|
||||||
|
return Rsa_VerifySha256(pemPublicKey, data, sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Rsa_VerifySha384Base64(pemPublicKey: String, data: String, signatureB64: String) -> bool {
|
||||||
|
let outlen: int = 0;
|
||||||
|
let sig: String = bux_base64_decode(signatureB64, String_Len(signatureB64) as int, &outlen);
|
||||||
|
return Rsa_VerifySha384(pemPublicKey, data, sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Rsa_VerifySha512Base64(pemPublicKey: String, data: String, signatureB64: String) -> bool {
|
||||||
|
let outlen: int = 0;
|
||||||
|
let sig: String = bux_base64_decode(signatureB64, String_Len(signatureB64) as int, &outlen);
|
||||||
|
return Rsa_VerifySha512(pemPublicKey, data, sig);
|
||||||
|
}
|
||||||
|
}
|
||||||
+346
@@ -1322,6 +1322,12 @@ void bux_assert(int cond, const char* file, int line, const char* expr) {
|
|||||||
#include <openssl/evp.h>
|
#include <openssl/evp.h>
|
||||||
#include <openssl/hmac.h>
|
#include <openssl/hmac.h>
|
||||||
#include <openssl/rand.h>
|
#include <openssl/rand.h>
|
||||||
|
#include <openssl/pem.h>
|
||||||
|
#include <openssl/rsa.h>
|
||||||
|
#include <openssl/ec.h>
|
||||||
|
#include <openssl/ecdsa.h>
|
||||||
|
#include <openssl/bio.h>
|
||||||
|
#include <openssl/buffer.h>
|
||||||
|
|
||||||
void bux_sha256(const char* data, int len, unsigned char* out) {
|
void bux_sha256(const char* data, int len, unsigned char* out) {
|
||||||
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
|
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
|
||||||
@@ -1369,3 +1375,343 @@ char* bux_bytes_to_hex(const unsigned char* data, int len) {
|
|||||||
out[len * 2] = '\0';
|
out[len * 2] = '\0';
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
* Extended cryptography primitives (OpenSSL)
|
||||||
|
* ============================================================================ */
|
||||||
|
|
||||||
|
static void bux_digest(const EVP_MD* md, const char* data, int len, unsigned char* out) {
|
||||||
|
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
|
||||||
|
EVP_DigestInit_ex(ctx, md, NULL);
|
||||||
|
EVP_DigestUpdate(ctx, data, (size_t)len);
|
||||||
|
EVP_DigestFinal_ex(ctx, out, NULL);
|
||||||
|
EVP_MD_CTX_free(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bux_sha1(const char* data, int len, unsigned char* out) {
|
||||||
|
bux_digest(EVP_sha1(), data, len, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bux_sha384(const char* data, int len, unsigned char* out) {
|
||||||
|
bux_digest(EVP_sha384(), data, len, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bux_sha512(const char* data, int len, unsigned char* out) {
|
||||||
|
bux_digest(EVP_sha512(), data, len, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- HMAC --- */
|
||||||
|
|
||||||
|
void bux_hmac_sha384(const char* key, int keylen, const char* msg, int msglen, unsigned char* out) {
|
||||||
|
unsigned int outlen = 48;
|
||||||
|
HMAC(EVP_sha384(), key, keylen, (const unsigned char*)msg, (size_t)msglen, out, &outlen);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bux_hmac_sha512(const char* key, int keylen, const char* msg, int msglen, unsigned char* out) {
|
||||||
|
unsigned int outlen = 64;
|
||||||
|
HMAC(EVP_sha512(), key, keylen, (const unsigned char*)msg, (size_t)msglen, out, &outlen);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Base64URL --- */
|
||||||
|
|
||||||
|
static const char b64url_table[] =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||||
|
|
||||||
|
char* bux_base64url_encode(const unsigned char* in, int inlen) {
|
||||||
|
int outlen = 4 * ((inlen + 2) / 3);
|
||||||
|
char* out = (char*)bux_alloc(outlen + 1);
|
||||||
|
int j = 0;
|
||||||
|
for (int i = 0; i < inlen; i += 3) {
|
||||||
|
int a = in[i];
|
||||||
|
int b = (i + 1 < inlen) ? in[i + 1] : 0;
|
||||||
|
int c = (i + 2 < inlen) ? in[i + 2] : 0;
|
||||||
|
out[j++] = b64url_table[(a >> 2) & 0x3F];
|
||||||
|
out[j++] = b64url_table[((a << 4) | (b >> 4)) & 0x3F];
|
||||||
|
if (i + 1 < inlen)
|
||||||
|
out[j++] = b64url_table[((b << 2) | (c >> 6)) & 0x3F];
|
||||||
|
if (i + 2 < inlen)
|
||||||
|
out[j++] = b64url_table[c & 0x3F];
|
||||||
|
}
|
||||||
|
out[j] = '\0';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int b64url_char_val(char c) {
|
||||||
|
if (c >= 'A' && c <= 'Z') return c - 'A';
|
||||||
|
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
|
||||||
|
if (c >= '0' && c <= '9') return c - '0' + 52;
|
||||||
|
if (c == '-') return 62;
|
||||||
|
if (c == '_') return 63;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* bux_base64url_decode(const char* in, int inlen, int* outlen) {
|
||||||
|
int maxlen = 3 * inlen / 4 + 1;
|
||||||
|
char* out = (char*)bux_alloc(maxlen);
|
||||||
|
int j = 0;
|
||||||
|
int buf = 0, bits = 0;
|
||||||
|
for (int i = 0; i < inlen; i++) {
|
||||||
|
int val = b64url_char_val(in[i]);
|
||||||
|
if (val < 0) continue;
|
||||||
|
buf = (buf << 6) | val;
|
||||||
|
bits += 6;
|
||||||
|
if (bits >= 8) {
|
||||||
|
bits -= 8;
|
||||||
|
out[j++] = (buf >> bits) & 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out[j] = '\0';
|
||||||
|
*outlen = j;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- AES-256-CBC --- */
|
||||||
|
|
||||||
|
static int aes_cbc_ctx_encrypt(const unsigned char* in, int inlen,
|
||||||
|
unsigned char* out, const unsigned char* key,
|
||||||
|
const unsigned char* iv, int encrypt) {
|
||||||
|
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
|
||||||
|
EVP_CipherInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv, encrypt);
|
||||||
|
int outlen = 0, tmplen = 0;
|
||||||
|
EVP_CipherUpdate(ctx, out, &tmplen, in, inlen);
|
||||||
|
outlen = tmplen;
|
||||||
|
EVP_CipherFinal_ex(ctx, out + outlen, &tmplen);
|
||||||
|
outlen += tmplen;
|
||||||
|
EVP_CIPHER_CTX_free(ctx);
|
||||||
|
return outlen;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* bux_aes_256_cbc_encrypt(const char* plain, int plainlen,
|
||||||
|
const char* key, const char* iv, int* outlen) {
|
||||||
|
int maxlen = plainlen + EVP_MAX_BLOCK_LENGTH;
|
||||||
|
char* out = (char*)bux_alloc(maxlen + 1);
|
||||||
|
*outlen = aes_cbc_ctx_encrypt((const unsigned char*)plain, plainlen,
|
||||||
|
(unsigned char*)out,
|
||||||
|
(const unsigned char*)key,
|
||||||
|
(const unsigned char*)iv, 1);
|
||||||
|
out[*outlen] = '\0';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* bux_aes_256_cbc_decrypt(const char* cipher, int cipherlen,
|
||||||
|
const char* key, const char* iv, int* outlen) {
|
||||||
|
char* out = (char*)bux_alloc(cipherlen + 1);
|
||||||
|
*outlen = aes_cbc_ctx_encrypt((const unsigned char*)cipher, cipherlen,
|
||||||
|
(unsigned char*)out,
|
||||||
|
(const unsigned char*)key,
|
||||||
|
(const unsigned char*)iv, 0);
|
||||||
|
out[*outlen] = '\0';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- AES-256-GCM --- */
|
||||||
|
|
||||||
|
char* bux_aes_256_gcm_encrypt(const char* plain, int plainlen,
|
||||||
|
const char* key, const char* iv,
|
||||||
|
unsigned char* tag, int* outlen) {
|
||||||
|
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
|
||||||
|
EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, (const unsigned char*)key,
|
||||||
|
(const unsigned char*)iv);
|
||||||
|
int maxlen = plainlen + EVP_MAX_BLOCK_LENGTH;
|
||||||
|
char* out = (char*)bux_alloc(maxlen);
|
||||||
|
int tmplen = 0;
|
||||||
|
EVP_EncryptUpdate(ctx, (unsigned char*)out, &tmplen,
|
||||||
|
(const unsigned char*)plain, plainlen);
|
||||||
|
*outlen = tmplen;
|
||||||
|
EVP_EncryptFinal_ex(ctx, (unsigned char*)out + *outlen, &tmplen);
|
||||||
|
*outlen += tmplen;
|
||||||
|
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag);
|
||||||
|
EVP_CIPHER_CTX_free(ctx);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* bux_aes_256_gcm_decrypt(const char* cipher, int cipherlen,
|
||||||
|
const char* key, const char* iv,
|
||||||
|
const char* tag, int* outlen) {
|
||||||
|
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
|
||||||
|
EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, (const unsigned char*)key,
|
||||||
|
(const unsigned char*)iv);
|
||||||
|
char* out = (char*)bux_alloc(cipherlen);
|
||||||
|
int tmplen = 0;
|
||||||
|
EVP_DecryptUpdate(ctx, (unsigned char*)out, &tmplen,
|
||||||
|
(const unsigned char*)cipher, cipherlen);
|
||||||
|
*outlen = tmplen;
|
||||||
|
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, (void*)tag);
|
||||||
|
int ret = EVP_DecryptFinal_ex(ctx, (unsigned char*)out + *outlen, &tmplen);
|
||||||
|
EVP_CIPHER_CTX_free(ctx);
|
||||||
|
if (ret <= 0) { *outlen = 0; out[0] = '\0'; return out; }
|
||||||
|
*outlen += tmplen;
|
||||||
|
out[*outlen] = '\0';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- RSA PKCS#1 v1.5 sign / verify --- */
|
||||||
|
|
||||||
|
static EVP_PKEY* bux_load_private_key(const char* pem, int len) {
|
||||||
|
BIO* bio = BIO_new_mem_buf(pem, len);
|
||||||
|
if (!bio) return NULL;
|
||||||
|
EVP_PKEY* pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL);
|
||||||
|
BIO_free(bio);
|
||||||
|
return pkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
static EVP_PKEY* bux_load_public_key(const char* pem, int len) {
|
||||||
|
BIO* bio = BIO_new_mem_buf(pem, len);
|
||||||
|
if (!bio) return NULL;
|
||||||
|
EVP_PKEY* pkey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL);
|
||||||
|
BIO_free(bio);
|
||||||
|
return pkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char* bux_rsa_sign_evp(const EVP_MD* md, const char* pem_key, int keylen,
|
||||||
|
const char* data, int datalen, int* siglen) {
|
||||||
|
EVP_PKEY* pkey = bux_load_private_key(pem_key, keylen);
|
||||||
|
if (!pkey) { *siglen = 0; return (char*)bux_alloc(1); }
|
||||||
|
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
|
||||||
|
EVP_SignInit_ex(ctx, md, NULL);
|
||||||
|
EVP_SignUpdate(ctx, data, (size_t)datalen);
|
||||||
|
size_t slen = (size_t)EVP_PKEY_size(pkey);
|
||||||
|
unsigned char* sig = (unsigned char*)bux_alloc(slen + 1);
|
||||||
|
EVP_SignFinal(ctx, sig, &slen, pkey);
|
||||||
|
*siglen = (int)slen;
|
||||||
|
sig[*siglen] = '\0';
|
||||||
|
EVP_MD_CTX_free(ctx);
|
||||||
|
EVP_PKEY_free(pkey);
|
||||||
|
return (char*)sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int bux_rsa_verify_evp(const EVP_MD* md, const char* pem_key, int keylen,
|
||||||
|
const char* data, int datalen,
|
||||||
|
const char* sig, int siglen) {
|
||||||
|
EVP_PKEY* pkey = bux_load_public_key(pem_key, keylen);
|
||||||
|
if (!pkey) return 0;
|
||||||
|
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
|
||||||
|
EVP_VerifyInit_ex(ctx, md, NULL);
|
||||||
|
EVP_VerifyUpdate(ctx, data, (size_t)datalen);
|
||||||
|
int ret = EVP_VerifyFinal(ctx, (const unsigned char*)sig, (size_t)siglen, pkey);
|
||||||
|
EVP_MD_CTX_free(ctx);
|
||||||
|
EVP_PKEY_free(pkey);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* bux_rsa_sign_sha256(const char* pem, int keylen, const char* data, int datalen, int* siglen) {
|
||||||
|
return bux_rsa_sign_evp(EVP_sha256(), pem, keylen, data, datalen, siglen);
|
||||||
|
}
|
||||||
|
char* bux_rsa_sign_sha384(const char* pem, int keylen, const char* data, int datalen, int* siglen) {
|
||||||
|
return bux_rsa_sign_evp(EVP_sha384(), pem, keylen, data, datalen, siglen);
|
||||||
|
}
|
||||||
|
char* bux_rsa_sign_sha512(const char* pem, int keylen, const char* data, int datalen, int* siglen) {
|
||||||
|
return bux_rsa_sign_evp(EVP_sha512(), pem, keylen, data, datalen, siglen);
|
||||||
|
}
|
||||||
|
|
||||||
|
int bux_rsa_verify_sha256(const char* pem, int keylen, const char* data, int datalen,
|
||||||
|
const char* sig, int siglen) {
|
||||||
|
return bux_rsa_verify_evp(EVP_sha256(), pem, keylen, data, datalen, sig, siglen);
|
||||||
|
}
|
||||||
|
int bux_rsa_verify_sha384(const char* pem, int keylen, const char* data, int datalen,
|
||||||
|
const char* sig, int siglen) {
|
||||||
|
return bux_rsa_verify_evp(EVP_sha384(), pem, keylen, data, datalen, sig, siglen);
|
||||||
|
}
|
||||||
|
int bux_rsa_verify_sha512(const char* pem, int keylen, const char* data, int datalen,
|
||||||
|
const char* sig, int siglen) {
|
||||||
|
return bux_rsa_verify_evp(EVP_sha512(), pem, keylen, data, datalen, sig, siglen);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- ECDSA P-256 / P-384 --- */
|
||||||
|
|
||||||
|
static char* bux_ecdsa_sign_evp(const EVP_MD* md, const char* pem, int keylen,
|
||||||
|
const char* data, int datalen, int* siglen) {
|
||||||
|
EVP_PKEY* pkey = bux_load_private_key(pem, keylen);
|
||||||
|
if (!pkey) { *siglen = 0; return (char*)bux_alloc(1); }
|
||||||
|
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
|
||||||
|
EVP_SignInit_ex(ctx, md, NULL);
|
||||||
|
EVP_SignUpdate(ctx, data, (size_t)datalen);
|
||||||
|
size_t slen = (size_t)EVP_PKEY_size(pkey);
|
||||||
|
unsigned char* sig = (unsigned char*)bux_alloc(slen + 1);
|
||||||
|
EVP_SignFinal(ctx, sig, &slen, pkey);
|
||||||
|
*siglen = (int)slen;
|
||||||
|
sig[*siglen] = '\0';
|
||||||
|
EVP_MD_CTX_free(ctx);
|
||||||
|
EVP_PKEY_free(pkey);
|
||||||
|
return (char*)sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int bux_ecdsa_verify_evp(const EVP_MD* md, const char* pem, int keylen,
|
||||||
|
const char* data, int datalen,
|
||||||
|
const char* sig, int siglen) {
|
||||||
|
EVP_PKEY* pkey = bux_load_public_key(pem, keylen);
|
||||||
|
if (!pkey) return 0;
|
||||||
|
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
|
||||||
|
EVP_VerifyInit_ex(ctx, md, NULL);
|
||||||
|
EVP_VerifyUpdate(ctx, data, (size_t)datalen);
|
||||||
|
int ret = EVP_VerifyFinal(ctx, (const unsigned char*)sig, (size_t)siglen, pkey);
|
||||||
|
EVP_MD_CTX_free(ctx);
|
||||||
|
EVP_PKEY_free(pkey);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* bux_ecdsa_sign_p256(const char* pem, int keylen, const char* data, int datalen, int* siglen) {
|
||||||
|
return bux_ecdsa_sign_evp(EVP_sha256(), pem, keylen, data, datalen, siglen);
|
||||||
|
}
|
||||||
|
char* bux_ecdsa_sign_p384(const char* pem, int keylen, const char* data, int datalen, int* siglen) {
|
||||||
|
return bux_ecdsa_sign_evp(EVP_sha384(), pem, keylen, data, datalen, siglen);
|
||||||
|
}
|
||||||
|
int bux_ecdsa_verify_p256(const char* pem, int keylen, const char* data, int datalen,
|
||||||
|
const char* sig, int siglen) {
|
||||||
|
return bux_ecdsa_verify_evp(EVP_sha256(), pem, keylen, data, datalen, sig, siglen);
|
||||||
|
}
|
||||||
|
int bux_ecdsa_verify_p384(const char* pem, int keylen, const char* data, int datalen,
|
||||||
|
const char* sig, int siglen) {
|
||||||
|
return bux_ecdsa_verify_evp(EVP_sha384(), pem, keylen, data, datalen, sig, siglen);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Ed25519 (OpenSSL 1.1.1+) --- */
|
||||||
|
|
||||||
|
int bux_ed25519_keypair(unsigned char* pub, unsigned char* priv) {
|
||||||
|
EVP_PKEY* pkey = NULL;
|
||||||
|
EVP_PKEY_CTX* pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL);
|
||||||
|
if (!pctx) return 0;
|
||||||
|
if (EVP_PKEY_keygen_init(pctx) <= 0 ||
|
||||||
|
EVP_PKEY_keygen(pctx, &pkey) <= 0) {
|
||||||
|
EVP_PKEY_CTX_free(pctx);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
EVP_PKEY_CTX_free(pctx);
|
||||||
|
size_t pub_len = 32, priv_len = 32;
|
||||||
|
EVP_PKEY_get_raw_public_key(pkey, pub, &pub_len);
|
||||||
|
EVP_PKEY_get_raw_private_key(pkey, priv, &priv_len);
|
||||||
|
EVP_PKEY_free(pkey);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bux_ed25519_sign(const char* priv, const char* data, int datalen, unsigned char* sig) {
|
||||||
|
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL,
|
||||||
|
(const unsigned char*)priv, 32);
|
||||||
|
if (!pkey) return 0;
|
||||||
|
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
|
||||||
|
int ret = 0;
|
||||||
|
if (EVP_DigestSignInit(ctx, NULL, NULL, NULL, pkey) > 0) {
|
||||||
|
size_t siglen = 64;
|
||||||
|
EVP_DigestSign(ctx, sig, &siglen, (const unsigned char*)data, (size_t)datalen);
|
||||||
|
ret = 1;
|
||||||
|
}
|
||||||
|
EVP_MD_CTX_free(ctx);
|
||||||
|
EVP_PKEY_free(pkey);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bux_ed25519_verify(const char* pub, const char* sig, const char* data, int datalen) {
|
||||||
|
EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL,
|
||||||
|
(const unsigned char*)pub, 32);
|
||||||
|
if (!pkey) return 0;
|
||||||
|
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
|
||||||
|
int ret = 0;
|
||||||
|
if (EVP_DigestVerifyInit(ctx, NULL, NULL, NULL, pkey) > 0) {
|
||||||
|
ret = EVP_DigestVerify(ctx, (const unsigned char*)sig, 64,
|
||||||
|
(const unsigned char*)data, (size_t)datalen);
|
||||||
|
ret = (ret == 1) ? 1 : 0;
|
||||||
|
}
|
||||||
|
EVP_MD_CTX_free(ctx);
|
||||||
|
EVP_PKEY_free(pkey);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
EXIT:137
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[Package]
|
||||||
|
Name = "test_crypto"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// Crypto Library Test — exercises all modules in lib/crypto/
|
||||||
|
// =============================================================================
|
||||||
|
module Main {
|
||||||
|
|
||||||
|
import Std::Io::{PrintLine, Print, PrintInt};
|
||||||
|
import Std::String::{String_Len, String_Eq};
|
||||||
|
import Std::Crypto::Base64::{Base64_Encode, Base64_Decode, Base64URL_Encode, Base64URL_Decode};
|
||||||
|
import Std::Crypto::Hash::{Hash_Sha256, Hash_Sha384, Hash_Sha512, Hash_Sha256Size, Hash_Sha384Size, Hash_Sha512Size};
|
||||||
|
import Std::Crypto::Hmac::{Hmac_Sha256, Hmac_Sha384, Hmac_Sha512};
|
||||||
|
import Std::Crypto::Random::{Random_Bytes, Random_Hex, Random_Base64, Random_Uint32};
|
||||||
|
import Std::Crypto::Jwt::{JwtAlg, Jwt_MakeHeader, Jwt_EncodeHS256, Jwt_Decode};
|
||||||
|
|
||||||
|
// --- Minimal test framework ---
|
||||||
|
var gPassed: int = 0;
|
||||||
|
var gFailed: int = 0;
|
||||||
|
|
||||||
|
func Assert(cond: bool, name: String) {
|
||||||
|
if cond {
|
||||||
|
gPassed = gPassed + 1;
|
||||||
|
Print(" PASS ");
|
||||||
|
} else {
|
||||||
|
gFailed = gFailed + 1;
|
||||||
|
Print(" FAIL ");
|
||||||
|
}
|
||||||
|
PrintLine(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Test: Base64 + Base64URL
|
||||||
|
// =============================================================================
|
||||||
|
func TestBase64() {
|
||||||
|
PrintLine("--- Base64 ---");
|
||||||
|
|
||||||
|
let encoded: String = Base64_Encode("hello");
|
||||||
|
Assert(String_Eq(encoded, "aGVsbG8="), "Base64_Encode('hello')");
|
||||||
|
|
||||||
|
let decoded: String = Base64_Decode("aGVsbG8=");
|
||||||
|
Assert(String_Eq(decoded, "hello"), "Base64_Decode('aGVsbG8=')");
|
||||||
|
|
||||||
|
let urlEnc: String = Base64URL_Encode("hello");
|
||||||
|
Assert(String_Eq(urlEnc, "aGVsbG8"), "Base64URL_Encode('hello') — no padding");
|
||||||
|
|
||||||
|
let urlDec: String = Base64URL_Decode("aGVsbG8");
|
||||||
|
Assert(String_Eq(urlDec, "hello"), "Base64URL_Decode('aGVsbG8')");
|
||||||
|
|
||||||
|
let original: String = "Bux crypto test!";
|
||||||
|
let rt: String = Base64_Decode(Base64_Encode(original));
|
||||||
|
Assert(String_Eq(rt, original), "Base64 round-trip");
|
||||||
|
|
||||||
|
let rtUrl: String = Base64URL_Decode(Base64URL_Encode(original));
|
||||||
|
Assert(String_Eq(rtUrl, original), "Base64URL round-trip");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Test: Hash (SHA-256, SHA-384, SHA-512)
|
||||||
|
// =============================================================================
|
||||||
|
func TestHash() {
|
||||||
|
PrintLine("--- Hash ---");
|
||||||
|
|
||||||
|
let sha256: String = Hash_Sha256("hello");
|
||||||
|
Assert(String_Eq(sha256,
|
||||||
|
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"),
|
||||||
|
"Hash_Sha256('hello')");
|
||||||
|
|
||||||
|
let sha384: String = Hash_Sha384("hello");
|
||||||
|
Assert(String_Eq(sha384,
|
||||||
|
"59e1748777448c69de6b800d7a33bbfb9ff1b463e44354c3553bcdb9c666fa90125a3c79f90397bdf5f6a13de828684f"),
|
||||||
|
"Hash_Sha384('hello')");
|
||||||
|
|
||||||
|
let sha512: String = Hash_Sha512("hello");
|
||||||
|
Assert(String_Eq(sha512,
|
||||||
|
"9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043"),
|
||||||
|
"Hash_Sha512('hello')");
|
||||||
|
|
||||||
|
let empty256: String = Hash_Sha256("");
|
||||||
|
Assert(String_Len(empty256) == 64, "Hash_Sha256('') has 64 hex chars");
|
||||||
|
|
||||||
|
Assert(Hash_Sha256Size() == 32, "Hash_Sha256Size() == 32");
|
||||||
|
Assert(Hash_Sha384Size() == 48, "Hash_Sha384Size() == 48");
|
||||||
|
Assert(Hash_Sha512Size() == 64, "Hash_Sha512Size() == 64");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Test: HMAC
|
||||||
|
// =============================================================================
|
||||||
|
func TestHmac() {
|
||||||
|
PrintLine("--- HMAC ---");
|
||||||
|
|
||||||
|
let hmac256: String = Hmac_Sha256("secret", "hello");
|
||||||
|
Assert(String_Eq(hmac256,
|
||||||
|
"88aab3ede8d3adf94d26ab90d3bafd4a2083070c3daec8e50b3899a4a5d1e0f8"),
|
||||||
|
"Hmac_Sha256('secret', 'hello')");
|
||||||
|
|
||||||
|
Assert(String_Len(Hmac_Sha384("s", "h")) == 96, "Hmac_Sha384 produces 96 hex chars");
|
||||||
|
Assert(String_Len(Hmac_Sha512("s", "h")) == 128, "Hmac_Sha512 produces 128 hex chars");
|
||||||
|
|
||||||
|
let h1: String = Hmac_Sha256("key", "msg");
|
||||||
|
let h2: String = Hmac_Sha256("key", "msg");
|
||||||
|
Assert(String_Eq(h1, h2), "HMAC deterministic");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Test: Random
|
||||||
|
// =============================================================================
|
||||||
|
func TestRandom() {
|
||||||
|
PrintLine("--- Random ---");
|
||||||
|
|
||||||
|
Assert(String_Len(Random_Bytes(32)) == 32, "Random_Bytes(32) len=32");
|
||||||
|
Assert(String_Len(Random_Hex(16)) == 32, "Random_Hex(16) len=32");
|
||||||
|
Assert(String_Len(Random_Base64(16)) > 0, "Random_Base64(16) non-empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Test: JWT HS256 encode/decode
|
||||||
|
// =============================================================================
|
||||||
|
func TestJwtHS256() {
|
||||||
|
PrintLine("--- JWT HS256 ---");
|
||||||
|
|
||||||
|
let token: String = Jwt_EncodeHS256("{\"sub\":\"test\"}", "secret");
|
||||||
|
Assert(String_Len(token) > 20, "Jwt_EncodeHS256 returns token");
|
||||||
|
|
||||||
|
var header: String = "";
|
||||||
|
var payload: String = "";
|
||||||
|
let alg: JwtAlg = JwtAlg { tag: JwtAlg_HS256 };
|
||||||
|
let ok: bool = Jwt_Decode(token, alg, "secret", &header, &payload);
|
||||||
|
Assert(ok, "Jwt_Decode HS256 verifies");
|
||||||
|
Assert(String_Eq(payload, "{\"sub\":\"test\"}"), "Jwt_Decode returns correct payload");
|
||||||
|
|
||||||
|
let wrongOk: bool = Jwt_Decode(token, alg, "wrong", &header, &payload);
|
||||||
|
Assert(!wrongOk, "Jwt_Decode rejects wrong secret");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Test: JWT algorithm headers
|
||||||
|
// =============================================================================
|
||||||
|
func TestJwtHeaders() {
|
||||||
|
PrintLine("--- JWT Headers ---");
|
||||||
|
|
||||||
|
Assert(String_Eq(Jwt_MakeHeader(JwtAlg { tag: JwtAlg_HS256 }),
|
||||||
|
"{\"alg\":\"HS256\",\"typ\":\"JWT\"}"), "JWT header HS256");
|
||||||
|
Assert(String_Eq(Jwt_MakeHeader(JwtAlg { tag: JwtAlg_RS256 }),
|
||||||
|
"{\"alg\":\"RS256\",\"typ\":\"JWT\"}"), "JWT header RS256");
|
||||||
|
Assert(String_Eq(Jwt_MakeHeader(JwtAlg { tag: JwtAlg_ES256 }),
|
||||||
|
"{\"alg\":\"ES256\",\"typ\":\"JWT\"}"), "JWT header ES256");
|
||||||
|
Assert(String_Eq(Jwt_MakeHeader(JwtAlg { tag: JwtAlg_EdDSA }),
|
||||||
|
"{\"alg\":\"EdDSA\",\"typ\":\"JWT\"}"), "JWT header EdDSA");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Main
|
||||||
|
// =============================================================================
|
||||||
|
func Main() -> int {
|
||||||
|
PrintLine("================================================");
|
||||||
|
PrintLine(" Bux Crypto Library — Test Suite");
|
||||||
|
PrintLine("================================================");
|
||||||
|
PrintLine("");
|
||||||
|
|
||||||
|
TestBase64();
|
||||||
|
TestHash();
|
||||||
|
TestHmac();
|
||||||
|
TestRandom();
|
||||||
|
TestJwtHS256();
|
||||||
|
TestJwtHeaders();
|
||||||
|
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("================================================");
|
||||||
|
Print(" Passed: ");
|
||||||
|
PrintInt(gPassed);
|
||||||
|
PrintLine("");
|
||||||
|
Print(" Failed: ");
|
||||||
|
PrintInt(gFailed);
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("================================================");
|
||||||
|
|
||||||
|
if gFailed > 0 { return 1; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // module Main
|
||||||
Reference in New Issue
Block a user