Mercurial
diff hg-web/main.c @ 175:71ad34a8bc9a hg-web
[HgWeb] Can stream hg response now. Added react page for hg web since we use json anyway.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Tue, 20 Jan 2026 06:06:47 -0800 |
| parents | 6de849867459 |
| children | 32ce881452fa |
line wrap: on
line diff
--- a/hg-web/main.c Mon Jan 19 18:59:23 2026 -0800 +++ b/hg-web/main.c Tue Jan 20 06:06:47 2026 -0800 @@ -30,16 +30,16 @@ for (size_t i = 0; i < len; i++) { - if (input_path[i] == '.' && (i == 0 || input_path[i-1] == '/')) { - if (i + 1 < len && input_path[i+1] == '.') { - // Skip ".." - i++; - continue; + if (input_path[i] == '.' && (i == 0 || input_path[i-1] == '/')) { + if (i + 1 < len && input_path[i+1] == '.') { + // Skip ".." + i++; + continue; + } + // Skip "." + continue; } - // Skip "." - continue; - } - result[j++] = input_path[i]; + result[j++] = input_path[i]; } result[j] = '\0'; @@ -55,18 +55,27 @@ Seobeo_Client_Response *hg_proxy_request( const char *method, const char *path, - const char *req_body) + const char *req_body, + const char *hg_custom) { char full_path[MAX_PATH]; snprintf(full_path, MAX_PATH, "http://%s:%s%s", HG_SERVE_HOST, HG_SERVE_PORT, path); + Seobeo_Log(SEOBEO_DEBUG, "HG Proxy PATH %s\n", full_path); Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create(full_path); - Seobeo_Client_Request_Add_Header_Array(p_req, "User-Agent: Seobeo/1.0 (Array Mode)"); + Seobeo_Client_Request_Set_Method(p_req, method); + Seobeo_Client_Request_Add_Header_Array(p_req, "User-Agent: Seobeo/1.0"); Seobeo_Client_Request_Add_Header_Array(p_req, "Accept: application/json"); - Seobeo_Client_Request_Add_Header_Array(p_req, "X-Test-Header: TestValue"); - if (strcmp(method, "POST")) - Seobeo_Client_Request_Set_Method(p_req, "POST"); - Seobeo_Client_Request_Set_Body(p_req, req_body, 0); + if (hg_custom && hg_custom[0] != '\0') + { + char buffer[1024]; + snprintf(buffer, 1024, "x-hgarg-1: %s", hg_custom); + Seobeo_Client_Request_Add_Header_Array(p_req, buffer); + Seobeo_Log(SEOBEO_DEBUG, "HG CUSTOM %s\n", buffer); + } + + if (req_body) + Seobeo_Client_Request_Set_Body(p_req, req_body, strlen(req_body)); Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req); Seobeo_Client_Request_Destroy(p_req); return p_resp; @@ -88,17 +97,14 @@ char hg_path[MAX_PATH]; if (strlen(safe_path) > 0) - snprintf(hg_path, sizeof(hg_path), "/file/tip/%s?style=json", safe_path); + snprintf(hg_path, sizeof(hg_path), "/file/tip/%s?style=json", safe_path); else - snprintf(hg_path, sizeof(hg_path), "/file/tip/?style=json"); + snprintf(hg_path, sizeof(hg_path), "/file/tip/?style=json"); - Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL); + Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL, NULL); Seobeo_Log(SEOBEO_DEBUG, "ApiListDirectory: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length); - char status[4]; - snprintf(status, 3, "%i", hg_response->status_code); - if (hg_response->status_code != 200) { Seobeo_Log(SEOBEO_DEBUG, "Failed to get directory from hg serve\n"); @@ -108,10 +114,14 @@ return resp; } + char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length); + char *temp2 = Dowa_Arena_Allocate(arena, 256); + snprintf(temp2, 256, "%zu", hg_response->body_length); + Dowa_HashMap_Push_Arena(resp, "status", "200", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); - Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); - Seobeo_Client_Response_Destroy(hg_response); + Dowa_HashMap_Push_Arena(resp, "body", temp1, arena); + Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena); return resp; } @@ -137,7 +147,7 @@ char hg_path[MAX_PATH]; snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", safe_path); - Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL); + Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL, NULL); Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length); @@ -160,11 +170,16 @@ Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); return resp; } - + + + char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length); + char *temp2 = Dowa_Arena_Allocate(arena, 256); + snprintf(temp2, 256, "%zu", hg_response->body_length); + Dowa_HashMap_Push_Arena(resp, "status", status, arena); Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); - Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); - Seobeo_Client_Response_Destroy(hg_response); + Dowa_HashMap_Push_Arena(resp, "body", temp1, arena); + Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena); return resp; } @@ -173,6 +188,117 @@ return ApiGetFile(req, arena); } +// Streaming handler for hg wire protocol - pipes data directly without buffering +void StreamHgWireProtocol(Seobeo_Handle *p_client, Seobeo_Request_Entry *req, Dowa_Arena *arena) +{ + void *method_kv = Dowa_HashMap_Get_Ptr(req, "HTTP_Method"); + const char *method = method_kv ? ((Seobeo_Request_Entry*)method_kv)->value : "GET"; + + void *query_kv = Dowa_HashMap_Get_Ptr(req, "QueryString"); + const char *query_string = query_kv ? ((Seobeo_Request_Entry*)query_kv)->value : ""; + + void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); + const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : ""; + + const char *hg_custom = req[7].value; + + Seobeo_Log(SEOBEO_DEBUG, "HG Stream Proxy: method=%s query=%s\n", method, query_string); + + // THINKING: Connect to hg serve + // This kinda blows, but not a good way to handle it since my client API assumes it is all stored in + // buffer and what not. + Seobeo_Handle *p_upstream = Seobeo_Stream_Handle_Client_Create(HG_SERVE_HOST, HG_SERVE_PORT, FALSE); + if (!p_upstream || p_upstream->socket < 0) + { + const char *err_resp = "HTTP/1.1 502 Bad Gateway\r\nContent-Length: 26\r\n\r\nFailed to connect upstream"; + Seobeo_Handle_Queue(p_client, (uint8*)err_resp, strlen(err_resp)); + Seobeo_Handle_Flush(p_client); + if (p_upstream) + Seobeo_Handle_Destroy(p_upstream); + return; + } + + // Create headers + // we only allow x-hgarg-1 and content-length + char request_buf[8192]; + int req_len = snprintf(request_buf, sizeof(request_buf), + "%s /?%s HTTP/1.1\r\n" + "Host: %s:%s\r\n" + "User-Agent: Seobeo/1.0\r\n" + "Connection: close\r\n", + method, query_string, HG_SERVE_HOST, HG_SERVE_PORT); + + if (hg_custom && hg_custom[0] != '\0') + req_len += snprintf(request_buf + req_len, sizeof(request_buf) - req_len, "x-hgarg-1: %s\r\n", hg_custom); + + if (req_body && req_body[0] != '\0') + req_len += snprintf(request_buf + req_len, sizeof(request_buf) - req_len, "Content-Length: %zu\r\n\r\n%s", strlen(req_body), req_body); + else + req_len += snprintf(request_buf + req_len, sizeof(request_buf) - req_len, "\r\n"); + + Seobeo_Handle_Queue(p_upstream, (uint8*)request_buf, req_len); + if (Seobeo_Handle_Flush(p_upstream) < 0) + { + const char *err_resp = "HTTP/1.1 502 Bad Gateway\r\nContent-Length: 21\r\n\r\nUpstream write failed"; + Seobeo_Handle_Queue(p_client, (uint8*)err_resp, strlen(err_resp)); + Seobeo_Handle_Flush(p_client); + Seobeo_Handle_Destroy(p_upstream); + return; + } + + // Responses + while (1) + { + int r = Seobeo_Handle_Read(p_upstream); + if (r < 0) + { + Seobeo_Handle_Destroy(p_upstream); + return; + } + if (p_upstream->read_buffer_len >= 4 && + strstr((char*)p_upstream->read_buffer, "\r\n\r\n") != NULL) + break; + if (r == 0) + continue; + } + + // TODO: Maybe make this into a separate function instead of internal function as doing this over and over again blows. + char *hdr_end = strstr((char*)p_upstream->read_buffer, "\r\n\r\n"); + if (!hdr_end) + { + Seobeo_Handle_Destroy(p_upstream); + return; + } + size_t hdr_len = hdr_end - (char*)p_upstream->read_buffer + 4; + Seobeo_Handle_Queue(p_client, p_upstream->read_buffer, hdr_len); + Seobeo_Handle_Flush(p_client); + + // All body + size_t body_in_buffer = p_upstream->read_buffer_len - hdr_len; + if (body_in_buffer > 0) + { + Seobeo_Handle_Queue(p_client, p_upstream->read_buffer + hdr_len, body_in_buffer); + Seobeo_Handle_Flush(p_client); + } + Seobeo_Handle_Consume(p_upstream, p_upstream->read_buffer_len); + while (1) + { + int n = Seobeo_Handle_Read(p_upstream); + if (n > 0) + { + Seobeo_Handle_Queue(p_client, p_upstream->read_buffer, p_upstream->read_buffer_len); + Seobeo_Handle_Flush(p_client); + Seobeo_Handle_Consume(p_upstream, p_upstream->read_buffer_len); + } + else if (n == -2) + break; + else if (n < 0) + break; + } + + Seobeo_Handle_Destroy(p_upstream); +} + Seobeo_Request_Entry* ApiHgWireProtocol(Seobeo_Request_Entry *req, Dowa_Arena *arena) { Seobeo_Request_Entry *resp = NULL; @@ -187,27 +313,32 @@ const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : ""; size_t body_len = strlen(req_body); + const char *hg_custom = req[7].value; Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: method=%s query=%s body_len=%zu\n", method, query_string, body_len); + Seobeo_Client_Response *hg_response; - if (strlen(query_string) > 0) - { - char temp_path[MAX_PATH]; - snprintf(temp_path, MAX_PATH, "?%s", query_string); - hg_response = hg_proxy_request(method, query_string, req_body); - } - else - hg_response = hg_proxy_request(method, "", req_body); + + char hg_path[MAX_PATH]; + snprintf(hg_path, sizeof(hg_path), "/?%s", query_string); + + hg_response = hg_proxy_request(method, hg_path, req_body, hg_custom); Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", hg_response->body_length); Seobeo_Request_Entry *kv = Dowa_HashMap_Get_Ptr(hg_response->headers, "Content-Type"); - char status[4]; - snprintf(status, 3, "%i", hg_response->status_code); + char *status = Dowa_Arena_Allocate(arena, 5); + snprintf(status, 4, "%i", hg_response->status_code); + + // Use binary-safe copy to handle null bytes in mercurial bundle data + char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length); + char *temp2 = Dowa_Arena_Allocate(arena, 256); + snprintf(temp2, 256, "%zu", hg_response->body_length); Dowa_HashMap_Push_Arena(resp, "status", status, arena); - Dowa_HashMap_Push_Arena(resp, "content-type", kv ? kv->value : "", arena); - Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); + Dowa_HashMap_Push_Arena(resp, "content-type", kv->value, arena); + Dowa_HashMap_Push_Arena(resp, "body", temp1, arena); + Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena); return resp; } @@ -219,12 +350,13 @@ Seobeo_Router_Register("GET", "/api/repo/file", ApiGetFile); Seobeo_Router_Register("GET", "/api/repo/readme", ApiGetReadme); - Seobeo_Router_Register("GET", "/repo", ApiHgWireProtocol); - Seobeo_Router_Register("POST", "/repo", ApiHgWireProtocol); + // Use streaming handler for hg wire protocol... + Seobeo_Router_Register_Stream("GET", "/repo", StreamHgWireProtocol); + Seobeo_Router_Register_Stream("POST", "/repo", StreamHgWireProtocol); printf("Starting on Port 6970...\n"); - int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 4); + int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 1); Seobeo_Router_Destroy();