Mercurial
view hg-web/main.c @ 121:7b1719fa918c
[Seobeo] Added web socket server.
| author | June Park <parkjune1995@gmail.com> |
|---|---|
| date | Thu, 08 Jan 2026 06:45:10 -0800 |
| parents | 1c446ab6f945 |
| children | ffb764d2fcc5 |
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 <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); Seobeo_Log(SEOBEO_INFO, "rel_path: %s\n", rel_path); Seobeo_Log(SEOBEO_INFO, "decoded_path: %s\n", decoded_path); Seobeo_Log(SEOBEO_INFO, "safe path: %s\n", safe_path); Seobeo_Log(SEOBEO_INFO, "REPO_ROOT: %s\n", REPO_ROOT); fflush(stdout); 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_INFO, "rel_path: %s\n", rel_path); Seobeo_Log(SEOBEO_INFO, "decoded_path: %s\n", decoded_path); Seobeo_Log(SEOBEO_INFO, "safe path: %s\n", safe_path); Seobeo_Log(SEOBEO_INFO, "REPO_ROOT: %s\n", REPO_ROOT); fflush(stdout); 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); } Seobeo_Request_Entry* ApiHgWireProtocol(Seobeo_Request_Entry *req, Dowa_Arena *arena) { Seobeo_Request_Entry *resp = NULL; void *cmd_kv = Dowa_HashMap_Get_Ptr(req, "query_cmd"); const char *cmd = cmd_kv ? ((Seobeo_Request_Entry*)cmd_kv)->value : ""; if (strlen(cmd) == 0) { Dowa_HashMap_Push_Arena(resp, "status", "404", arena); return resp; } Seobeo_Log(SEOBEO_DEBUG, "cmd: %s\n", cmd); char command[MAX_PATH]; snprintf(command, sizeof(command), "hg -R %s serve --stdio 2>&1", REPO_ROOT); FILE *hg_pipe = popen(command, "r+"); if (!hg_pipe) { Seobeo_Log(SEOBEO_DEBUG, "Failed to open pipe\n"); return resp; } // 2. Write the command fprintf(hg_pipe, "capabilities\n"); fflush(hg_pipe); // 3. Read the response int buffer_size = 1024 * 1024 * 5; char *output = Dowa_Arena_Allocate(arena, buffer_size); if (fgets(output, buffer_size, hg_pipe) != NULL) { Seobeo_Log(SEOBEO_DEBUG, "SUCCESS! Received: %s\n", output); } else { Seobeo_Log(SEOBEO_DEBUG, "FAILURE: No output received from hg.\n"); } // 4. Close and check exit code int status = pclose(hg_pipe); Seobeo_Log(SEOBEO_DEBUG, "Process exited with status: %d\n", status); Seobeo_Log(SEOBEO_DEBUG, "body: %s\n", output); Dowa_HashMap_Push_Arena(resp, "status", "200", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/mercurial-0.2", arena); Dowa_HashMap_Push_Arena(resp, "body", output, 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; }