Mercurial
comparison hg-web/main.c @ 147:6de849867459 hg-web
[HgWeb] Updated logic to use Seobeo Client.
| author | June Park <parkjune1995@gmail.com> |
|---|---|
| date | Fri, 09 Jan 2026 18:39:34 -0800 |
| parents | ffb764d2fcc5 |
| children | 71ad34a8bc9a |
comparison
equal
deleted
inserted
replaced
| 146:8e56f800b7e4 | 147:6de849867459 |
|---|---|
| 9 #include <netinet/in.h> | 9 #include <netinet/in.h> |
| 10 #include <arpa/inet.h> | 10 #include <arpa/inet.h> |
| 11 #include <netdb.h> | 11 #include <netdb.h> |
| 12 | 12 |
| 13 #define HG_SERVE_HOST "127.0.0.1" | 13 #define HG_SERVE_HOST "127.0.0.1" |
| 14 #define HG_SERVE_PORT 4444 | 14 #define HG_SERVE_PORT "4444" |
| 15 | 15 |
| 16 #define MAX_PATH 4096 | 16 #define MAX_PATH 4096 |
| 17 | 17 |
| 18 // TODO: Move this to seobeo.... | |
| 19 // Asked AI to create this lol, probably should learn to decode it myself.. | |
| 20 static void url_decode(char *dst, const char *src) | |
| 21 { | |
| 22 char a, b; | |
| 23 while (*src) { | |
| 24 if ((*src == '%') && | |
| 25 ((a = src[1]) && (b = src[2])) && | |
| 26 (isxdigit(a) && isxdigit(b))) { | |
| 27 if (a >= 'a') a -= 'a'-'A'; | |
| 28 if (a >= 'A') a -= ('A' - 10); | |
| 29 else a -= '0'; | |
| 30 if (b >= 'a') b -= 'a'-'A'; | |
| 31 if (b >= 'A') b -= ('A' - 10); | |
| 32 else b -= '0'; | |
| 33 *dst++ = 16*a+b; | |
| 34 src+=3; | |
| 35 } else if (*src == '+') { | |
| 36 *dst++ = ' '; | |
| 37 src++; | |
| 38 } else { | |
| 39 *dst++ = *src++; | |
| 40 } | |
| 41 } | |
| 42 *dst = '\0'; | |
| 43 } | |
| 44 | |
| 45 static char* sanitize_path(const char *input_path, Dowa_Arena *arena) | 18 static char* sanitize_path(const char *input_path, Dowa_Arena *arena) |
| 46 { | 19 { |
| 47 if (!input_path || strlen(input_path) == 0) | 20 if (!input_path || strlen(input_path) == 0) |
| 48 { | 21 { |
| 49 char *empty = Dowa_Arena_Allocate(arena, 1); | 22 char *empty = Dowa_Arena_Allocate(arena, 1); |
| 50 empty[0] = '\0'; | 23 empty[0] = '\0'; |
| 51 return empty; | 24 return empty; |
| 52 } | 25 } |
| 53 | 26 |
| 54 size_t len = strlen(input_path); | 27 size_t len = strlen(input_path); |
| 55 char *result = Dowa_Arena_Allocate(arena, len + 1); | 28 char *result = Dowa_Arena_Allocate(arena, len + 1); |
| 56 size_t j = 0; | 29 size_t j = 0; |
| 57 | 30 |
| 58 for (size_t i = 0; i < len; i++) | 31 for (size_t i = 0; i < len; i++) |
| 59 { | 32 { |
| 60 if (input_path[i] == '.' && (i == 0 || input_path[i-1] == '/')) { | 33 if (input_path[i] == '.' && (i == 0 || input_path[i-1] == '/')) { |
| 61 if (i + 1 < len && input_path[i+1] == '.') { | 34 if (i + 1 < len && input_path[i+1] == '.') { |
| 62 // Skip ".." | 35 // Skip ".." |
| 63 i++; | 36 i++; |
| 64 continue; | 37 continue; |
| 65 } | |
| 66 // Skip "." | |
| 67 continue; | |
| 68 } | 38 } |
| 69 result[j++] = input_path[i]; | 39 // Skip "." |
| 40 continue; | |
| 41 } | |
| 42 result[j++] = input_path[i]; | |
| 70 } | 43 } |
| 71 result[j] = '\0'; | 44 result[j] = '\0'; |
| 72 | 45 |
| 73 // Remove leading/trailing slashes | 46 // Remove leading/trailing slashes |
| 74 while (result[0] == '/') | 47 while (result[0] == '/') |
| 75 memmove(result, result + 1, strlen(result)); | 48 memmove(result, result + 1, strlen(result)); |
| 76 while (j > 0 && result[j-1] == '/') | 49 while (j > 0 && result[j-1] == '/') |
| 77 result[--j] = '\0'; | 50 result[--j] = '\0'; |
| 78 | 51 |
| 79 return result; | 52 return result; |
| 80 } | 53 } |
| 81 | 54 |
| 82 // Helper to connect to hg serve | 55 Seobeo_Client_Response *hg_proxy_request( |
| 83 static int hg_proxy_connect(void) | 56 const char *method, |
| 84 { | 57 const char *path, |
| 85 int sock = socket(AF_INET, SOCK_STREAM, 0); | 58 const char *req_body) |
| 86 if (sock < 0) | 59 { |
| 87 { | 60 char full_path[MAX_PATH]; |
| 88 Seobeo_Log(SEOBEO_DEBUG, "Failed to create socket\n"); | 61 snprintf(full_path, MAX_PATH, "http://%s:%s%s", HG_SERVE_HOST, HG_SERVE_PORT, path); |
| 89 return -1; | 62 Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create(full_path); |
| 90 } | 63 Seobeo_Client_Request_Add_Header_Array(p_req, "User-Agent: Seobeo/1.0 (Array Mode)"); |
| 91 | 64 Seobeo_Client_Request_Add_Header_Array(p_req, "Accept: application/json"); |
| 92 struct sockaddr_in server_addr; | 65 Seobeo_Client_Request_Add_Header_Array(p_req, "X-Test-Header: TestValue"); |
| 93 memset(&server_addr, 0, sizeof(server_addr)); | 66 if (strcmp(method, "POST")) |
| 94 server_addr.sin_family = AF_INET; | 67 Seobeo_Client_Request_Set_Method(p_req, "POST"); |
| 95 server_addr.sin_port = htons(HG_SERVE_PORT); | 68 |
| 96 inet_pton(AF_INET, HG_SERVE_HOST, &server_addr.sin_addr); | 69 Seobeo_Client_Request_Set_Body(p_req, req_body, 0); |
| 97 | 70 Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req); |
| 98 if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) | 71 Seobeo_Client_Request_Destroy(p_req); |
| 99 { | 72 return p_resp; |
| 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 } | 73 } |
| 223 | 74 |
| 224 Seobeo_Request_Entry* ApiListDirectory(Seobeo_Request_Entry *req, Dowa_Arena *arena) | 75 Seobeo_Request_Entry* ApiListDirectory(Seobeo_Request_Entry *req, Dowa_Arena *arena) |
| 225 { | 76 { |
| 226 Seobeo_Request_Entry *resp = NULL; | 77 Seobeo_Request_Entry *resp = NULL; |
| 227 | 78 |
| 228 void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path"); | 79 void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path"); |
| 229 const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : ""; | 80 const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : ""; |
| 230 | 81 |
| 231 char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1); | 82 char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1); |
| 232 url_decode(decoded_path, rel_path); | 83 Seobeo_Url_Decode(decoded_path, rel_path); |
| 233 | 84 |
| 234 char *safe_path = sanitize_path(decoded_path, arena); | 85 char *safe_path = sanitize_path(decoded_path, arena); |
| 235 | 86 |
| 236 Seobeo_Log(SEOBEO_INFO, "ApiListDirectory: safe_path='%s'\n", safe_path); | 87 Seobeo_Log(SEOBEO_INFO, "ApiListDirectory: safe_path='%s'\n", safe_path); |
| 237 | 88 |
| 238 char hg_path[MAX_PATH]; | 89 char hg_path[MAX_PATH]; |
| 239 if (strlen(safe_path) > 0) | 90 if (strlen(safe_path) > 0) |
| 240 snprintf(hg_path, sizeof(hg_path), "/file/tip/%s?style=json", safe_path); | 91 snprintf(hg_path, sizeof(hg_path), "/file/tip/%s?style=json", safe_path); |
| 241 else | 92 else |
| 242 snprintf(hg_path, sizeof(hg_path), "/file/tip/?style=json"); | 93 snprintf(hg_path, sizeof(hg_path), "/file/tip/?style=json"); |
| 243 | 94 |
| 244 char status[4] = "200"; | 95 Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL); |
| 245 char content_type[256] = ""; | 96 |
| 246 size_t body_len = 0; | 97 Seobeo_Log(SEOBEO_DEBUG, "ApiListDirectory: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length); |
| 247 char *hg_response = hg_proxy_request("GET", hg_path, NULL, 0, status, content_type, &body_len, arena); | 98 |
| 248 | 99 char status[4]; |
| 249 Seobeo_Log(SEOBEO_DEBUG, "ApiListDirectory: status=%s body_len=%zu\n", status, body_len); | 100 snprintf(status, 3, "%i", hg_response->status_code); |
| 250 | 101 |
| 251 if (!hg_response || status[0] != '2') | 102 if (hg_response->status_code != 200) |
| 252 { | 103 { |
| 253 Seobeo_Log(SEOBEO_DEBUG, "Failed to get directory from hg serve\n"); | 104 Seobeo_Log(SEOBEO_DEBUG, "Failed to get directory from hg serve\n"); |
| 254 Dowa_HashMap_Push_Arena(resp, "status", "502", arena); | 105 Dowa_HashMap_Push_Arena(resp, "status", "502", arena); |
| 255 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | 106 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); |
| 256 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to connect to hg serve\"}", arena); | 107 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to connect to hg serve\"}", arena); |
| 257 return resp; | 108 return resp; |
| 258 } | 109 } |
| 259 char *json = hg_response; | |
| 260 | 110 |
| 261 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | 111 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); |
| 262 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | 112 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); |
| 263 Dowa_HashMap_Push_Arena(resp, "body", json, arena); | 113 Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); |
| 264 | 114 Seobeo_Client_Response_Destroy(hg_response); |
| 265 return resp; | 115 return resp; |
| 266 } | 116 } |
| 267 | 117 |
| 268 Seobeo_Request_Entry* ApiGetFile(Seobeo_Request_Entry *req, Dowa_Arena *arena) | 118 Seobeo_Request_Entry* ApiGetFile(Seobeo_Request_Entry *req, Dowa_Arena *arena) |
| 269 { | 119 { |
| 270 Seobeo_Request_Entry *resp = NULL; | 120 Seobeo_Request_Entry *resp = NULL; |
| 271 | 121 |
| 272 void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path"); | 122 void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path"); |
| 273 const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : ""; | 123 const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : ""; |
| 274 char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1); | 124 char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1); |
| 275 url_decode(decoded_path, rel_path); | 125 Seobeo_Url_Decode(decoded_path, rel_path); |
| 276 char *safe_path = sanitize_path(decoded_path, arena); | 126 char *safe_path = sanitize_path(decoded_path, arena); |
| 277 | 127 |
| 278 Seobeo_Log(SEOBEO_INFO, "ApiGetFile: safe_path='%s'\n", safe_path); | 128 Seobeo_Log(SEOBEO_INFO, "ApiGetFile: safe_path='%s'\n", safe_path); |
| 279 | 129 |
| 280 if (strlen(safe_path) == 0) | 130 if (strlen(safe_path) == 0) |
| 283 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | 133 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); |
| 284 Dowa_HashMap_Push_Arena(resp, "body", "File path required", arena); | 134 Dowa_HashMap_Push_Arena(resp, "body", "File path required", arena); |
| 285 return resp; | 135 return resp; |
| 286 } | 136 } |
| 287 | 137 |
| 288 // Build hg serve URL: /raw-file/tip/<path> | |
| 289 char hg_path[MAX_PATH]; | 138 char hg_path[MAX_PATH]; |
| 290 snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", safe_path); | 139 snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", safe_path); |
| 291 | 140 Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL); |
| 292 char status[4] = "200"; | 141 |
| 293 char content_type[256] = ""; | 142 Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length); |
| 294 size_t body_len = 0; | 143 |
| 295 char *body = hg_proxy_request("GET", hg_path, NULL, 0, status, content_type, &body_len, arena); | 144 char status[4]; |
| 296 | 145 snprintf(status, 3, "%i", hg_response->status_code); |
| 297 Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: status=%s body_len=%zu\n", status, body_len); | 146 |
| 298 | 147 if (!hg_response->body) |
| 299 if (!body) | |
| 300 { | 148 { |
| 301 Dowa_HashMap_Push_Arena(resp, "status", "502", arena); | 149 Dowa_HashMap_Push_Arena(resp, "status", "502", arena); |
| 302 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | 150 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); |
| 303 Dowa_HashMap_Push_Arena(resp, "body", "Failed to connect to hg serve", arena); | 151 Dowa_HashMap_Push_Arena(resp, "body", "Failed to connect to hg serve", arena); |
| 304 return resp; | 152 return resp; |
| 305 } | 153 } |
| 306 | 154 |
| 307 if (status[0] != '2') | 155 if (hg_response->status_code != 200) |
| 308 { | 156 { |
| 309 Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: error response: %s\n", body); | 157 Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: error hg_response: %s\n", hg_response->body); |
| 310 Dowa_HashMap_Push_Arena(resp, "status", status, arena); | 158 Dowa_HashMap_Push_Arena(resp, "status", status, arena); |
| 311 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | 159 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); |
| 312 // Return actual error from hg serve if available | 160 Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); |
| 313 Dowa_HashMap_Push_Arena(resp, "body", body_len > 0 ? body : "File not found", arena); | 161 return resp; |
| 314 return resp; | 162 } |
| 315 } | 163 |
| 316 | 164 Dowa_HashMap_Push_Arena(resp, "status", status, arena); |
| 317 // Use content-type from hg serve, or determine from extension | 165 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); |
| 318 const char *final_content_type = content_type; | 166 Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); |
| 319 if (strlen(content_type) == 0 || strcmp(content_type, "application/octet-stream") == 0) | 167 Seobeo_Client_Response_Destroy(hg_response); |
| 320 { | |
| 321 final_content_type = "text/plain"; | |
| 322 if (strstr(safe_path, ".md")) final_content_type = "text/markdown"; | |
| 323 else if (strstr(safe_path, ".html")) final_content_type = "text/html"; | |
| 324 else if (strstr(safe_path, ".css")) final_content_type = "text/css"; | |
| 325 else if (strstr(safe_path, ".js")) final_content_type = "application/javascript"; | |
| 326 else if (strstr(safe_path, ".json")) final_content_type = "application/json"; | |
| 327 } | |
| 328 | |
| 329 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | |
| 330 Dowa_HashMap_Push_Arena(resp, "content-type", final_content_type, arena); | |
| 331 Dowa_HashMap_Push_Arena(resp, "body", body, arena); | |
| 332 | 168 |
| 333 return resp; | 169 return resp; |
| 334 } | 170 } |
| 335 | 171 |
| 336 Seobeo_Request_Entry* ApiGetReadme(Seobeo_Request_Entry *req, Dowa_Arena *arena) { | 172 Seobeo_Request_Entry* ApiGetReadme(Seobeo_Request_Entry *req, Dowa_Arena *arena) { |
| 337 return ApiGetFile(req, arena); | 173 return ApiGetFile(req, arena); |
| 338 } | 174 } |
| 339 | 175 |
| 340 Seobeo_Request_Entry* ApiHgWireProtocol(Seobeo_Request_Entry *req, Dowa_Arena *arena) | 176 Seobeo_Request_Entry* ApiHgWireProtocol(Seobeo_Request_Entry *req, Dowa_Arena *arena) |
| 341 { | 177 { |
| 342 Seobeo_Request_Entry *resp = NULL; | 178 Seobeo_Request_Entry *resp = NULL; |
| 343 | 179 |
| 344 // Get method | 180 void *method_kv = Dowa_HashMap_Get_Ptr(req, "HTTP_Method"); |
| 345 void *method_kv = Dowa_HashMap_Get_Ptr(req, "HTTP_Method"); | 181 const char *method = method_kv ? ((Seobeo_Request_Entry*)method_kv)->value : "GET"; |
| 346 const char *method = method_kv ? ((Seobeo_Request_Entry*)method_kv)->value : "GET"; | 182 |
| 347 | 183 void *query_kv = Dowa_HashMap_Get_Ptr(req, "QueryString"); |
| 348 // Get query string | 184 const char *query_string = query_kv ? ((Seobeo_Request_Entry*)query_kv)->value : ""; |
| 349 void *query_kv = Dowa_HashMap_Get_Ptr(req, "QueryString"); | 185 |
| 350 const char *query_string = query_kv ? ((Seobeo_Request_Entry*)query_kv)->value : ""; | 186 void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); |
| 351 | 187 const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : ""; |
| 352 // Get request body for POST | 188 size_t body_len = strlen(req_body); |
| 353 void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); | 189 |
| 354 const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : ""; | 190 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: method=%s query=%s body_len=%zu\n", method, query_string, body_len); |
| 355 size_t body_len = strlen(req_body); | 191 Seobeo_Client_Response *hg_response; |
| 356 | 192 if (strlen(query_string) > 0) |
| 357 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: method=%s query=%s body_len=%zu\n", method, query_string, body_len); | 193 { |
| 358 | 194 char temp_path[MAX_PATH]; |
| 359 // Connect to hg serve | 195 snprintf(temp_path, MAX_PATH, "?%s", query_string); |
| 360 int sock = hg_proxy_connect(); | 196 hg_response = hg_proxy_request(method, query_string, req_body); |
| 361 if (sock < 0) | 197 } |
| 362 { | 198 else |
| 363 Dowa_HashMap_Push_Arena(resp, "status", "502", arena); | 199 hg_response = hg_proxy_request(method, "", req_body); |
| 364 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | 200 |
| 365 Dowa_HashMap_Push_Arena(resp, "body", "Failed to connect to hg serve", arena); | 201 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", hg_response->body_length); |
| 366 return resp; | 202 |
| 367 } | 203 Seobeo_Request_Entry *kv = Dowa_HashMap_Get_Ptr(hg_response->headers, "Content-Type"); |
| 368 | 204 |
| 369 // Build the HTTP request to forward to hg serve | 205 char status[4]; |
| 370 char http_request[MAX_PATH * 2]; | 206 snprintf(status, 3, "%i", hg_response->status_code); |
| 371 if (strlen(query_string) > 0) | 207 |
| 372 { | 208 Dowa_HashMap_Push_Arena(resp, "status", status, arena); |
| 373 snprintf(http_request, sizeof(http_request), | 209 Dowa_HashMap_Push_Arena(resp, "content-type", kv ? kv->value : "", arena); |
| 374 "%s /?%s HTTP/1.1\r\n" | 210 Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); |
| 375 "Host: %s:%d\r\n" | 211 |
| 376 "Connection: close\r\n" | 212 return resp; |
| 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); | |
| 400 return resp; | |
| 401 } | |
| 402 | |
| 403 // Send body if present | |
| 404 if (body_len > 0) | |
| 405 { | |
| 406 send(sock, req_body, body_len, 0); | |
| 407 } | |
| 408 | |
| 409 // Read response from hg serve | |
| 410 int buffer_size = 1024 * 1024 * 5; // 5MB | |
| 411 char *response_buf = Dowa_Arena_Allocate(arena, buffer_size); | |
| 412 size_t total_read = 0; | |
| 413 ssize_t bytes_read; | |
| 414 | |
| 415 while ((bytes_read = recv(sock, response_buf + total_read, buffer_size - total_read - 1, 0)) > 0) | |
| 416 { | |
| 417 total_read += bytes_read; | |
| 418 if (total_read >= (size_t)(buffer_size - 1)) break; | |
| 419 } | |
| 420 response_buf[total_read] = '\0'; | |
| 421 close(sock); | |
| 422 | |
| 423 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", total_read); | |
| 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); | |
| 471 | |
| 472 return resp; | |
| 473 } | 213 } |
| 474 | 214 |
| 475 int main(void) { | 215 int main(void) { |
| 476 Seobeo_Router_Init(); | 216 Seobeo_Router_Init(); |
| 477 | 217 |
| 481 | 221 |
| 482 Seobeo_Router_Register("GET", "/repo", ApiHgWireProtocol); | 222 Seobeo_Router_Register("GET", "/repo", ApiHgWireProtocol); |
| 483 Seobeo_Router_Register("POST", "/repo", ApiHgWireProtocol); | 223 Seobeo_Router_Register("POST", "/repo", ApiHgWireProtocol); |
| 484 | 224 |
| 485 printf("Starting on Port 6970...\n"); | 225 printf("Starting on Port 6970...\n"); |
| 486 printf("Repository: %s\n", REPO_ROOT); | |
| 487 | 226 |
| 488 int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 4); | 227 int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 4); |
| 489 | 228 |
| 490 Seobeo_Router_Destroy(); | 229 Seobeo_Router_Destroy(); |
| 491 | 230 |