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 }