diff hg-web/main.c @ 104:2301aeb7503b

[Hg Web] Super simple mercurial server.
author June Park <parkjune1995@gmail.com>
date Sat, 03 Jan 2026 10:20:45 -0800
parents
children daf2d393741a
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/main.c	Sat Jan 03 10:20:45 2026 -0800
@@ -0,0 +1,279 @@
+#include "seobeo/seobeo.h"
+#include "dowa/dowa.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <dirent.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#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 int is_directory(const char *path)
+{
+  struct stat st;
+  if (stat(path, &st) != 0) return 0;
+  return S_ISDIR(st.st_mode);
+}
+
+static int file_exists(const char *path)
+{
+  struct stat st;
+  return stat(path, &st) == 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;
+}
+
+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);
+  char full_path[MAX_PATH];
+  if (strlen(safe_path) > 0)
+    snprintf(full_path, sizeof(full_path), "%s/%s", REPO_ROOT, safe_path);
+  else
+    snprintf(full_path, sizeof(full_path), "%s", REPO_ROOT);
+
+  if (!is_directory(full_path))
+  {
+    char *error_json = Dowa_Arena_Allocate(arena, 256);
+    snprintf(error_json, 256, "{\"error\":\"Directory not found\"}");
+
+    Dowa_HashMap_Push_Arena(resp, "status", "404", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", error_json, arena);
+    return resp;
+  }
+
+  DIR *dir = opendir(full_path);
+  if (!dir)
+  {
+    char *error_json = Dowa_Arena_Allocate(arena, 256);
+    snprintf(error_json, 256, "{\"error\":\"Cannot open directory\"}");
+
+    Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", error_json, arena);
+    return resp;
+  }
+
+  char *json = Dowa_Arena_Allocate(arena, 1024 * 100);
+  strcpy(json, "{\"files\":[");
+
+  struct dirent *entry;
+  int first = 1;
+
+  while ((entry = readdir(dir)) != NULL)
+  {
+    if (entry->d_name[0] == '.') continue;
+
+    char entry_path[MAX_PATH];
+    snprintf(entry_path, sizeof(entry_path), "%s/%s", full_path, entry->d_name);
+
+    int is_dir = is_directory(entry_path);
+
+    char entry_rel_path[MAX_PATH];
+    if (strlen(safe_path) > 0)
+      snprintf(entry_rel_path, sizeof(entry_rel_path), "%s/%s", safe_path, entry->d_name);
+    else
+      snprintf(entry_rel_path, sizeof(entry_rel_path), "%s", entry->d_name);
+
+    if (!first) strcat(json, ",");
+    first = 0;
+
+    char entry_json[MAX_PATH * 2];
+    snprintf(entry_json, sizeof(entry_json),
+             "{\"name\":\"%s\",\"type\":\"%s\",\"path\":\"%s\"}",
+             entry->d_name,
+             is_dir ? "directory" : "file",
+             entry_rel_path);
+    strcat(json, entry_json);
+  }
+
+  closedir(dir);
+  strcat(json, "]}");
+
+  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_DEBUG, "rel_path: %s\n", rel_path);
+  Seobeo_Log(SEOBEO_DEBUG, "decoded_path: %s\n", decoded_path);
+  Seobeo_Log(SEOBEO_DEBUG, "safe path: %s\n", safe_path);
+
+
+  if (strlen(safe_path) == 0)
+  {
+    char *error = Dowa_Arena_Allocate(arena, 64);
+    strcpy(error, "File path required");
+
+    Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", error, arena);
+    return resp;
+  }
+
+  char full_path[MAX_PATH];
+  snprintf(full_path, sizeof(full_path), "%s/%s", REPO_ROOT, safe_path);
+  FILE *file = fopen(full_path, "rb");
+  if (!file)
+  {
+    char *error_msg = "File not found.";
+    Dowa_HashMap_Push_Arena(resp, "status", "404", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", error_msg, arena);
+    return resp;
+  }
+
+  fseek(file, 0, SEEK_END);
+  size_t file_size = ftell(file);
+  fseek(file, 0, SEEK_SET);
+
+  char *file_data = malloc(file_size + 1);
+  if (!file_data)
+  {
+    fclose(file);
+    char *error_msg = "Memory allocation failed";
+    Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", error_msg, arena);
+    return resp;
+  }
+
+  fread(file_data, 1, file_size, file);
+  file_data[file_size] = '\0';
+  fclose(file);
+
+  char *body = Dowa_Arena_Allocate(arena, file_size + 1);
+  memcpy(body, file_data, file_size);
+  body[file_size] = '\0';
+  free(file_data);
+
+  if (!body)
+  {
+    char *error = Dowa_Arena_Allocate(arena, 64);
+    strcpy(error, "Cannot read file");
+
+    Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", error, arena);
+    return resp;
+  }
+
+  const char *content_type = "text/plain";
+  if (strstr(safe_path, ".md")) content_type = "text/markdown";
+  else if (strstr(safe_path, ".html")) content_type = "text/html";
+  else if (strstr(safe_path, ".css")) content_type = "text/css";
+  else if (strstr(safe_path, ".js")) content_type = "application/javascript";
+  else if (strstr(safe_path, ".json")) content_type = "application/json";
+
+  Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
+  Dowa_HashMap_Push_Arena(resp, "content-type", 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);
+}
+
+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);
+
+  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;
+}