Mercurial
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) { |