Mercurial
view hg-web/main.c @ 177:24fe8ff94056
Fixed few issues with current setup.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Wed, 21 Jan 2026 19:40:48 -0800 |
| parents | ffb764d2fcc5 |
| children | 6de849867459 |
line wrap: on
line source
#include "seobeo/seobeo.h" #include "dowa/dowa.h" #include <stdio.h> #include <stdlib.h> #include <string.h> #include <ctype.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #define HG_SERVE_HOST "127.0.0.1" #define HG_SERVE_PORT 4444 #define MAX_PATH 4096 // TODO: Move this to seobeo.... // Asked AI to create this lol, probably should learn to decode it myself.. static void url_decode(char *dst, const char *src) { char a, b; while (*src) { if ((*src == '%') && ((a = src[1]) && (b = src[2])) && (isxdigit(a) && isxdigit(b))) { if (a >= 'a') a -= 'a'-'A'; if (a >= 'A') a -= ('A' - 10); else a -= '0'; if (b >= 'a') b -= 'a'-'A'; if (b >= 'A') b -= ('A' - 10); else b -= '0'; *dst++ = 16*a+b; src+=3; } else if (*src == '+') { *dst++ = ' '; src++; } else { *dst++ = *src++; } } *dst = '\0'; } static char* sanitize_path(const char *input_path, Dowa_Arena *arena) { if (!input_path || strlen(input_path) == 0) { char *empty = Dowa_Arena_Allocate(arena, 1); empty[0] = '\0'; return empty; } size_t len = strlen(input_path); char *result = Dowa_Arena_Allocate(arena, len + 1); size_t j = 0; 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; } // Skip "." continue; } result[j++] = input_path[i]; } result[j] = '\0'; // Remove leading/trailing slashes while (result[0] == '/') memmove(result, result + 1, strlen(result)); while (j > 0 && result[j-1] == '/') result[--j] = '\0'; return result; } // Helper to connect to hg serve static int hg_proxy_connect(void) { int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { Seobeo_Log(SEOBEO_DEBUG, "Failed to create socket\n"); return -1; } struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(HG_SERVE_PORT); inet_pton(AF_INET, HG_SERVE_HOST, &server_addr.sin_addr); if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { Seobeo_Log(SEOBEO_DEBUG, "Failed to connect to hg serve at %s:%d\n", HG_SERVE_HOST, HG_SERVE_PORT); close(sock); return -1; } return sock; } // Generic helper to proxy a request to hg serve and get the response body // Returns allocated body on success, NULL on failure // out_status and out_content_type are optional output parameters // out_body_len returns the actual body length (for binary content) static char* hg_proxy_request( const char *method, const char *path, const char *req_body, size_t body_len, char *out_status, // should be at least 4 bytes char *out_content_type, // should be at least 256 bytes size_t *out_body_len, // optional: returns actual body length Dowa_Arena *arena) { int sock = hg_proxy_connect(); if (sock < 0) return NULL; // Build HTTP request char http_request[MAX_PATH * 2]; snprintf(http_request, sizeof(http_request), "%s %s HTTP/1.1\r\n" "Host: %s:%d\r\n" "Connection: close\r\n" "Accept: application/json, text/plain, */*\r\n" "Content-Length: %zu\r\n" "\r\n", method, path, HG_SERVE_HOST, HG_SERVE_PORT, body_len); Seobeo_Log(SEOBEO_DEBUG, "HG Proxy request: %s %s\n", method, path); if (send(sock, http_request, strlen(http_request), 0) < 0) { close(sock); return NULL; } if (body_len > 0 && req_body) { send(sock, req_body, body_len, 0); } // Read response int buffer_size = 1024 * 1024 * 5; // 5MB char *response_buf = Dowa_Arena_Allocate(arena, buffer_size); size_t total_read = 0; ssize_t bytes_read; while ((bytes_read = recv(sock, response_buf + total_read, buffer_size - total_read - 1, 0)) > 0) { total_read += bytes_read; if (total_read >= (size_t)(buffer_size - 1)) break; } response_buf[total_read] = '\0'; close(sock); Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", total_read); // Parse response headers - use memmem to handle binary content char *headers_end = NULL; for (size_t i = 0; i + 3 < total_read; i++) { if (response_buf[i] == '\r' && response_buf[i+1] == '\n' && response_buf[i+2] == '\r' && response_buf[i+3] == '\n') { headers_end = response_buf + i; break; } } if (!headers_end) return NULL; // Extract status if (out_status && strncmp(response_buf, "HTTP/", 5) == 0) { char *status_start = strchr(response_buf, ' '); if (status_start) { strncpy(out_status, status_start + 1, 3); out_status[3] = '\0'; } } // Extract content-type if (out_content_type) { out_content_type[0] = '\0'; char *ct_header = strcasestr(response_buf, "Content-Type:"); if (ct_header && ct_header < headers_end) { ct_header += 13; while (*ct_header == ' ') ct_header++; char *ct_end = strpbrk(ct_header, "\r\n"); if (ct_end) { size_t ct_len = ct_end - ct_header; if (ct_len < 256) { strncpy(out_content_type, ct_header, ct_len); out_content_type[ct_len] = '\0'; } } } } // Return body (copy to fresh allocation for clean pointer) char *body = headers_end + 4; size_t body_size = total_read - (body - response_buf); if (out_body_len) *out_body_len = body_size; char *result = Dowa_Arena_Allocate(arena, body_size + 1); memcpy(result, body, body_size); result[body_size] = '\0'; return result; } Seobeo_Request_Entry* ApiListDirectory(Seobeo_Request_Entry *req, Dowa_Arena *arena) { Seobeo_Request_Entry *resp = NULL; void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path"); const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : ""; char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1); url_decode(decoded_path, rel_path); char *safe_path = sanitize_path(decoded_path, arena); Seobeo_Log(SEOBEO_INFO, "ApiListDirectory: safe_path='%s'\n", safe_path); char hg_path[MAX_PATH]; if (strlen(safe_path) > 0) snprintf(hg_path, sizeof(hg_path), "/file/tip/%s?style=json", safe_path); else snprintf(hg_path, sizeof(hg_path), "/file/tip/?style=json"); char status[4] = "200"; char content_type[256] = ""; size_t body_len = 0; char *hg_response = hg_proxy_request("GET", hg_path, NULL, 0, status, content_type, &body_len, arena); Seobeo_Log(SEOBEO_DEBUG, "ApiListDirectory: status=%s body_len=%zu\n", status, body_len); if (!hg_response || status[0] != '2') { Seobeo_Log(SEOBEO_DEBUG, "Failed to get directory from hg serve\n"); Dowa_HashMap_Push_Arena(resp, "status", "502", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to connect to hg serve\"}", arena); return resp; } char *json = hg_response; Dowa_HashMap_Push_Arena(resp, "status", "200", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); Dowa_HashMap_Push_Arena(resp, "body", json, arena); return resp; } Seobeo_Request_Entry* ApiGetFile(Seobeo_Request_Entry *req, Dowa_Arena *arena) { Seobeo_Request_Entry *resp = NULL; void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path"); const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : ""; char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1); url_decode(decoded_path, rel_path); char *safe_path = sanitize_path(decoded_path, arena); Seobeo_Log(SEOBEO_INFO, "ApiGetFile: safe_path='%s'\n", safe_path); if (strlen(safe_path) == 0) { Dowa_HashMap_Push_Arena(resp, "status", "400", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); Dowa_HashMap_Push_Arena(resp, "body", "File path required", arena); return resp; } // Build hg serve URL: /raw-file/tip/<path> char hg_path[MAX_PATH]; snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", safe_path); char status[4] = "200"; char content_type[256] = ""; size_t body_len = 0; char *body = hg_proxy_request("GET", hg_path, NULL, 0, status, content_type, &body_len, arena); Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: status=%s body_len=%zu\n", status, body_len); if (!body) { Dowa_HashMap_Push_Arena(resp, "status", "502", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); Dowa_HashMap_Push_Arena(resp, "body", "Failed to connect to hg serve", arena); return resp; } if (status[0] != '2') { Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: error response: %s\n", body); Dowa_HashMap_Push_Arena(resp, "status", status, arena); Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); // Return actual error from hg serve if available Dowa_HashMap_Push_Arena(resp, "body", body_len > 0 ? body : "File not found", arena); return resp; } // Use content-type from hg serve, or determine from extension const char *final_content_type = content_type; if (strlen(content_type) == 0 || strcmp(content_type, "application/octet-stream") == 0) { final_content_type = "text/plain"; if (strstr(safe_path, ".md")) final_content_type = "text/markdown"; else if (strstr(safe_path, ".html")) final_content_type = "text/html"; else if (strstr(safe_path, ".css")) final_content_type = "text/css"; else if (strstr(safe_path, ".js")) final_content_type = "application/javascript"; else if (strstr(safe_path, ".json")) final_content_type = "application/json"; } Dowa_HashMap_Push_Arena(resp, "status", "200", arena); Dowa_HashMap_Push_Arena(resp, "content-type", final_content_type, arena); Dowa_HashMap_Push_Arena(resp, "body", body, arena); return resp; } Seobeo_Request_Entry* ApiGetReadme(Seobeo_Request_Entry *req, Dowa_Arena *arena) { return ApiGetFile(req, arena); } Seobeo_Request_Entry* ApiHgWireProtocol(Seobeo_Request_Entry *req, Dowa_Arena *arena) { Seobeo_Request_Entry *resp = NULL; // Get method void *method_kv = Dowa_HashMap_Get_Ptr(req, "HTTP_Method"); const char *method = method_kv ? ((Seobeo_Request_Entry*)method_kv)->value : "GET"; // Get query string void *query_kv = Dowa_HashMap_Get_Ptr(req, "QueryString"); const char *query_string = query_kv ? ((Seobeo_Request_Entry*)query_kv)->value : ""; // Get request body for POST void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : ""; size_t body_len = strlen(req_body); Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: method=%s query=%s body_len=%zu\n", method, query_string, body_len); // Connect to hg serve int sock = hg_proxy_connect(); if (sock < 0) { Dowa_HashMap_Push_Arena(resp, "status", "502", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); Dowa_HashMap_Push_Arena(resp, "body", "Failed to connect to hg serve", arena); return resp; } // Build the HTTP request to forward to hg serve char http_request[MAX_PATH * 2]; if (strlen(query_string) > 0) { snprintf(http_request, sizeof(http_request), "%s /?%s HTTP/1.1\r\n" "Host: %s:%d\r\n" "Connection: close\r\n" "Content-Length: %zu\r\n" "\r\n", method, query_string, HG_SERVE_HOST, HG_SERVE_PORT, body_len); } else { snprintf(http_request, sizeof(http_request), "%s / HTTP/1.1\r\n" "Host: %s:%d\r\n" "Connection: close\r\n" "Content-Length: %zu\r\n" "\r\n", method, HG_SERVE_HOST, HG_SERVE_PORT, body_len); } // Send HTTP request headers if (send(sock, http_request, strlen(http_request), 0) < 0) { Seobeo_Log(SEOBEO_DEBUG, "Failed to send request to hg serve\n"); close(sock); Dowa_HashMap_Push_Arena(resp, "status", "502", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); Dowa_HashMap_Push_Arena(resp, "body", "Failed to send to hg serve", arena); return resp; } // Send body if present if (body_len > 0) { send(sock, req_body, body_len, 0); } // Read response from hg serve int buffer_size = 1024 * 1024 * 5; // 5MB char *response_buf = Dowa_Arena_Allocate(arena, buffer_size); size_t total_read = 0; ssize_t bytes_read; while ((bytes_read = recv(sock, response_buf + total_read, buffer_size - total_read - 1, 0)) > 0) { total_read += bytes_read; if (total_read >= (size_t)(buffer_size - 1)) break; } response_buf[total_read] = '\0'; close(sock); Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", total_read); // Parse HTTP response - find headers end char *headers_end = strstr(response_buf, "\r\n\r\n"); if (!headers_end) { Dowa_HashMap_Push_Arena(resp, "status", "502", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); Dowa_HashMap_Push_Arena(resp, "body", "Invalid response from hg serve", arena); return resp; } // Extract status code from first line (e.g., "HTTP/1.1 200 OK") char status_code[4] = "200"; if (strncmp(response_buf, "HTTP/", 5) == 0) { char *status_start = strchr(response_buf, ' '); if (status_start) { strncpy(status_code, status_start + 1, 3); status_code[3] = '\0'; } } // Extract content-type from headers const char *content_type = "application/mercurial-0.1"; char *ct_header = strcasestr(response_buf, "Content-Type:"); if (ct_header && ct_header < headers_end) { ct_header += 13; // Skip "Content-Type:" while (*ct_header == ' ') ct_header++; char *ct_end = strpbrk(ct_header, "\r\n"); if (ct_end) { size_t ct_len = ct_end - ct_header; char *ct_copy = Dowa_Arena_Allocate(arena, ct_len + 1); strncpy(ct_copy, ct_header, ct_len); ct_copy[ct_len] = '\0'; content_type = ct_copy; } } // Body starts after \r\n\r\n char *body = headers_end + 4; Dowa_HashMap_Push_Arena(resp, "status", status_code, arena); Dowa_HashMap_Push_Arena(resp, "content-type", content_type, arena); Dowa_HashMap_Push_Arena(resp, "body", body, arena); return resp; } int main(void) { Seobeo_Router_Init(); Seobeo_Router_Register("GET", "/api/repo/list", ApiListDirectory); 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); printf("Starting on Port 6970...\n"); printf("Repository: %s\n", REPO_ROOT); int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 4); Seobeo_Router_Destroy(); return result; }