Mercurial
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 174:1ba8c1df082c | 175:71ad34a8bc9a |
|---|---|
| 28 char *result = Dowa_Arena_Allocate(arena, len + 1); | 28 char *result = Dowa_Arena_Allocate(arena, len + 1); |
| 29 size_t j = 0; | 29 size_t j = 0; |
| 30 | 30 |
| 31 for (size_t i = 0; i < len; i++) | 31 for (size_t i = 0; i < len; i++) |
| 32 { | 32 { |
| 33 if (input_path[i] == '.' && (i == 0 || input_path[i-1] == '/')) { | 33 if (input_path[i] == '.' && (i == 0 || input_path[i-1] == '/')) { |
| 34 if (i + 1 < len && input_path[i+1] == '.') { | 34 if (i + 1 < len && input_path[i+1] == '.') { |
| 35 // Skip ".." | 35 // Skip ".." |
| 36 i++; | 36 i++; |
| 37 continue; | 37 continue; |
| 38 } | |
| 39 // Skip "." | |
| 40 continue; | |
| 38 } | 41 } |
| 39 // Skip "." | 42 result[j++] = input_path[i]; |
| 40 continue; | |
| 41 } | |
| 42 result[j++] = input_path[i]; | |
| 43 } | 43 } |
| 44 result[j] = '\0'; | 44 result[j] = '\0'; |
| 45 | 45 |
| 46 // Remove leading/trailing slashes | 46 // Remove leading/trailing slashes |
| 47 while (result[0] == '/') | 47 while (result[0] == '/') |
| 53 } | 53 } |
| 54 | 54 |
| 55 Seobeo_Client_Response *hg_proxy_request( | 55 Seobeo_Client_Response *hg_proxy_request( |
| 56 const char *method, | 56 const char *method, |
| 57 const char *path, | 57 const char *path, |
| 58 const char *req_body) | 58 const char *req_body, |
| 59 const char *hg_custom) | |
| 59 { | 60 { |
| 60 char full_path[MAX_PATH]; | 61 char full_path[MAX_PATH]; |
| 61 snprintf(full_path, MAX_PATH, "http://%s:%s%s", HG_SERVE_HOST, HG_SERVE_PORT, path); | 62 snprintf(full_path, MAX_PATH, "http://%s:%s%s", HG_SERVE_HOST, HG_SERVE_PORT, path); |
| 63 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy PATH %s\n", full_path); | |
| 62 Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create(full_path); | 64 Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create(full_path); |
| 63 Seobeo_Client_Request_Add_Header_Array(p_req, "User-Agent: Seobeo/1.0 (Array Mode)"); | 65 Seobeo_Client_Request_Set_Method(p_req, method); |
| 66 Seobeo_Client_Request_Add_Header_Array(p_req, "User-Agent: Seobeo/1.0"); | |
| 64 Seobeo_Client_Request_Add_Header_Array(p_req, "Accept: application/json"); | 67 Seobeo_Client_Request_Add_Header_Array(p_req, "Accept: application/json"); |
| 65 Seobeo_Client_Request_Add_Header_Array(p_req, "X-Test-Header: TestValue"); | 68 |
| 66 if (strcmp(method, "POST")) | 69 if (hg_custom && hg_custom[0] != '\0') |
| 67 Seobeo_Client_Request_Set_Method(p_req, "POST"); | 70 { |
| 68 | 71 char buffer[1024]; |
| 69 Seobeo_Client_Request_Set_Body(p_req, req_body, 0); | 72 snprintf(buffer, 1024, "x-hgarg-1: %s", hg_custom); |
| 73 Seobeo_Client_Request_Add_Header_Array(p_req, buffer); | |
| 74 Seobeo_Log(SEOBEO_DEBUG, "HG CUSTOM %s\n", buffer); | |
| 75 } | |
| 76 | |
| 77 if (req_body) | |
| 78 Seobeo_Client_Request_Set_Body(p_req, req_body, strlen(req_body)); | |
| 70 Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req); | 79 Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req); |
| 71 Seobeo_Client_Request_Destroy(p_req); | 80 Seobeo_Client_Request_Destroy(p_req); |
| 72 return p_resp; | 81 return p_resp; |
| 73 } | 82 } |
| 74 | 83 |
| 86 | 95 |
| 87 Seobeo_Log(SEOBEO_INFO, "ApiListDirectory: safe_path='%s'\n", safe_path); | 96 Seobeo_Log(SEOBEO_INFO, "ApiListDirectory: safe_path='%s'\n", safe_path); |
| 88 | 97 |
| 89 char hg_path[MAX_PATH]; | 98 char hg_path[MAX_PATH]; |
| 90 if (strlen(safe_path) > 0) | 99 if (strlen(safe_path) > 0) |
| 91 snprintf(hg_path, sizeof(hg_path), "/file/tip/%s?style=json", safe_path); | 100 snprintf(hg_path, sizeof(hg_path), "/file/tip/%s?style=json", safe_path); |
| 92 else | 101 else |
| 93 snprintf(hg_path, sizeof(hg_path), "/file/tip/?style=json"); | 102 snprintf(hg_path, sizeof(hg_path), "/file/tip/?style=json"); |
| 94 | 103 |
| 95 Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL); | 104 Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL, NULL); |
| 96 | 105 |
| 97 Seobeo_Log(SEOBEO_DEBUG, "ApiListDirectory: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length); | 106 Seobeo_Log(SEOBEO_DEBUG, "ApiListDirectory: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length); |
| 98 | |
| 99 char status[4]; | |
| 100 snprintf(status, 3, "%i", hg_response->status_code); | |
| 101 | 107 |
| 102 if (hg_response->status_code != 200) | 108 if (hg_response->status_code != 200) |
| 103 { | 109 { |
| 104 Seobeo_Log(SEOBEO_DEBUG, "Failed to get directory from hg serve\n"); | 110 Seobeo_Log(SEOBEO_DEBUG, "Failed to get directory from hg serve\n"); |
| 105 Dowa_HashMap_Push_Arena(resp, "status", "502", arena); | 111 Dowa_HashMap_Push_Arena(resp, "status", "502", arena); |
| 106 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | 112 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); |
| 107 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to connect to hg serve\"}", arena); | 113 Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to connect to hg serve\"}", arena); |
| 108 return resp; | 114 return resp; |
| 109 } | 115 } |
| 110 | 116 |
| 117 char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length); | |
| 118 char *temp2 = Dowa_Arena_Allocate(arena, 256); | |
| 119 snprintf(temp2, 256, "%zu", hg_response->body_length); | |
| 120 | |
| 111 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); | 121 Dowa_HashMap_Push_Arena(resp, "status", "200", arena); |
| 112 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); | 122 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); |
| 113 Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); | 123 Dowa_HashMap_Push_Arena(resp, "body", temp1, arena); |
| 114 Seobeo_Client_Response_Destroy(hg_response); | 124 Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena); |
| 115 return resp; | 125 return resp; |
| 116 } | 126 } |
| 117 | 127 |
| 118 Seobeo_Request_Entry* ApiGetFile(Seobeo_Request_Entry *req, Dowa_Arena *arena) | 128 Seobeo_Request_Entry* ApiGetFile(Seobeo_Request_Entry *req, Dowa_Arena *arena) |
| 119 { | 129 { |
| 135 return resp; | 145 return resp; |
| 136 } | 146 } |
| 137 | 147 |
| 138 char hg_path[MAX_PATH]; | 148 char hg_path[MAX_PATH]; |
| 139 snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", safe_path); | 149 snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", safe_path); |
| 140 Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL); | 150 Seobeo_Client_Response *hg_response = hg_proxy_request("GET", hg_path, NULL, NULL); |
| 141 | 151 |
| 142 Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length); | 152 Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length); |
| 143 | 153 |
| 144 char status[4]; | 154 char status[4]; |
| 145 snprintf(status, 3, "%i", hg_response->status_code); | 155 snprintf(status, 3, "%i", hg_response->status_code); |
| 158 Dowa_HashMap_Push_Arena(resp, "status", status, arena); | 168 Dowa_HashMap_Push_Arena(resp, "status", status, arena); |
| 159 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | 169 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); |
| 160 Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); | 170 Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); |
| 161 return resp; | 171 return resp; |
| 162 } | 172 } |
| 163 | 173 |
| 174 | |
| 175 char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length); | |
| 176 char *temp2 = Dowa_Arena_Allocate(arena, 256); | |
| 177 snprintf(temp2, 256, "%zu", hg_response->body_length); | |
| 178 | |
| 164 Dowa_HashMap_Push_Arena(resp, "status", status, arena); | 179 Dowa_HashMap_Push_Arena(resp, "status", status, arena); |
| 165 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); | 180 Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); |
| 166 Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); | 181 Dowa_HashMap_Push_Arena(resp, "body", temp1, arena); |
| 167 Seobeo_Client_Response_Destroy(hg_response); | 182 Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena); |
| 168 | 183 |
| 169 return resp; | 184 return resp; |
| 170 } | 185 } |
| 171 | 186 |
| 172 Seobeo_Request_Entry* ApiGetReadme(Seobeo_Request_Entry *req, Dowa_Arena *arena) { | 187 Seobeo_Request_Entry* ApiGetReadme(Seobeo_Request_Entry *req, Dowa_Arena *arena) { |
| 173 return ApiGetFile(req, arena); | 188 return ApiGetFile(req, arena); |
| 189 } | |
| 190 | |
| 191 // Streaming handler for hg wire protocol - pipes data directly without buffering | |
| 192 void StreamHgWireProtocol(Seobeo_Handle *p_client, Seobeo_Request_Entry *req, Dowa_Arena *arena) | |
| 193 { | |
| 194 void *method_kv = Dowa_HashMap_Get_Ptr(req, "HTTP_Method"); | |
| 195 const char *method = method_kv ? ((Seobeo_Request_Entry*)method_kv)->value : "GET"; | |
| 196 | |
| 197 void *query_kv = Dowa_HashMap_Get_Ptr(req, "QueryString"); | |
| 198 const char *query_string = query_kv ? ((Seobeo_Request_Entry*)query_kv)->value : ""; | |
| 199 | |
| 200 void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); | |
| 201 const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : ""; | |
| 202 | |
| 203 const char *hg_custom = req[7].value; | |
| 204 | |
| 205 Seobeo_Log(SEOBEO_DEBUG, "HG Stream Proxy: method=%s query=%s\n", method, query_string); | |
| 206 | |
| 207 // THINKING: Connect to hg serve | |
| 208 // This kinda blows, but not a good way to handle it since my client API assumes it is all stored in | |
| 209 // buffer and what not. | |
| 210 Seobeo_Handle *p_upstream = Seobeo_Stream_Handle_Client_Create(HG_SERVE_HOST, HG_SERVE_PORT, FALSE); | |
| 211 if (!p_upstream || p_upstream->socket < 0) | |
| 212 { | |
| 213 const char *err_resp = "HTTP/1.1 502 Bad Gateway\r\nContent-Length: 26\r\n\r\nFailed to connect upstream"; | |
| 214 Seobeo_Handle_Queue(p_client, (uint8*)err_resp, strlen(err_resp)); | |
| 215 Seobeo_Handle_Flush(p_client); | |
| 216 if (p_upstream) | |
| 217 Seobeo_Handle_Destroy(p_upstream); | |
| 218 return; | |
| 219 } | |
| 220 | |
| 221 // Create headers | |
| 222 // we only allow x-hgarg-1 and content-length | |
| 223 char request_buf[8192]; | |
| 224 int req_len = snprintf(request_buf, sizeof(request_buf), | |
| 225 "%s /?%s HTTP/1.1\r\n" | |
| 226 "Host: %s:%s\r\n" | |
| 227 "User-Agent: Seobeo/1.0\r\n" | |
| 228 "Connection: close\r\n", | |
| 229 method, query_string, HG_SERVE_HOST, HG_SERVE_PORT); | |
| 230 | |
| 231 if (hg_custom && hg_custom[0] != '\0') | |
| 232 req_len += snprintf(request_buf + req_len, sizeof(request_buf) - req_len, "x-hgarg-1: %s\r\n", hg_custom); | |
| 233 | |
| 234 if (req_body && req_body[0] != '\0') | |
| 235 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); | |
| 236 else | |
| 237 req_len += snprintf(request_buf + req_len, sizeof(request_buf) - req_len, "\r\n"); | |
| 238 | |
| 239 Seobeo_Handle_Queue(p_upstream, (uint8*)request_buf, req_len); | |
| 240 if (Seobeo_Handle_Flush(p_upstream) < 0) | |
| 241 { | |
| 242 const char *err_resp = "HTTP/1.1 502 Bad Gateway\r\nContent-Length: 21\r\n\r\nUpstream write failed"; | |
| 243 Seobeo_Handle_Queue(p_client, (uint8*)err_resp, strlen(err_resp)); | |
| 244 Seobeo_Handle_Flush(p_client); | |
| 245 Seobeo_Handle_Destroy(p_upstream); | |
| 246 return; | |
| 247 } | |
| 248 | |
| 249 // Responses | |
| 250 while (1) | |
| 251 { | |
| 252 int r = Seobeo_Handle_Read(p_upstream); | |
| 253 if (r < 0) | |
| 254 { | |
| 255 Seobeo_Handle_Destroy(p_upstream); | |
| 256 return; | |
| 257 } | |
| 258 if (p_upstream->read_buffer_len >= 4 && | |
| 259 strstr((char*)p_upstream->read_buffer, "\r\n\r\n") != NULL) | |
| 260 break; | |
| 261 if (r == 0) | |
| 262 continue; | |
| 263 } | |
| 264 | |
| 265 // TODO: Maybe make this into a separate function instead of internal function as doing this over and over again blows. | |
| 266 char *hdr_end = strstr((char*)p_upstream->read_buffer, "\r\n\r\n"); | |
| 267 if (!hdr_end) | |
| 268 { | |
| 269 Seobeo_Handle_Destroy(p_upstream); | |
| 270 return; | |
| 271 } | |
| 272 size_t hdr_len = hdr_end - (char*)p_upstream->read_buffer + 4; | |
| 273 Seobeo_Handle_Queue(p_client, p_upstream->read_buffer, hdr_len); | |
| 274 Seobeo_Handle_Flush(p_client); | |
| 275 | |
| 276 // All body | |
| 277 size_t body_in_buffer = p_upstream->read_buffer_len - hdr_len; | |
| 278 if (body_in_buffer > 0) | |
| 279 { | |
| 280 Seobeo_Handle_Queue(p_client, p_upstream->read_buffer + hdr_len, body_in_buffer); | |
| 281 Seobeo_Handle_Flush(p_client); | |
| 282 } | |
| 283 Seobeo_Handle_Consume(p_upstream, p_upstream->read_buffer_len); | |
| 284 while (1) | |
| 285 { | |
| 286 int n = Seobeo_Handle_Read(p_upstream); | |
| 287 if (n > 0) | |
| 288 { | |
| 289 Seobeo_Handle_Queue(p_client, p_upstream->read_buffer, p_upstream->read_buffer_len); | |
| 290 Seobeo_Handle_Flush(p_client); | |
| 291 Seobeo_Handle_Consume(p_upstream, p_upstream->read_buffer_len); | |
| 292 } | |
| 293 else if (n == -2) | |
| 294 break; | |
| 295 else if (n < 0) | |
| 296 break; | |
| 297 } | |
| 298 | |
| 299 Seobeo_Handle_Destroy(p_upstream); | |
| 174 } | 300 } |
| 175 | 301 |
| 176 Seobeo_Request_Entry* ApiHgWireProtocol(Seobeo_Request_Entry *req, Dowa_Arena *arena) | 302 Seobeo_Request_Entry* ApiHgWireProtocol(Seobeo_Request_Entry *req, Dowa_Arena *arena) |
| 177 { | 303 { |
| 178 Seobeo_Request_Entry *resp = NULL; | 304 Seobeo_Request_Entry *resp = NULL; |
| 185 | 311 |
| 186 void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); | 312 void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); |
| 187 const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : ""; | 313 const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : ""; |
| 188 size_t body_len = strlen(req_body); | 314 size_t body_len = strlen(req_body); |
| 189 | 315 |
| 316 const char *hg_custom = req[7].value; | |
| 190 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: method=%s query=%s body_len=%zu\n", method, query_string, body_len); | 317 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: method=%s query=%s body_len=%zu\n", method, query_string, body_len); |
| 318 | |
| 191 Seobeo_Client_Response *hg_response; | 319 Seobeo_Client_Response *hg_response; |
| 192 if (strlen(query_string) > 0) | 320 |
| 193 { | 321 char hg_path[MAX_PATH]; |
| 194 char temp_path[MAX_PATH]; | 322 snprintf(hg_path, sizeof(hg_path), "/?%s", query_string); |
| 195 snprintf(temp_path, MAX_PATH, "?%s", query_string); | 323 |
| 196 hg_response = hg_proxy_request(method, query_string, req_body); | 324 hg_response = hg_proxy_request(method, hg_path, req_body, hg_custom); |
| 197 } | |
| 198 else | |
| 199 hg_response = hg_proxy_request(method, "", req_body); | |
| 200 | 325 |
| 201 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", hg_response->body_length); | 326 Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", hg_response->body_length); |
| 202 | 327 |
| 203 Seobeo_Request_Entry *kv = Dowa_HashMap_Get_Ptr(hg_response->headers, "Content-Type"); | 328 Seobeo_Request_Entry *kv = Dowa_HashMap_Get_Ptr(hg_response->headers, "Content-Type"); |
| 204 | 329 |
| 205 char status[4]; | 330 char *status = Dowa_Arena_Allocate(arena, 5); |
| 206 snprintf(status, 3, "%i", hg_response->status_code); | 331 snprintf(status, 4, "%i", hg_response->status_code); |
| 332 | |
| 333 // Use binary-safe copy to handle null bytes in mercurial bundle data | |
| 334 char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length); | |
| 335 char *temp2 = Dowa_Arena_Allocate(arena, 256); | |
| 336 snprintf(temp2, 256, "%zu", hg_response->body_length); | |
| 207 | 337 |
| 208 Dowa_HashMap_Push_Arena(resp, "status", status, arena); | 338 Dowa_HashMap_Push_Arena(resp, "status", status, arena); |
| 209 Dowa_HashMap_Push_Arena(resp, "content-type", kv ? kv->value : "", arena); | 339 Dowa_HashMap_Push_Arena(resp, "content-type", kv->value, arena); |
| 210 Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena); | 340 Dowa_HashMap_Push_Arena(resp, "body", temp1, arena); |
| 341 Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena); | |
| 211 | 342 |
| 212 return resp; | 343 return resp; |
| 213 } | 344 } |
| 214 | 345 |
| 215 int main(void) { | 346 int main(void) { |
| 217 | 348 |
| 218 Seobeo_Router_Register("GET", "/api/repo/list", ApiListDirectory); | 349 Seobeo_Router_Register("GET", "/api/repo/list", ApiListDirectory); |
| 219 Seobeo_Router_Register("GET", "/api/repo/file", ApiGetFile); | 350 Seobeo_Router_Register("GET", "/api/repo/file", ApiGetFile); |
| 220 Seobeo_Router_Register("GET", "/api/repo/readme", ApiGetReadme); | 351 Seobeo_Router_Register("GET", "/api/repo/readme", ApiGetReadme); |
| 221 | 352 |
| 222 Seobeo_Router_Register("GET", "/repo", ApiHgWireProtocol); | 353 // Use streaming handler for hg wire protocol... |
| 223 Seobeo_Router_Register("POST", "/repo", ApiHgWireProtocol); | 354 Seobeo_Router_Register_Stream("GET", "/repo", StreamHgWireProtocol); |
| 355 Seobeo_Router_Register_Stream("POST", "/repo", StreamHgWireProtocol); | |
| 224 | 356 |
| 225 printf("Starting on Port 6970...\n"); | 357 printf("Starting on Port 6970...\n"); |
| 226 | 358 |
| 227 int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 4); | 359 int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 1); |
| 228 | 360 |
| 229 Seobeo_Router_Destroy(); | 361 Seobeo_Router_Destroy(); |
| 230 | 362 |
| 231 return result; | 363 return result; |
| 232 } | 364 } |