Mercurial
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 } |