comparison hg-web/main.c @ 135:ffb764d2fcc5

[HgWeb] Updated hg web so it works
author June Park <parkjune1995@gmail.com>
date Fri, 09 Jan 2026 11:17:20 -0800
parents 1c446ab6f945
children 6de849867459
comparison
equal deleted inserted replaced
133:902e29c38d66 135:ffb764d2fcc5
2 #include "dowa/dowa.h" 2 #include "dowa/dowa.h"
3 #include <stdio.h> 3 #include <stdio.h>
4 #include <stdlib.h> 4 #include <stdlib.h>
5 #include <string.h> 5 #include <string.h>
6 #include <ctype.h> 6 #include <ctype.h>
7 #include <dirent.h>
8 #include <sys/stat.h>
9 #include <unistd.h> 7 #include <unistd.h>
8 #include <sys/socket.h>
9 #include <netinet/in.h>
10 #include <arpa/inet.h>
11 #include <netdb.h>
12
13 #define HG_SERVE_HOST "127.0.0.1"
14 #define HG_SERVE_PORT 4444
10 15
11 #define MAX_PATH 4096 16 #define MAX_PATH 4096
12 17
13 // TODO: Move this to seobeo.... 18 // TODO: Move this to seobeo....
14 // Asked AI to create this lol, probably should learn to decode it myself.. 19 // Asked AI to create this lol, probably should learn to decode it myself..
35 } 40 }
36 } 41 }
37 *dst = '\0'; 42 *dst = '\0';
38 } 43 }
39 44
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) 45 static char* sanitize_path(const char *input_path, Dowa_Arena *arena)
54 { 46 {
55 if (!input_path || strlen(input_path) == 0) 47 if (!input_path || strlen(input_path) == 0)
56 { 48 {
57 char *empty = Dowa_Arena_Allocate(arena, 1); 49 char *empty = Dowa_Arena_Allocate(arena, 1);
85 result[--j] = '\0'; 77 result[--j] = '\0';
86 78
87 return result; 79 return result;
88 } 80 }
89 81
82 // Helper to connect to hg serve
83 static int hg_proxy_connect(void)
84 {
85 int sock = socket(AF_INET, SOCK_STREAM, 0);
86 if (sock < 0)
87 {
88 Seobeo_Log(SEOBEO_DEBUG, "Failed to create socket\n");
89 return -1;
90 }
91
92 struct sockaddr_in server_addr;
93 memset(&server_addr, 0, sizeof(server_addr));
94 server_addr.sin_family = AF_INET;
95 server_addr.sin_port = htons(HG_SERVE_PORT);
96 inet_pton(AF_INET, HG_SERVE_HOST, &server_addr.sin_addr);
97
98 if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0)
99 {
100 Seobeo_Log(SEOBEO_DEBUG, "Failed to connect to hg serve at %s:%d\n", HG_SERVE_HOST, HG_SERVE_PORT);
101 close(sock);
102 return -1;
103 }
104
105 return sock;
106 }
107
108 // Generic helper to proxy a request to hg serve and get the response body
109 // Returns allocated body on success, NULL on failure
110 // out_status and out_content_type are optional output parameters
111 // out_body_len returns the actual body length (for binary content)
112 static char* hg_proxy_request(
113 const char *method,
114 const char *path,
115 const char *req_body,
116 size_t body_len,
117 char *out_status, // should be at least 4 bytes
118 char *out_content_type, // should be at least 256 bytes
119 size_t *out_body_len, // optional: returns actual body length
120 Dowa_Arena *arena)
121 {
122 int sock = hg_proxy_connect();
123 if (sock < 0) return NULL;
124
125 // Build HTTP request
126 char http_request[MAX_PATH * 2];
127 snprintf(http_request, sizeof(http_request),
128 "%s %s HTTP/1.1\r\n"
129 "Host: %s:%d\r\n"
130 "Connection: close\r\n"
131 "Accept: application/json, text/plain, */*\r\n"
132 "Content-Length: %zu\r\n"
133 "\r\n",
134 method, path, HG_SERVE_HOST, HG_SERVE_PORT, body_len);
135
136 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy request: %s %s\n", method, path);
137
138 if (send(sock, http_request, strlen(http_request), 0) < 0)
139 {
140 close(sock);
141 return NULL;
142 }
143
144 if (body_len > 0 && req_body)
145 {
146 send(sock, req_body, body_len, 0);
147 }
148
149 // Read response
150 int buffer_size = 1024 * 1024 * 5; // 5MB
151 char *response_buf = Dowa_Arena_Allocate(arena, buffer_size);
152 size_t total_read = 0;
153 ssize_t bytes_read;
154
155 while ((bytes_read = recv(sock, response_buf + total_read, buffer_size - total_read - 1, 0)) > 0)
156 {
157 total_read += bytes_read;
158 if (total_read >= (size_t)(buffer_size - 1)) break;
159 }
160 response_buf[total_read] = '\0';
161 close(sock);
162
163 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", total_read);
164
165 // Parse response headers - use memmem to handle binary content
166 char *headers_end = NULL;
167 for (size_t i = 0; i + 3 < total_read; i++)
168 {
169 if (response_buf[i] == '\r' && response_buf[i+1] == '\n' &&
170 response_buf[i+2] == '\r' && response_buf[i+3] == '\n')
171 {
172 headers_end = response_buf + i;
173 break;
174 }
175 }
176 if (!headers_end) return NULL;
177
178 // Extract status
179 if (out_status && strncmp(response_buf, "HTTP/", 5) == 0)
180 {
181 char *status_start = strchr(response_buf, ' ');
182 if (status_start)
183 {
184 strncpy(out_status, status_start + 1, 3);
185 out_status[3] = '\0';
186 }
187 }
188
189 // Extract content-type
190 if (out_content_type)
191 {
192 out_content_type[0] = '\0';
193 char *ct_header = strcasestr(response_buf, "Content-Type:");
194 if (ct_header && ct_header < headers_end)
195 {
196 ct_header += 13;
197 while (*ct_header == ' ') ct_header++;
198 char *ct_end = strpbrk(ct_header, "\r\n");
199 if (ct_end)
200 {
201 size_t ct_len = ct_end - ct_header;
202 if (ct_len < 256)
203 {
204 strncpy(out_content_type, ct_header, ct_len);
205 out_content_type[ct_len] = '\0';
206 }
207 }
208 }
209 }
210
211 // Return body (copy to fresh allocation for clean pointer)
212 char *body = headers_end + 4;
213 size_t body_size = total_read - (body - response_buf);
214
215 if (out_body_len) *out_body_len = body_size;
216
217 char *result = Dowa_Arena_Allocate(arena, body_size + 1);
218 memcpy(result, body, body_size);
219 result[body_size] = '\0';
220
221 return result;
222 }
223
90 Seobeo_Request_Entry* ApiListDirectory(Seobeo_Request_Entry *req, Dowa_Arena *arena) 224 Seobeo_Request_Entry* ApiListDirectory(Seobeo_Request_Entry *req, Dowa_Arena *arena)
91 { 225 {
92 Seobeo_Request_Entry *resp = NULL; 226 Seobeo_Request_Entry *resp = NULL;
93 227
94 void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path"); 228 void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path");
97 char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1); 231 char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1);
98 url_decode(decoded_path, rel_path); 232 url_decode(decoded_path, rel_path);
99 233
100 char *safe_path = sanitize_path(decoded_path, arena); 234 char *safe_path = sanitize_path(decoded_path, arena);
101 235
102 Seobeo_Log(SEOBEO_INFO, "rel_path: %s\n", rel_path); 236 Seobeo_Log(SEOBEO_INFO, "ApiListDirectory: safe_path='%s'\n", safe_path);
103 Seobeo_Log(SEOBEO_INFO, "decoded_path: %s\n", decoded_path); 237
104 Seobeo_Log(SEOBEO_INFO, "safe path: %s\n", safe_path); 238 char hg_path[MAX_PATH];
105 Seobeo_Log(SEOBEO_INFO, "REPO_ROOT: %s\n", REPO_ROOT);
106 fflush(stdout);
107
108 char full_path[MAX_PATH];
109 if (strlen(safe_path) > 0) 239 if (strlen(safe_path) > 0)
110 snprintf(full_path, sizeof(full_path), "%s/%s", REPO_ROOT, safe_path); 240 snprintf(hg_path, sizeof(hg_path), "/file/tip/%s?style=json", safe_path);
111 else 241 else
112 snprintf(full_path, sizeof(full_path), "%s", REPO_ROOT); 242 snprintf(hg_path, sizeof(hg_path), "/file/tip/?style=json");
113 243
114 if (!is_directory(full_path)) 244 char status[4] = "200";
115 { 245 char content_type[256] = "";
116 char *error_json = Dowa_Arena_Allocate(arena, 256); 246 size_t body_len = 0;
117 snprintf(error_json, 256, "{\"error\":\"Directory not found\"}"); 247 char *hg_response = hg_proxy_request("GET", hg_path, NULL, 0, status, content_type, &body_len, arena);
118 248
119 Dowa_HashMap_Push_Arena(resp, "status", "404", arena); 249 Seobeo_Log(SEOBEO_DEBUG, "ApiListDirectory: status=%s body_len=%zu\n", status, body_len);
250
251 if (!hg_response || status[0] != '2')
252 {
253 Seobeo_Log(SEOBEO_DEBUG, "Failed to get directory from hg serve\n");
254 Dowa_HashMap_Push_Arena(resp, "status", "502", arena);
120 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); 255 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
121 Dowa_HashMap_Push_Arena(resp, "body", error_json, arena); 256 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to connect to hg serve\"}", arena);
122 return resp; 257 return resp;
123 } 258 }
124 259 char *json = hg_response;
125 DIR *dir = opendir(full_path);
126 if (!dir)
127 {
128 char *error_json = Dowa_Arena_Allocate(arena, 256);
129 snprintf(error_json, 256, "{\"error\":\"Cannot open directory\"}");
130
131 Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
132 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
133 Dowa_HashMap_Push_Arena(resp, "body", error_json, arena);
134 return resp;
135 }
136
137 char *json = Dowa_Arena_Allocate(arena, 1024 * 100);
138 strcpy(json, "{\"files\":[");
139
140 struct dirent *entry;
141 int first = 1;
142
143 while ((entry = readdir(dir)) != NULL)
144 {
145 if (entry->d_name[0] == '.') continue;
146
147 char entry_path[MAX_PATH];
148 snprintf(entry_path, sizeof(entry_path), "%s/%s", full_path, entry->d_name);
149
150 int is_dir = is_directory(entry_path);
151
152 char entry_rel_path[MAX_PATH];
153 if (strlen(safe_path) > 0)
154 snprintf(entry_rel_path, sizeof(entry_rel_path), "%s/%s", safe_path, entry->d_name);
155 else
156 snprintf(entry_rel_path, sizeof(entry_rel_path), "%s", entry->d_name);
157
158 if (!first) strcat(json, ",");
159 first = 0;
160
161 char entry_json[MAX_PATH * 2];
162 snprintf(entry_json, sizeof(entry_json),
163 "{\"name\":\"%s\",\"type\":\"%s\",\"path\":\"%s\"}",
164 entry->d_name,
165 is_dir ? "directory" : "file",
166 entry_rel_path);
167 strcat(json, entry_json);
168 }
169
170 closedir(dir);
171 strcat(json, "]}");
172 260
173 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); 261 Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
174 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); 262 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
175 Dowa_HashMap_Push_Arena(resp, "body", json, arena); 263 Dowa_HashMap_Push_Arena(resp, "body", json, arena);
176 264
185 const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : ""; 273 const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : "";
186 char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1); 274 char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1);
187 url_decode(decoded_path, rel_path); 275 url_decode(decoded_path, rel_path);
188 char *safe_path = sanitize_path(decoded_path, arena); 276 char *safe_path = sanitize_path(decoded_path, arena);
189 277
190 Seobeo_Log(SEOBEO_INFO, "rel_path: %s\n", rel_path); 278 Seobeo_Log(SEOBEO_INFO, "ApiGetFile: safe_path='%s'\n", safe_path);
191 Seobeo_Log(SEOBEO_INFO, "decoded_path: %s\n", decoded_path);
192 Seobeo_Log(SEOBEO_INFO, "safe path: %s\n", safe_path);
193 Seobeo_Log(SEOBEO_INFO, "REPO_ROOT: %s\n", REPO_ROOT);
194 fflush(stdout);
195 279
196 if (strlen(safe_path) == 0) 280 if (strlen(safe_path) == 0)
197 { 281 {
198 char *error = Dowa_Arena_Allocate(arena, 64);
199 strcpy(error, "File path required");
200
201 Dowa_HashMap_Push_Arena(resp, "status", "400", arena); 282 Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
202 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); 283 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
203 Dowa_HashMap_Push_Arena(resp, "body", error, arena); 284 Dowa_HashMap_Push_Arena(resp, "body", "File path required", arena);
204 return resp; 285 return resp;
205 } 286 }
206 287
207 char full_path[MAX_PATH]; 288 // Build hg serve URL: /raw-file/tip/<path>
208 snprintf(full_path, sizeof(full_path), "%s/%s", REPO_ROOT, safe_path); 289 char hg_path[MAX_PATH];
209 FILE *file = fopen(full_path, "rb"); 290 snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", safe_path);
210 if (!file) 291
211 { 292 char status[4] = "200";
212 char *error_msg = "File not found."; 293 char content_type[256] = "";
213 Dowa_HashMap_Push_Arena(resp, "status", "404", arena); 294 size_t body_len = 0;
295 char *body = hg_proxy_request("GET", hg_path, NULL, 0, status, content_type, &body_len, arena);
296
297 Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: status=%s body_len=%zu\n", status, body_len);
298
299 if (!body)
300 {
301 Dowa_HashMap_Push_Arena(resp, "status", "502", arena);
214 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); 302 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
215 Dowa_HashMap_Push_Arena(resp, "body", error_msg, arena); 303 Dowa_HashMap_Push_Arena(resp, "body", "Failed to connect to hg serve", arena);
216 return resp; 304 return resp;
217 } 305 }
218 306
219 fseek(file, 0, SEEK_END); 307 if (status[0] != '2')
220 size_t file_size = ftell(file); 308 {
221 fseek(file, 0, SEEK_SET); 309 Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: error response: %s\n", body);
222 310 Dowa_HashMap_Push_Arena(resp, "status", status, arena);
223 char *file_data = malloc(file_size + 1);
224 if (!file_data)
225 {
226 fclose(file);
227 char *error_msg = "Memory allocation failed";
228 Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
229 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); 311 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
230 Dowa_HashMap_Push_Arena(resp, "body", error_msg, arena); 312 // Return actual error from hg serve if available
313 Dowa_HashMap_Push_Arena(resp, "body", body_len > 0 ? body : "File not found", arena);
231 return resp; 314 return resp;
232 } 315 }
233 316
234 fread(file_data, 1, file_size, file); 317 // Use content-type from hg serve, or determine from extension
235 file_data[file_size] = '\0'; 318 const char *final_content_type = content_type;
236 fclose(file); 319 if (strlen(content_type) == 0 || strcmp(content_type, "application/octet-stream") == 0)
237 320 {
238 char *body = Dowa_Arena_Allocate(arena, file_size + 1); 321 final_content_type = "text/plain";
239 memcpy(body, file_data, file_size); 322 if (strstr(safe_path, ".md")) final_content_type = "text/markdown";
240 body[file_size] = '\0'; 323 else if (strstr(safe_path, ".html")) final_content_type = "text/html";
241 free(file_data); 324 else if (strstr(safe_path, ".css")) final_content_type = "text/css";
242 325 else if (strstr(safe_path, ".js")) final_content_type = "application/javascript";
243 if (!body) 326 else if (strstr(safe_path, ".json")) final_content_type = "application/json";
244 { 327 }
245 char *error = Dowa_Arena_Allocate(arena, 64);
246 strcpy(error, "Cannot read file");
247
248 Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
249 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
250 Dowa_HashMap_Push_Arena(resp, "body", error, arena);
251 return resp;
252 }
253
254 const char *content_type = "text/plain";
255 if (strstr(safe_path, ".md")) content_type = "text/markdown";
256 else if (strstr(safe_path, ".html")) content_type = "text/html";
257 else if (strstr(safe_path, ".css")) content_type = "text/css";
258 else if (strstr(safe_path, ".js")) content_type = "application/javascript";
259 else if (strstr(safe_path, ".json")) content_type = "application/json";
260 328
261 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); 329 Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
262 Dowa_HashMap_Push_Arena(resp, "content-type", content_type, arena); 330 Dowa_HashMap_Push_Arena(resp, "content-type", final_content_type, arena);
263 Dowa_HashMap_Push_Arena(resp, "body", body, arena); 331 Dowa_HashMap_Push_Arena(resp, "body", body, arena);
264 332
265 return resp; 333 return resp;
266 } 334 }
267 335
271 339
272 Seobeo_Request_Entry* ApiHgWireProtocol(Seobeo_Request_Entry *req, Dowa_Arena *arena) 340 Seobeo_Request_Entry* ApiHgWireProtocol(Seobeo_Request_Entry *req, Dowa_Arena *arena)
273 { 341 {
274 Seobeo_Request_Entry *resp = NULL; 342 Seobeo_Request_Entry *resp = NULL;
275 343
276 void *cmd_kv = Dowa_HashMap_Get_Ptr(req, "query_cmd"); 344 // Get method
277 const char *cmd = cmd_kv ? ((Seobeo_Request_Entry*)cmd_kv)->value : ""; 345 void *method_kv = Dowa_HashMap_Get_Ptr(req, "HTTP_Method");
278 if (strlen(cmd) == 0) 346 const char *method = method_kv ? ((Seobeo_Request_Entry*)method_kv)->value : "GET";
279 { 347
280 Dowa_HashMap_Push_Arena(resp, "status", "404", arena); 348 // Get query string
349 void *query_kv = Dowa_HashMap_Get_Ptr(req, "QueryString");
350 const char *query_string = query_kv ? ((Seobeo_Request_Entry*)query_kv)->value : "";
351
352 // Get request body for POST
353 void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body");
354 const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : "";
355 size_t body_len = strlen(req_body);
356
357 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: method=%s query=%s body_len=%zu\n", method, query_string, body_len);
358
359 // Connect to hg serve
360 int sock = hg_proxy_connect();
361 if (sock < 0)
362 {
363 Dowa_HashMap_Push_Arena(resp, "status", "502", arena);
364 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
365 Dowa_HashMap_Push_Arena(resp, "body", "Failed to connect to hg serve", arena);
281 return resp; 366 return resp;
282 } 367 }
283 Seobeo_Log(SEOBEO_DEBUG, "cmd: %s\n", cmd); 368
284 369 // Build the HTTP request to forward to hg serve
285 char command[MAX_PATH]; 370 char http_request[MAX_PATH * 2];
286 snprintf(command, sizeof(command), "hg -R %s serve --stdio 2>&1", REPO_ROOT); 371 if (strlen(query_string) > 0)
287 372 {
288 FILE *hg_pipe = popen(command, "r+"); 373 snprintf(http_request, sizeof(http_request),
289 if (!hg_pipe) 374 "%s /?%s HTTP/1.1\r\n"
290 { 375 "Host: %s:%d\r\n"
291 Seobeo_Log(SEOBEO_DEBUG, "Failed to open pipe\n"); 376 "Connection: close\r\n"
377 "Content-Length: %zu\r\n"
378 "\r\n",
379 method, query_string, HG_SERVE_HOST, HG_SERVE_PORT, body_len);
380 }
381 else
382 {
383 snprintf(http_request, sizeof(http_request),
384 "%s / HTTP/1.1\r\n"
385 "Host: %s:%d\r\n"
386 "Connection: close\r\n"
387 "Content-Length: %zu\r\n"
388 "\r\n",
389 method, HG_SERVE_HOST, HG_SERVE_PORT, body_len);
390 }
391
392 // Send HTTP request headers
393 if (send(sock, http_request, strlen(http_request), 0) < 0)
394 {
395 Seobeo_Log(SEOBEO_DEBUG, "Failed to send request to hg serve\n");
396 close(sock);
397 Dowa_HashMap_Push_Arena(resp, "status", "502", arena);
398 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
399 Dowa_HashMap_Push_Arena(resp, "body", "Failed to send to hg serve", arena);
292 return resp; 400 return resp;
293 } 401 }
294 402
295 // 2. Write the command 403 // Send body if present
296 fprintf(hg_pipe, "capabilities\n"); 404 if (body_len > 0)
297 fflush(hg_pipe); 405 {
298 406 send(sock, req_body, body_len, 0);
299 // 3. Read the response 407 }
300 int buffer_size = 1024 * 1024 * 5; 408
301 char *output = Dowa_Arena_Allocate(arena, buffer_size); 409 // Read response from hg serve
302 if (fgets(output, buffer_size, hg_pipe) != NULL) { 410 int buffer_size = 1024 * 1024 * 5; // 5MB
303 Seobeo_Log(SEOBEO_DEBUG, "SUCCESS! Received: %s\n", output); 411 char *response_buf = Dowa_Arena_Allocate(arena, buffer_size);
304 } else { 412 size_t total_read = 0;
305 Seobeo_Log(SEOBEO_DEBUG, "FAILURE: No output received from hg.\n"); 413 ssize_t bytes_read;
306 } 414
307 415 while ((bytes_read = recv(sock, response_buf + total_read, buffer_size - total_read - 1, 0)) > 0)
308 // 4. Close and check exit code 416 {
309 int status = pclose(hg_pipe); 417 total_read += bytes_read;
310 Seobeo_Log(SEOBEO_DEBUG, "Process exited with status: %d\n", status); 418 if (total_read >= (size_t)(buffer_size - 1)) break;
311 419 }
312 Seobeo_Log(SEOBEO_DEBUG, "body: %s\n", output); 420 response_buf[total_read] = '\0';
313 421 close(sock);
314 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); 422
315 Dowa_HashMap_Push_Arena(resp, "content-type", "application/mercurial-0.2", arena); 423 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", total_read);
316 Dowa_HashMap_Push_Arena(resp, "body", output, arena); 424
425 // Parse HTTP response - find headers end
426 char *headers_end = strstr(response_buf, "\r\n\r\n");
427 if (!headers_end)
428 {
429 Dowa_HashMap_Push_Arena(resp, "status", "502", arena);
430 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
431 Dowa_HashMap_Push_Arena(resp, "body", "Invalid response from hg serve", arena);
432 return resp;
433 }
434
435 // Extract status code from first line (e.g., "HTTP/1.1 200 OK")
436 char status_code[4] = "200";
437 if (strncmp(response_buf, "HTTP/", 5) == 0)
438 {
439 char *status_start = strchr(response_buf, ' ');
440 if (status_start)
441 {
442 strncpy(status_code, status_start + 1, 3);
443 status_code[3] = '\0';
444 }
445 }
446
447 // Extract content-type from headers
448 const char *content_type = "application/mercurial-0.1";
449 char *ct_header = strcasestr(response_buf, "Content-Type:");
450 if (ct_header && ct_header < headers_end)
451 {
452 ct_header += 13; // Skip "Content-Type:"
453 while (*ct_header == ' ') ct_header++;
454 char *ct_end = strpbrk(ct_header, "\r\n");
455 if (ct_end)
456 {
457 size_t ct_len = ct_end - ct_header;
458 char *ct_copy = Dowa_Arena_Allocate(arena, ct_len + 1);
459 strncpy(ct_copy, ct_header, ct_len);
460 ct_copy[ct_len] = '\0';
461 content_type = ct_copy;
462 }
463 }
464
465 // Body starts after \r\n\r\n
466 char *body = headers_end + 4;
467
468 Dowa_HashMap_Push_Arena(resp, "status", status_code, arena);
469 Dowa_HashMap_Push_Arena(resp, "content-type", content_type, arena);
470 Dowa_HashMap_Push_Arena(resp, "body", body, arena);
317 471
318 return resp; 472 return resp;
319 } 473 }
320 474
321 int main(void) { 475 int main(void) {