quickjs-http.c (15289B)
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 26 #include "curl/multi.h" 27 #include "cutils.h" 28 #include "quickjs-http.h" 29 #include "list.h" 30 31 struct CurlClientState { 32 pthread_t thread; 33 pthread_mutex_t mutex; 34 BOOL started; 35 BOOL stopped; 36 CURLSH *curlsh; 37 CURLM *curlm; 38 int last_request_id; 39 struct list_head request_list; /* list of CurlRequestState.link */ 40 struct list_head add_queue; /* multi_add_handle queue */ 41 struct list_head cancel_queue; /* multi_remove_handle queue */ 42 }; 43 44 struct CurlRequestState { 45 struct CurlClientState *ccs; 46 struct list_head link_req; /* for request_list */ 47 struct list_head link_add; /* for add_queue */ 48 struct list_head link_cancel; /* for cancel_queue */ 49 DynBuf response_data; 50 BOOL cancelled; 51 CURL *curl; 52 int request_id; 53 enum JSHttpRedirectFlag redirect; 54 JSHttpResponseCb response_cb; 55 void *response_cb_cls; 56 // Request headers 57 struct curl_slist *req_headers; 58 struct curl_slist *resp_headers; 59 char *errbuf; 60 }; 61 62 // Must only be called with locked client mutex 63 static void destroy_curl_request_state(struct CurlRequestState *crs) 64 { 65 struct CurlClientState *ccs; 66 67 if (!crs) { 68 return; 69 } 70 ccs = crs->ccs; 71 crs->ccs = NULL; 72 73 list_del(&crs->link_req); 74 curl_slist_free_all(crs->req_headers); 75 curl_slist_free_all(crs->resp_headers); 76 dbuf_free(&crs->response_data); 77 if (crs->curl) { 78 curl_easy_cleanup(crs->curl); 79 crs->curl = NULL; 80 } 81 free(crs->errbuf); 82 free(crs); 83 } 84 85 static void * 86 handle_done(CURL *curl, CURLcode res) 87 { 88 struct CurlRequestState *crs = NULL; 89 struct CurlClientState *ccs = NULL; 90 struct JSHttpResponseInfo hri = { 0 }; 91 long resp_code; 92 char **headers = NULL; 93 BOOL cancelled; 94 95 curl_easy_getinfo(curl, CURLINFO_PRIVATE, &crs); 96 ccs = crs->ccs; 97 98 hri.request_id = crs->request_id; 99 100 if (CURLE_OK == res) { 101 int num_headers = 0; 102 int i; 103 char **headers; 104 struct curl_slist *sl = crs->resp_headers; 105 char *url = NULL; 106 107 curl_easy_getinfo(curl, CURLINFO_REDIRECT_URL, &url); 108 109 if (crs->redirect == JS_HTTP_REDIRECT_ERROR && NULL != url) { 110 hri.status = 0; 111 hri.errmsg = crs->errbuf; 112 strncpy(crs->errbuf, "Got redirect status, but redirects are not allowed for this request", CURL_ERROR_SIZE); 113 goto done; 114 } 115 116 while (sl != NULL) { 117 if (NULL != strchr(sl->data, ':')) { 118 num_headers++; 119 } 120 sl = sl->next; 121 } 122 123 headers = malloc((num_headers + 1) * sizeof(char *)); 124 if (!headers) { 125 hri.status = 0; 126 goto done; 127 } 128 memset(headers, 0, (num_headers + 1) * sizeof (char *)); 129 sl = crs->resp_headers; 130 i = 0; 131 while (sl != NULL) { 132 if (NULL != strchr(sl->data, ':')) { 133 headers[i] = sl->data; 134 i++; 135 } 136 sl = sl->next; 137 } 138 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &resp_code); 139 hri.status = resp_code; 140 hri.body = crs->response_data.buf; 141 hri.body_len = crs->response_data.size; 142 hri.response_headers = headers; 143 hri.num_response_headers = num_headers; 144 } else { 145 hri.status = 0; 146 hri.errmsg = crs->errbuf; 147 } 148 149 done: 150 151 pthread_mutex_lock(&ccs->mutex); 152 cancelled = crs->cancelled; 153 pthread_mutex_unlock(&ccs->mutex); 154 155 if (cancelled == FALSE) { 156 // FIXME: What if this CB somehow destroys the client? 157 crs->response_cb(crs->response_cb_cls, &hri); 158 } 159 160 if (NULL != headers) { 161 for (char **h=headers; *h; h++) { 162 free(*h); 163 } 164 free(headers); 165 } 166 pthread_mutex_lock(&ccs->mutex); 167 destroy_curl_request_state(crs); 168 pthread_mutex_unlock(&ccs->mutex); 169 return NULL; 170 } 171 172 static size_t curl_header_callback(char *buffer, size_t size, 173 size_t nitems, void *userdata) 174 { 175 struct CurlRequestState *crs = userdata; 176 size_t sz = size * nitems; 177 char *hval; 178 179 hval = strndup(buffer, sz); 180 if (!hval) { 181 return 0; 182 } 183 crs->resp_headers = curl_slist_append(crs->resp_headers, hval); 184 free(hval); 185 return sz; 186 } 187 188 189 static size_t curl_write_cb(void *data, size_t size, size_t nmemb, void *userp) 190 { 191 size_t realsize = size * nmemb; 192 struct CurlRequestState *rctx = userp; 193 194 if (0 != dbuf_put(&rctx->response_data, data, realsize)) { 195 return 0; 196 } 197 198 return realsize; 199 } 200 201 202 static int 203 create_impl(void *cls, struct JSHttpRequestInfo *req_info) 204 { 205 struct CurlClientState *ccs = cls; 206 struct CurlRequestState *crs; 207 pthread_t thread; 208 int res; 209 CURL *curl; 210 BOOL debug = req_info->debug > 0; 211 const char *method = req_info->method; 212 213 crs = malloc(sizeof *crs); 214 if (!crs) { 215 return -1; 216 } 217 memset(crs, 0, sizeof *crs); 218 crs->request_id = ++ccs->last_request_id; 219 crs->ccs = ccs; 220 crs->response_cb = req_info->response_cb; 221 crs->response_cb_cls = req_info->response_cb_cls; 222 crs->errbuf = malloc(CURL_ERROR_SIZE); 223 if (!crs->errbuf) { 224 goto error; 225 } 226 memset(crs->errbuf, 0, CURL_ERROR_SIZE); 227 dbuf_init(&crs->response_data); 228 229 curl = curl_easy_init(); 230 crs->curl = curl; 231 curl_easy_setopt(curl, CURLOPT_PRIVATE, crs); 232 curl_easy_setopt(curl, CURLOPT_SHARE, ccs->curlsh); 233 curl_easy_setopt(curl, CURLOPT_URL, req_info->url); 234 curl_easy_setopt(curl, CURLOPT_DNS_SERVERS, "9.9.9.9"); 235 curl_easy_setopt(curl, CURLOPT_USERAGENT, "qtart"); 236 curl_easy_setopt(curl, CURLOPT_CAINFO, "/etc/ssl/certs/ca-certificates.crt"); 237 curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, curl_header_callback); 238 curl_easy_setopt(curl, CURLOPT_HEADERDATA, crs); 239 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb); 240 curl_easy_setopt(curl, CURLOPT_WRITEDATA, crs); 241 242 curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, crs->errbuf); 243 244 #ifdef QTART_INSECURE_SKIP_TLS_VERIFICATION 245 // This is only a temporary hack to use the libcurl HTTP client implementation 246 // on platforms (like iOS) where we can't easily access the root store. 247 // Outside of testing, such platforms should supply a native HTTP client 248 // implementation and not use the libcurl implementation compiled 249 // into qtart. 250 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); 251 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); 252 #endif 253 254 if (req_info->timeout_ms < 0) { 255 curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 0L); 256 } else if (0 == req_info->timeout_ms) { 257 // Default timeout of 5 minutes. 258 curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 5L * 60000L); 259 } else { 260 curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, (long) req_info->timeout_ms); 261 } 262 263 if (debug == TRUE) { 264 curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); 265 } 266 267 crs->redirect = req_info->redirect; 268 269 switch (req_info->redirect) { 270 case JS_HTTP_REDIRECT_TRANSPARENT: 271 curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 272 break; 273 case JS_HTTP_REDIRECT_MANUAL: 274 curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); 275 break; 276 case JS_HTTP_REDIRECT_ERROR: 277 curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); 278 break; 279 default: 280 assert(0); 281 } 282 283 if (0 == strcasecmp(req_info->method, "get")) { 284 curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 285 } else if (0 == strcasecmp(method, "delete")) { 286 curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 287 curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); 288 } else if (0 == strcasecmp(method, "head")) { 289 curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); 290 } else if ((0 == strcasecmp(method, "post")) || 291 (0 == strcasecmp(method, "put"))) { 292 curl_easy_setopt(curl, CURLOPT_POST, 1L); 293 if (0 == strcasecmp(method, "put")) { 294 curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); 295 } 296 if (req_info->req_body_len > 0) { 297 curl_off_t len = req_info->req_body_len; 298 curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, len); 299 curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, req_info->req_body); 300 } 301 } else { 302 goto error; 303 } 304 305 if (req_info->request_headers != NULL) { 306 char **h = req_info->request_headers; 307 while (*h) { 308 crs->req_headers = curl_slist_append(crs->req_headers, *h); 309 h++; 310 } 311 } 312 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, crs->req_headers); 313 314 pthread_mutex_lock(&ccs->mutex); 315 list_add_tail(&crs->link_add, &ccs->add_queue); 316 list_add_tail(&crs->link_req, &ccs->request_list); 317 pthread_mutex_unlock(&ccs->mutex); 318 319 curl_multi_wakeup(ccs->curlm); 320 321 return crs->request_id; 322 error: 323 if (crs) { 324 dbuf_free(&crs->response_data); 325 if (crs->errbuf) { 326 free(crs->errbuf); 327 } 328 if (crs->curl) { 329 curl_easy_cleanup(crs->curl); 330 } 331 free(crs); 332 } 333 return -1; 334 } 335 336 static int 337 destroy_impl(void *cls, int request_id) 338 { 339 struct list_head *el; 340 struct CurlClientState *ccs = cls; 341 342 pthread_mutex_lock(&ccs->mutex); 343 344 list_for_each(el, &ccs->request_list) { 345 struct CurlRequestState *crs = list_entry(el, struct CurlRequestState, link_req); 346 if (crs->request_id == request_id && !crs->cancelled) { 347 list_add_tail(&crs->link_cancel, &ccs->cancel_queue); 348 } 349 } 350 351 pthread_mutex_unlock(&ccs->mutex); 352 353 curl_multi_wakeup(ccs->curlm); 354 355 return 0; 356 } 357 358 /** 359 * Entry point for the thread that processes HTTP requests with libcurl. 360 */ 361 static void * 362 curl_multi_thread_run(void *cls) 363 { 364 struct CurlClientState *ccs = cls; 365 struct list_head *el, *el1; 366 int still_running; 367 struct CURLMsg *m; 368 BOOL stopped; 369 370 while (1) { 371 CURLMcode mc; 372 373 mc = curl_multi_perform(ccs->curlm, &still_running); 374 375 if (CURLM_OK != mc) { 376 fprintf(stderr, "curl_multi_perform failed\n"); 377 break; 378 } 379 380 mc = curl_multi_poll(ccs->curlm, NULL, 0, 1000, NULL); 381 if (CURLM_OK != mc) { 382 fprintf(stderr, "curl_multi_poll failed\n"); 383 break; 384 } 385 386 pthread_mutex_lock(&ccs->mutex); 387 stopped = ccs->stopped; 388 pthread_mutex_unlock(&ccs->mutex); 389 390 if (stopped) { 391 break; 392 } 393 394 do { 395 396 // Add new requests in queue 397 pthread_mutex_lock(&ccs->mutex); 398 list_for_each_safe(el, el1, &ccs->add_queue) { 399 struct CurlRequestState *crs = list_entry(el, struct CurlRequestState, link_add); 400 curl_multi_add_handle(ccs->curlm, crs->curl); 401 list_del(el); 402 } 403 pthread_mutex_unlock(&ccs->mutex); 404 405 // Cancel requests in queue 406 pthread_mutex_lock(&ccs->mutex); 407 list_for_each_safe(el, el1, &ccs->cancel_queue) { 408 struct CurlRequestState *crs = list_entry(el, struct CurlRequestState, link_cancel); 409 curl_multi_remove_handle(ccs->curlm, crs->curl); 410 crs->cancelled = TRUE; 411 list_del(el); 412 } 413 pthread_mutex_unlock(&ccs->mutex); 414 415 // Process finished request 416 int msgq = 0; 417 m = curl_multi_info_read(ccs->curlm, &msgq); 418 if (m && (m->msg == CURLMSG_DONE)) { 419 CURL *e = m->easy_handle; 420 curl_multi_remove_handle(ccs->curlm, e); 421 handle_done(e, m->data.result); 422 } 423 } while(m); 424 } 425 if (CURLM_OK != curl_multi_cleanup(ccs->curlm)) { 426 fprintf(stderr, "warning: curl_multi_cleanup failed\n"); 427 } 428 if (CURLSHE_OK != curl_share_cleanup(ccs->curlsh)) { 429 fprintf(stderr, "warning: curl_share_cleanup failed\n"); 430 } 431 return NULL; 432 } 433 434 struct JSHttpClientImplementation * 435 js_curl_http_client_create() 436 { 437 struct JSHttpClientImplementation *impl = NULL; 438 struct CurlClientState *ccs = NULL; 439 int res; 440 441 ccs = malloc(sizeof *ccs); 442 if (!ccs) { 443 goto error; 444 } 445 446 pthread_mutex_init(&ccs->mutex, NULL); 447 ccs->started = FALSE; 448 ccs->stopped = FALSE; 449 ccs->last_request_id = 0; 450 ccs->curlsh = curl_share_init(); 451 if (!ccs->curlsh) { 452 goto error; 453 } 454 ccs->curlm = curl_multi_init(); 455 if (!ccs->curlm) { 456 goto error; 457 } 458 init_list_head(&ccs->request_list); 459 init_list_head(&ccs->add_queue); 460 init_list_head(&ccs->cancel_queue); 461 462 curl_share_setopt(ccs->curlsh, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS); 463 curl_share_setopt(ccs->curlsh, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); 464 curl_share_setopt(ccs->curlsh, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT); 465 466 impl = malloc(sizeof *impl); 467 if (!impl) { 468 goto error; 469 } 470 impl->req_create = &create_impl; 471 impl->req_cancel = &destroy_impl; 472 impl->cls = ccs; 473 474 res = pthread_create(&ccs->thread, NULL, &curl_multi_thread_run, ccs); 475 ccs->started = TRUE; 476 477 if (0 != res) { 478 goto error; 479 } 480 481 return impl; 482 error: 483 if (ccs) { 484 curl_share_cleanup(ccs->curlsh); 485 curl_multi_cleanup(ccs->curlm); 486 free(ccs); 487 } 488 if (impl) { 489 free(impl); 490 } 491 return NULL; 492 } 493 494 static void 495 destroy_client_state(struct CurlClientState *ccs) 496 { 497 struct list_head *el, *el1; 498 if (!ccs) { 499 return; 500 } 501 if (ccs->started == TRUE) { 502 void *retval; 503 int res; 504 505 pthread_mutex_lock(&ccs->mutex); 506 ccs->stopped = TRUE; 507 pthread_mutex_unlock(&ccs->mutex); 508 curl_multi_wakeup(ccs->curlm); 509 res = pthread_join(ccs->thread, &retval); 510 if (0 != res) { 511 fprintf(stderr, "warning: could not join with curl thread\n"); 512 } 513 ccs->started = FALSE; 514 } 515 pthread_mutex_lock(&ccs->mutex); 516 list_for_each_safe(el, el1, &ccs->request_list) { 517 struct CurlRequestState *crs = list_entry(el, struct CurlRequestState, link_req); 518 destroy_curl_request_state(crs); 519 } 520 pthread_mutex_unlock(&ccs->mutex); 521 pthread_mutex_destroy(&ccs->mutex); 522 free(ccs); 523 } 524 525 void 526 js_curl_http_client_destroy(struct JSHttpClientImplementation *impl) 527 { 528 if (!impl) { 529 return; 530 } 531 destroy_client_state(impl->cls); 532 impl->cls = NULL; 533 free(impl); 534 } 535