Rewrite jwt-pitbul and boko-framework with modern Bux constructs
- jwt-pitbul: algebraic enums, Array<String>, methods on JwtAlg, string interpolation, structured Result/Option error handling. - boko-framework: StringMap<String> for headers/query/path params, Request/Response methods, StringBuilder, for loops, algebraic enums. - Update READMEs and bump versions to 0.2.0.
This commit is contained in:
@@ -4,12 +4,14 @@
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
This version (0.2.0) is rewritten with modern Bux: `struct` methods, generic `StringMap<String>` for headers/query/path params, `for` loops, algebraic enums, `StringBuilder`, and string interpolation.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd apps/boko-framework
|
cd apps/boko-framework
|
||||||
../../buxc build
|
../../buxc build
|
||||||
./boko-framework
|
./build/boko-framework
|
||||||
```
|
```
|
||||||
|
|
||||||
Open `http://localhost:8080` — you'll see the demo landing page.
|
Open `http://localhost:8080` — you'll see the demo landing page.
|
||||||
@@ -20,7 +22,7 @@ Boko follows a **single-dispatch-function** pattern. You define one function —
|
|||||||
|
|
||||||
```bux
|
```bux
|
||||||
import Boko::{Request, Response, Response_Json, Response_Html, Response_NotFound,
|
import Boko::{Request, Response, Response_Json, Response_Html, Response_NotFound,
|
||||||
Path_Match, Request_GetQuery, Request_GetPathParam,
|
Path_Match,
|
||||||
App, App_New, App_Run};
|
App, App_New, App_Run};
|
||||||
|
|
||||||
func Boko_Router(req: Request) -> Response {
|
func Boko_Router(req: Request) -> Response {
|
||||||
@@ -36,14 +38,14 @@ func Boko_Router(req: Request) -> Response {
|
|||||||
|
|
||||||
// Route: GET /users/{id} — path parameter
|
// Route: GET /users/{id} — path parameter
|
||||||
if Path_Match("/users/{id}", req.path, &req) {
|
if Path_Match("/users/{id}", req.path, &req) {
|
||||||
let id: String = Request_GetPathParam(&req, "id");
|
let id: String = req.GetPathParam("id");
|
||||||
// Build JSON response with id
|
// Build JSON response with id
|
||||||
return Response_Json(...);
|
return Response_Json(...);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Route: GET /search?q=... — query parameter
|
// Route: GET /search?q=... — query parameter
|
||||||
if String_Eq(req.path, "/search") {
|
if String_Eq(req.path, "/search") {
|
||||||
let q: String = Request_GetQuery(&req, "q");
|
let q: String = req.GetQuery("q");
|
||||||
return Response_Json(...);
|
return Response_Json(...);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,16 +65,19 @@ func Main() -> int {
|
|||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
|-------|------|-------------|
|
|-------|------|-------------|
|
||||||
| `method` | `HttpVerb` | GET, POST, PUT, DELETE |
|
| `method` | `HttpVerb` | GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS |
|
||||||
| `path` | `String` | Request path (without query string) |
|
| `path` | `String` | Request path (without query string) |
|
||||||
| `body` | `String` | Request body (for POST/PUT) |
|
| `body` | `String` | Request body (for POST/PUT) |
|
||||||
| `headerCount` | `int` | Number of headers |
|
| `headers` | `StringMap<String>` | Parsed request headers |
|
||||||
|
| `query` | `StringMap<String>` | Parsed query parameters |
|
||||||
|
| `pathParams` | `StringMap<String>` | Extracted path parameters |
|
||||||
|
|
||||||
| Function | Returns | Description |
|
| Method | Returns | Description |
|
||||||
|----------|---------|-------------|
|
|--------|---------|-------------|
|
||||||
| `Request_GetHeader(req, name)` | `String` | Get header value by name |
|
| `req.GetHeader(name)` | `String` | Get header value by name |
|
||||||
| `Request_GetQuery(req, name)` | `String` | Get query parameter by name |
|
| `req.GetQuery(name)` | `String` | Get query parameter by name |
|
||||||
| `Request_GetPathParam(req, name)` | `String` | Get extracted path parameter |
|
| `req.HasQuery(name)` | `bool` | Check if query parameter exists |
|
||||||
|
| `req.GetPathParam(name)` | `String` | Get extracted path parameter |
|
||||||
|
|
||||||
### Response
|
### Response
|
||||||
|
|
||||||
@@ -96,12 +101,12 @@ func Main() -> int {
|
|||||||
// Extracts: id=42, postId=7
|
// Extracts: id=42, postId=7
|
||||||
|
|
||||||
if Path_Match("/users/{id}/posts/{postId}", req.path, &req) {
|
if Path_Match("/users/{id}/posts/{postId}", req.path, &req) {
|
||||||
let userId: String = Request_GetPathParam(&req, "id"); // "42"
|
let userId: String = req.GetPathParam("id"); // "42"
|
||||||
let postId: String = Request_GetPathParam(&req, "postId"); // "7"
|
let postId: String = req.GetPathParam("postId"); // "7"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The function returns `true` if the pattern matches and populates `req.pathParamKeys` / `req.pathParamValues`.
|
The function returns `true` if the pattern matches and populates `req.pathParams`.
|
||||||
|
|
||||||
### App
|
### App
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[Package]
|
[Package]
|
||||||
Name = "boko-framework"
|
Name = "boko-framework"
|
||||||
Version = "0.1.0"
|
Version = "0.2.0"
|
||||||
Type = "bin"
|
Type = "bin"
|
||||||
Description = "Boko — async web framework for Bux, inspired by FastAPI"
|
Description = "Boko — async web framework for Bux, inspired by FastAPI"
|
||||||
|
|
||||||
|
|||||||
+199
-231
@@ -1,28 +1,22 @@
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Boko — Async Web Framework for Bux (inspired by FastAPI)
|
// Boko — Async Web Framework for Bux (inspired by FastAPI)
|
||||||
|
// Rewritten with modern Bux: methods on structs, generic StringMap, for-in
|
||||||
|
// loops, algebraic enums, StringBuilder, and string interpolation.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
module Boko {
|
module Boko {
|
||||||
|
|
||||||
import Std::Io::{PrintLine, Print, PrintInt};
|
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::Net::{Net_Create, Net_SetReuse, Net_Bind, Net_Listen, Net_Accept, Net_Send, Net_Recv, Net_Close, Net_LastError};
|
||||||
import Std::String::{String_Len, String_Eq, String_StartsWith, String_Contains, String_Offset};
|
import Std::String::{
|
||||||
import Std::Task::{Task_Join, TaskHandle};
|
String_Len, String_Eq, String_IsNull,
|
||||||
|
String_StartsWith, String_Contains,
|
||||||
|
String_Find, String_Offset, String_Slice,
|
||||||
|
String_SplitCount, String_SplitPart,
|
||||||
|
StringBuilder, StringBuilder_New, StringBuilder_Append,
|
||||||
|
StringBuilder_AppendInt, StringBuilder_Build, StringBuilder_Free
|
||||||
|
};
|
||||||
|
import Std::Map::{StringMap, StringMap_New, StringMap_Set, StringMap_Get, StringMap_Has};
|
||||||
|
|
||||||
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
|
// HTTP Methods
|
||||||
@@ -37,6 +31,28 @@ enum HttpVerb {
|
|||||||
OPTIONS,
|
OPTIONS,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HttpVerb_MethodName(verb: HttpVerb) -> String {
|
||||||
|
if verb.tag == HttpVerb_GET { return "GET"; }
|
||||||
|
if verb.tag == HttpVerb_POST { return "POST"; }
|
||||||
|
if verb.tag == HttpVerb_PUT { return "PUT"; }
|
||||||
|
if verb.tag == HttpVerb_DELETE { return "DELETE"; }
|
||||||
|
if verb.tag == HttpVerb_PATCH { return "PATCH"; }
|
||||||
|
if verb.tag == HttpVerb_HEAD { return "HEAD"; }
|
||||||
|
if verb.tag == HttpVerb_OPTIONS { return "OPTIONS"; }
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
func HttpVerb_Parse(methodStr: String) -> HttpVerb {
|
||||||
|
if String_Eq(methodStr, "GET") { return HttpVerb { tag: HttpVerb_GET }; }
|
||||||
|
if String_Eq(methodStr, "POST") { return HttpVerb { tag: HttpVerb_POST }; }
|
||||||
|
if String_Eq(methodStr, "PUT") { return HttpVerb { tag: HttpVerb_PUT }; }
|
||||||
|
if String_Eq(methodStr, "DELETE") { return HttpVerb { tag: HttpVerb_DELETE }; }
|
||||||
|
if String_Eq(methodStr, "PATCH") { return HttpVerb { tag: HttpVerb_PATCH }; }
|
||||||
|
if String_Eq(methodStr, "HEAD") { return HttpVerb { tag: HttpVerb_HEAD }; }
|
||||||
|
if String_Eq(methodStr, "OPTIONS") { return HttpVerb { tag: HttpVerb_OPTIONS }; }
|
||||||
|
return HttpVerb { tag: HttpVerb_GET };
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Request — parsed incoming HTTP request
|
// Request — parsed incoming HTTP request
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -44,15 +60,36 @@ struct Request {
|
|||||||
method: HttpVerb,
|
method: HttpVerb,
|
||||||
path: String,
|
path: String,
|
||||||
body: String,
|
body: String,
|
||||||
headerKeys: *String,
|
headers: StringMap<String>,
|
||||||
headerValues: *String,
|
query: StringMap<String>,
|
||||||
headerCount: int,
|
pathParams: StringMap<String>,
|
||||||
queryKeys: *String,
|
}
|
||||||
queryValues: *String,
|
|
||||||
queryCount: int,
|
extend Request {
|
||||||
pathParamKeys: *String,
|
func GetHeader(self: Request, name: String) -> String {
|
||||||
pathParamValues: *String,
|
if StringMap_Has<String>(&self.headers, name) {
|
||||||
pathParamCount: int,
|
return StringMap_Get<String>(&self.headers, name);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetQuery(self: Request, name: String) -> String {
|
||||||
|
if StringMap_Has<String>(&self.query, name) {
|
||||||
|
return StringMap_Get<String>(&self.query, name);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
func HasQuery(self: Request, name: String) -> bool {
|
||||||
|
return StringMap_Has<String>(&self.query, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPathParam(self: Request, name: String) -> String {
|
||||||
|
if StringMap_Has<String>(&self.pathParams, name) {
|
||||||
|
return StringMap_Get<String>(&self.pathParams, name);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -65,63 +102,9 @@ struct Response {
|
|||||||
extraHeaders: String,
|
extraHeaders: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// --- Constructors ---
|
||||||
// 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 {
|
func Response_New(status: int, contentType: String, body: String) -> Response {
|
||||||
var resp: Response;
|
return Response { statusCode: status, contentType: contentType, body: body, extraHeaders: "" };
|
||||||
resp.statusCode = status;
|
|
||||||
resp.contentType = contentType;
|
|
||||||
resp.body = body;
|
|
||||||
resp.extraHeaders = "";
|
|
||||||
return resp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Response_Ok(body: String) -> Response {
|
func Response_Ok(body: String) -> Response {
|
||||||
@@ -141,118 +124,143 @@ func Response_Text(text: String) -> Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Response_Redirect(url: String) -> Response {
|
func Response_Redirect(url: String) -> Response {
|
||||||
var resp: Response;
|
let sb: StringBuilder = StringBuilder_New();
|
||||||
resp.statusCode = 302;
|
StringBuilder_Append(&sb, "Location: ");
|
||||||
resp.contentType = "";
|
StringBuilder_Append(&sb, url);
|
||||||
resp.body = "";
|
StringBuilder_Append(&sb, "\r\n");
|
||||||
let sb: *void = bux_sb_new(256);
|
let headers: String = StringBuilder_Build(&sb);
|
||||||
bux_sb_append(sb, "Location: ");
|
StringBuilder_Free(&sb);
|
||||||
bux_sb_append(sb, url);
|
return Response { statusCode: 302, contentType: "", body: "", extraHeaders: headers };
|
||||||
bux_sb_append(sb, "\r\n");
|
|
||||||
resp.extraHeaders = bux_sb_build(sb);
|
|
||||||
bux_sb_free(sb);
|
|
||||||
return resp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Response_NotFound() -> Response {
|
func Response_NotFound() -> Response {
|
||||||
return Response_New(404, "application/json; charset=utf-8",
|
return Response_New(404, "application/json; charset=utf-8", "{\"error\":\"not_found\"}");
|
||||||
"{\"error\":\"not_found\"}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Response_Error(status: int, message: String) -> Response {
|
func Response_Error(status: int, message: String) -> Response {
|
||||||
return Response_New(status, "application/json; charset=utf-8", message);
|
let sb: StringBuilder = StringBuilder_New();
|
||||||
|
StringBuilder_Append(&sb, "{\"error\":\"");
|
||||||
|
StringBuilder_Append(&sb, message);
|
||||||
|
StringBuilder_Append(&sb, "\"}");
|
||||||
|
let body: String = StringBuilder_Build(&sb);
|
||||||
|
StringBuilder_Free(&sb);
|
||||||
|
return Response_New(status, "application/json; charset=utf-8", body);
|
||||||
}
|
}
|
||||||
|
|
||||||
func Response_NoContent() -> Response {
|
func Response_NoContent() -> Response {
|
||||||
return Response_New(204, "", "");
|
return Response_New(204, "", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Instance helpers ---
|
||||||
|
extend Response {
|
||||||
|
func WithHeader(self: Response, key: String, value: String) -> Response {
|
||||||
|
let sb: StringBuilder = StringBuilder_New();
|
||||||
|
StringBuilder_Append(&sb, self.extraHeaders);
|
||||||
|
StringBuilder_Append(&sb, key);
|
||||||
|
StringBuilder_Append(&sb, ": ");
|
||||||
|
StringBuilder_Append(&sb, value);
|
||||||
|
StringBuilder_Append(&sb, "\r\n");
|
||||||
|
self.extraHeaders = StringBuilder_Build(&sb);
|
||||||
|
StringBuilder_Free(&sb);
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Status phrase lookup
|
||||||
|
// =============================================================================
|
||||||
|
func StatusPhrase(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 == 400 { return "Bad Request"; }
|
||||||
|
if code == 401 { return "Unauthorized"; }
|
||||||
|
if code == 403 { return "Forbidden"; }
|
||||||
|
if code == 404 { return "Not Found"; }
|
||||||
|
if code == 500 { return "Internal Server Error"; }
|
||||||
|
if code == 502 { return "Bad Gateway"; }
|
||||||
|
if code == 503 { return "Service Unavailable"; }
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Build HTTP response string from Response struct
|
// Build HTTP response string from Response struct
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
func Response_Build(resp: Response) -> String {
|
func Response_Build(resp: Response) -> String {
|
||||||
let sb: *void = bux_sb_new(2048);
|
let sb: StringBuilder = StringBuilder_New();
|
||||||
|
|
||||||
// Status line
|
// Status line
|
||||||
bux_sb_append(sb, "HTTP/1.1 ");
|
StringBuilder_Append(&sb, "HTTP/1.1 ");
|
||||||
bux_sb_append_int(sb, resp.statusCode as int64);
|
StringBuilder_AppendInt(&sb, resp.statusCode as int64);
|
||||||
bux_sb_append(sb, " ");
|
StringBuilder_Append(&sb, " ");
|
||||||
if resp.statusCode == 200 { bux_sb_append(sb, "OK"); }
|
StringBuilder_Append(&sb, StatusPhrase(resp.statusCode));
|
||||||
else if resp.statusCode == 201 { bux_sb_append(sb, "Created"); }
|
StringBuilder_Append(&sb, "\r\n");
|
||||||
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
|
// Server
|
||||||
bux_sb_append(sb, "Server: Boko/0.1.0 (Bux)\r\n");
|
StringBuilder_Append(&sb, "Server: Boko/0.2.0 (Bux)\r\n");
|
||||||
|
|
||||||
// Extra headers (redirects, CORS, etc.)
|
// Extra headers
|
||||||
if bux_strlen(resp.extraHeaders) > 0 {
|
if String_Len(resp.extraHeaders) > 0 {
|
||||||
bux_sb_append(sb, resp.extraHeaders);
|
StringBuilder_Append(&sb, resp.extraHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content-Type
|
// Content-Type
|
||||||
if bux_strlen(resp.contentType) > 0 {
|
if String_Len(resp.contentType) > 0 {
|
||||||
bux_sb_append(sb, "Content-Type: ");
|
StringBuilder_Append(&sb, "Content-Type: ");
|
||||||
bux_sb_append(sb, resp.contentType);
|
StringBuilder_Append(&sb, resp.contentType);
|
||||||
bux_sb_append(sb, "\r\n");
|
StringBuilder_Append(&sb, "\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content-Length
|
// Content-Length
|
||||||
let bodyLen: uint = bux_strlen(resp.body);
|
let bodyLen: uint = String_Len(resp.body);
|
||||||
bux_sb_append(sb, "Content-Length: ");
|
StringBuilder_Append(&sb, "Content-Length: ");
|
||||||
bux_sb_append_int(sb, bodyLen as int64);
|
StringBuilder_AppendInt(&sb, bodyLen as int64);
|
||||||
bux_sb_append(sb, "\r\n");
|
StringBuilder_Append(&sb, "\r\n");
|
||||||
|
|
||||||
// Connection close
|
// Connection close
|
||||||
bux_sb_append(sb, "Connection: close\r\n");
|
StringBuilder_Append(&sb, "Connection: close\r\n");
|
||||||
|
StringBuilder_Append(&sb, "\r\n");
|
||||||
bux_sb_append(sb, "\r\n");
|
|
||||||
|
|
||||||
if bodyLen > 0 {
|
if bodyLen > 0 {
|
||||||
bux_sb_append(sb, resp.body);
|
StringBuilder_Append(&sb, resp.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
let result: String = bux_sb_build(sb);
|
let result: String = StringBuilder_Build(&sb);
|
||||||
bux_sb_free(sb);
|
StringBuilder_Free(&sb);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Parse query string: ?key=val&key2=val2 → key-value arrays
|
// Parse query string: ?key=val&key2=val2 → generic StringMap
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
func Query_Parse(req: *Request, queryString: String) {
|
func Query_Parse(queryString: String) -> StringMap<String> {
|
||||||
if bux_strlen(queryString) == 0 { return; }
|
let map: StringMap<String> = StringMap_New<String>(8);
|
||||||
let pairCount: uint = bux_str_split_count(queryString, "&");
|
if String_Len(queryString) == 0 {
|
||||||
let cap: int = pairCount as int + 1;
|
return map;
|
||||||
req.queryKeys = bux_alloc(cap as uint * 8) as *String;
|
}
|
||||||
req.queryValues = bux_alloc(cap as uint * 8) as *String;
|
|
||||||
req.queryCount = 0;
|
|
||||||
|
|
||||||
|
let pairCount: uint = String_SplitCount(queryString, "&");
|
||||||
var i: uint = 0;
|
var i: uint = 0;
|
||||||
while i < pairCount {
|
while i < pairCount {
|
||||||
let pair: String = bux_str_split_part(queryString, "&", i);
|
let pair: String = String_SplitPart(queryString, "&", i);
|
||||||
let eqPos: String = bux_strstr(pair, "=");
|
let eqPos: String = String_Find(pair, "=");
|
||||||
var key: String = pair;
|
var key: String = pair;
|
||||||
var value: String = "";
|
var value: String = "";
|
||||||
if String_Len(eqPos) > 0 {
|
if !String_IsNull(eqPos) {
|
||||||
let keyLen: uint = String_Offset(eqPos, pair);
|
let keyLen: uint = String_Offset(eqPos, pair);
|
||||||
key = bux_str_slice(pair, 0, keyLen);
|
key = String_Slice(pair, 0, keyLen);
|
||||||
let valStart: uint = keyLen + 1;
|
let valStart: uint = keyLen + 1;
|
||||||
let pairLen: uint = bux_strlen(pair);
|
let pairLen: uint = String_Len(pair);
|
||||||
if valStart < pairLen {
|
if valStart < pairLen {
|
||||||
value = bux_str_slice(pair, valStart, pairLen - valStart);
|
value = String_Slice(pair, valStart, pairLen - valStart);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
req.queryKeys[req.queryCount] = key;
|
StringMap_Set<String>(&map, key, value);
|
||||||
req.queryValues[req.queryCount] = value;
|
|
||||||
req.queryCount = req.queryCount + 1;
|
|
||||||
i = i + 1;
|
i = i + 1;
|
||||||
}
|
}
|
||||||
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -263,50 +271,45 @@ func Request_Parse(raw: String) -> Request {
|
|||||||
req.method = HttpVerb { tag: HttpVerb_GET };
|
req.method = HttpVerb { tag: HttpVerb_GET };
|
||||||
req.path = "/";
|
req.path = "/";
|
||||||
req.body = "";
|
req.body = "";
|
||||||
req.headerCount = 0;
|
req.headers = StringMap_New<String>(8);
|
||||||
req.queryCount = 0;
|
req.query = StringMap_New<String>(4);
|
||||||
req.pathParamCount = 0;
|
req.pathParams = StringMap_New<String>(4);
|
||||||
|
|
||||||
if String_Len(raw) == 0 { return req; }
|
if String_Len(raw) == 0 { return req; }
|
||||||
let rawLen: uint = bux_strlen(raw);
|
|
||||||
if rawLen == 0 { return req; }
|
|
||||||
|
|
||||||
// Find header/body boundary
|
// Find header/body boundary
|
||||||
let boundary: String = bux_strstr(raw, "\r\n\r\n");
|
let boundary: String = String_Find(raw, "\r\n\r\n");
|
||||||
|
let rawLen: uint = String_Len(raw);
|
||||||
var headerLen: uint = rawLen;
|
var headerLen: uint = rawLen;
|
||||||
if String_Len(boundary) > 0 {
|
if !String_IsNull(boundary) {
|
||||||
headerLen = String_Offset(boundary, raw);
|
headerLen = String_Offset(boundary, raw);
|
||||||
let bodyStart: uint = headerLen + 4;
|
let bodyStart: uint = headerLen + 4;
|
||||||
if bodyStart < rawLen {
|
if bodyStart < rawLen {
|
||||||
req.body = bux_str_slice(raw, bodyStart, rawLen - bodyStart);
|
req.body = String_Slice(raw, bodyStart, rawLen - bodyStart);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if headerLen == 0 { return req; }
|
if headerLen == 0 { return req; }
|
||||||
let headerBlock: String = bux_str_slice(raw, 0, headerLen);
|
let headerBlock: String = String_Slice(raw, 0, headerLen);
|
||||||
let lineCount: uint = bux_str_split_count(headerBlock, "\r\n");
|
let lineCount: uint = String_SplitCount(headerBlock, "\r\n");
|
||||||
if lineCount == 0 { return req; }
|
if lineCount == 0 { return req; }
|
||||||
|
|
||||||
// Parse request line
|
// Parse request line
|
||||||
let requestLine: String = bux_str_split_part(headerBlock, "\r\n", 0);
|
let requestLine: String = String_SplitPart(headerBlock, "\r\n", 0);
|
||||||
if bux_strlen(requestLine) > 0 {
|
if String_Len(requestLine) > 0 {
|
||||||
let methodStr: String = bux_str_split_part(requestLine, " ", 0);
|
let methodStr: String = String_SplitPart(requestLine, " ", 0);
|
||||||
if String_Eq(methodStr, "GET") { req.method = HttpVerb { tag: HttpVerb_GET }; }
|
req.method = HttpVerb_Parse(methodStr);
|
||||||
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);
|
let fullPath: String = String_SplitPart(requestLine, " ", 1);
|
||||||
// Split path and query string
|
let qmark: String = String_Find(fullPath, "?");
|
||||||
let qmark: String = bux_strstr(fullPath, "?");
|
if !String_IsNull(qmark) {
|
||||||
if String_Len(qmark) > 0 {
|
|
||||||
let pathLen: uint = String_Offset(qmark, fullPath);
|
let pathLen: uint = String_Offset(qmark, fullPath);
|
||||||
req.path = bux_str_slice(fullPath, 0, pathLen);
|
req.path = String_Slice(fullPath, 0, pathLen);
|
||||||
let qsStart: uint = pathLen + 1;
|
let qsStart: uint = pathLen + 1;
|
||||||
let fullLen: uint = bux_strlen(fullPath);
|
let fullLen: uint = String_Len(fullPath);
|
||||||
if qsStart < fullLen {
|
if qsStart < fullLen {
|
||||||
let qs: String = bux_str_slice(fullPath, qsStart, fullLen - qsStart);
|
let qs: String = String_Slice(fullPath, qsStart, fullLen - qsStart);
|
||||||
Query_Parse(&req, qs);
|
req.query = Query_Parse(qs);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
req.path = fullPath;
|
req.path = fullPath;
|
||||||
@@ -314,26 +317,20 @@ func Request_Parse(raw: String) -> Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse headers
|
// 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;
|
var i: uint = 1;
|
||||||
while i < lineCount {
|
while i < lineCount {
|
||||||
let line: String = bux_str_split_part(headerBlock, "\r\n", i);
|
let line: String = String_SplitPart(headerBlock, "\r\n", i);
|
||||||
let colonPos: String = bux_strstr(line, ": ");
|
let colonPos: String = String_Find(line, ": ");
|
||||||
if String_Len(colonPos) > 0 {
|
if !String_IsNull(colonPos) {
|
||||||
let keyLen: uint = String_Offset(colonPos, line);
|
let keyLen: uint = String_Offset(colonPos, line);
|
||||||
let key: String = bux_str_slice(line, 0, keyLen);
|
let key: String = String_Slice(line, 0, keyLen);
|
||||||
let valStart: uint = keyLen + 2;
|
let valStart: uint = keyLen + 2;
|
||||||
let lineLen: uint = bux_strlen(line);
|
let lineLen: uint = String_Len(line);
|
||||||
var value: String = "";
|
var value: String = "";
|
||||||
if valStart < lineLen {
|
if valStart < lineLen {
|
||||||
value = bux_str_slice(line, valStart, lineLen - valStart);
|
value = String_Slice(line, valStart, lineLen - valStart);
|
||||||
}
|
}
|
||||||
req.headerKeys[req.headerCount] = key;
|
StringMap_Set<String>(&req.headers, key, value);
|
||||||
req.headerValues[req.headerCount] = value;
|
|
||||||
req.headerCount = req.headerCount + 1;
|
|
||||||
}
|
}
|
||||||
i = i + 1;
|
i = i + 1;
|
||||||
}
|
}
|
||||||
@@ -345,48 +342,23 @@ func Request_Parse(raw: String) -> Request {
|
|||||||
// Path pattern matching: /users/{id} against /users/42 → extracts id=42
|
// Path pattern matching: /users/{id} against /users/42 → extracts id=42
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
func Path_Match(pattern: String, path: String, req: *Request) -> bool {
|
func Path_Match(pattern: String, path: String, req: *Request) -> bool {
|
||||||
// Split both by "/"
|
let patParts: uint = String_SplitCount(pattern, "/");
|
||||||
let patParts: uint = bux_str_split_count(pattern, "/");
|
let pathParts: uint = String_SplitCount(path, "/");
|
||||||
let pathParts: uint = bux_str_split_count(path, "/");
|
|
||||||
|
|
||||||
if patParts != pathParts { return false; }
|
if patParts != pathParts { return false; }
|
||||||
|
|
||||||
// Count params to allocate
|
req.pathParams = StringMap_New<String>(8);
|
||||||
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;
|
var i: uint = 0;
|
||||||
while i < patParts {
|
while i < patParts {
|
||||||
let patPart: String = bux_str_split_part(pattern, "/", i);
|
let patPart: String = String_SplitPart(pattern, "/", i);
|
||||||
let pathPart: String = bux_str_split_part(path, "/", i);
|
let pathPart: String = String_SplitPart(path, "/", i);
|
||||||
|
|
||||||
if String_StartsWith(patPart, "{") && String_Contains(patPart, "}") {
|
if String_StartsWith(patPart, "{") && String_Contains(patPart, "}") {
|
||||||
// Extract param name from {name}
|
let nameLen: uint = String_Len(patPart) - 2;
|
||||||
let braceOpen: uint = 1; // skip "{"
|
let name: String = String_Slice(patPart, 1, nameLen);
|
||||||
let braceClose: String = bux_strstr(patPart, "}");
|
StringMap_Set<String>(&req.pathParams, name, pathPart);
|
||||||
var nameLen: uint = 0;
|
|
||||||
if String_Len(braceClose) > 0 {
|
|
||||||
nameLen = String_Offset(braceClose, patPart) - 1;
|
|
||||||
}
|
|
||||||
let name: String = bux_str_slice(patPart, braceOpen, nameLen);
|
|
||||||
req.pathParamKeys[req.pathParamCount] = name;
|
|
||||||
req.pathParamValues[req.pathParamCount] = pathPart;
|
|
||||||
req.pathParamCount = req.pathParamCount + 1;
|
|
||||||
} else {
|
} else {
|
||||||
// Literal match
|
|
||||||
if !String_Eq(patPart, pathPart) { return false; }
|
if !String_Eq(patPart, pathPart) { return false; }
|
||||||
}
|
}
|
||||||
i = i + 1;
|
i = i + 1;
|
||||||
@@ -396,14 +368,16 @@ func Path_Match(pattern: String, path: String, req: *Request) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// App constructor
|
// App — server instance
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
struct App {
|
||||||
|
port: int,
|
||||||
|
threadCount: int,
|
||||||
|
serverName: String,
|
||||||
|
}
|
||||||
|
|
||||||
func App_New(port: int, threadCount: int) -> App {
|
func App_New(port: int, threadCount: int) -> App {
|
||||||
var app: App;
|
return App { port: port, threadCount: threadCount, serverName: "Boko/0.2.0 (Bux)" };
|
||||||
app.port = port;
|
|
||||||
app.threadCount = threadCount;
|
|
||||||
app.serverName = "Boko/0.1.0 (Bux)";
|
|
||||||
return app;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -417,19 +391,13 @@ func Boko_Router(req: Request) -> Response;
|
|||||||
func App_HandleConnection(clientFd: int) {
|
func App_HandleConnection(clientFd: int) {
|
||||||
let raw: String = Net_Recv(clientFd, 8192);
|
let raw: String = Net_Recv(clientFd, 8192);
|
||||||
if String_Len(raw) == 0 { return; }
|
if String_Len(raw) == 0 { return; }
|
||||||
if bux_strlen(raw) == 0 { return; }
|
|
||||||
|
|
||||||
let req: Request = Request_Parse(raw);
|
let req: Request = Request_Parse(raw);
|
||||||
|
|
||||||
// Call user-defined dispatch (forward declaration — user must implement)
|
|
||||||
let resp: Response = Boko_Router(req);
|
let resp: Response = Boko_Router(req);
|
||||||
|
|
||||||
// Log
|
// Log
|
||||||
if req.method.tag == HttpVerb_GET { Print("GET "); }
|
Print(HttpVerb_MethodName(req.method));
|
||||||
else if req.method.tag == HttpVerb_POST { Print("POST "); }
|
Print(" ");
|
||||||
else if req.method.tag == HttpVerb_PUT { Print("PUT "); }
|
|
||||||
else if req.method.tag == HttpVerb_DELETE { Print("DELETE "); }
|
|
||||||
else { Print("? "); }
|
|
||||||
Print(req.path);
|
Print(req.path);
|
||||||
Print(" → ");
|
Print(" → ");
|
||||||
PrintInt(resp.statusCode);
|
PrintInt(resp.statusCode);
|
||||||
@@ -457,9 +425,9 @@ func App_Worker(serverFd: int) {
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
func App_Run(app: *App) {
|
func App_Run(app: *App) {
|
||||||
PrintLine("╔══════════════════════════════════════╗");
|
PrintLine("╔══════════════════════════════════════╗");
|
||||||
PrintLine("║ Boko Framework v0.1.0 ║");
|
PrintLine("║ Boko Framework v0.2.0 ║");
|
||||||
PrintLine("║ Async web framework for Bux ║");
|
PrintLine("║ Async web framework for Bux ║");
|
||||||
PrintLine("╚══════════════════════════════════════╝");
|
PrintLine("╚══════════════════════════════════════════════════════╝");
|
||||||
PrintLine("");
|
PrintLine("");
|
||||||
|
|
||||||
let fd: int = Net_Create();
|
let fd: int = Net_Create();
|
||||||
|
|||||||
@@ -1,25 +1,22 @@
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Boko Example App — demonstrates the framework API
|
// Boko Example App — demonstrates the modern framework API
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
module Main {
|
module Main {
|
||||||
|
|
||||||
import Std::Io::{PrintLine, Print};
|
import Std::Io::{PrintLine, Print};
|
||||||
import Std::String::{String_Eq};
|
import Std::String::{
|
||||||
|
String_Eq, String_Len,
|
||||||
|
StringBuilder, StringBuilder_New, StringBuilder_Append,
|
||||||
|
StringBuilder_Build, StringBuilder_Free
|
||||||
|
};
|
||||||
import Boko::{
|
import Boko::{
|
||||||
App, App_New, App_Run,
|
App, App_New, App_Run,
|
||||||
Request, Response,
|
Request, Response,
|
||||||
Request_GetQuery, Request_GetPathParam,
|
|
||||||
Response_Html, Response_Json, Response_NotFound, Response_Redirect,
|
Response_Html, Response_Json, Response_NotFound, Response_Redirect,
|
||||||
Path_Match,
|
Path_Match,
|
||||||
HttpVerb
|
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)
|
// Boko_Router — user-defined dispatch (called by the framework)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -31,7 +28,7 @@ func Boko_Router(req: Request) -> Response {
|
|||||||
|
|
||||||
// --- GET /api/health ---
|
// --- GET /api/health ---
|
||||||
if String_Eq(req.path, "/api/health") {
|
if String_Eq(req.path, "/api/health") {
|
||||||
return Response_Json("{\"status\":\"ok\",\"framework\":\"Boko\",\"version\":\"0.1.0\"}");
|
return Response_Json("{\"status\":\"ok\",\"framework\":\"Boko\",\"version\":\"0.2.0\"}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- GET /api/info ---
|
// --- GET /api/info ---
|
||||||
@@ -41,43 +38,38 @@ func Boko_Router(req: Request) -> Response {
|
|||||||
|
|
||||||
// --- GET /hello?name=World ---
|
// --- GET /hello?name=World ---
|
||||||
if String_Eq(req.path, "/hello") {
|
if String_Eq(req.path, "/hello") {
|
||||||
let name: String = Request_GetQuery(&req, "name");
|
var name: String = req.GetQuery("name");
|
||||||
if bux_strlen(name) == 0 { name = "World"; }
|
if String_Len(name) == 0 { name = "World"; }
|
||||||
let sb: *void = bux_sb_new(256);
|
let html: String = f"<h1>Hello, {name}!</h1>";
|
||||||
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);
|
return Response_Html(html);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- GET /users/{id} ---
|
// --- GET /users/{id} ---
|
||||||
if Path_Match("/users/{id}", req.path, &req) {
|
if Path_Match("/users/{id}", req.path, &req) {
|
||||||
let id: String = Request_GetPathParam(&req, "id");
|
let id: String = req.GetPathParam("id");
|
||||||
let sb: *void = bux_sb_new(128);
|
let sb: StringBuilder = StringBuilder_New();
|
||||||
bux_sb_append(sb, "{\"id\":");
|
StringBuilder_Append(&sb, "{\"id\":");
|
||||||
bux_sb_append(sb, id);
|
StringBuilder_Append(&sb, id);
|
||||||
bux_sb_append(sb, ",\"name\":\"User ");
|
StringBuilder_Append(&sb, ",\"name\":\"User ");
|
||||||
bux_sb_append(sb, id);
|
StringBuilder_Append(&sb, id);
|
||||||
bux_sb_append(sb, "\"}");
|
StringBuilder_Append(&sb, "\"}");
|
||||||
let json: String = bux_sb_build(sb);
|
let json: String = StringBuilder_Build(&sb);
|
||||||
bux_sb_free(sb);
|
StringBuilder_Free(&sb);
|
||||||
return Response_Json(json);
|
return Response_Json(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- GET /posts/{postId}/comments/{commentId} ---
|
// --- GET /posts/{postId}/comments/{commentId} ---
|
||||||
if Path_Match("/posts/{postId}/comments/{commentId}", req.path, &req) {
|
if Path_Match("/posts/{postId}/comments/{commentId}", req.path, &req) {
|
||||||
let pid: String = Request_GetPathParam(&req, "postId");
|
let pid: String = req.GetPathParam("postId");
|
||||||
let cid: String = Request_GetPathParam(&req, "commentId");
|
let cid: String = req.GetPathParam("commentId");
|
||||||
let sb: *void = bux_sb_new(128);
|
let sb: StringBuilder = StringBuilder_New();
|
||||||
bux_sb_append(sb, "{\"postId\":");
|
StringBuilder_Append(&sb, "{\"postId\":");
|
||||||
bux_sb_append(sb, pid);
|
StringBuilder_Append(&sb, pid);
|
||||||
bux_sb_append(sb, ",\"commentId\":");
|
StringBuilder_Append(&sb, ",\"commentId\":");
|
||||||
bux_sb_append(sb, cid);
|
StringBuilder_Append(&sb, cid);
|
||||||
bux_sb_append(sb, ",\"text\":\"Great post!\"}");
|
StringBuilder_Append(&sb, ",\"text\":\"Great post!\"}");
|
||||||
let json: String = bux_sb_build(sb);
|
let json: String = StringBuilder_Build(&sb);
|
||||||
bux_sb_free(sb);
|
StringBuilder_Free(&sb);
|
||||||
return Response_Json(json);
|
return Response_Json(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,12 +80,12 @@ func Boko_Router(req: Request) -> Response {
|
|||||||
|
|
||||||
// --- POST /api/echo ---
|
// --- POST /api/echo ---
|
||||||
if String_Eq(req.path, "/api/echo") && req.method.tag == HttpVerb_POST {
|
if String_Eq(req.path, "/api/echo") && req.method.tag == HttpVerb_POST {
|
||||||
let sb: *void = bux_sb_new(256);
|
let sb: StringBuilder = StringBuilder_New();
|
||||||
bux_sb_append(sb, "{\"echo\":");
|
StringBuilder_Append(&sb, "{\"echo\":");
|
||||||
bux_sb_append(sb, req.body);
|
StringBuilder_Append(&sb, req.body);
|
||||||
bux_sb_append(sb, "}");
|
StringBuilder_Append(&sb, "}");
|
||||||
let json: String = bux_sb_build(sb);
|
let json: String = StringBuilder_Build(&sb);
|
||||||
bux_sb_free(sb);
|
StringBuilder_Free(&sb);
|
||||||
return Response_Json(json);
|
return Response_Json(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,14 @@
|
|||||||
|
|
||||||
A standalone command-line utility for working with JSON Web Tokens (RFC 7519). Built on `Std::Crypto`, backed by OpenSSL.
|
A standalone command-line utility for working with JSON Web Tokens (RFC 7519). Built on `Std::Crypto`, backed by OpenSSL.
|
||||||
|
|
||||||
|
This version (0.2.0) is rewritten with modern Bux: algebraic enums, generic `Array<String>`, methods on `JwtAlg`, string interpolation, `for` loops, and structured `Result`/`Option`-style error handling.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd apps/jwt-pitbul
|
cd apps/jwt-pitbul
|
||||||
../../buxc build
|
../../buxc build
|
||||||
./jwt-pitbul
|
./build/jwt-pitbul
|
||||||
```
|
```
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[Package]
|
[Package]
|
||||||
Name = "jwt-pitbul"
|
Name = "jwt-pitbul"
|
||||||
Version = "0.1.0"
|
Version = "0.2.0"
|
||||||
Type = "bin"
|
Type = "bin"
|
||||||
Description = "JWT CLI tool — encode, decode, verify, and key generation"
|
Description = "JWT CLI tool — encode, decode, verify, and key generation"
|
||||||
|
|
||||||
|
|||||||
+303
-158
@@ -1,11 +1,17 @@
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
// jwt-pitbul — JWT CLI Tool (encode, decode, verify, keygen)
|
// jwt-pitbul — JWT CLI Tool (encode, decode, verify, keygen)
|
||||||
// Built with Bux + Std::Crypto
|
// Rewritten with modern Bux: algebraic enums, methods, string interpolation,
|
||||||
|
// for-in loops, generic arrays, and structured error handling.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
module Main {
|
module Main {
|
||||||
|
|
||||||
import Std::Io::{PrintLine, Print, PrintInt};
|
import Std::Io::{PrintLine, Print, PrintInt};
|
||||||
import Std::String::{String_Len, String_Eq, String_Contains};
|
import Std::String::{
|
||||||
|
String_Len, String_Eq,
|
||||||
|
String_SplitCount, String_SplitPart,
|
||||||
|
StringBuilder, StringBuilder_New, StringBuilder_Append,
|
||||||
|
StringBuilder_AppendInt, StringBuilder_Build, StringBuilder_Free
|
||||||
|
};
|
||||||
import Std::Crypto::Jwt::{
|
import Std::Crypto::Jwt::{
|
||||||
JwtAlg,
|
JwtAlg,
|
||||||
Jwt_MakeHeader,
|
Jwt_MakeHeader,
|
||||||
@@ -22,24 +28,191 @@ extern func bux_argv(index: int) -> String;
|
|||||||
extern func bux_alloc(size: uint) -> *void;
|
extern func bux_alloc(size: uint) -> *void;
|
||||||
extern func bux_read_file(path: String) -> String;
|
extern func bux_read_file(path: String) -> String;
|
||||||
extern func bux_file_exists(path: String) -> int;
|
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;
|
extern func bux_base64_encode(data: String, len: int) -> String;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Constants
|
||||||
|
// =============================================================================
|
||||||
|
const AppName: String = "jwt-pitbul";
|
||||||
|
const Version: String = "0.2.0";
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Algebraic enums for optionals and results
|
||||||
|
// =============================================================================
|
||||||
|
enum AlgOption {
|
||||||
|
Some(JwtAlg),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
func AlgOption_MakeSome(value: JwtAlg) -> AlgOption {
|
||||||
|
let o: AlgOption = AlgOption { tag: AlgOption_Some };
|
||||||
|
o.data.Some_0 = value;
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
func AlgOption_MakeNone() -> AlgOption {
|
||||||
|
return AlgOption { tag: AlgOption_None };
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CmdOption {
|
||||||
|
Some(Cmd),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
func CmdOption_MakeSome(value: Cmd) -> CmdOption {
|
||||||
|
let o: CmdOption = CmdOption { tag: CmdOption_Some };
|
||||||
|
o.data.Some_0 = value;
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
func CmdOption_MakeNone() -> CmdOption {
|
||||||
|
return CmdOption { tag: CmdOption_None };
|
||||||
|
}
|
||||||
|
|
||||||
|
enum KeyTypeOption {
|
||||||
|
Some(KeyType),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
func KeyTypeOption_MakeSome(value: KeyType) -> KeyTypeOption {
|
||||||
|
let o: KeyTypeOption = KeyTypeOption { tag: KeyTypeOption_Some };
|
||||||
|
o.data.Some_0 = value;
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
func KeyTypeOption_MakeNone() -> KeyTypeOption {
|
||||||
|
return KeyTypeOption { tag: KeyTypeOption_None };
|
||||||
|
}
|
||||||
|
|
||||||
|
enum KeyResult {
|
||||||
|
Ok(String),
|
||||||
|
Err(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
func KeyResult_MakeOk(value: String) -> KeyResult {
|
||||||
|
let r: KeyResult = KeyResult { tag: KeyResult_Ok };
|
||||||
|
r.data.Ok_0 = value;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
func KeyResult_MakeErr(msg: String) -> KeyResult {
|
||||||
|
let r: KeyResult = KeyResult { tag: KeyResult_Err };
|
||||||
|
r.data.Err_0 = msg;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ExitResult {
|
||||||
|
Ok(int),
|
||||||
|
Err(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExitResult_MakeOk(value: int) -> ExitResult {
|
||||||
|
let r: ExitResult = ExitResult { tag: ExitResult_Ok };
|
||||||
|
r.data.Ok_0 = value;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExitResult_MakeErr(msg: String) -> ExitResult {
|
||||||
|
let r: ExitResult = ExitResult { tag: ExitResult_Err };
|
||||||
|
r.data.Err_0 = msg;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Commands
|
||||||
|
// =============================================================================
|
||||||
|
enum Cmd {
|
||||||
|
Sign,
|
||||||
|
Verify,
|
||||||
|
Decode,
|
||||||
|
Keygen,
|
||||||
|
Help,
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseCmd(name: String) -> CmdOption {
|
||||||
|
if String_Eq(name, "sign") { return CmdOption_MakeSome(Cmd { tag: Cmd_Sign }); }
|
||||||
|
if String_Eq(name, "verify") { return CmdOption_MakeSome(Cmd { tag: Cmd_Verify }); }
|
||||||
|
if String_Eq(name, "decode") { return CmdOption_MakeSome(Cmd { tag: Cmd_Decode }); }
|
||||||
|
if String_Eq(name, "keygen") { return CmdOption_MakeSome(Cmd { tag: Cmd_Keygen }); }
|
||||||
|
if String_Eq(name, "help") || String_Eq(name, "--help") || String_Eq(name, "-h") {
|
||||||
|
return CmdOption_MakeSome(Cmd { tag: Cmd_Help });
|
||||||
|
}
|
||||||
|
return CmdOption_MakeNone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Key type generation
|
||||||
|
// =============================================================================
|
||||||
|
enum KeyType {
|
||||||
|
Rsa,
|
||||||
|
Ecdsa,
|
||||||
|
Ed25519,
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseKeyType(name: String) -> KeyTypeOption {
|
||||||
|
if String_Eq(name, "rsa") { return KeyTypeOption_MakeSome(KeyType { tag: KeyType_Rsa }); }
|
||||||
|
if String_Eq(name, "ecdsa") { return KeyTypeOption_MakeSome(KeyType { tag: KeyType_Ecdsa }); }
|
||||||
|
if String_Eq(name, "ed25519") { return KeyTypeOption_MakeSome(KeyType { tag: KeyType_Ed25519 }); }
|
||||||
|
return KeyTypeOption_MakeNone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Extend JwtAlg with parsing / introspection methods
|
||||||
|
// =============================================================================
|
||||||
|
extend JwtAlg {
|
||||||
|
func Name(self: JwtAlg) -> String {
|
||||||
|
match self {
|
||||||
|
JwtAlg::HS256 => "HS256",
|
||||||
|
JwtAlg::HS384 => "HS384",
|
||||||
|
JwtAlg::HS512 => "HS512",
|
||||||
|
JwtAlg::RS256 => "RS256",
|
||||||
|
JwtAlg::RS384 => "RS384",
|
||||||
|
JwtAlg::RS512 => "RS512",
|
||||||
|
JwtAlg::ES256 => "ES256",
|
||||||
|
JwtAlg::ES384 => "ES384",
|
||||||
|
JwtAlg::EdDSA => "EdDSA",
|
||||||
|
_ => "HS256",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsHmac(self: JwtAlg) -> bool {
|
||||||
|
return self.tag == JwtAlg_HS256 || self.tag == JwtAlg_HS384 || self.tag == JwtAlg_HS512;
|
||||||
|
}
|
||||||
|
|
||||||
|
func NeedsPemFile(self: JwtAlg) -> bool {
|
||||||
|
return self.tag == JwtAlg_RS256 || self.tag == JwtAlg_RS384 || self.tag == JwtAlg_RS512 ||
|
||||||
|
self.tag == JwtAlg_ES256 || self.tag == JwtAlg_ES384;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseAlg(name: String) -> AlgOption {
|
||||||
|
if String_Eq(name, "HS256") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_HS256 }); }
|
||||||
|
if String_Eq(name, "HS384") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_HS384 }); }
|
||||||
|
if String_Eq(name, "HS512") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_HS512 }); }
|
||||||
|
if String_Eq(name, "RS256") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_RS256 }); }
|
||||||
|
if String_Eq(name, "RS384") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_RS384 }); }
|
||||||
|
if String_Eq(name, "RS512") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_RS512 }); }
|
||||||
|
if String_Eq(name, "ES256") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_ES256 }); }
|
||||||
|
if String_Eq(name, "ES384") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_ES384 }); }
|
||||||
|
if String_Eq(name, "EdDSA") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_EdDSA }); }
|
||||||
|
return AlgOption_MakeNone();
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Help / Usage
|
// Help / Usage
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
func PrintUsage() {
|
func PrintUsage() {
|
||||||
PrintLine("╔══════════════════════════════════════════════════════╗");
|
PrintLine("╔══════════════════════════════════════════════════════╗");
|
||||||
PrintLine("║ jwt-pitbul — JWT CLI Tool v0.1.0 ║");
|
PrintLine(f"║ {AppName} — JWT CLI Tool v{Version} ║");
|
||||||
PrintLine("║ Sign, verify, decode JSON Web Tokens ║");
|
PrintLine("║ Sign, verify, decode JSON Web Tokens ║");
|
||||||
PrintLine("╚══════════════════════════════════════════════════════╝");
|
PrintLine("╚══════════════════════════════════════════════════════╝");
|
||||||
PrintLine("");
|
PrintLine("");
|
||||||
PrintLine("Usage:");
|
PrintLine("Usage:");
|
||||||
PrintLine(" jwt-pitbul sign <alg> <key> <claims>");
|
PrintLine(f" {AppName} sign <alg> <key> <claims>");
|
||||||
PrintLine(" jwt-pitbul verify <token> <alg> <key>");
|
PrintLine(f" {AppName} verify <token> <alg> <key>");
|
||||||
PrintLine(" jwt-pitbul decode <token>");
|
PrintLine(f" {AppName} decode <token>");
|
||||||
PrintLine(" jwt-pitbul keygen <type>");
|
PrintLine(f" {AppName} keygen <type>");
|
||||||
PrintLine("");
|
PrintLine("");
|
||||||
PrintLine("Commands:");
|
PrintLine("Commands:");
|
||||||
PrintLine(" sign Create a signed JWT from claims JSON");
|
PrintLine(" sign Create a signed JWT from claims JSON");
|
||||||
@@ -60,103 +233,87 @@ func PrintUsage() {
|
|||||||
PrintLine(" EdDSA: base64-encoded 32-byte raw key");
|
PrintLine(" EdDSA: base64-encoded 32-byte raw key");
|
||||||
PrintLine("");
|
PrintLine("");
|
||||||
PrintLine("Key generation:");
|
PrintLine("Key generation:");
|
||||||
PrintLine(" jwt-pitbul keygen rsa # RSA 2048-bit (PEM)");
|
PrintLine(f" {AppName} keygen rsa # RSA 2048-bit (PEM)");
|
||||||
PrintLine(" jwt-pitbul keygen ecdsa # ECDSA P-256 (PEM)");
|
PrintLine(f" {AppName} keygen ecdsa # ECDSA P-256 (PEM)");
|
||||||
PrintLine(" jwt-pitbul keygen ed25519 # Ed25519 (raw base64)");
|
PrintLine(f" {AppName} keygen ed25519 # Ed25519 (raw base64)");
|
||||||
PrintLine("");
|
PrintLine("");
|
||||||
PrintLine("Examples:");
|
PrintLine("Examples:");
|
||||||
PrintLine(" jwt-pitbul sign HS256 'my-secret' '{\"sub\":\"123\"}'");
|
Print(f" {AppName} sign HS256 'my-secret' ");
|
||||||
PrintLine(" jwt-pitbul verify eyJh... HS256 'my-secret'");
|
PrintLine("'{\"sub\":\"123\"}'");
|
||||||
PrintLine(" jwt-pitbul decode eyJh...");
|
PrintLine(f" {AppName} verify eyJh... HS256 'my-secret'");
|
||||||
PrintLine(" jwt-pitbul keygen ed25519");
|
PrintLine(f" {AppName} decode eyJh...");
|
||||||
|
PrintLine(f" {AppName} keygen ed25519");
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Algorithm name → JwtAlg enum
|
// Key resolution — for RSA/ECDSA, read PEM file; for HMAC/EdDSA, pass through
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
func ParseAlg(name: String) -> JwtAlg {
|
func ResolveKey(alg: JwtAlg, keyArg: String) -> KeyResult {
|
||||||
if String_Eq(name, "HS256") { return JwtAlg { tag: JwtAlg_HS256 }; }
|
if !alg.NeedsPemFile() {
|
||||||
if String_Eq(name, "HS384") { return JwtAlg { tag: JwtAlg_HS384 }; }
|
return KeyResult_MakeOk(keyArg);
|
||||||
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 bux_file_exists(keyArg) == 0 {
|
||||||
if String_Eq(name, "RS512") { return JwtAlg { tag: JwtAlg_RS512 }; }
|
return KeyResult_MakeErr(f"ERROR: PEM file not found: {keyArg}");
|
||||||
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 {
|
|
||||||
// RSA and ECDSA: key is a path to PEM file
|
|
||||||
if alg.tag == JwtAlg_RS256 || alg.tag == JwtAlg_RS384 || alg.tag == JwtAlg_RS512 ||
|
|
||||||
alg.tag == JwtAlg_ES256 || alg.tag == JwtAlg_ES384 {
|
|
||||||
if bux_file_exists(keyArg) != 0 {
|
|
||||||
let pem: String = bux_read_file(keyArg);
|
let pem: String = bux_read_file(keyArg);
|
||||||
if String_Len(pem) > 0 && String_Len(pem) > 0 {
|
if String_Len(pem) == 0 {
|
||||||
return pem;
|
return KeyResult_MakeErr(f"ERROR: could not read PEM file: {keyArg}");
|
||||||
}
|
}
|
||||||
Print("ERROR: could not read PEM file: ");
|
return KeyResult_MakeOk(pem);
|
||||||
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 {
|
|
||||||
return alg.tag == JwtAlg_HS256 || alg.tag == JwtAlg_HS384 || alg.tag == JwtAlg_HS512;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Command: sign
|
// Command: sign
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
func CmdSign(algName: String, keyArg: String, claimsJson: String) -> int {
|
func CmdSign(algName: String, keyArg: String, claimsJson: String) -> ExitResult {
|
||||||
let alg: JwtAlg = ParseAlg(algName);
|
let algOpt: AlgOption = ParseAlg(algName);
|
||||||
let key: String = ResolveKey(alg, keyArg);
|
if algOpt.tag != AlgOption_Some {
|
||||||
if String_Len(key) == 0 {
|
return ExitResult_MakeErr(f"ERROR: unknown algorithm '{algName}'");
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
let alg: JwtAlg = algOpt.data.Some_0;
|
||||||
|
|
||||||
|
let keyRes: KeyResult = ResolveKey(alg, keyArg);
|
||||||
|
if keyRes.tag != KeyResult_Ok {
|
||||||
|
return ExitResult_MakeErr(keyRes.data.Err_0);
|
||||||
|
}
|
||||||
|
let key: String = keyRes.data.Ok_0;
|
||||||
|
|
||||||
let header: String = Jwt_MakeHeader(alg);
|
let header: String = Jwt_MakeHeader(alg);
|
||||||
let token: String = Jwt_Encode(header, claimsJson, alg, key);
|
let token: String = Jwt_Encode(header, claimsJson, alg, key);
|
||||||
|
|
||||||
if String_Len(token) == 0 {
|
if String_Len(token) == 0 {
|
||||||
PrintLine("ERROR: signing failed");
|
return ExitResult_MakeErr("ERROR: signing failed");
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PrintLine(token);
|
PrintLine(token);
|
||||||
return 0;
|
return ExitResult_MakeOk(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Command: verify
|
// Command: verify
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
func CmdVerify(token: String, algName: String, keyArg: String) -> int {
|
func CmdVerify(token: String, algName: String, keyArg: String) -> ExitResult {
|
||||||
let alg: JwtAlg = ParseAlg(algName);
|
let algOpt: AlgOption = ParseAlg(algName);
|
||||||
let key: String = ResolveKey(alg, keyArg);
|
if algOpt.tag != AlgOption_Some {
|
||||||
if String_Len(key) == 0 {
|
return ExitResult_MakeErr(f"ERROR: unknown algorithm '{algName}'");
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
let alg: JwtAlg = algOpt.data.Some_0;
|
||||||
|
|
||||||
|
let keyRes: KeyResult = ResolveKey(alg, keyArg);
|
||||||
|
if keyRes.tag != KeyResult_Ok {
|
||||||
|
return ExitResult_MakeErr(keyRes.data.Err_0);
|
||||||
|
}
|
||||||
|
let key: String = keyRes.data.Ok_0;
|
||||||
|
|
||||||
var header: String = "";
|
var header: String = "";
|
||||||
var payload: String = "";
|
var payload: String = "";
|
||||||
|
|
||||||
let ok: bool = Jwt_Decode(token, alg, key, &header, &payload);
|
if !Jwt_Decode(token, alg, key, &header, &payload) {
|
||||||
|
return ExitResult_MakeErr("✗ Signature INVALID (or malformed token)");
|
||||||
|
}
|
||||||
|
|
||||||
if ok {
|
|
||||||
PrintLine("✓ Signature valid");
|
PrintLine("✓ Signature valid");
|
||||||
PrintLine("");
|
PrintLine("");
|
||||||
PrintLine("Header:");
|
PrintLine("Header:");
|
||||||
@@ -164,26 +321,21 @@ func CmdVerify(token: String, algName: String, keyArg: String) -> int {
|
|||||||
PrintLine("");
|
PrintLine("");
|
||||||
PrintLine("Payload:");
|
PrintLine("Payload:");
|
||||||
PrintLine(payload);
|
PrintLine(payload);
|
||||||
return 0;
|
return ExitResult_MakeOk(0);
|
||||||
}
|
|
||||||
|
|
||||||
PrintLine("✗ Signature INVALID (or malformed token)");
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Command: decode (no verification)
|
// Command: decode (no verification)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
func CmdDecode(token: String) -> int {
|
func CmdDecode(token: String) -> ExitResult {
|
||||||
let partCount: uint = bux_str_split_count(token, ".");
|
let partCount: uint = String_SplitCount(token, ".");
|
||||||
if partCount != 3 {
|
if partCount != 3 {
|
||||||
PrintLine("ERROR: not a valid JWT (expected 3 parts)");
|
return ExitResult_MakeErr("ERROR: not a valid JWT (expected 3 parts)");
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let headerB64: String = bux_str_split_part(token, ".", 0);
|
let headerB64: String = String_SplitPart(token, ".", 0);
|
||||||
let payloadB64: String = bux_str_split_part(token, ".", 1);
|
let payloadB64: String = String_SplitPart(token, ".", 1);
|
||||||
let sigB64: String = bux_str_split_part(token, ".", 2);
|
let sigB64: String = String_SplitPart(token, ".", 2);
|
||||||
|
|
||||||
let headerJson: String = Base64URL_Decode(headerB64);
|
let headerJson: String = Base64URL_Decode(headerB64);
|
||||||
let payloadJson: String = Base64URL_Decode(payloadB64);
|
let payloadJson: String = Base64URL_Decode(payloadB64);
|
||||||
@@ -199,52 +351,84 @@ func CmdDecode(token: String) -> int {
|
|||||||
Print("Signature (base64url): ");
|
Print("Signature (base64url): ");
|
||||||
PrintLine(sigB64);
|
PrintLine(sigB64);
|
||||||
|
|
||||||
return 0;
|
return ExitResult_MakeOk(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Command: keygen
|
// Command: keygen
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
func CmdKeygen(keyType: String) -> int {
|
func CmdKeygen(keyType: String) -> ExitResult {
|
||||||
if String_Eq(keyType, "ed25519") {
|
let ktOpt: KeyTypeOption = ParseKeyType(keyType);
|
||||||
|
if ktOpt.tag != KeyTypeOption_Some {
|
||||||
|
return ExitResult_MakeErr(f"ERROR: unknown key type '{keyType}'. Use: rsa, ecdsa, ed25519");
|
||||||
|
}
|
||||||
|
let kt: KeyType = ktOpt.data.Some_0;
|
||||||
|
|
||||||
|
if kt.tag == KeyType_Ed25519 {
|
||||||
let pubBuf: *void = bux_alloc(32);
|
let pubBuf: *void = bux_alloc(32);
|
||||||
let priv: *void = bux_alloc(32);
|
let priv: *void = bux_alloc(32);
|
||||||
if !Ed25519_Keypair(pubBuf, priv) {
|
if !Ed25519_Keypair(pubBuf, priv) {
|
||||||
PrintLine("ERROR: Ed25519 key generation failed (OpenSSL 1.1.1+ required)");
|
return ExitResult_MakeErr("ERROR: Ed25519 key generation failed (OpenSSL 1.1.1+ required)");
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
let pubB64: String = bux_base64_encode(pubBuf as String, 32);
|
let pubB64: String = bux_base64_encode(pubBuf as String, 32);
|
||||||
let privB64: String = bux_base64_encode(priv as String, 32);
|
let privB64: String = bux_base64_encode(priv as String, 32);
|
||||||
PrintLine("Ed25519 keypair (base64):");
|
PrintLine("Ed25519 keypair (base64):");
|
||||||
Print(" Public: ");
|
PrintLine(f" Public: {pubB64}");
|
||||||
PrintLine(pubB64);
|
PrintLine(f" Private: {privB64}");
|
||||||
Print(" Private: ");
|
return ExitResult_MakeOk(0);
|
||||||
PrintLine(privB64);
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
if kt.tag == KeyType_Rsa {
|
||||||
if String_Eq(keyType, "rsa") {
|
|
||||||
PrintLine("RSA key generation requires OpenSSL CLI:");
|
PrintLine("RSA key generation requires OpenSSL CLI:");
|
||||||
PrintLine(" openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048");
|
PrintLine(" openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048");
|
||||||
PrintLine(" openssl rsa -in private.pem -pubout -out public.pem");
|
PrintLine(" openssl rsa -in private.pem -pubout -out public.pem");
|
||||||
PrintLine("");
|
PrintLine("");
|
||||||
PrintLine("(Key generation from within Bux requires PEM write support — coming soon)");
|
PrintLine("(Key generation from within Bux requires PEM write support — coming soon)");
|
||||||
return 0;
|
return ExitResult_MakeOk(0);
|
||||||
}
|
}
|
||||||
|
if kt.tag == KeyType_Ecdsa {
|
||||||
if String_Eq(keyType, "ecdsa") {
|
|
||||||
PrintLine("ECDSA key generation requires OpenSSL CLI:");
|
PrintLine("ECDSA key generation requires OpenSSL CLI:");
|
||||||
PrintLine(" openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem");
|
PrintLine(" openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem");
|
||||||
PrintLine(" openssl ec -in ec_private.pem -pubout -out ec_public.pem");
|
PrintLine(" openssl ec -in ec_private.pem -pubout -out ec_public.pem");
|
||||||
PrintLine("");
|
PrintLine("");
|
||||||
PrintLine("(Key generation from within Bux requires PEM write support — coming soon)");
|
PrintLine("(Key generation from within Bux requires PEM write support — coming soon)");
|
||||||
return 0;
|
return ExitResult_MakeOk(0);
|
||||||
|
}
|
||||||
|
return ExitResult_MakeErr(f"ERROR: unknown key type '{keyType}'");
|
||||||
}
|
}
|
||||||
|
|
||||||
Print("ERROR: unknown key type '");
|
// =============================================================================
|
||||||
Print(keyType);
|
// Dispatch a parsed command to its handler
|
||||||
PrintLine("'. Use: rsa, ecdsa, ed25519");
|
// =============================================================================
|
||||||
return 1;
|
func RunCommand(cmd: Cmd, args: Array<String>) -> ExitResult {
|
||||||
|
if cmd.tag == Cmd_Help {
|
||||||
|
PrintUsage();
|
||||||
|
return ExitResult_MakeOk(0);
|
||||||
|
}
|
||||||
|
if cmd.tag == Cmd_Sign {
|
||||||
|
if args.len < 5 {
|
||||||
|
return ExitResult_MakeErr("ERROR: 'sign' requires: <alg> <key> <claims_json>");
|
||||||
|
}
|
||||||
|
return CmdSign(args[2], args[3], args[4]);
|
||||||
|
}
|
||||||
|
if cmd.tag == Cmd_Verify {
|
||||||
|
if args.len < 5 {
|
||||||
|
return ExitResult_MakeErr("ERROR: 'verify' requires: <token> <alg> <key>");
|
||||||
|
}
|
||||||
|
return CmdVerify(args[2], args[3], args[4]);
|
||||||
|
}
|
||||||
|
if cmd.tag == Cmd_Decode {
|
||||||
|
if args.len < 3 {
|
||||||
|
return ExitResult_MakeErr("ERROR: 'decode' requires: <token>");
|
||||||
|
}
|
||||||
|
return CmdDecode(args[2]);
|
||||||
|
}
|
||||||
|
if cmd.tag == Cmd_Keygen {
|
||||||
|
if args.len < 3 {
|
||||||
|
return ExitResult_MakeErr("ERROR: 'keygen' requires: <type>");
|
||||||
|
}
|
||||||
|
return CmdKeygen(args[2]);
|
||||||
|
}
|
||||||
|
return ExitResult_MakeErr("ERROR: unhandled command");
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -253,71 +437,32 @@ func CmdKeygen(keyType: String) -> int {
|
|||||||
func Main() -> int {
|
func Main() -> int {
|
||||||
let argc: int = bux_argc();
|
let argc: int = bux_argc();
|
||||||
|
|
||||||
if argc < 2 {
|
// Collect CLI arguments into a generic Array<String>
|
||||||
PrintUsage();
|
let args: Array<String> = Array_New<String>(argc as uint);
|
||||||
return 0;
|
for i in 0..argc {
|
||||||
|
Array_Push<String>(&args, bux_argv(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect arguments
|
if args.len < 2 {
|
||||||
let args: *String = bux_alloc(argc as uint * 8) as *String;
|
PrintUsage();
|
||||||
var i: int = 0;
|
return 0;
|
||||||
while i < argc {
|
|
||||||
args[i] = bux_argv(i);
|
|
||||||
i = i + 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let command: String = args[1];
|
let command: String = args[1];
|
||||||
|
let cmdOpt: CmdOption = ParseCmd(command);
|
||||||
if String_Eq(command, "help") || String_Eq(command, "--help") || String_Eq(command, "-h") {
|
if cmdOpt.tag != CmdOption_Some {
|
||||||
PrintUsage();
|
PrintLine(f"ERROR: unknown command '{command}'");
|
||||||
return 0;
|
PrintLine(f"Run '{AppName} help' for usage.");
|
||||||
}
|
|
||||||
|
|
||||||
// --- 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 1;
|
||||||
}
|
}
|
||||||
return CmdSign(args[2], args[3], args[4]);
|
let cmd: Cmd = cmdOpt.data.Some_0;
|
||||||
}
|
|
||||||
|
|
||||||
// --- verify <token> <alg> <key> ---
|
let result: ExitResult = RunCommand(cmd, args);
|
||||||
if String_Eq(command, "verify") {
|
if result.tag == ExitResult_Err {
|
||||||
if argc < 5 {
|
PrintLine(result.data.Err_0);
|
||||||
PrintLine("ERROR: 'verify' requires: <token> <alg> <key>");
|
|
||||||
PrintLine(" jwt-pitbul verify eyJh... HS256 'secret'");
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return CmdVerify(args[2], args[3], args[4]);
|
return result.data.Ok_0;
|
||||||
}
|
|
||||||
|
|
||||||
// --- 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
|
} // module Main
|
||||||
|
|||||||
Reference in New Issue
Block a user