Mercurial
changeset 175:71ad34a8bc9a hg-web
[HgWeb] Can stream hg response now. Added react page for hg web since we use json anyway.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Tue, 20 Jan 2026 06:06:47 -0800 |
| parents | 1ba8c1df082c |
| children | fed99fc04e12 |
| files | gui_ze/gui_ze.bzl hg-web/BUILD hg-web/main.c hg-web/src/index.html hg-web/src/main.tsx hg-web/src/repo-browser.tsx seobeo/BUILD seobeo/s_http_client.c seobeo/s_web.c seobeo/seobeo.h seobeo/seobeo_internal.h |
| diffstat | 11 files changed, 569 insertions(+), 79 deletions(-) [+] |
line wrap: on
line diff
--- a/gui_ze/gui_ze.bzl Mon Jan 19 18:59:23 2026 -0800 +++ b/gui_ze/gui_ze.bzl Tue Jan 20 06:06:47 2026 -0800 @@ -176,11 +176,12 @@ command = """ cp -r third_party/bun/** . \ && cp -r {src_folder}/** . \ - && export NODE_PATH=./node_modules && {bun_path} build {input_path} --outfile {output_path} + && export NODE_PATH=./node_modules && {bun_path} build {input_path} --outfile {output_path} --target browser """.format( bun_path = ctx.executable._bun.path, src_folder = ctx.attr.src_folder, - input_path = ctx.file.src.path.split("/")[-1], + # Fix this lol + input_path = "/".join(ctx.file.src.path.split("/")[-2:]), output_path = out.path, ), progress_message = "Bundling {} with Bun!\n\n".format(ctx.file.src.path),
--- a/hg-web/BUILD Mon Jan 19 18:59:23 2026 -0800 +++ b/hg-web/BUILD Tue Jan 20 06:06:47 2026 -0800 @@ -1,19 +1,46 @@ load("@rules_cc//cc:cc_binary.bzl", "cc_binary") -load("//gui_ze:gui_ze.bzl", "move_files_into_dir", "bundle") +load("//gui_ze:gui_ze.bzl", "move_files_into_dir", "bundle", "bun_build") + +filegroup( + name = "raw_file", + srcs = glob(["src/**"]), +) + +filegroup( + name = "src_files", + srcs = [":raw_file", ":compiled_ts"], +) + +filegroup( + name = "all_ts_files", + srcs = glob([ + "**/*.ts", + "**/*.tsx", + "**/*.js", + "**/*.jsx", + ], allow_empty=True) +) + +bun_build( + name = "page", + src = "src/main.tsx", + src_folder = "hg-web", + data = [ + "//third_party/bun:bun_files", + ":all_ts_files", + ], + visibility = ["//visibility:public"], +) move_files_into_dir( name = "compiled_ts", srcs = [ "//markdown_converter:markdown_to_html", + ":page", ], dest = "src", ) -filegroup( - name = "src_files", - srcs = glob(["src/**"]) + [":compiled_ts"], -) - cc_binary( name = "hg_web_server", srcs = ["main.c"], @@ -32,7 +59,7 @@ cc_binary( name = "hg_web_server_debug", srcs = ["main.c"], - deps = ["//seobeo:seobeo_tcp_server_ws_debug"], + deps = ["//seobeo:seobeo"], data = [":src_files"], )
--- a/hg-web/main.c Mon Jan 19 18:59:23 2026 -0800 +++ b/hg-web/main.c Tue Jan 20 06:06:47 2026 -0800 @@ -30,16 +30,16 @@ for (size_t i = 0; i < len; i++) { - if (input_path[i] == '.' && (i == 0 || input_path[i-1] == '/')) { - if (i + 1 < len && input_path[i+1] == '.') { - // Skip ".." - i++; - continue; + if (input_path[i] == '.' && (i == 0 || input_path[i-1] == '/')) { + if (i + 1 < len && input_path[i+1] == '.') { + // Skip ".." + i++; + continue; + } + // Skip "." + continue; } - // Skip "." - continue; - } - result[j++] = input_path[i]; + result[j++] = input_path[i]; } result[j] = '\0'; @@ -55,18 +55,27 @@ Seobeo_Client_Response *hg_proxy_request( const char *method, const char *path, - const char *req_body) + const char *req_body, + const char *hg_custom) { char full_path[MAX_PATH]; snprintf(full_path, MAX_PATH, "http://%s:%s%s", HG_SERVE_HOST, HG_SERVE_PORT, path); + Seobeo_Log(SEOBEO_DEBUG, "HG Proxy PATH %s\n", full_path); Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create(full_path); - Seobeo_Client_Request_Add_Header_Array(p_req, "User-Agent: Seobeo/1.0 (Array Mode)"); + Seobeo_Client_Request_Set_Method(p_req, method); + Seobeo_Client_Request_Add_Header_Array(p_req, "User-Agent: Seobeo/1.0"); Seobeo_Client_Request_Add_Header_Array(p_req, "Accept: application/json"); - Seobeo_Client_Request_Add_Header_Array(p_req, "X-Test-Header: TestValue"); - if (strcmp(method, "POST")) - Seobeo_Client_Request_Set_Method(p_req, "POST"); - Seobeo_Client_Request_Set_Body(p_req, req_body, 0); + if (hg_custom && hg_custom[0] != '\0') + { + char buffer[1024]; + snprintf(buffer, 1024, "x-hgarg-1: %s", hg_custom); + Seobeo_Client_Request_Add_Header_Array(p_req, buffer); + Seobeo_Log(SEOBEO_DEBUG, "HG CUSTOM %s\n", buffer); + } + + if (req_body) + Seobeo_Client_Request_Set_Body(p_req, req_body, strlen(req_body)); Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req); Seobeo_Client_Request_Destroy(p_req); return p_resp; @@ -88,17 +97,14 @@ char hg_path[MAX_PATH]; if (strlen(safe_path) > 0) - snprintf(hg_path, sizeof(hg_path), "/file/tip/%s?style=json", safe_path); + snprintf(hg_path, sizeof(hg_path), "/file/tip/%s?style=json", safe_path); else - snprintf(hg_path, sizeof(hg_path), "/file/tip/?style=json"); + snprintf(hg_path, sizeof(hg_path), "/file/tip/?style=json"); - Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL); + Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL, NULL); Seobeo_Log(SEOBEO_DEBUG, "ApiListDirectory: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length); - char status[4]; - snprintf(status, 3, "%i", hg_response->status_code); - if (hg_response->status_code != 200) { Seobeo_Log(SEOBEO_DEBUG, "Failed to get directory from hg serve\n"); @@ -108,10 +114,14 @@ return resp; } + char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length); + char *temp2 = Dowa_Arena_Allocate(arena, 256); + snprintf(temp2, 256, "%zu", hg_response->body_length); + Dowa_HashMap_Push_Arena(resp, "status", "200", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); - Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); - Seobeo_Client_Response_Destroy(hg_response); + Dowa_HashMap_Push_Arena(resp, "body", temp1, arena); + Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena); return resp; } @@ -137,7 +147,7 @@ char hg_path[MAX_PATH]; snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", safe_path); - Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL); + Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL, NULL); Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length); @@ -160,11 +170,16 @@ Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); return resp; } - + + + char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length); + char *temp2 = Dowa_Arena_Allocate(arena, 256); + snprintf(temp2, 256, "%zu", hg_response->body_length); + Dowa_HashMap_Push_Arena(resp, "status", status, arena); Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); - Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); - Seobeo_Client_Response_Destroy(hg_response); + Dowa_HashMap_Push_Arena(resp, "body", temp1, arena); + Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena); return resp; } @@ -173,6 +188,117 @@ return ApiGetFile(req, arena); } +// Streaming handler for hg wire protocol - pipes data directly without buffering +void StreamHgWireProtocol(Seobeo_Handle *p_client, Seobeo_Request_Entry *req, Dowa_Arena *arena) +{ + void *method_kv = Dowa_HashMap_Get_Ptr(req, "HTTP_Method"); + const char *method = method_kv ? ((Seobeo_Request_Entry*)method_kv)->value : "GET"; + + void *query_kv = Dowa_HashMap_Get_Ptr(req, "QueryString"); + const char *query_string = query_kv ? ((Seobeo_Request_Entry*)query_kv)->value : ""; + + void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); + const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : ""; + + const char *hg_custom = req[7].value; + + Seobeo_Log(SEOBEO_DEBUG, "HG Stream Proxy: method=%s query=%s\n", method, query_string); + + // THINKING: Connect to hg serve + // This kinda blows, but not a good way to handle it since my client API assumes it is all stored in + // buffer and what not. + Seobeo_Handle *p_upstream = Seobeo_Stream_Handle_Client_Create(HG_SERVE_HOST, HG_SERVE_PORT, FALSE); + if (!p_upstream || p_upstream->socket < 0) + { + const char *err_resp = "HTTP/1.1 502 Bad Gateway\r\nContent-Length: 26\r\n\r\nFailed to connect upstream"; + Seobeo_Handle_Queue(p_client, (uint8*)err_resp, strlen(err_resp)); + Seobeo_Handle_Flush(p_client); + if (p_upstream) + Seobeo_Handle_Destroy(p_upstream); + return; + } + + // Create headers + // we only allow x-hgarg-1 and content-length + char request_buf[8192]; + int req_len = snprintf(request_buf, sizeof(request_buf), + "%s /?%s HTTP/1.1\r\n" + "Host: %s:%s\r\n" + "User-Agent: Seobeo/1.0\r\n" + "Connection: close\r\n", + method, query_string, HG_SERVE_HOST, HG_SERVE_PORT); + + if (hg_custom && hg_custom[0] != '\0') + req_len += snprintf(request_buf + req_len, sizeof(request_buf) - req_len, "x-hgarg-1: %s\r\n", hg_custom); + + if (req_body && req_body[0] != '\0') + req_len += snprintf(request_buf + req_len, sizeof(request_buf) - req_len, "Content-Length: %zu\r\n\r\n%s", strlen(req_body), req_body); + else + req_len += snprintf(request_buf + req_len, sizeof(request_buf) - req_len, "\r\n"); + + Seobeo_Handle_Queue(p_upstream, (uint8*)request_buf, req_len); + if (Seobeo_Handle_Flush(p_upstream) < 0) + { + const char *err_resp = "HTTP/1.1 502 Bad Gateway\r\nContent-Length: 21\r\n\r\nUpstream write failed"; + Seobeo_Handle_Queue(p_client, (uint8*)err_resp, strlen(err_resp)); + Seobeo_Handle_Flush(p_client); + Seobeo_Handle_Destroy(p_upstream); + return; + } + + // Responses + while (1) + { + int r = Seobeo_Handle_Read(p_upstream); + if (r < 0) + { + Seobeo_Handle_Destroy(p_upstream); + return; + } + if (p_upstream->read_buffer_len >= 4 && + strstr((char*)p_upstream->read_buffer, "\r\n\r\n") != NULL) + break; + if (r == 0) + continue; + } + + // TODO: Maybe make this into a separate function instead of internal function as doing this over and over again blows. + char *hdr_end = strstr((char*)p_upstream->read_buffer, "\r\n\r\n"); + if (!hdr_end) + { + Seobeo_Handle_Destroy(p_upstream); + return; + } + size_t hdr_len = hdr_end - (char*)p_upstream->read_buffer + 4; + Seobeo_Handle_Queue(p_client, p_upstream->read_buffer, hdr_len); + Seobeo_Handle_Flush(p_client); + + // All body + size_t body_in_buffer = p_upstream->read_buffer_len - hdr_len; + if (body_in_buffer > 0) + { + Seobeo_Handle_Queue(p_client, p_upstream->read_buffer + hdr_len, body_in_buffer); + Seobeo_Handle_Flush(p_client); + } + Seobeo_Handle_Consume(p_upstream, p_upstream->read_buffer_len); + while (1) + { + int n = Seobeo_Handle_Read(p_upstream); + if (n > 0) + { + Seobeo_Handle_Queue(p_client, p_upstream->read_buffer, p_upstream->read_buffer_len); + Seobeo_Handle_Flush(p_client); + Seobeo_Handle_Consume(p_upstream, p_upstream->read_buffer_len); + } + else if (n == -2) + break; + else if (n < 0) + break; + } + + Seobeo_Handle_Destroy(p_upstream); +} + Seobeo_Request_Entry* ApiHgWireProtocol(Seobeo_Request_Entry *req, Dowa_Arena *arena) { Seobeo_Request_Entry *resp = NULL; @@ -187,27 +313,32 @@ const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : ""; size_t body_len = strlen(req_body); + const char *hg_custom = req[7].value; Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: method=%s query=%s body_len=%zu\n", method, query_string, body_len); + Seobeo_Client_Response *hg_response; - if (strlen(query_string) > 0) - { - char temp_path[MAX_PATH]; - snprintf(temp_path, MAX_PATH, "?%s", query_string); - hg_response = hg_proxy_request(method, query_string, req_body); - } - else - hg_response = hg_proxy_request(method, "", req_body); + + char hg_path[MAX_PATH]; + snprintf(hg_path, sizeof(hg_path), "/?%s", query_string); + + hg_response = hg_proxy_request(method, hg_path, req_body, hg_custom); Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", hg_response->body_length); Seobeo_Request_Entry *kv = Dowa_HashMap_Get_Ptr(hg_response->headers, "Content-Type"); - char status[4]; - snprintf(status, 3, "%i", hg_response->status_code); + char *status = Dowa_Arena_Allocate(arena, 5); + snprintf(status, 4, "%i", hg_response->status_code); + + // Use binary-safe copy to handle null bytes in mercurial bundle data + char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length); + char *temp2 = Dowa_Arena_Allocate(arena, 256); + snprintf(temp2, 256, "%zu", hg_response->body_length); Dowa_HashMap_Push_Arena(resp, "status", status, arena); - Dowa_HashMap_Push_Arena(resp, "content-type", kv ? kv->value : "", arena); - Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); + Dowa_HashMap_Push_Arena(resp, "content-type", kv->value, arena); + Dowa_HashMap_Push_Arena(resp, "body", temp1, arena); + Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena); return resp; } @@ -219,12 +350,13 @@ Seobeo_Router_Register("GET", "/api/repo/file", ApiGetFile); Seobeo_Router_Register("GET", "/api/repo/readme", ApiGetReadme); - Seobeo_Router_Register("GET", "/repo", ApiHgWireProtocol); - Seobeo_Router_Register("POST", "/repo", ApiHgWireProtocol); + // Use streaming handler for hg wire protocol... + Seobeo_Router_Register_Stream("GET", "/repo", StreamHgWireProtocol); + Seobeo_Router_Register_Stream("POST", "/repo", StreamHgWireProtocol); printf("Starting on Port 6970...\n"); - int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 4); + int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 1); Seobeo_Router_Destroy();
--- a/hg-web/src/index.html Mon Jan 19 18:59:23 2026 -0800 +++ b/hg-web/src/index.html Tue Jan 20 06:06:47 2026 -0800 @@ -9,14 +9,16 @@ </head> <body> <main> - <div class="header"> + <div id="root"></div> + +<!-- <div class="header"> <h1>Zenbu Repository</h1> <p class="description">Browse and clone this mercurial repository</p> </div> <div class="clone-info"> <strong>Clone this repository:</strong><br> - <code>hg clone http://zenbu.babocoder.com</code> + <code>hg clone http://zenbu.babocoder.com/repo</code> </div> <div class="breadcrumb" id="breadcrumb"></div> @@ -30,10 +32,10 @@ <div class="empty-state" id="emptyState" style="display: none;"> <p>No files found in this directory</p> - </div> + </div> --> </main> <script src="/markdown_to_html.js"></script> - <script src="/index.js"></script> + <script src="/page.js"></script> </body> </html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg-web/src/main.tsx Tue Jan 20 06:06:47 2026 -0800 @@ -0,0 +1,8 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { RepoBrowser } from "./repo-browser"; + +const root = ReactDOM.createRoot(document.getElementById('root')); + +// Use JSX syntax (<RepoBrowser />) +root.render(<RepoBrowser />);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg-web/src/repo-browser.tsx Tue Jan 20 06:06:47 2026 -0800 @@ -0,0 +1,257 @@ +import React, { useState, useEffect } from 'react'; + +const API_BASE = '/api/repo'; + +/** + * Component: Breadcrumb + * Renders the navigation path at the top + */ +function Breadcrumb({ currentPath, onNavigate }) { + if (!currentPath) { + return ( + <nav id="breadcrumb"> + <span className="nav-item active">Root</span> + </nav> + ); + } + + const parts = currentPath.split('/').filter(p => p); + + // Create cumulative paths for links + // e.g., src/components -> ['src', 'src/components'] + const crumbs = parts.map((part, index) => ({ + name: part, + fullPath: parts.slice(0, index + 1).join('/') + })); + + return ( + <nav id="breadcrumb"> + <a + href="/" + onClick={(e) => { e.preventDefault(); onNavigate(''); }} + > + Root + </a> + {crumbs.map((crumb, index) => { + const isLast = index === crumbs.length - 1; + return ( + <React.Fragment key={crumb.fullPath}> + <span className="separator"> / </span> + {isLast ? ( + <span className="nav-item active">{crumb.name}</span> + ) : ( + <a + href={`?path=${encodeURIComponent(crumb.fullPath)}`} + onClick={(e) => { e.preventDefault(); onNavigate(crumb.fullPath); }} + > + {crumb.name} + </a> + )} + </React.Fragment> + ); + })} + </nav> + ); +} + +/** + * Component: FileList + * Renders the table of directories and files + */ +function FileList({ directories, files, onNavigate }) { + const isEmpty = directories.length === 0 && files.length === 0; + + if (isEmpty) { + return <div className="empty-state">No files found.</div>; + } + + return ( + <div id="fileList"> + {/* Render Directories */} + {directories.map((dir) => ( + <FileRow + key={dir.abspath} + item={dir} + icon="📁" + isDir={true} + onNavigate={onNavigate} + /> + ))} + + {/* Render Files */} + {files.map((file) => ( + <FileRow + key={file.abspath} + item={file} + icon="📄" + isDir={false} + /> + ))} + </div> + ); +} + +/** + * Component: FileRow + * Individual item row + */ +function FileRow({ item, icon, isDir, onNavigate }) { + const handleClick = (e) => { + if (isDir) { + e.preventDefault(); + onNavigate(item.abspath); + } + // Files let the default <a> behavior happen (download/open in new tab) + }; + + // Files link to the raw content API, Dirs link to the app view + const href = isDir + ? `?path=${encodeURIComponent(item.abspath)}` + : `/api/repo/file?path=${encodeURIComponent(item.abspath)}`; + + const target = isDir ? undefined : "_blank"; + + return ( + <div className={`file-item ${item.type}`}> + <span className="icon">{icon}</span> + <span className="name"> + <a href={href} onClick={handleClick} target={target} rel="noreferrer"> + {item.basename} + </a> + </span> + </div> + ); +} + +/** + * Component: ReadmeViewer + * Renders the README content + */ +function ReadmeViewer({ content }) { + if (!content) return null; + + return ( + <div id="readmeSection" style={{ marginTop: '20px', borderTop: '1px solid #eee' }}> + <h3>README.md</h3> + <div id="readmeContent"> + <pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}> + {content} + </pre> + </div> + </div> + ); +} + + + +/** + * Main Application Component + */ +function RepoBrowser() { + // State management for path, data, and UI states + const [currentPath, setCurrentPath] = useState(getCurrentPath()); + const [content, setContent] = useState({ files: [], directories: [] }); + const [readme, setReadme] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + // Helper to get path from URL query params + function getCurrentPath() { + const params = new URLSearchParams(window.location.search); + return params.get('path') || ''; + } + + // Effect: Handle Browser Navigation (Back/Forward buttons) + useEffect(() => { + const handlePopState = () => setCurrentPath(getCurrentPath()); + window.addEventListener('popstate', handlePopState); + return () => window.removeEventListener('popstate', handlePopState); + }, []); + + // Effect: Fetch Data whenever currentPath changes + useEffect(() => { + fetchDirectory(currentPath); + fetchReadme(currentPath); + }, [currentPath]); + + // Internal navigation handler (avoids full page reload) + const navigate = (path) => { + const newUrl = path ? `?path=${encodeURIComponent(path)}` : '/'; + window.history.pushState({ path }, '', newUrl); + setCurrentPath(path); + }; + + const fetchDirectory = async (path) => { + setLoading(true); + setError(null); + try { + const url = path + ? `${API_BASE}/list?path=${encodeURIComponent(path)}` + : `${API_BASE}/list`; + + const response = await fetch(url); + const data = await response.json(); + + if (data.error) throw new Error(data.error); + + // Ensure we always have arrays even if API returns null + setContent({ + files: data.files || [], + directories: data.directories || [] + }); + } catch (err) { + console.error('Error loading directory:', err); + setError(err.message); + } finally { + setLoading(false); + } + }; + + const fetchReadme = async (path) => { + setReadme(null); // Reset previous readme + try { + const readmePath = path ? `${path}/README.md` : 'README.md'; + const response = await fetch(`${API_BASE}/readme?path=${encodeURIComponent(readmePath)}`); + + if (response.ok) { + const text = await response.text(); + setReadme(text); + } + } catch (err) { + // Silently fail for Readme as it's optional + } + }; + + return ( + <div className="repo-container"> + <div class="header"> + <h1>Zenbu Repository</h1> + <p class="description">Browse and clone this mercurial repository</p> + </div> + + <div class="clone-info"> + <strong>Clone this repository:</strong> + <p><code>hg clone http://zenbu.babocoder.com/repo</code></p> + </div> + + <Breadcrumb currentPath={currentPath} onNavigate={navigate} /> + + {error && <div className="error-message">Error: {error}</div>} + + {loading ? ( + <div className="loading">Loading...</div> + ) : ( + <> + <FileList + directories={content.directories} + files={content.files} + onNavigate={navigate} + /> + <ReadmeViewer content={readme} /> + </> + )} + </div> + ); +} + +export { RepoBrowser };
--- a/seobeo/BUILD Mon Jan 19 18:59:23 2026 -0800 +++ b/seobeo/BUILD Tue Jan 20 06:06:47 2026 -0800 @@ -308,7 +308,7 @@ "//dowa:dowa", "@openssl//:ssl", ], - defines = ["SEOBEO_WEBSOCKET_SERVER"], + defines = ["SEOBEO_WEBSOCKET_SERVER", "SEOBEO_ENABLE_DEBUG"], target_compatible_with = [ "@platforms//os:osx", ], @@ -334,7 +334,7 @@ "//dowa:dowa", "@openssl//:ssl", ], - defines = ["SEOBEO_WEBSOCKET_SERVER"], + defines = ["SEOBEO_WEBSOCKET_SERVER", "SEOBEO_ENABLE_DEBUG"], target_compatible_with = [ "@platforms//os:linux", ],
--- a/seobeo/s_http_client.c Mon Jan 19 18:59:23 2026 -0800 +++ b/seobeo/s_http_client.c Tue Jan 20 06:06:47 2026 -0800 @@ -69,7 +69,7 @@ memset(p_req, 0, sizeof(Seobeo_Client_Request)); - p_req->p_arena = Dowa_Arena_Create(1024 * 1024); + p_req->p_arena = Dowa_Arena_Create(1024 * 1024 * 5); if (!p_req->p_arena) { free(p_req); @@ -231,7 +231,7 @@ memset(p_resp, 0, sizeof(Seobeo_Client_Response)); - p_resp->p_arena = Dowa_Arena_Create(1024 * 1024 * 5); // 5 MB + p_resp->p_arena = Dowa_Arena_Create(1024 * 1024 * 10); // 10 MB if (!p_resp->p_arena) { free(p_resp); @@ -391,7 +391,7 @@ } else { - size_t cap = 1024 * 1024 * 3; + size_t cap = 1024 * 1024 * 5; size_t used = 0; char *body = download_path ? NULL : Dowa_Arena_Allocate(p_resp->p_arena, cap); @@ -414,7 +414,6 @@ } memcpy(body + used, p_handle->read_buffer, p_handle->read_buffer_len); used += p_handle->read_buffer_len; - Seobeo_Log(SEOBEO_DEBUG, "Copied %zu bytes, total %zu/%zu\n", used, used + p_handle->read_buffer_len, body_len); } Seobeo_Handle_Consume(p_handle, (uint32)p_handle->read_buffer_len); }
--- a/seobeo/s_web.c Mon Jan 19 18:59:23 2026 -0800 +++ b/seobeo/s_web.c Tue Jan 20 06:06:47 2026 -0800 @@ -96,13 +96,14 @@ // Recording IP to see who is ddosing or any web scrappers... void *p_real_ip_kv = Dowa_HashMap_Get_Ptr(p_req_map, "X-Real-IP"); const char *real_ip = p_real_ip_kv ? ((Seobeo_Request_Entry*)p_real_ip_kv)->value : NULL; + // Fallback if (!real_ip) { void *p_forwarded_kv = Dowa_HashMap_Get_Ptr(p_req_map, "X-Forwarded-For"); real_ip = p_forwarded_kv ? ((Seobeo_Request_Entry*)p_forwarded_kv)->value : NULL; } - // Fallback + if (!real_ip) real_ip = p_cli_handle->host; @@ -137,7 +138,7 @@ // --- Check for WebSocket upgrade request --- #ifdef SEOBEO_WEBSOCKET_SERVER - Seobeo_Log(SEOBEO_DEBUG, "Web soceket path \n"); + Seobeo_Log(SEOBEO_DEBUG, "Web socket path \n"); if (Seobeo_WebSocket_Server_Handle_Upgrade(p_cli_handle, p_req_map, path)) { Seobeo_Log(SEOBEO_INFO, "WebSocket connection established\n"); @@ -149,7 +150,15 @@ } #endif - // --- Try to match API route first --- + // --- Try to match streaming route first --- + Seobeo_Stream_Handler stream_handler = Seobeo_Router_Find_Stream_Handler(method, path, &p_req_map, p_request_arena); + if (stream_handler != NULL) + { + stream_handler(p_cli_handle, p_req_map, p_response_arena); + goto clean_up; + } + + // --- Try to match API route --- Seobeo_Route_Handler handler = Seobeo_Router_Find_Handler(method, path, &p_req_map, p_request_arena); if (handler != NULL) { @@ -293,7 +302,10 @@ break; if (r == 0) - return 1; // EAGAIN, try again later TODO: Add this as part of Handle struct. + { + Seobeo_Log(SEOBEO_INFO, "Waiting?\n"); + continue; // EAGAIN, try again later TODO: Add this as part of Handle struct. + } } // "METHOD SP PATH SP VERSION CRLF" @@ -408,7 +420,6 @@ char *next = strstr(line, "\r\n"); if (!next) break; - // split at colon char *colon = memchr(line, ':', next - line); if (colon) { @@ -432,7 +443,6 @@ memcpy(val, val_start, value_len); val[value_len] = '\0'; - // Both key and value are arena-allocated, hashmap will use them Dowa_HashMap_Push_Arena(*pp_map, key, val, p_arena); } @@ -450,7 +460,6 @@ Seobeo_Log(SEOBEO_DEBUG, "Content-Length=%zu, reading body in chunks...\n", body_len); - // Allocate buffer for entire body char *body = Dowa_Arena_Allocate(p_arena, body_len + 1); if (!body) { @@ -567,6 +576,7 @@ char *method; // "GET", "POST", "PUT", "DELETE" char *path_pattern; // "/v1/users/:id/posts/:post_id" Seobeo_Route_Handler handler; + Seobeo_Stream_Handler stream_handler; // For streaming responses // Pre-parsed path segments for efficient matching char **path_segments; // ["v1", "users", ":id", "posts", ":post_id"] @@ -588,6 +598,25 @@ route.method = strdup(method); route.path_pattern = strdup(path_pattern); route.handler = handler; + route.stream_handler = NULL; + route.path_segments = Dowa_String_Split(path_pattern, "/", strlen(path_pattern), 1, NULL); + route.segment_count = Dowa_Array_Length(route.path_segments); + route.is_param = (boolean*)malloc(sizeof(boolean) * route.segment_count); + + for (size_t i = 0; i < route.segment_count; i++) + route.is_param[i] = (route.path_segments[i][0] == ':'); + + Dowa_Array_Push(g_routes, route); +} + +void Seobeo_Router_Register_Stream(const char *method, const char *path_pattern, Seobeo_Stream_Handler handler) +{ + Seobeo_Route route = {0}; + + route.method = strdup(method); + route.path_pattern = strdup(path_pattern); + route.handler = NULL; + route.stream_handler = handler; route.path_segments = Dowa_String_Split(path_pattern, "/", strlen(path_pattern), 1, NULL); route.segment_count = Dowa_Array_Length(route.path_segments); route.is_param = (boolean*)malloc(sizeof(boolean) * route.segment_count); @@ -670,6 +699,29 @@ return NULL; } +Seobeo_Stream_Handler Seobeo_Router_Find_Stream_Handler( + const char *method, + const char *path, + Seobeo_Request_Entry **pp_request_map, + Dowa_Arena *p_arena) +{ + if (g_routes == NULL || method == NULL || path == NULL) + return NULL; + + size_t route_count = Dowa_Array_Length(g_routes); + for (size_t i = 0; i < route_count; i++) + { + Seobeo_Route *route = &g_routes[i]; + if (strcmp(route->method, method) != 0) + continue; + + if (route->stream_handler && match_route_and_extract(route, path, pp_request_map, p_arena)) + return route->stream_handler; + } + + return NULL; +} + void Seobeo_Router_Send_Response( Seobeo_Handle *p_handle, Seobeo_Request_Entry *p_response_map, @@ -696,24 +748,23 @@ const char *body = ""; void *p_body_kv = Dowa_HashMap_Get_Ptr(p_response_map, "body"); if (p_body_kv) - { body = ((Seobeo_Request_Entry*)p_body_kv)->value; - } const char *content_type = "text/html"; void *p_content_type_kv = Dowa_HashMap_Get_Ptr(p_response_map, "content-type"); if (p_content_type_kv) - { content_type = ((Seobeo_Request_Entry*)p_content_type_kv)->value; - } - size_t body_length = strlen(body); + // TODO: Update this to be integer + size_t body_length; void *p_content_length_kv = Dowa_HashMap_Get_Ptr(p_response_map, "content-length"); if (p_content_length_kv) { const char *content_length_str = ((Seobeo_Request_Entry*)p_content_length_kv)->value; body_length = atoi(content_length_str); } + else + body_length = strlen(body); char *header = Dowa_Arena_Allocate(p_arena, 4096); Seobeo_Web_Header_Generate(header, status, content_type, body_length); @@ -734,6 +785,8 @@ free(temp); } + printf("hEADER %s\n", header); + Seobeo_Handle_Queue(p_handle, (uint8_t*)header, strlen(header)); Seobeo_Handle_Queue(p_handle, (uint8_t*)body, body_length); Seobeo_Handle_Flush(p_handle);
--- a/seobeo/seobeo.h Mon Jan 19 18:59:23 2026 -0800 +++ b/seobeo/seobeo.h Tue Jan 20 06:06:47 2026 -0800 @@ -325,16 +325,20 @@ extern void Seobeo_WebSocket_Server_Connection_Close(Seobeo_WebSocket_Server_Connection *p_conn, uint16 code, const char *reason); /* Initialize the router system (called automatically by Seobeo_Web_Server_Start) */ -extern void Seobeo_Router_Init(); +extern void Seobeo_Router_Init(); /* Register an API route handler. Call before starting server. */ -extern void Seobeo_Router_Register(const char *method, const char *path_pattern, Seobeo_Route_Handler handler); +extern void Seobeo_Router_Register(const char *method, const char *path_pattern, Seobeo_Route_Handler handler); +/* Register a streaming route handler. Handler receives client handle for direct streaming. */ +extern void Seobeo_Router_Register_Stream(const char *method, const char *path_pattern, Seobeo_Stream_Handler handler); /* Clean up router resources */ -extern void Seobeo_Router_Destroy(); +extern void Seobeo_Router_Destroy(); /* Find matching route handler (internal use) */ -extern Seobeo_Route_Handler Seobeo_Router_Find_Handler(const char *method, const char *path, Seobeo_Request_Entry **pp_request_map, Dowa_Arena *p_arena); +extern Seobeo_Route_Handler Seobeo_Router_Find_Handler(const char *method, const char *path, Seobeo_Request_Entry **pp_request_map, Dowa_Arena *p_arena); /* Send HTTP response from response map (internal use) */ -extern void Seobeo_Router_Send_Response(Seobeo_Handle *p_handle, Seobeo_Request_Entry *p_response_map, Dowa_Arena *p_arena); -extern char *Seobeo_Web_LoadFile(const char *file_path, size_t *p_file_size); +extern void Seobeo_Router_Send_Response(Seobeo_Handle *p_handle, Seobeo_Request_Entry *p_response_map, Dowa_Arena *p_arena); +extern char *Seobeo_Web_LoadFile(const char *file_path, size_t *p_file_size); +extern Seobeo_Stream_Handler Seobeo_Router_Find_Stream_Handler(const char *method, const char *path, Seobeo_Request_Entry **pp_request_map, Dowa_Arena *p_arena); + // --- Helper functions --- // /* Destroy handle. It will handle all NULL poointers. */
--- a/seobeo/seobeo_internal.h Mon Jan 19 18:59:23 2026 -0800 +++ b/seobeo/seobeo_internal.h Tue Jan 20 06:06:47 2026 -0800 @@ -83,6 +83,13 @@ Dowa_Arena *p_arena ); +// Streaming handler - gets direct access to client handle for streaming responses +typedef void (*Seobeo_Stream_Handler)( + Seobeo_Handle *p_client_handle, + Seobeo_Request_Entry *p_request_map, + Dowa_Arena *p_arena +); + // --- Parse Header into Dowa Map ---// extern int Seobeo_Web_Header_Parse(Seobeo_Handle *p_handle, Seobeo_Request_Entry **pp_map, Dowa_Arena *p_arena);