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