comparison seobeo/s_http_client.c @ 221:ce7f4400c2de hg-web

[hg-web] Harden forge and add changeset UI
author MrJuneJune <me@mrjunejune.com>
date Sun, 02 Aug 2026 09:01:24 -0700
parents 71ad34a8bc9a
children
comparison
equal deleted inserted replaced
219:8c9bb0b0759e 221:ce7f4400c2de
1 #include "seobeo/seobeo.h" 1 #include "seobeo/seobeo.h"
2 #include <ctype.h> 2 #include <ctype.h>
3 #include <time.h>
4
5 static int64_t Seobeo_Client_Monotonic_Milliseconds(void)
6 {
7 struct timespec now;
8 clock_gettime(CLOCK_MONOTONIC, &now);
9 return (int64_t)now.tv_sec * 1000 + now.tv_nsec / 1000000;
10 }
11
12 static boolean Seobeo_Client_Read_Timed_Out(int32 timeout_ms, int64_t last_progress_ms)
13 {
14 return timeout_ms > 0 &&
15 Seobeo_Client_Monotonic_Milliseconds() - last_progress_ms >= timeout_ms;
16 }
17
18 static size_t Seobeo_Client_Find_Header_Length(const uint8 *buffer, size_t length)
19 {
20 if (!buffer || length < 4)
21 return 0;
22 for (size_t i = 0; i + 3 < length; i++)
23 {
24 if (buffer[i] == '\r' && buffer[i + 1] == '\n' &&
25 buffer[i + 2] == '\r' && buffer[i + 3] == '\n')
26 return i + 4;
27 }
28 return 0;
29 }
30
31 static const char *Seobeo_Client_Header_Value(
32 Seobeo_Request_Entry *headers,
33 const char *name)
34 {
35 if (!headers || !name)
36 return NULL;
37 for (size_t i = 0; i < Dowa_Array_Length(headers); i++)
38 {
39 if (headers[i].key && strcasecmp(headers[i].key, name) == 0)
40 return headers[i].value;
41 }
42 return NULL;
43 }
3 44
4 static void Seobeo_Client_Parse_Url(const char *url, char **p_host, 45 static void Seobeo_Client_Parse_Url(const char *url, char **p_host,
5 char **p_port, char **p_path, boolean *p_use_tls, Dowa_Arena *p_arena) 46 char **p_port, char **p_path, boolean *p_use_tls, Dowa_Arena *p_arena)
6 { 47 {
7 if (!url) 48 if (!url)
85 p_req->method = Dowa_Arena_Allocate(p_req->p_arena, 4); 126 p_req->method = Dowa_Arena_Allocate(p_req->p_arena, 4);
86 strcpy(p_req->method, "GET"); 127 strcpy(p_req->method, "GET");
87 128
88 p_req->follow_redirects = FALSE; 129 p_req->follow_redirects = FALSE;
89 p_req->max_redirects = 10; 130 p_req->max_redirects = 10;
131 p_req->timeout_ms = 0;
90 132
91 return p_req; 133 return p_req;
92 } 134 }
93 135
94 void Seobeo_Client_Request_Set_Method(Seobeo_Client_Request *p_req, const char *method) 136 void Seobeo_Client_Request_Set_Method(Seobeo_Client_Request *p_req, const char *method)
147 189
148 p_req->follow_redirects = follow; 190 p_req->follow_redirects = follow;
149 p_req->max_redirects = max_redirects > 0 ? max_redirects : 10; 191 p_req->max_redirects = max_redirects > 0 ? max_redirects : 10;
150 } 192 }
151 193
194 void Seobeo_Client_Request_Set_Timeout_Milliseconds(Seobeo_Client_Request *p_req, int32 timeout_ms)
195 {
196 if (!p_req)
197 return;
198 p_req->timeout_ms = timeout_ms > 0 ? timeout_ms : 0;
199 }
200
152 void Seobeo_Client_Request_Set_Download_Path(Seobeo_Client_Request *p_req, const char *path) 201 void Seobeo_Client_Request_Set_Download_Path(Seobeo_Client_Request *p_req, const char *path)
153 { 202 {
154 if (!p_req || !path) 203 if (!p_req || !path)
155 return; 204 return;
156 205
221 offset += snprintf(buffer + offset, buffer_size - offset, "\r\n"); 270 offset += snprintf(buffer + offset, buffer_size - offset, "\r\n");
222 271
223 return offset; 272 return offset;
224 } 273 }
225 274
226 static Seobeo_Client_Response *Seobeo_Client_Parse_Response(Seobeo_Handle *p_handle, const char *download_path) 275 static Seobeo_Client_Response *Seobeo_Client_Parse_Response(
276 Seobeo_Handle *p_handle,
277 const char *download_path,
278 int32 timeout_ms)
227 { 279 {
228 Seobeo_Client_Response *p_resp = malloc(sizeof(Seobeo_Client_Response)); 280 Seobeo_Client_Response *p_resp = malloc(sizeof(Seobeo_Client_Response));
229 if (!p_resp) 281 if (!p_resp)
230 return NULL; 282 return NULL;
231 283
236 { 288 {
237 free(p_resp); 289 free(p_resp);
238 return NULL; 290 return NULL;
239 } 291 }
240 292
293 int64_t last_progress_ms = Seobeo_Client_Monotonic_Milliseconds();
241 while (TRUE) 294 while (TRUE)
242 { 295 {
243 int r = Seobeo_Handle_Read(p_handle); 296 int r = Seobeo_Handle_Read(p_handle);
244 if (r < 0)
245 return p_resp;
246 if (r == -2) 297 if (r == -2)
247 break; 298 break;
248 299 if (r < 0)
249 if (p_handle->read_buffer_len >= 4 && strstr((char*)p_handle->read_buffer, "\r\n\r\n") != NULL) 300 {
301 Seobeo_Client_Response_Destroy(p_resp);
302 return NULL;
303 }
304 if (r > 0)
305 last_progress_ms = Seobeo_Client_Monotonic_Milliseconds();
306
307 if (Seobeo_Client_Find_Header_Length(
308 p_handle->read_buffer, p_handle->read_buffer_len) > 0)
250 break; 309 break;
251 310
252 if (r == 0) 311 if (r == 0)
312 {
313 if (Seobeo_Client_Read_Timed_Out(timeout_ms, last_progress_ms))
314 {
315 Seobeo_Log(SEOBEO_ERROR, "HTTP response header timed out\n");
316 Seobeo_Client_Response_Destroy(p_resp);
317 return NULL;
318 }
319 usleep(1000);
253 continue; 320 continue;
254 } 321 }
255 322 }
256 char *buf = (char*)p_handle->read_buffer; 323
257 char *hdr_end = strstr(buf, "\r\n\r\n"); 324 size_t hdr_len = Seobeo_Client_Find_Header_Length(
258 if (!hdr_end) 325 p_handle->read_buffer, p_handle->read_buffer_len);
259 return p_resp; 326 if (hdr_len == 0)
260 327 {
261 size_t hdr_len = hdr_end - buf + 4; 328 Seobeo_Client_Response_Destroy(p_resp);
329 return NULL;
330 }
331
332 char *buf = Dowa_Arena_Allocate(p_resp->p_arena, hdr_len + 1);
333 memcpy(buf, p_handle->read_buffer, hdr_len);
334 buf[hdr_len] = '\0';
335 char *hdr_end = buf + hdr_len - 4;
262 336
263 char version[16]; 337 char version[16];
264 int status_code; 338 int status_code;
265 char status_text[256]; 339 char status_text[256];
266 int scan_result = sscanf(buf, "%15s %d %255[^\r\n]", version, &status_code, status_text); 340 int scan_result = sscanf(buf, "%15s %d %255[^\r\n]", version, &status_code, status_text);
326 line = next + 2; 400 line = next + 2;
327 } 401 }
328 402
329 Seobeo_Handle_Consume(p_handle, (uint32)hdr_len); 403 Seobeo_Handle_Consume(p_handle, (uint32)hdr_len);
330 404
331 void *p_cl_kv = Dowa_HashMap_Get_Ptr(p_resp->headers, "Content-Length");
332 size_t body_len = 0; 405 size_t body_len = 0;
333 if (p_cl_kv) 406 const char *content_length = Seobeo_Client_Header_Value(
334 { 407 p_resp->headers, "Content-Length");
335 const char *content_length_str = ((Seobeo_Request_Entry*)p_cl_kv)->value; 408 if (content_length)
336 body_len = atoi(content_length_str); 409 {
410 body_len = (size_t)strtoull(content_length, NULL, 10);
337 } 411 }
338 412
339 FILE *p_file = NULL; 413 FILE *p_file = NULL;
340 if (download_path) 414 if (download_path)
341 { 415 {
364 else 438 else
365 memcpy(body + total_read, p_handle->read_buffer, to_copy); 439 memcpy(body + total_read, p_handle->read_buffer, to_copy);
366 440
367 total_read += to_copy; 441 total_read += to_copy;
368 Seobeo_Handle_Consume(p_handle, (uint32)to_copy); 442 Seobeo_Handle_Consume(p_handle, (uint32)to_copy);
443 last_progress_ms = Seobeo_Client_Monotonic_Milliseconds();
369 } 444 }
370 445
371 if (total_read < body_len) 446 if (total_read < body_len)
372 { 447 {
373 int r = Seobeo_Handle_Read(p_handle); 448 int r = Seobeo_Handle_Read(p_handle);
374 if (r < 0 || r == -2) 449 if (r == -2 || r < 0)
375 break; 450 {
451 if (p_file) fclose(p_file);
452 Seobeo_Client_Response_Destroy(p_resp);
453 return NULL;
454 }
455 if (r > 0)
456 last_progress_ms = Seobeo_Client_Monotonic_Milliseconds();
376 if (r == 0) 457 if (r == 0)
458 {
459 if (Seobeo_Client_Read_Timed_Out(timeout_ms, last_progress_ms))
460 {
461 Seobeo_Log(SEOBEO_ERROR, "HTTP response body timed out\n");
462 if (p_file) fclose(p_file);
463 Seobeo_Client_Response_Destroy(p_resp);
464 return NULL;
465 }
466 usleep(1000);
377 continue; 467 continue;
468 }
378 } 469 }
379 } 470 }
380 471
381 if (!download_path) 472 if (!download_path)
382 { 473 {
395 size_t used = 0; 486 size_t used = 0;
396 char *body = download_path ? NULL : Dowa_Arena_Allocate(p_resp->p_arena, cap); 487 char *body = download_path ? NULL : Dowa_Arena_Allocate(p_resp->p_arena, cap);
397 488
398 while (1) 489 while (1)
399 { 490 {
400 int n = Seobeo_Handle_Read(p_handle); 491 int n = p_handle->read_buffer_len > 0
492 ? (int)p_handle->read_buffer_len
493 : Seobeo_Handle_Read(p_handle);
401 if (n > 0) 494 if (n > 0)
402 { 495 {
496 last_progress_ms = Seobeo_Client_Monotonic_Milliseconds();
403 if (download_path) 497 if (download_path)
404 { 498 {
405 fwrite(p_handle->read_buffer, 1, p_handle->read_buffer_len, p_file); 499 fwrite(p_handle->read_buffer, 1, p_handle->read_buffer_len, p_file);
406 used += p_handle->read_buffer_len; 500 used += p_handle->read_buffer_len;
407 } 501 }
418 Seobeo_Handle_Consume(p_handle, (uint32)p_handle->read_buffer_len); 512 Seobeo_Handle_Consume(p_handle, (uint32)p_handle->read_buffer_len);
419 } 513 }
420 else if (n == -2) 514 else if (n == -2)
421 break; 515 break;
422 else if (n == 0) 516 else if (n == 0)
517 {
518 if (Seobeo_Client_Read_Timed_Out(timeout_ms, last_progress_ms))
519 {
520 Seobeo_Log(SEOBEO_ERROR, "HTTP response body timed out\n");
521 if (p_file) fclose(p_file);
522 Seobeo_Client_Response_Destroy(p_resp);
523 return NULL;
524 }
525 usleep(1000);
423 continue; 526 continue;
527 }
424 else 528 else
425 break; 529 {
530 if (p_file) fclose(p_file);
531 Seobeo_Client_Response_Destroy(p_resp);
532 return NULL;
533 }
426 } 534 }
427 535
428 if (!download_path) 536 if (!download_path)
429 { 537 {
430 p_resp->body = body; 538 p_resp->body = body;
466 { 574 {
467 Seobeo_Handle_Destroy(p_handle); 575 Seobeo_Handle_Destroy(p_handle);
468 return NULL; 576 return NULL;
469 } 577 }
470 578
471 Seobeo_Client_Response *p_resp = Seobeo_Client_Parse_Response(p_handle, p_req->download_path); 579 Seobeo_Client_Response *p_resp =
580 Seobeo_Client_Parse_Response(p_handle, p_req->download_path, p_req->timeout_ms);
472 581
473 Seobeo_Handle_Destroy(p_handle); 582 Seobeo_Handle_Destroy(p_handle);
474 583
475 return p_resp; 584 return p_resp;
476 } 585 }