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 }