quickjs-tart

quickjs-based runtime for wallet-core logic
Log | Files | Refs | README | LICENSE

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