quickjs-http.c (20500B)
1 /* 2 This file is part of GNU Taler 3 Copyright (C) 2024 Taler Systems SA 4 5 GNU Taler is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 #include <stdlib.h> 18 #include <pthread.h> 19 #include <stdio.h> 20 #include <curl/curl.h> 21 #include <arpa/inet.h> 22 #include <strings.h> 23 #include <string.h> 24 #include <assert.h> 25 #include <unistd.h> 26 27 #include "curl/multi.h" 28 #include "cutils.h" 29 #include "quickjs-http.h" 30 #include "list.h" 31 32 struct CurlClientState { 33 pthread_t thread; 34 pthread_mutex_t mutex; 35 pthread_cond_t callback_done; 36 BOOL started; 37 BOOL stopped; 38 CURLSH *curlsh; 39 CURLM *curlm; 40 int last_request_id; 41 struct list_head request_list; /* list of CurlRequestState.link */ 42 struct list_head add_queue; /* multi_add_handle queue */ 43 struct list_head cancel_queue; /* multi_remove_handle queue */ 44 }; 45 46 struct CurlRequestState { 47 struct CurlClientState *ccs; 48 struct list_head link_req; /* for request_list */ 49 struct list_head link_add; /* for add_queue */ 50 struct list_head link_cancel; /* for cancel_queue */ 51 DynBuf response_data; 52 BOOL cancelled; 53 BOOL cancel_queued; 54 BOOL added; 55 BOOL callback_active; 56 CURL *curl; 57 int request_id; 58 enum JSHttpRedirectFlag redirect; 59 JSHttpResponseCb response_cb; 60 void *response_cb_cls; 61 // Request headers 62 struct curl_slist *req_headers; 63 struct curl_slist *resp_headers; 64 char *errbuf; 65 size_t response_header_bytes; 66 }; 67 68 #define MAX_HTTP_RESPONSE_SIZE (64U * 1024U * 1024U) 69 #define MAX_HTTP_HEADER_SIZE (1024U * 1024U) 70 71 static pthread_once_t curl_global_once = PTHREAD_ONCE_INIT; 72 static CURLcode curl_global_status = CURLE_FAILED_INIT; 73 74 static void init_curl_global_state(void) 75 { 76 curl_global_status = curl_global_init(CURL_GLOBAL_DEFAULT); 77 } 78 79 static const char * 80 find_system_ca_bundle(void) 81 { 82 static const char * const candidates[] = { 83 "/etc/ssl/certs/ca-certificates.crt", 84 "/etc/pki/tls/certs/ca-bundle.crt", 85 "/usr/share/ssl/certs/ca-bundle.crt", 86 "/usr/local/share/certs/ca-root-nss.crt", 87 "/etc/ssl/cert.pem", 88 "/var/lib/ca-certificates/ca-bundle.pem", 89 NULL, 90 }; 91 92 for (const char * const *candidate = candidates; 93 NULL != *candidate; 94 candidate++) { 95 if (0 == access(*candidate, R_OK)) { 96 return *candidate; 97 } 98 } 99 return NULL; 100 } 101 102 /** 103 * libcurl itself does not read the curl command-line tool's CA environment 104 * variables. Our minimal Meson curl build also has no configured default CA 105 * location, so reproduce curl's environment handling and then fall back to 106 * its standard Unix bundle search order. 107 */ 108 static CURLcode 109 configure_ca_locations(CURL *curl) 110 { 111 const char *ca_bundle = getenv("CURL_CA_BUNDLE"); 112 const char *ca_path = NULL; 113 114 if (NULL == ca_bundle || '\0' == ca_bundle[0]) { 115 ca_bundle = getenv("SSL_CERT_FILE"); 116 ca_path = getenv("SSL_CERT_DIR"); 117 if (NULL != ca_bundle && '\0' == ca_bundle[0]) { 118 ca_bundle = NULL; 119 } 120 if (NULL != ca_path && '\0' == ca_path[0]) { 121 ca_path = NULL; 122 } 123 } 124 if (NULL == ca_bundle && NULL == ca_path) { 125 ca_bundle = find_system_ca_bundle(); 126 } 127 if (NULL != ca_bundle) { 128 CURLcode result = curl_easy_setopt(curl, CURLOPT_CAINFO, ca_bundle); 129 130 if (CURLE_OK != result) { 131 return result; 132 } 133 } 134 if (NULL != ca_path) { 135 return curl_easy_setopt(curl, CURLOPT_CAPATH, ca_path); 136 } 137 return CURLE_OK; 138 } 139 140 // Must only be called with locked client mutex 141 static void destroy_curl_request_state(struct CurlRequestState *crs) 142 { 143 if (!crs) { 144 return; 145 } 146 147 if (crs->link_add.prev) { 148 list_del(&crs->link_add); 149 } 150 if (crs->link_cancel.prev) { 151 list_del(&crs->link_cancel); 152 } 153 if (crs->link_req.prev) { 154 list_del(&crs->link_req); 155 } 156 curl_slist_free_all(crs->req_headers); 157 curl_slist_free_all(crs->resp_headers); 158 dbuf_free(&crs->response_data); 159 if (crs->curl) { 160 curl_easy_cleanup(crs->curl); 161 crs->curl = NULL; 162 } 163 free(crs->errbuf); 164 free(crs); 165 } 166 167 static void * 168 handle_done(CURL *curl, CURLcode res) 169 { 170 struct CurlRequestState *crs = NULL; 171 struct CurlClientState *ccs = NULL; 172 struct JSHttpResponseInfo hri = { 0 }; 173 long resp_code; 174 char **headers = NULL; 175 BOOL cancelled; 176 177 curl_easy_getinfo(curl, CURLINFO_PRIVATE, &crs); 178 ccs = crs->ccs; 179 180 hri.request_id = crs->request_id; 181 182 if (CURLE_OK == res) { 183 int num_headers = 0; 184 int i; 185 struct curl_slist *sl = crs->resp_headers; 186 char *url = NULL; 187 188 curl_easy_getinfo(curl, CURLINFO_REDIRECT_URL, &url); 189 190 if (crs->redirect == JS_HTTP_REDIRECT_ERROR && NULL != url) { 191 hri.status = 0; 192 hri.errmsg = crs->errbuf; 193 strncpy(crs->errbuf, "Got redirect status, but redirects are not allowed for this request", CURL_ERROR_SIZE); 194 goto done; 195 } 196 197 while (sl != NULL) { 198 if (NULL != strchr(sl->data, ':')) { 199 num_headers++; 200 } 201 sl = sl->next; 202 } 203 204 headers = malloc((num_headers + 1) * sizeof(char *)); 205 if (!headers) { 206 hri.status = 0; 207 goto done; 208 } 209 memset(headers, 0, (num_headers + 1) * sizeof (char *)); 210 sl = crs->resp_headers; 211 i = 0; 212 while (sl != NULL) { 213 if (NULL != strchr(sl->data, ':')) { 214 headers[i] = sl->data; 215 i++; 216 } 217 sl = sl->next; 218 } 219 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &resp_code); 220 hri.status = resp_code; 221 hri.body = crs->response_data.buf; 222 hri.body_len = crs->response_data.size; 223 hri.response_headers = headers; 224 hri.num_response_headers = num_headers; 225 } else { 226 hri.status = 0; 227 hri.errmsg = crs->errbuf; 228 } 229 230 done: 231 232 pthread_mutex_lock(&ccs->mutex); 233 cancelled = crs->cancelled; 234 if (!cancelled) { 235 crs->callback_active = TRUE; 236 } 237 pthread_mutex_unlock(&ccs->mutex); 238 239 if (cancelled == FALSE) { 240 // FIXME: What if this CB somehow destroys the client? 241 crs->response_cb(crs->response_cb_cls, &hri); 242 } 243 244 pthread_mutex_lock(&ccs->mutex); 245 crs->callback_active = FALSE; 246 pthread_cond_broadcast(&ccs->callback_done); 247 destroy_curl_request_state(crs); 248 pthread_mutex_unlock(&ccs->mutex); 249 free(headers); 250 return NULL; 251 } 252 253 static size_t curl_header_callback(char *buffer, size_t size, 254 size_t nitems, void *userdata) 255 { 256 struct CurlRequestState *crs = userdata; 257 size_t sz = size * nitems; 258 char *hval; 259 struct curl_slist *new_headers; 260 261 if (size != 0 && sz / size != nitems) { 262 return 0; 263 } 264 if (sz > MAX_HTTP_HEADER_SIZE - crs->response_header_bytes) { 265 return 0; 266 } 267 268 hval = strndup(buffer, sz); 269 if (!hval) { 270 return 0; 271 } 272 new_headers = curl_slist_append(crs->resp_headers, hval); 273 free(hval); 274 if (!new_headers) { 275 return 0; 276 } 277 crs->resp_headers = new_headers; 278 crs->response_header_bytes += sz; 279 return sz; 280 } 281 282 283 static size_t curl_write_cb(char *data, size_t size, size_t nmemb, void *userp) 284 { 285 size_t realsize = size * nmemb; 286 struct CurlRequestState *rctx = userp; 287 288 if (size != 0 && realsize / size != nmemb) { 289 return 0; 290 } 291 if (realsize > MAX_HTTP_RESPONSE_SIZE - rctx->response_data.size) { 292 return 0; 293 } 294 if (0 != dbuf_put(&rctx->response_data, 295 (const uint8_t *) data, 296 realsize)) { 297 return 0; 298 } 299 300 return realsize; 301 } 302 303 304 static int 305 create_impl(void *cls, struct JSHttpRequestInfo *req_info) 306 { 307 struct CurlClientState *ccs = cls; 308 struct CurlRequestState *crs; 309 CURL *curl; 310 BOOL debug = req_info->debug > 0; 311 const char *method = req_info->method; 312 struct curl_slist *new_headers; 313 314 pthread_mutex_lock(&ccs->mutex); 315 if (ccs->stopped) { 316 pthread_mutex_unlock(&ccs->mutex); 317 return -1; 318 } 319 pthread_mutex_unlock(&ccs->mutex); 320 321 crs = malloc(sizeof *crs); 322 if (!crs) { 323 return -1; 324 } 325 memset(crs, 0, sizeof *crs); 326 crs->ccs = ccs; 327 crs->response_cb = req_info->response_cb; 328 crs->response_cb_cls = req_info->response_cb_cls; 329 crs->errbuf = malloc(CURL_ERROR_SIZE); 330 if (!crs->errbuf) { 331 goto error; 332 } 333 memset(crs->errbuf, 0, CURL_ERROR_SIZE); 334 dbuf_init(&crs->response_data); 335 336 curl = curl_easy_init(); 337 if (!curl) { 338 goto error; 339 } 340 crs->curl = curl; 341 curl_easy_setopt(curl, CURLOPT_PRIVATE, crs); 342 curl_easy_setopt(curl, CURLOPT_SHARE, ccs->curlsh); 343 curl_easy_setopt(curl, CURLOPT_URL, req_info->url); 344 curl_easy_setopt(curl, CURLOPT_USERAGENT, "qtart"); 345 curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, "http,https"); 346 curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS_STR, "http,https"); 347 curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 10L); 348 if (CURLE_OK != configure_ca_locations(curl)) { 349 goto error; 350 } 351 curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, curl_header_callback); 352 curl_easy_setopt(curl, CURLOPT_HEADERDATA, crs); 353 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb); 354 curl_easy_setopt(curl, CURLOPT_WRITEDATA, crs); 355 356 curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, crs->errbuf); 357 358 #ifdef QTART_INSECURE_SKIP_TLS_VERIFICATION 359 // This is only a temporary hack to use the libcurl HTTP client implementation 360 // on platforms (like iOS) where we can't easily access the root store. 361 // Outside of testing, such platforms should supply a native HTTP client 362 // implementation and not use the libcurl implementation compiled 363 // into qtart. 364 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); 365 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); 366 #endif 367 368 if (req_info->timeout_ms < 0) { 369 curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 0L); 370 } else if (0 == req_info->timeout_ms) { 371 // Default timeout of 5 minutes. 372 curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 5L * 60000L); 373 } else { 374 curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, (long) req_info->timeout_ms); 375 } 376 377 if (debug == TRUE) { 378 curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); 379 } 380 381 crs->redirect = req_info->redirect; 382 383 switch (req_info->redirect) { 384 case JS_HTTP_REDIRECT_TRANSPARENT: 385 curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 386 break; 387 case JS_HTTP_REDIRECT_MANUAL: 388 curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); 389 break; 390 case JS_HTTP_REDIRECT_ERROR: 391 curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); 392 break; 393 default: 394 assert(0); 395 } 396 397 if (0 == strcasecmp(req_info->method, "get")) { 398 curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 399 } else if (0 == strcasecmp(method, "delete")) { 400 curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 401 curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); 402 } else if (0 == strcasecmp(method, "head")) { 403 curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); 404 } else if ((0 == strcasecmp(method, "post")) || 405 (0 == strcasecmp(method, "put"))) { 406 curl_easy_setopt(curl, CURLOPT_POST, 1L); 407 if (0 == strcasecmp(method, "put")) { 408 curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); 409 } 410 if (req_info->req_body_len > 0) { 411 curl_off_t len = req_info->req_body_len; 412 curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, len); 413 curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, req_info->req_body); 414 } 415 } else { 416 goto error; 417 } 418 419 if (req_info->request_headers != NULL) { 420 char **h = req_info->request_headers; 421 while (*h) { 422 new_headers = curl_slist_append(crs->req_headers, *h); 423 if (!new_headers) { 424 goto error; 425 } 426 crs->req_headers = new_headers; 427 h++; 428 } 429 } 430 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, crs->req_headers); 431 432 pthread_mutex_lock(&ccs->mutex); 433 if (ccs->stopped) { 434 pthread_mutex_unlock(&ccs->mutex); 435 goto error; 436 } 437 if (ccs->last_request_id == INT_MAX) { 438 ccs->last_request_id = 1; 439 } else { 440 ccs->last_request_id++; 441 } 442 crs->request_id = ccs->last_request_id; 443 list_add_tail(&crs->link_add, &ccs->add_queue); 444 list_add_tail(&crs->link_req, &ccs->request_list); 445 pthread_mutex_unlock(&ccs->mutex); 446 447 (void) curl_multi_wakeup(ccs->curlm); 448 449 return crs->request_id; 450 error: 451 if (crs) { 452 dbuf_free(&crs->response_data); 453 if (crs->errbuf) { 454 free(crs->errbuf); 455 } 456 if (crs->curl) { 457 curl_easy_cleanup(crs->curl); 458 } 459 curl_slist_free_all(crs->req_headers); 460 curl_slist_free_all(crs->resp_headers); 461 free(crs); 462 } 463 return -1; 464 } 465 466 static int 467 destroy_impl(void *cls, int request_id) 468 { 469 struct list_head *el; 470 struct CurlClientState *ccs = cls; 471 472 pthread_mutex_lock(&ccs->mutex); 473 474 list_for_each(el, &ccs->request_list) { 475 struct CurlRequestState *crs = list_entry(el, struct CurlRequestState, link_req); 476 if (crs->request_id == request_id) { 477 crs->cancelled = TRUE; 478 while (crs->callback_active && 479 !pthread_equal(pthread_self(), ccs->thread)) { 480 pthread_cond_wait(&ccs->callback_done, &ccs->mutex); 481 pthread_mutex_unlock(&ccs->mutex); 482 return destroy_impl(cls, request_id); 483 } 484 if (!crs->callback_active && !crs->cancel_queued) { 485 crs->cancel_queued = TRUE; 486 list_add_tail(&crs->link_cancel, &ccs->cancel_queue); 487 } 488 break; 489 } 490 } 491 492 pthread_mutex_unlock(&ccs->mutex); 493 494 if (ccs->curlm) { 495 (void) curl_multi_wakeup(ccs->curlm); 496 } 497 498 return 0; 499 } 500 501 /** 502 * Entry point for the thread that processes HTTP requests with libcurl. 503 */ 504 static void * 505 curl_multi_thread_run(void *cls) 506 { 507 struct CurlClientState *ccs = cls; 508 struct list_head *el, *el1; 509 int still_running; 510 struct CURLMsg *m; 511 BOOL stopped; 512 513 while (1) { 514 CURLMcode mc; 515 516 mc = curl_multi_perform(ccs->curlm, &still_running); 517 518 if (CURLM_OK != mc) { 519 fprintf(stderr, "curl_multi_perform failed\n"); 520 break; 521 } 522 523 mc = curl_multi_poll(ccs->curlm, NULL, 0, 1000, NULL); 524 if (CURLM_OK != mc) { 525 fprintf(stderr, "curl_multi_poll failed\n"); 526 break; 527 } 528 529 pthread_mutex_lock(&ccs->mutex); 530 stopped = ccs->stopped; 531 pthread_mutex_unlock(&ccs->mutex); 532 533 if (stopped) { 534 break; 535 } 536 537 do { 538 539 // Add new requests in queue 540 pthread_mutex_lock(&ccs->mutex); 541 list_for_each_safe(el, el1, &ccs->add_queue) { 542 struct CurlRequestState *crs = list_entry(el, struct CurlRequestState, link_add); 543 if (CURLM_OK == curl_multi_add_handle(ccs->curlm, crs->curl)) { 544 crs->added = TRUE; 545 } else { 546 crs->cancelled = TRUE; 547 if (!crs->cancel_queued) { 548 crs->cancel_queued = TRUE; 549 list_add_tail(&crs->link_cancel, &ccs->cancel_queue); 550 } 551 } 552 list_del(el); 553 } 554 pthread_mutex_unlock(&ccs->mutex); 555 556 // Process finished requests before cancellation so CURLMSG_DONE 557 // never references an easy handle that cancellation already freed. 558 int msgq = 0; 559 while ((m = curl_multi_info_read(ccs->curlm, &msgq)) != NULL) { 560 if (m->msg == CURLMSG_DONE) { 561 CURL *e = m->easy_handle; 562 curl_multi_remove_handle(ccs->curlm, e); 563 handle_done(e, m->data.result); 564 } 565 } 566 567 // Cancel requests in queue 568 pthread_mutex_lock(&ccs->mutex); 569 list_for_each_safe(el, el1, &ccs->cancel_queue) { 570 struct CurlRequestState *crs = list_entry(el, struct CurlRequestState, link_cancel); 571 if (crs->added) { 572 curl_multi_remove_handle(ccs->curlm, crs->curl); 573 } 574 destroy_curl_request_state(crs); 575 } 576 pthread_mutex_unlock(&ccs->mutex); 577 } while (m); 578 } 579 pthread_mutex_lock(&ccs->mutex); 580 ccs->stopped = TRUE; 581 pthread_mutex_unlock(&ccs->mutex); 582 return NULL; 583 } 584 585 struct JSHttpClientImplementation * 586 js_curl_http_client_create() 587 { 588 struct JSHttpClientImplementation *impl = NULL; 589 struct CurlClientState *ccs = NULL; 590 int res; 591 592 if (0 != pthread_once(&curl_global_once, init_curl_global_state) || 593 CURLE_OK != curl_global_status) { 594 return NULL; 595 } 596 597 ccs = calloc(1, sizeof *ccs); 598 if (!ccs) { 599 goto error; 600 } 601 602 pthread_mutex_init(&ccs->mutex, NULL); 603 pthread_cond_init(&ccs->callback_done, NULL); 604 ccs->curlsh = curl_share_init(); 605 if (!ccs->curlsh) { 606 goto error; 607 } 608 ccs->curlm = curl_multi_init(); 609 if (!ccs->curlm) { 610 goto error; 611 } 612 init_list_head(&ccs->request_list); 613 init_list_head(&ccs->add_queue); 614 init_list_head(&ccs->cancel_queue); 615 616 curl_share_setopt(ccs->curlsh, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS); 617 curl_share_setopt(ccs->curlsh, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); 618 curl_share_setopt(ccs->curlsh, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT); 619 620 impl = malloc(sizeof *impl); 621 if (!impl) { 622 goto error; 623 } 624 impl->req_create = &create_impl; 625 impl->req_cancel = &destroy_impl; 626 impl->cls = ccs; 627 628 res = pthread_create(&ccs->thread, NULL, &curl_multi_thread_run, ccs); 629 if (0 != res) { 630 goto error; 631 } 632 ccs->started = TRUE; 633 634 return impl; 635 error: 636 if (ccs) { 637 if (ccs->curlsh) { 638 curl_share_cleanup(ccs->curlsh); 639 } 640 if (ccs->curlm) { 641 curl_multi_cleanup(ccs->curlm); 642 } 643 pthread_cond_destroy(&ccs->callback_done); 644 pthread_mutex_destroy(&ccs->mutex); 645 free(ccs); 646 } 647 if (impl) { 648 free(impl); 649 } 650 return NULL; 651 } 652 653 static void 654 destroy_client_state(struct CurlClientState *ccs) 655 { 656 struct list_head *el, *el1; 657 if (!ccs) { 658 return; 659 } 660 if (ccs->started == TRUE) { 661 void *retval; 662 int res; 663 664 pthread_mutex_lock(&ccs->mutex); 665 ccs->stopped = TRUE; 666 pthread_mutex_unlock(&ccs->mutex); 667 curl_multi_wakeup(ccs->curlm); 668 res = pthread_join(ccs->thread, &retval); 669 if (0 != res) { 670 fprintf(stderr, "warning: could not join with curl thread\n"); 671 } 672 ccs->started = FALSE; 673 } 674 pthread_mutex_lock(&ccs->mutex); 675 list_for_each_safe(el, el1, &ccs->request_list) { 676 struct CurlRequestState *crs = list_entry(el, struct CurlRequestState, link_req); 677 if (crs->added) { 678 curl_multi_remove_handle(ccs->curlm, crs->curl); 679 } 680 destroy_curl_request_state(crs); 681 } 682 pthread_mutex_unlock(&ccs->mutex); 683 if (CURLM_OK != curl_multi_cleanup(ccs->curlm)) { 684 fprintf(stderr, "warning: curl_multi_cleanup failed\n"); 685 } 686 if (CURLSHE_OK != curl_share_cleanup(ccs->curlsh)) { 687 fprintf(stderr, "warning: curl_share_cleanup failed\n"); 688 } 689 pthread_cond_destroy(&ccs->callback_done); 690 pthread_mutex_destroy(&ccs->mutex); 691 free(ccs); 692 } 693 694 void 695 js_curl_http_client_destroy(struct JSHttpClientImplementation *impl) 696 { 697 if (!impl) { 698 return; 699 } 700 destroy_client_state(impl->cls); 701 impl->cls = NULL; 702 free(impl); 703 }