diff hg-web/main.c @ 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 6de849867459
children 32ce881452fa
line wrap: on
line diff
--- 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();