view hg-web/main.c @ 201:6cdee35a7ba9

[MrJuneJune] notes
author MrJuneJune <me@mrjunejune.com>
date Sun, 15 Feb 2026 07:07:50 -0800
parents 9f4429c49733
children
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

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;
}

Seobeo_Client_Response  *hg_proxy_request(
  const char *method,
  const char *path,
  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_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");

  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;
}

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);
  Seobeo_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");

  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);

  if (hg_response->status_code != 200)
  {
    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 *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", temp1, arena);
  Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena);
  return resp;
}

Seobeo_Request_Entry* ApiGetGraph(Seobeo_Request_Entry *req, Dowa_Arena *arena)
{
  Seobeo_Request_Entry *resp = NULL;

  void *path_kv = Dowa_HashMap_Get_Ptr(req, "QueryString");
  const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : "";
  Seobeo_Log(SEOBEO_INFO, "ApiGetGraph: rel_path='%s'\n", rel_path);
  void *graph_id_kv = Dowa_HashMap_Get_Ptr(req, ":graph_id");
  char *graph_id = ((Seobeo_Request_Entry*)graph_id_kv)->value;
  Seobeo_Log(SEOBEO_INFO, "ApiGetGraph: graph_id='%s'\n", graph_id);
  char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1);
  Seobeo_Url_Decode(decoded_path, rel_path);
  char *safe_path = sanitize_path(decoded_path, arena);

  Seobeo_Log(SEOBEO_INFO, "ApiGetGraph: 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;
  }

  char hg_path[MAX_PATH];
  // void *graph_id_kv = Dowa_HashMap_Get_Ptr(req, ":graph_id");
  // char *graph_id = ((Seobeo_Request_Entry*)graph_id_kv)->value;
  snprintf(hg_path, sizeof(hg_path), "/graph/%s?%s", graph_id, safe_path);
  Seobeo_Client_Response  *hg_response = hg_proxy_request("GET", hg_path, NULL, NULL);

  Seobeo_Log(SEOBEO_DEBUG, "ApiGetGraph: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length);

  char status[4];
  snprintf(status, 4, "%i", hg_response->status_code);

  if (!hg_response->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 (hg_response->status_code != 200)
  {
    Seobeo_Log(SEOBEO_DEBUG, "ApiGetGraph: error hg_response: %s\n", hg_response->body);
    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);
    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", "text/plain", arena);
  Dowa_HashMap_Push_Arena(resp, "body", temp1, arena);
  Dowa_HashMap_Push_Arena(resp, "content-length", temp2, 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);
  Seobeo_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;
  }

  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, NULL);

  Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length);

  char status[4];
  snprintf(status, 4, "%i", hg_response->status_code);

  if (!hg_response->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 (hg_response->status_code != 200)
  {
    Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: error hg_response: %s\n", hg_response->body);
    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);
    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", "text/plain", arena);
  Dowa_HashMap_Push_Arena(resp, "body", temp1, arena);
  Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena);

  return resp;
}

Seobeo_Request_Entry* ApiGetReadme(Seobeo_Request_Entry *req, Dowa_Arena *arena) {
  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;

  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 : "";
  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;

  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 = 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->value, arena);
  Dowa_HashMap_Push_Arena(resp, "body", temp1, arena);
  Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena);

  return resp;
}

Seobeo_Request_Entry* GetReactHome(Seobeo_Request_Entry *req, Dowa_Arena *arena)
{
  size_t file_size = 0;
  char *html = Seobeo_Web_LoadFile("/index.html", &file_size);

  printf("%s", html);
  Seobeo_Request_Entry *resp = NULL; 
  Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
  Dowa_HashMap_Push_Arena(resp, "content-type", "text/html", arena);
  Dowa_HashMap_Push_Arena(resp, "body", html, arena);
  return resp;
}

int main(void) {
  Seobeo_Router_Init();


  Seobeo_Router_Register("GET", "/", GetReactHome);
  Seobeo_Router_Register("GET", "/directories", GetReactHome);
  Seobeo_Router_Register("GET", "/graph", GetReactHome);

  Seobeo_Router_Register("GET", "/api/repo/list", ApiListDirectory);
  Seobeo_Router_Register("GET", "/api/repo/file", ApiGetFile);
  Seobeo_Router_Register("GET", "/api/graph/:graph_id", ApiGetGraph);
  Seobeo_Router_Register("GET", "/api/repo/readme", ApiGetReadme);

  // 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, 1);

  Seobeo_Router_Destroy();

  return result;
}