Mercurial
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 103:f6d2f2eaaf84 | 104:2301aeb7503b |
|---|---|
| 1 #include "seobeo/seobeo.h" | |
| 2 #include "dowa/dowa.h" | |
| 3 #include <stdio.h> | |
| 4 #include <stdlib.h> | |
| 5 #include <string.h> | |
| 6 #include <ctype.h> | |
| 7 #include <dirent.h> | |
| 8 #include <sys/stat.h> | |
| 9 #include <unistd.h> | |
| 10 | |
| 11 #define MAX_PATH 4096 | |
| 12 | |
| 13 // TODO: Move this to seobeo.... | |
| 14 // Asked AI to create this lol, probably should learn to decode it myself.. | |
| 15 static void url_decode(char *dst, const char *src) | |
| 16 { | |
| 17 char a, b; | |
| 18 while (*src) { | |
| 19 if ((*src == '%') && | |
| 20 ((a = src[1]) && (b = src[2])) && | |
| 21 (isxdigit(a) && isxdigit(b))) { | |
| 22 if (a >= 'a') a -= 'a'-'A'; | |
| 23 if (a >= 'A') a -= ('A' - 10); | |
| 24 else a -= '0'; | |
| 25 if (b >= 'a') b -= 'a'-'A'; | |
| 26 if (b >= 'A') b -= ('A' - 10); | |
| 27 else b -= '0'; | |
| 28 *dst++ = 16*a+b; | |
| 29 src+=3; | |
| 30 } else if (*src == '+') { | |
| 31 *dst++ = ' '; | |
| 32 src++; | |
| 33 } else { | |
| 34 *dst++ = *src++; | |
| 35 } | |
| 36 } | |
| 37 *dst = '\0'; | |
| 38 } | |
| 39 | |
| 40 static int is_directory(const char *path) | |
| 41 { | |
| 42 struct stat st; | |
| 43 if (stat(path, &st) != 0) return 0; | |
| 44 return S_ISDIR(st.st_mode); | |
| 45 } | |
| 46 | |
| 47 static int file_exists(const char *path) | |
| 48 { | |
| 49 struct stat st; | |
| 50 return stat(path, &st) == 0; | |
| 51 } | |
| 52 | |
| 53 static char* sanitize_path(const char *input_path, Dowa_Arena *arena) | |
| 54 { | |
| 55 if (!input_path || strlen(input_path) == 0) | |
| 56 { | |
| 57 char *empty = Dowa_Arena_Allocate(arena, 1); | |
| 58 empty[0] = '\0'; | |
| 59 return empty; | |
| 60 } | |
| 61 | |
| 62 size_t len = strlen(input_path); | |
| 63 char *result = Dowa_Arena_Allocate(arena, len + 1); | |
| 64 size_t j = 0; | |
| 65 | |
| 66 for (size_t i = 0; i < len; i++) | |
| 67 { | |
| 68 if (input_path[i] == '.' && (i == 0 || input_path[i-1] == '/')) { | |
| 69 if (i + 1 < len && input_path[i+1] == '.') { | |
| 70 // Skip ".." | |
| 71 i++; | |
| 72 continue; | |
| 73 } | |
| 74 // Skip "." | |
| 75 continue; | |
| 76 } | |
| 77 result[j++] = input_path[i]; | |
| 78 } | |
| 79 result[j] = '\0'; | |
| 80 | |
| 81 // Remove leading/trailing slashes | |
| 82 while (result[0] == '/') | |
| 83 memmove(result, result + 1, strlen(result)); | |
| 84 while (j > 0 && result[j-1] == '/') | |
| 85 result[--j] = '\0'; | |
| 86 | |
| 87 return result; | |
| 88 } | |
| 89 | |
| 90 Seobeo_Request_Entry* ApiListDirectory(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 91 { | |
| 92 Seobeo_Request_Entry *resp = NULL; | |
| 93 | |
| 94 void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path"); | |
| 95 const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : ""; | |
| 96 | |
| 97 char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1); | |
| 98 url_decode(decoded_path, rel_path); | |
| 99 | |
| 100 char *safe_path = sanitize_path(decoded_path, arena); | |
| 101 char full_path[MAX_PATH]; | |
| 102 if (strlen(safe_path) > 0) | |
| 103 snprintf(full_path, sizeof(full_path), "%s/%s", REPO_ROOT, safe_path); | |
| 104 else | |
| 105 snprintf(full_path, sizeof(full_path), "%s", REPO_ROOT); | |
| 106 | |
| 107 if (!is_directory(full_path)) | |
| 108 { | |
| 109 char *error_json = Dowa_Arena_Allocate(arena, 256); | |
| 110 snprintf(error_json, 256, "{\"error\":\"Directory not found\"}"); | |
| 111 | |
| 112 Dowa_HashMap_Push_Arena(resp, "status", "404", arena); | |
| 113 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 114 Dowa_HashMap_Push_Arena(resp, "body", error_json, arena); | |
| 115 return resp; | |
| 116 } | |
| 117 | |
| 118 DIR *dir = opendir(full_path); | |
| 119 if (!dir) | |
| 120 { | |
| 121 char *error_json = Dowa_Arena_Allocate(arena, 256); | |
| 122 snprintf(error_json, 256, "{\"error\":\"Cannot open directory\"}"); | |
| 123 | |
| 124 Dowa_HashMap_Push_Arena(resp, "status", "500", arena); | |
| 125 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 126 Dowa_HashMap_Push_Arena(resp, "body", error_json, arena); | |
| 127 return resp; | |
| 128 } | |
| 129 | |
| 130 char *json = Dowa_Arena_Allocate(arena, 1024 * 100); | |
| 131 strcpy(json, "{\"files\":["); | |
| 132 | |
| 133 struct dirent *entry; | |
| 134 int first = 1; | |
| 135 | |
| 136 while ((entry = readdir(dir)) != NULL) | |
| 137 { | |
| 138 if (entry->d_name[0] == '.') continue; | |
| 139 | |
| 140 char entry_path[MAX_PATH]; | |
| 141 snprintf(entry_path, sizeof(entry_path), "%s/%s", full_path, entry->d_name); | |
| 142 | |
| 143 int is_dir = is_directory(entry_path); | |
| 144 | |
| 145 char entry_rel_path[MAX_PATH]; | |
| 146 if (strlen(safe_path) > 0) | |
| 147 snprintf(entry_rel_path, sizeof(entry_rel_path), "%s/%s", safe_path, entry->d_name); | |
| 148 else | |
| 149 snprintf(entry_rel_path, sizeof(entry_rel_path), "%s", entry->d_name); | |
| 150 | |
| 151 if (!first) strcat(json, ","); | |
| 152 first = 0; | |
| 153 | |
| 154 char entry_json[MAX_PATH * 2]; | |
| 155 snprintf(entry_json, sizeof(entry_json), | |
| 156 "{\"name\":\"%s\",\"type\":\"%s\",\"path\":\"%s\"}", | |
| 157 entry->d_name, | |
| 158 is_dir ? "directory" : "file", | |
| 159 entry_rel_path); | |
| 160 strcat(json, entry_json); | |
| 161 } | |
| 162 | |
| 163 closedir(dir); | |
| 164 strcat(json, "]}"); | |
| 165 | |
| 166 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | |
| 167 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | |
| 168 Dowa_HashMap_Push_Arena(resp, "body", json, arena); | |
| 169 | |
| 170 return resp; | |
| 171 } | |
| 172 | |
| 173 Seobeo_Request_Entry* ApiGetFile(Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 174 { | |
| 175 Seobeo_Request_Entry *resp = NULL; | |
| 176 | |
| 177 void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path"); | |
| 178 const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : ""; | |
| 179 char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1); | |
| 180 url_decode(decoded_path, rel_path); | |
| 181 char *safe_path = sanitize_path(decoded_path, arena); | |
| 182 | |
| 183 Seobeo_Log(SEOBEO_DEBUG, "rel_path: %s\n", rel_path); | |
| 184 Seobeo_Log(SEOBEO_DEBUG, "decoded_path: %s\n", decoded_path); | |
| 185 Seobeo_Log(SEOBEO_DEBUG, "safe path: %s\n", safe_path); | |
| 186 | |
| 187 | |
| 188 if (strlen(safe_path) == 0) | |
| 189 { | |
| 190 char *error = Dowa_Arena_Allocate(arena, 64); | |
| 191 strcpy(error, "File path required"); | |
| 192 | |
| 193 Dowa_HashMap_Push_Arena(resp, "status", "400", arena); | |
| 194 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | |
| 195 Dowa_HashMap_Push_Arena(resp, "body", error, arena); | |
| 196 return resp; | |
| 197 } | |
| 198 | |
| 199 char full_path[MAX_PATH]; | |
| 200 snprintf(full_path, sizeof(full_path), "%s/%s", REPO_ROOT, safe_path); | |
| 201 FILE *file = fopen(full_path, "rb"); | |
| 202 if (!file) | |
| 203 { | |
| 204 char *error_msg = "File not found."; | |
| 205 Dowa_HashMap_Push_Arena(resp, "status", "404", arena); | |
| 206 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | |
| 207 Dowa_HashMap_Push_Arena(resp, "body", error_msg, arena); | |
| 208 return resp; | |
| 209 } | |
| 210 | |
| 211 fseek(file, 0, SEEK_END); | |
| 212 size_t file_size = ftell(file); | |
| 213 fseek(file, 0, SEEK_SET); | |
| 214 | |
| 215 char *file_data = malloc(file_size + 1); | |
| 216 if (!file_data) | |
| 217 { | |
| 218 fclose(file); | |
| 219 char *error_msg = "Memory allocation failed"; | |
| 220 Dowa_HashMap_Push_Arena(resp, "status", "500", arena); | |
| 221 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | |
| 222 Dowa_HashMap_Push_Arena(resp, "body", error_msg, arena); | |
| 223 return resp; | |
| 224 } | |
| 225 | |
| 226 fread(file_data, 1, file_size, file); | |
| 227 file_data[file_size] = '\0'; | |
| 228 fclose(file); | |
| 229 | |
| 230 char *body = Dowa_Arena_Allocate(arena, file_size + 1); | |
| 231 memcpy(body, file_data, file_size); | |
| 232 body[file_size] = '\0'; | |
| 233 free(file_data); | |
| 234 | |
| 235 if (!body) | |
| 236 { | |
| 237 char *error = Dowa_Arena_Allocate(arena, 64); | |
| 238 strcpy(error, "Cannot read file"); | |
| 239 | |
| 240 Dowa_HashMap_Push_Arena(resp, "status", "500", arena); | |
| 241 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | |
| 242 Dowa_HashMap_Push_Arena(resp, "body", error, arena); | |
| 243 return resp; | |
| 244 } | |
| 245 | |
| 246 const char *content_type = "text/plain"; | |
| 247 if (strstr(safe_path, ".md")) content_type = "text/markdown"; | |
| 248 else if (strstr(safe_path, ".html")) content_type = "text/html"; | |
| 249 else if (strstr(safe_path, ".css")) content_type = "text/css"; | |
| 250 else if (strstr(safe_path, ".js")) content_type = "application/javascript"; | |
| 251 else if (strstr(safe_path, ".json")) content_type = "application/json"; | |
| 252 | |
| 253 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | |
| 254 Dowa_HashMap_Push_Arena(resp, "content-type", content_type, arena); | |
| 255 Dowa_HashMap_Push_Arena(resp, "body", body, arena); | |
| 256 | |
| 257 return resp; | |
| 258 } | |
| 259 | |
| 260 Seobeo_Request_Entry* ApiGetReadme(Seobeo_Request_Entry *req, Dowa_Arena *arena) { | |
| 261 return ApiGetFile(req, arena); | |
| 262 } | |
| 263 | |
| 264 int main(void) { | |
| 265 Seobeo_Router_Init(); | |
| 266 | |
| 267 Seobeo_Router_Register("GET", "/api/repo/list", ApiListDirectory); | |
| 268 Seobeo_Router_Register("GET", "/api/repo/file", ApiGetFile); | |
| 269 Seobeo_Router_Register("GET", "/api/repo/readme", ApiGetReadme); | |
| 270 | |
| 271 printf("Starting on Port 6970...\n"); | |
| 272 printf("Repository: %s\n", REPO_ROOT); | |
| 273 | |
| 274 int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 4); | |
| 275 | |
| 276 Seobeo_Router_Destroy(); | |
| 277 | |
| 278 return result; | |
| 279 } |