comparison seobeo/s_http_client.c @ 119:c39582f937e5

[Seobeo Client] Added client side logic which will be used for all my other calls instead of curl.
author June Park <parkjune1995@gmail.com>
date Wed, 07 Jan 2026 16:05:57 -0800
parents
children 058de208e640
comparison
equal deleted inserted replaced
118:249881ceff7b 119:c39582f937e5
1 #include "seobeo/seobeo.h"
2 #include <ctype.h>
3
4 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)
6 {
7 if (!url)
8 return;
9
10 const char *start = url;
11 *p_use_tls = FALSE;
12
13 if (strncmp(url, "https://", 8) == 0)
14 {
15 *p_use_tls = TRUE;
16 start = url + 8;
17 }
18 else if (strncmp(url, "http://", 7) == 0)
19 {
20 *p_use_tls = FALSE;
21 start = url + 7;
22 }
23
24 const char *slash = strchr(start, '/');
25 const char *colon = strchr(start, ':');
26
27 if (colon && (!slash || colon < slash))
28 {
29 size_t host_len = colon - start;
30 *p_host = Dowa_Arena_Allocate(p_arena, host_len + 1);
31 memcpy(*p_host, start, host_len);
32 (*p_host)[host_len] = '\0';
33
34 const char *port_start = colon + 1;
35 size_t port_len = slash ? (slash - port_start) : strlen(port_start);
36 *p_port = Dowa_Arena_Allocate(p_arena, port_len + 1);
37 memcpy(*p_port, port_start, port_len);
38 (*p_port)[port_len] = '\0';
39 }
40 else
41 {
42 size_t host_len = slash ? (slash - start) : strlen(start);
43 *p_host = Dowa_Arena_Allocate(p_arena, host_len + 1);
44 memcpy(*p_host, start, host_len);
45 (*p_host)[host_len] = '\0';
46
47 *p_port = Dowa_Arena_Allocate(p_arena, 8);
48 strcpy(*p_port, *p_use_tls ? "443" : "80");
49 }
50
51 if (slash)
52 {
53 size_t path_len = strlen(slash);
54 *p_path = Dowa_Arena_Allocate(p_arena, path_len + 1);
55 strcpy(*p_path, slash);
56 }
57 else
58 {
59 *p_path = Dowa_Arena_Allocate(p_arena, 2);
60 strcpy(*p_path, "/");
61 }
62 }
63
64 Seobeo_Client_Request *Seobeo_Client_Request_Create(const char *url)
65 {
66 Seobeo_Client_Request *p_req = malloc(sizeof(Seobeo_Client_Request));
67 if (!p_req)
68 return NULL;
69
70 memset(p_req, 0, sizeof(Seobeo_Client_Request));
71
72 p_req->p_arena = Dowa_Arena_Create(1024 * 1024);
73 if (!p_req->p_arena)
74 {
75 free(p_req);
76 return NULL;
77 }
78
79 size_t url_len = strlen(url);
80 p_req->url = Dowa_Arena_Allocate(p_req->p_arena, url_len + 1);
81 strcpy(p_req->url, url);
82
83 Seobeo_Client_Parse_Url(url, &p_req->host, &p_req->port, &p_req->path, &p_req->use_tls, p_req->p_arena);
84
85 p_req->method = Dowa_Arena_Allocate(p_req->p_arena, 4);
86 strcpy(p_req->method, "GET");
87
88 p_req->follow_redirects = FALSE;
89 p_req->max_redirects = 10;
90
91 return p_req;
92 }
93
94 void Seobeo_Client_Request_Set_Method(Seobeo_Client_Request *p_req, const char *method)
95 {
96 if (!p_req || !method)
97 return;
98
99 size_t len = strlen(method);
100 p_req->method = Dowa_Arena_Allocate(p_req->p_arena, len + 1);
101 strcpy(p_req->method, method);
102 }
103
104 void Seobeo_Client_Request_Add_Header_Map(Seobeo_Client_Request *p_req, const char *key, const char *value)
105 {
106 if (!p_req || !key || !value)
107 return;
108
109 char *key_copy = Dowa_Arena_Allocate(p_req->p_arena, strlen(key) + 1);
110 strcpy(key_copy, key);
111
112 char *value_copy = Dowa_Arena_Allocate(p_req->p_arena, strlen(value) + 1);
113 strcpy(value_copy, value);
114
115 Dowa_HashMap_Push_Arena(p_req->headers_map, key_copy, value_copy, p_req->p_arena);
116 }
117
118 void Seobeo_Client_Request_Add_Header_Array(Seobeo_Client_Request *p_req, const char *header)
119 {
120 if (!p_req || !header)
121 return;
122
123 char *header_copy = Dowa_Arena_Allocate(p_req->p_arena, strlen(header) + 1);
124 strcpy(header_copy, header);
125
126 Dowa_Array_Push_Arena(p_req->headers_array, header_copy, p_req->p_arena);
127 }
128
129 void Seobeo_Client_Request_Set_Body(Seobeo_Client_Request *p_req, const char *body, size_t length)
130 {
131 if (!p_req || !body)
132 return;
133
134 if (length == 0)
135 length = strlen(body);
136
137 p_req->body = Dowa_Arena_Allocate(p_req->p_arena, length + 1);
138 memcpy(p_req->body, body, length);
139 p_req->body[length] = '\0';
140 p_req->body_length = length;
141 }
142
143 void Seobeo_Client_Request_Set_Follow_Redirects(Seobeo_Client_Request *p_req, boolean follow, int32 max_redirects)
144 {
145 if (!p_req)
146 return;
147
148 p_req->follow_redirects = follow;
149 p_req->max_redirects = max_redirects > 0 ? max_redirects : 10;
150 }
151
152 void Seobeo_Client_Request_Set_Download_Path(Seobeo_Client_Request *p_req, const char *path)
153 {
154 if (!p_req || !path)
155 return;
156
157 size_t len = strlen(path);
158 p_req->download_path = Dowa_Arena_Allocate(p_req->p_arena, len + 1);
159 strcpy(p_req->download_path, path);
160 }
161
162 static int Seobeo_Client_Build_Request_Header(Seobeo_Client_Request *p_req, char *buffer, size_t buffer_size)
163 {
164 int offset = 0;
165
166 offset += snprintf(buffer + offset, buffer_size - offset,
167 "%s %s HTTP/1.1\r\n"
168 "Host: %s\r\n",
169 p_req->method, p_req->path, p_req->host);
170
171 boolean has_content_length = FALSE;
172 boolean has_connection = FALSE;
173
174 if (p_req->headers_map)
175 {
176 size_t count = Dowa_Array_Length(p_req->headers_map);
177 for (size_t i = 0; i < count; i++)
178 {
179 const char *key = p_req->headers_map[i].key;
180 const char *value = p_req->headers_map[i].value;
181
182 if (strcasecmp(key, "content-length") == 0)
183 has_content_length = TRUE;
184 if (strcasecmp(key, "connection") == 0)
185 has_connection = TRUE;
186
187 offset += snprintf(buffer + offset, buffer_size - offset,
188 "%s: %s\r\n", key, value);
189 }
190 }
191
192 if (p_req->headers_array)
193 {
194 size_t count = Dowa_Array_Length(p_req->headers_array);
195 for (size_t i = 0; i < count; i++)
196 {
197 const char *header = p_req->headers_array[i];
198
199 if (strncasecmp(header, "content-length:", 15) == 0)
200 has_content_length = TRUE;
201 if (strncasecmp(header, "connection:", 11) == 0)
202 has_connection = TRUE;
203
204 offset += snprintf(buffer + offset, buffer_size - offset,
205 "%s\r\n", header);
206 }
207 }
208
209 if (p_req->body && !has_content_length)
210 {
211 offset += snprintf(buffer + offset, buffer_size - offset,
212 "Content-Length: %zu\r\n", p_req->body_length);
213 }
214
215 if (!has_connection)
216 {
217 offset += snprintf(buffer + offset, buffer_size - offset,
218 "Connection: close\r\n");
219 }
220
221 offset += snprintf(buffer + offset, buffer_size - offset, "\r\n");
222
223 return offset;
224 }
225
226 static Seobeo_Client_Response *Seobeo_Client_Parse_Response(Seobeo_Handle *p_handle, const char *download_path)
227 {
228 Seobeo_Client_Response *p_resp = malloc(sizeof(Seobeo_Client_Response));
229 if (!p_resp)
230 return NULL;
231
232 memset(p_resp, 0, sizeof(Seobeo_Client_Response));
233
234 p_resp->p_arena = Dowa_Arena_Create(1024 * 1024);
235 if (!p_resp->p_arena)
236 {
237 free(p_resp);
238 return NULL;
239 }
240
241 while (1)
242 {
243 int r = Seobeo_Handle_Read(p_handle);
244 if (r < 0)
245 return p_resp;
246 if (r == -2)
247 break;
248
249 if (p_handle->read_buffer_len >= 4 && strstr((char*)p_handle->read_buffer, "\r\n\r\n") != NULL)
250 break;
251
252 if (r == 0)
253 continue;
254 }
255
256 char *buf = (char*)p_handle->read_buffer;
257 char *hdr_end = strstr(buf, "\r\n\r\n");
258 if (!hdr_end)
259 return p_resp;
260
261 size_t hdr_len = hdr_end - buf + 4;
262
263 char version[16];
264 int status_code;
265 char status_text[256];
266 int scan_result = sscanf(buf, "%15s %d %255[^\r\n]", version, &status_code, status_text);
267
268 if (scan_result >= 2)
269 {
270 p_resp->status_code = status_code;
271 if (scan_result >= 3)
272 {
273 size_t len = strlen(status_text);
274 p_resp->status_text = Dowa_Arena_Allocate(p_resp->p_arena, len + 1);
275 strcpy(p_resp->status_text, status_text);
276 }
277 }
278
279 char *line = buf + strlen(version) + 1;
280 while (*line && !isdigit(*line))
281 line++;
282 while (*line && isdigit(*line))
283 line++;
284 while (*line && *line == ' ')
285 line++;
286 while (*line && *line != '\r')
287 line++;
288 line += 2;
289
290 while (line < hdr_end)
291 {
292 char *next = strstr(line, "\r\n");
293 if (!next)
294 break;
295
296 char *colon = memchr(line, ':', next - line);
297 if (colon)
298 {
299 size_t key_len = colon - line;
300 size_t value_len = next - colon - 1;
301
302 char *val_start = colon + 1;
303 if (*val_start == ' ')
304 {
305 val_start++;
306 value_len--;
307 }
308
309 char *key = Dowa_Arena_Allocate(p_resp->p_arena, key_len + 1);
310 memcpy(key, line, key_len);
311 key[key_len] = '\0';
312
313 char *val = Dowa_Arena_Allocate(p_resp->p_arena, value_len + 1);
314 memcpy(val, val_start, value_len);
315 val[value_len] = '\0';
316
317 Dowa_HashMap_Push_Arena(p_resp->headers, key, val, p_resp->p_arena);
318
319 if (strcasecmp(key, "Location") == 0)
320 {
321 p_resp->redirect_url = Dowa_Arena_Allocate(p_resp->p_arena, value_len + 1);
322 strcpy(p_resp->redirect_url, val);
323 }
324 }
325
326 line = next + 2;
327 }
328
329 Seobeo_Handle_Consume(p_handle, (uint32)hdr_len);
330
331 void *p_cl_kv = Dowa_HashMap_Get_Ptr(p_resp->headers, "Content-Length");
332 size_t body_len = 0;
333 if (p_cl_kv)
334 {
335 const char *content_length_str = ((Seobeo_Request_Entry*)p_cl_kv)->value;
336 body_len = atoi(content_length_str);
337 }
338
339 FILE *p_file = NULL;
340 if (download_path)
341 {
342 p_file = fopen(download_path, "wb");
343 if (!p_file)
344 {
345 Seobeo_Log(SEOBEO_ERROR, "Failed to open file for writing: %s\n", download_path);
346 return p_resp;
347 }
348 }
349
350 if (body_len > 0)
351 {
352 char *body = download_path ? NULL : Dowa_Arena_Allocate(p_resp->p_arena, body_len + 1);
353 size_t total_read = 0;
354
355 while (total_read < body_len)
356 {
357 size_t available = p_handle->read_buffer_len;
358 size_t to_copy = (body_len - total_read) < available ? (body_len - total_read) : available;
359
360 if (to_copy > 0)
361 {
362 if (download_path)
363 fwrite(p_handle->read_buffer, 1, to_copy, p_file);
364 else
365 memcpy(body + total_read, p_handle->read_buffer, to_copy);
366
367 total_read += to_copy;
368 Seobeo_Handle_Consume(p_handle, (uint32)to_copy);
369 }
370
371 if (total_read < body_len)
372 {
373 int r = Seobeo_Handle_Read(p_handle);
374 if (r < 0 || r == -2)
375 break;
376 if (r == 0)
377 continue;
378 }
379 }
380
381 if (!download_path)
382 {
383 body[body_len] = '\0';
384 p_resp->body = body;
385 p_resp->body_length = body_len;
386 }
387 else
388 {
389 p_resp->body_length = total_read;
390 }
391 }
392 else
393 {
394 size_t cap = 1024 * 8;
395 size_t used = 0;
396 char *body = download_path ? NULL : Dowa_Arena_Allocate(p_resp->p_arena, cap);
397
398 while (1)
399 {
400 int n = Seobeo_Handle_Read(p_handle);
401 if (n > 0)
402 {
403 if (download_path)
404 {
405 fwrite(p_handle->read_buffer, 1, p_handle->read_buffer_len, p_file);
406 used += p_handle->read_buffer_len;
407 }
408 else
409 {
410 if (used + p_handle->read_buffer_len >= cap)
411 {
412 Seobeo_Log(SEOBEO_WARNING, "Response body too large, truncating...\n");
413 break;
414 }
415 memcpy(body + used, p_handle->read_buffer, p_handle->read_buffer_len);
416 used += p_handle->read_buffer_len;
417 }
418 Seobeo_Handle_Consume(p_handle, (uint32)p_handle->read_buffer_len);
419 }
420 else if (n == -2)
421 break;
422 else if (n == 0)
423 continue;
424 else
425 break;
426 }
427
428 if (!download_path)
429 {
430 p_resp->body = body;
431 p_resp->body_length = used;
432 if (used < cap)
433 body[used] = '\0';
434 }
435 else
436 {
437 p_resp->body_length = used;
438 }
439 }
440
441 if (p_file)
442 fclose(p_file);
443
444 return p_resp;
445 }
446
447 static Seobeo_Client_Response *Seobeo_Client_Execute_Single(Seobeo_Client_Request *p_req)
448 {
449 Seobeo_Handle *p_handle = Seobeo_Stream_Handle_Client_Create(p_req->host, p_req->port, p_req->use_tls);
450 if (!p_handle || p_handle->socket < 0)
451 {
452 if (p_handle)
453 Seobeo_Handle_Destroy(p_handle);
454 return NULL;
455 }
456
457 char request_buffer[8192];
458 int request_len = Seobeo_Client_Build_Request_Header(p_req, request_buffer, sizeof(request_buffer));
459
460 Seobeo_Handle_Queue(p_handle, (uint8*)request_buffer, (uint32)request_len);
461
462 if (p_req->body && p_req->body_length > 0)
463 Seobeo_Handle_Queue(p_handle, (uint8*)p_req->body, (uint32)p_req->body_length);
464
465 if (Seobeo_Handle_Flush(p_handle) < 0)
466 {
467 Seobeo_Handle_Destroy(p_handle);
468 return NULL;
469 }
470
471 Seobeo_Client_Response *p_resp = Seobeo_Client_Parse_Response(p_handle, p_req->download_path);
472
473 Seobeo_Handle_Destroy(p_handle);
474
475 return p_resp;
476 }
477
478 Seobeo_Client_Response *Seobeo_Client_Request_Execute(Seobeo_Client_Request *p_req)
479 {
480 if (!p_req)
481 return NULL;
482
483 Seobeo_Client_Response *p_resp = Seobeo_Client_Execute_Single(p_req);
484
485 if (!p_resp)
486 return NULL;
487
488 int redirect_count = 0;
489 while (p_req->follow_redirects &&
490 p_resp->redirect_url &&
491 (p_resp->status_code == 301 || p_resp->status_code == 302 ||
492 p_resp->status_code == 303 || p_resp->status_code == 307 ||
493 p_resp->status_code == 308) &&
494 redirect_count < p_req->max_redirects)
495 {
496 char *redirect_url = malloc(strlen(p_resp->redirect_url) + 1);
497 strcpy(redirect_url, p_resp->redirect_url);
498
499 Seobeo_Client_Response_Destroy(p_resp);
500
501 // Relative redirect
502 if (redirect_url[0] == '/')
503 {
504 size_t path_len = strlen(redirect_url);
505 p_req->path = Dowa_Arena_Allocate(p_req->p_arena, path_len + 1);
506 strcpy(p_req->path, redirect_url);
507
508 size_t url_len = strlen(p_req->host) + strlen(p_req->port) + path_len + 16;
509 p_req->url = Dowa_Arena_Allocate(p_req->p_arena, url_len);
510 snprintf(p_req->url, url_len, "%s://%s:%s%s",
511 p_req->use_tls ? "https" : "http",
512 p_req->host, p_req->port, p_req->path);
513 }
514 else
515 {
516 size_t url_len = strlen(redirect_url);
517 p_req->url = Dowa_Arena_Allocate(p_req->p_arena, url_len + 1);
518 strcpy(p_req->url, redirect_url);
519
520 Seobeo_Client_Parse_Url(redirect_url, &p_req->host, &p_req->port, &p_req->path, &p_req->use_tls, p_req->p_arena);
521 }
522
523 free(redirect_url);
524
525 p_resp = Seobeo_Client_Execute_Single(p_req);
526 if (!p_resp)
527 return NULL;
528
529 redirect_count++;
530 }
531
532 return p_resp;
533 }
534
535 void Seobeo_Client_Request_Destroy(Seobeo_Client_Request *p_req)
536 {
537 if (!p_req)
538 return;
539
540 if (p_req->p_arena)
541 Dowa_Arena_Free(p_req->p_arena);
542
543 if (p_req->headers_map)
544 Dowa_HashMap_Free(p_req->headers_map);
545
546 if (p_req->headers_array)
547 Dowa_Array_Free(p_req->headers_array);
548
549 free(p_req);
550 }
551
552 void Seobeo_Client_Response_Destroy(Seobeo_Client_Response *p_resp)
553 {
554 if (!p_resp)
555 return;
556
557 if (p_resp->p_arena)
558 Dowa_Arena_Free(p_resp->p_arena);
559
560 if (p_resp->headers)
561 Dowa_HashMap_Free(p_resp->headers);
562
563 free(p_resp);
564 }