1#include <errno.h>
   2#include <iconv.h>
   3#include <stdlib.h>
   4#include <string.h>
   5
   6#include <pw.h>
   7#include <pwlib/socket.h>
   8
   9#include "pw_curl.h"
  10
  11/****************************************************************
  12 * Helper functions
  13 */
  14
  15[[nodiscard]] static bool decode(PwValuePtr str, PwValuePtr charset, PwValuePtr result)
  16/*
  17 * Decode raw bytes of `str` and encode in UTF-32 result.
  18 * Character size of `str` must be 1.
  19 */
  20{
  21    PW_CSTRING(fromcode, charset);
  22    iconv_t cd = iconv_open("UTF-32LE", fromcode);
  23    if (cd == (iconv_t) -1) {
  24        pw_set_status(PwErrno(errno));
  25        return false;
  26    }
  27    unsigned str_length;
  28    char* inbuf = (char*) _pw_string_start_length(str, &str_length);
  29    size_t inbytesleft = str_length;
  30
  31    size_t outbytesleft = str_length * 4;
  32    char result_buf[outbytesleft];
  33    char* outbuf = result_buf;
  34
  35    size_t ret = iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft);
  36    if (ret == (size_t) -1) {
  37        pw_set_status(PwErrno(errno));
  38        iconv_close(cd);
  39        return false;
  40    }
  41    unsigned result_length = (str_length * 4 - outbytesleft) / 4;
  42
  43    if (!pw_create_empty_string(result_length, 4, result)) {
  44        iconv_close(cd);
  45        return false;
  46    }
  47
  48    uint8_t* result_ptr = _pw_string_start(result);
  49    memcpy(result_ptr, result_buf, result_length * 4);
  50
  51    if (result->embedded) {
  52        result->embedded_length = result_length;
  53    } else {
  54        result->length = result_length;
  55    }
  56
  57    iconv_close(cd);
  58    return true;
  59}
  60
  61static char* get_response_header(CURL* easy_handle, char* name)
  62/*
  63 * Get the last instance of header.
  64 * Multiple instances of header with the same name are not needed for internal purposes yet.
  65 *
  66 * Return the pointer to an internally allocated memory.
  67 * The header should be parsed immediately, otherwise subsequent calls to CURL API
  68 * will clobber it.
  69 */
  70{
  71    char* last_value = nullptr;
  72    int last_request = 0;
  73    int last_amount = 1;
  74
  75    struct curl_header* hdr;
  76
  77    // iterate over issued requests to get the last header
  78    for (int i = 0;; i++) {
  79        CURLHcode err = curl_easy_header(easy_handle, name, 0, CURLH_HEADER, i, &hdr);
  80        if (err) {
  81            break;
  82        }
  83        last_value = hdr->value;
  84        last_amount = hdr->amount;
  85        last_request = i;
  86    }
  87    if (last_value == nullptr) {
  88        return nullptr;
  89    }
  90    // get last instance of header
  91    CURLHcode err = curl_easy_header(easy_handle, name, last_amount - 1, CURLH_HEADER, last_request, &hdr);
  92    if (err) {
  93        return nullptr;
  94    } else {
  95        return hdr->value;
  96    }
  97}
  98
  99/****************************************************************
 100 * Wrapper type for CURLcode errors CURLE_*
 101 */
 102
 103uint16_t PwTypeId_CurlStatus = 0;
 104uint16_t PwInterfaceId_CurlStatus = 0;
 105
 106uint16_t PW_ERROR_CURL = 0;
 107uint16_t PW_ERROR_REQUEST_ALREADY_ADDED = 0;
 108
 109typedef struct {
 110    PwCtorArgs* next;
 111    uint16_t type_id;
 112
 113    PwCurlStatusData error;
 114    char* file_name;
 115    unsigned line_number;
 116
 117} CurlStatusCtorArgs;
 118
 119
 120[[noreturn]] static void panic_bad_error_type(PwCurlErrorType error_type)
 121{
 122    pw_panic("Bad Curl error type %d\n", error_type);
 123}
 124
 125// constructors
 126
 127#define PwCurlStatus(_error_type, _error)  \
 128    __extension__  \
 129    ({  \
 130        CurlStatusCtorArgs args = {  \
 131            .type_id     = PwTypeId_CurlStatus,  \
 132            .error       = { .error_type = _error_type },  \
 133            .file_name   = __FILE__,  \
 134            .line_number = __LINE__  \
 135        };  \
 136        switch (_error_type) {  \
 137            case PwCurlError_Easy:   args.error.curl_error   = (CURLcode)   _error; break;  \
 138            case PwCurlError_Multi:  args.error.curlm_error  = (CURLMcode)  _error; break;  \
 139            case PwCurlError_Share:  args.error.curlsh_error = (CURLSHcode) _error; break;  \
 140            case PwCurlError_Url:    args.error.curlu_error  = (CURLUcode)  _error; break;  \
 141            case PwCurlError_Header: args.error.curlh_error  = (CURLHcode)  _error; break;  \
 142            default: panic_bad_error_type(_error_type);  \
 143        }  \
 144        _PwValue status = PW_NULL;  \
 145        if (!pw_create2(PwTypeId_CurlStatus, &args, &status)) {  \
 146            pw_move(&status, &current_task->status);  \
 147        }  \
 148        status;  \
 149    })
 150
 151/*
 152 * Basic interface
 153 */
 154
 155static bool curl_status_create(PwMethod_Basic_create* mthis, PwValuePtr result, PwCtorArgs* ctor_args)
 156{
 157    if (!pw_super(mthis, result, ctor_args)) {
 158        return false;
 159    }
 160    CurlStatusCtorArgs* args = pw_this_ctor_args();
 161
 162    result->status_code = PW_ERROR_CURL;
 163    _pw_set_status_location(result, args->file_name, args->line_number);
 164
 165    const char* error_desc;
 166    char herror[64];
 167    switch (args->error.error_type) {
 168        case PwCurlError_Easy:   error_desc = curl_easy_strerror(args->error.curl_error); break;
 169        case PwCurlError_Multi:  error_desc = curl_multi_strerror(args->error.curlm_error); break;
 170        case PwCurlError_Share:  error_desc = curl_share_strerror(args->error.curlsh_error); break;
 171        case PwCurlError_Url:    error_desc = curl_url_strerror(args->error.curlu_error); break;
 172        case PwCurlError_Header:
 173            error_desc = herror;
 174            snprintf(herror, sizeof(herror), "Header error %d", args->error.curlh_error);
 175            break;
 176        default: panic_bad_error_type(args->error.error_type);
 177    }
 178    _pw_set_status_desc(result, error_desc);
 179
 180    // data is allocated by _pw_set_status_desc, okay to update it
 181    PwCurlStatusData* curl_status = pw_this_data(result);
 182    *curl_status = args->error;
 183    return true;
 184}
 185
 186static PwInterface_Basic curl_status_basic_interface = {
 187    .create = { .func = curl_status_create }
 188};
 189
 190/*
 191 * CurlStatus interface
 192 */
 193
 194static bool curl_status_curl_error(PwMethod_CurlStatus_curl_error* mthis, PwValuePtr self, PwCurlStatusData* error)
 195{
 196    PwCurlStatusData* curl_status = pw_this_data(self);
 197    *error = *curl_status;
 198    return true;
 199}
 200
 201static PwInterface_CurlStatus curl_status_interface = {
 202#define X(name, ...) .name = { .func = curl_status_##name } __VA_OPT__(,)
 203    PW_CURL_STATUS_INTERFACE_METHODS
 204#undef X
 205};
 206
 207/****************************************************************
 208 * Curl Request: wrapper type for Curl easy handle
 209 */
 210
 211uint16_t PwTypeId_CurlRequest;
 212uint16_t PwInterfaceId_CurlRequest;
 213
 214typedef struct {
 215
 216    CURL* easy_handle;
 217
 218    // requests are stored in a doubly-linked list
 219    PwValuePtr prev;
 220    PwValuePtr next;
 221    PwValuePtr self;  // pointer to the allocated memory block for self
 222
 223    // backlink to curl session struct_data without refcounting, just indicates that request is involved in multi handle
 224    uint8_t* session;
 225
 226    _PwValue url;  // initial URL
 227
 228    // Parsed headers, call curl_request_parse_headers for that.
 229    _PwValue attachment_filename;
 230    _PwValue media_type;
 231    _PwValue media_subtype;
 232    _PwValue media_type_params;  // map
 233    _PwValue disposition_type;
 234    _PwValue disposition_params; // values can be strings of maps containing charset, language, and value
 235
 236    struct curl_slist* headers;  // keep list of headers here for CURLOPT_HTTPHEADER
 237
 238    bool content_started;
 239
 240
 241} CurlRequestData;
 242
 243/*
 244 * Basic interface
 245 */
 246
 247static bool curl_request_create(PwMethod_Basic_create* mthis, PwValuePtr result, PwCtorArgs* ctor_args)
 248{
 249    if (!pw_super(mthis, result, ctor_args)) {
 250        return false;
 251    }
 252    CurlRequestData* req = pw_this_data(result);
 253
 254    req->easy_handle = curl_easy_init();
 255    if (req->easy_handle) {
 256        return true;
 257    }
 258    pw_set_status(PwCurlStatus(PwCurlError_Easy, CURLE_FAILED_INIT));
 259    if (!pw_super_call(destroy, mthis, result, nullptr)) { /* no op */ }
 260    return false;
 261}
 262
 263static void reset_request(CurlRequestData* req)
 264{
 265    pw_destroy(&req->url);
 266    pw_destroy(&req->attachment_filename);
 267    pw_destroy(&req->media_type);
 268    pw_destroy(&req->media_subtype);
 269    pw_destroy(&req->media_type_params);
 270    pw_destroy(&req->disposition_type);
 271    pw_destroy(&req->disposition_params);
 272
 273    if (req->headers) {
 274        curl_slist_free_all(req->headers);
 275        req->headers = nullptr;
 276    }
 277
 278    req->content_started = false;
 279}
 280
 281static bool curl_request_destroy(PwMethod_Basic_destroy* mthis, PwValuePtr self, _PwCompoundChain* tail)
 282{
 283    CurlRequestData* req = pw_this_data(self);
 284
 285    pw_assert(req->session == nullptr);
 286
 287    reset_request(req);
 288
 289    if (req->easy_handle) {
 290        curl_easy_cleanup(req->easy_handle);
 291        req->easy_handle = nullptr;
 292    }
 293    return pw_super(mthis, self, tail);
 294}
 295
 296static PwInterface_Basic curl_request_basic_interface = {
 297    .create  = { .func = curl_request_create },
 298    .destroy = { .func = curl_request_destroy }
 299    // also deepcopy, which could use curl_easy_duphandle()
 300};
 301
 302/*
 303 * CurlRequest interface
 304 */
 305
 306static bool curl_request_setopt(PwMethod_CurlRequest_setopt* mthis, PwValuePtr self, CURLoption option, PwValuePtr value)
 307{
 308    CurlRequestData* req = pw_this_data(self);
 309
 310    if(option < CURLOPTTYPE_OBJECTPOINT) {
 311        // long
 312        long v;
 313        if (pw_is_int(value)) {
 314            v = (long) value->signed_value;  // XXX use same field of the union for both signed and unsigned integer
 315        } else if (pw_is_bool(value)) {
 316            v = (long) value->bool_value;
 317        } else {
 318            pw_set_status(PwStatus(PW_ERROR_INCOMPATIBLE_TYPE), "Expected integer value for CURLOPTTYPE_LONG");
 319            return false;
 320        }
 321        CURLcode err = curl_easy_setopt(req->easy_handle, option, v);
 322        if (err) {
 323            pw_set_status(PwCurlStatus(PwCurlError_Easy, err));
 324            return false;
 325        }
 326    } else if(option < CURLOPTTYPE_OFF_T) {
 327        // pointer
 328        CURLcode err;
 329        if (pw_is_string(value)) {
 330            PW_CSTRING(v, value);
 331            err = curl_easy_setopt(req->easy_handle, option, v);
 332        } else if (pw_is_ptr(value)) {
 333            err = curl_easy_setopt(req->easy_handle, option, value->ptr);
 334        } else {
 335            pw_set_status(PwStatus(PW_ERROR_INCOMPATIBLE_TYPE), "Expected string or pointer value for CURLOPTTYPE_OBJECTPOINT/FUNCTIONPOINT");
 336            return false;
 337        }
 338        if (err) {
 339            pw_set_status(PwCurlStatus(PwCurlError_Easy, err));
 340            return false;
 341        }
 342    } else if(option < CURLOPTTYPE_BLOB) {
 343        // curl_off_t
 344        if (!pw_is_int(value)) {
 345            pw_set_status(PwStatus(PW_ERROR_INCOMPATIBLE_TYPE), "Expected integer value for CURLOPTTYPE_LONG");
 346            return false;
 347        }
 348        curl_off_t v = (curl_off_t) value->signed_value;  // XXX use same field of the union for both signed and unsigned integer
 349        CURLcode err = curl_easy_setopt(req->easy_handle, option, v);
 350        if (err) {
 351            pw_set_status(PwCurlStatus(PwCurlError_Easy, err));
 352            return false;
 353        }
 354    }  else {
 355        // Value is a blob
 356        pw_set_status(PwStatus(PW_ERROR_NOT_IMPLEMENTED), "CURLOPTTYPE_BLOB support is not implemented yet");
 357        return false;
 358    }
 359    return true;
 360}
 361
 362static bool curl_request_set_headers(PwMethod_CurlRequest_set_headers* mthis, PwValuePtr self, PwValuePtr headers)
 363{
 364    CurlRequestData* req = pw_this_data(self);
 365
 366    if (headers == nullptr || pw_is_null(headers)) {
 367
 368        // reset headers
 369        CURLcode err = curl_easy_setopt(req->easy_handle, CURLOPT_HTTPHEADER, nullptr);
 370        if (err) {
 371            pw_set_status(PwCurlStatus(PwCurlError_Easy, err));
 372            return false;
 373        }
 374        if (req->headers) {
 375            curl_slist_free_all(req->headers);
 376            req->headers = nullptr;
 377        }
 378        return true;
 379    }
 380
 381    if (pw_is_string(headers)) {
 382
 383        // append single header
 384        PW_CSTRING(hdr, headers);
 385        struct curl_slist* temp = curl_slist_append(req->headers, hdr);
 386        if (!temp) {
 387            pw_set_status(PwStatus(PW_ERROR_OOM));
 388            return false;
 389        }
 390        req->headers = temp;
 391
 392    } else if (pw_is_array(headers)) {
 393
 394        // append multiple headers from array
 395        for (unsigned i = 0, n = pw_array_length(headers); i < n; i++) {
 396            PwValue item = PW_NULL;
 397            if (!pw_array_item(headers, i, &item)) {
 398                return false;
 399            }
 400            PW_CSTRING(hdr, &item);
 401            struct curl_slist* temp = curl_slist_append(req->headers, hdr);
 402            if (!temp) {
 403                pw_set_status(PwStatus(PW_ERROR_OOM));
 404                return false;
 405            }
 406            req->headers = temp;
 407        }
 408    } else if (pw_is_map(headers)) {
 409
 410        // append multiple headers from map
 411        for (unsigned i = 0, n = pw_map_length(headers); i < n; i++) {
 412            PwValue key = PW_NULL;
 413            PwValue value = PW_NULL;
 414            if (!pw_map_item(headers, i, &key, &value)) {
 415                return false;
 416            }
 417            if (pw_is_null(&value)) {
 418                // supress header
 419                unsigned k_len = pw_strlen_in_utf8(&key);
 420                char hdr[k_len + 2];
 421                pw_string_to_utf8(&key, hdr);
 422                hdr[k_len] = ':';
 423                hdr[k_len + 1] = '0';
 424                struct curl_slist* temp = curl_slist_append(req->headers, hdr);
 425                if (!temp) {
 426                    pw_set_status(PwStatus(PW_ERROR_OOM));
 427                    return false;
 428                }
 429                req->headers = temp;
 430            } else if (pw_is_string(&value)) {
 431                unsigned k_len = pw_strlen_in_utf8(&key);
 432                unsigned v_len = pw_strlen_in_utf8(&value);
 433                char hdr[k_len + v_len + 3];
 434                pw_string_to_utf8(&key, hdr);
 435                if (v_len == 0) {
 436                    // header with empty value
 437                    hdr[k_len] = ';';
 438                    hdr[k_len + 1] = '0';
 439                } else {
 440                    hdr[k_len] = ':';
 441                    hdr[k_len + 1] = ' ';
 442                    pw_string_to_utf8(&value, &hdr[k_len + 2]);
 443                    hdr[k_len + 2 + v_len] = '0';
 444                }
 445                struct curl_slist* temp = curl_slist_append(req->headers, hdr);
 446                if (!temp) {
 447                    pw_set_status(PwStatus(PW_ERROR_OOM));
 448                    return false;
 449                }
 450                req->headers = temp;
 451            } else {
 452                pw_set_status(PwStatus(PW_ERROR_INCOMPATIBLE_TYPE), "Expected string as a value for HTTP header");
 453                return false;
 454            }
 455        }
 456    }
 457    CURLcode err = curl_easy_setopt(req->easy_handle, CURLOPT_HTTPHEADER, req->headers);
 458    if (err) {
 459        pw_set_status(PwCurlStatus(PwCurlError_Easy, err));
 460        return false;
 461    }
 462    return true;
 463}
 464
 465static bool curl_request_reset(PwMethod_CurlRequest_reset* mthis, PwValuePtr self)
 466{
 467    CurlRequestData* req = pw_this_data(self);
 468
 469    if (req->session) {
 470        // https://curl.se/libcurl/c/libcurl-multi.html says:
 471        //   "set new options to it and add it again with curl_multi_add_handle to start another transfer"
 472        // Which means: to start new request, remove easy handle from multi, then reset, then add again.
 473        // Reset cannot happen when the request is being managed by session, right?
 474        pw_set_status(PwStatus(PW_ERROR_REQUEST_ALREADY_ADDED));
 475        return false;
 476    }
 477
 478    reset_request(req);
 479    curl_easy_reset(req->easy_handle);
 480    return true;
 481}
 482
 483static bool curl_request_get_url(PwMethod_CurlRequest_get_url* mthis, PwValuePtr self, PwValuePtr result)
 484{
 485    CurlRequestData* req = pw_this_data(self);
 486    pw_clone2(result, &req->url);
 487    return true;
 488}
 489
 490static bool curl_request_set_url(PwMethod_CurlRequest_set_url* mthis, PwValuePtr self, PwValuePtr url)
 491{
 492    CurlRequestData* req = pw_this_data(self);
 493
 494    pw_clone2(&req->url, url);
 495
 496    PW_CSTRING(url_cstr, url);
 497    CURLcode err = curl_easy_setopt(req->easy_handle, CURLOPT_URL, url_cstr);
 498    if (err) {
 499        pw_set_status(PwCurlStatus(PwCurlError_Easy, err));
 500        pw_destroy(&req->url);
 501        return false;
 502    }
 503    return true;
 504}
 505
 506static bool curl_request_getinfo(PwMethod_CurlRequest_getinfo* mthis, PwValuePtr self, CURLINFO info, PwValuePtr result)
 507{
 508    CurlRequestData* req = pw_this_data(self);
 509
 510    switch (info & CURLINFO_TYPEMASK) {
 511        case CURLINFO_STRING: {
 512            char* v = nullptr;
 513            CURLcode err = curl_easy_getinfo(req->easy_handle, info, &v);
 514            if (err) {
 515                pw_set_status(PwCurlStatus(PwCurlError_Easy, err));
 516                return false;
 517            }
 518            if (v) {
 519                if (!pw_create_string(result, v)) {
 520                    return false;
 521                }
 522            } else {
 523                pw_destroy(result);
 524                *result = PwNull();
 525            }
 526            return true;
 527        }
 528        case CURLINFO_LONG: {
 529            long v;
 530            CURLcode err = curl_easy_getinfo(req->easy_handle, info, &v);
 531            if (err) {
 532                pw_set_status(PwCurlStatus(PwCurlError_Easy, err));
 533                return false;
 534            }
 535            pw_destroy(result);
 536            *result = PwSigned(v);
 537            return true;
 538        }
 539        case CURLINFO_DOUBLE: {
 540            double v;
 541            CURLcode err = curl_easy_getinfo(req->easy_handle, info, &v);
 542            if (err) {
 543                pw_set_status(PwCurlStatus(PwCurlError_Easy, err));
 544                return false;
 545            }
 546            pw_destroy(result);
 547            *result = PwFloat(v);
 548            return true;
 549        }
 550        case CURLINFO_SLIST: {
 551            struct curl_slist* v = nullptr;
 552            CURLcode err = curl_easy_getinfo(req->easy_handle, info, &v);
 553            if (err) {
 554                pw_set_status(PwCurlStatus(PwCurlError_Easy, err));
 555                return false;
 556            }
 557            if (v) {
 558                if (!pw_create(PwTypeId_BasicArray, result)) {
 559                    return false;
 560                }
 561                struct curl_slist* elem = v;
 562                while(elem) {
 563                    PwValue s = PW_NULL;
 564                    if (!pw_create_string(&s, elem->data)) {
 565                        curl_slist_free_all(v);
 566                        return false;
 567                    }
 568                    if (!pw_array_append(result, &s)) {
 569                        curl_slist_free_all(v);
 570                        return false;
 571                    }
 572                    elem = elem->next;
 573                }
 574                curl_slist_free_all(v);
 575            } else {
 576                pw_destroy(result);
 577                *result = PwNull();
 578            }
 579            return true;
 580        }
 581        case CURLINFO_SOCKET: {
 582            curl_socket_t sockfd;
 583            CURLcode err = curl_easy_getinfo(req->easy_handle, info, &sockfd);
 584            if (err || sockfd == CURL_SOCKET_BAD) {
 585                pw_set_status(PwCurlStatus(PwCurlError_Easy, err));
 586                return false;
 587            }
 588            PwValue socket = PW_NULL;
 589            if (!pw_create(PwTypeId_Socket, &socket)) {
 590                return false;
 591            }
 592            if (!pw_set_fd(&socket, sockfd, false)) {
 593                return false;
 594            }
 595            pw_move(result, &socket);
 596            return true;
 597        }
 598        case CURLINFO_OFF_T: {
 599            curl_off_t v;
 600            CURLcode err = curl_easy_getinfo(req->easy_handle, info, &v);
 601            if (err) {
 602                pw_set_status(PwCurlStatus(PwCurlError_Easy, err));
 603                return false;
 604            }
 605            pw_destroy(result);
 606            *result = PwSigned(v);
 607            return true;
 608        }
 609        default:
 610            pw_set_status(PwCurlStatus(PwCurlError_Easy, CURLE_UNKNOWN_OPTION));
 611            return false;
 612    }
 613}
 614
 615[[nodiscard]] static bool parse_content_type(CurlRequestData* req)
 616/*
 617 * Parse content-type header
 618 */
 619{
 620    if (pw_is_string(&req->media_type) && pw_strlen(&req->media_type) != 0) {
 621        // already parsed
 622        return true;
 623    }
 624    char* content_type;
 625    CURLcode err = curl_easy_getinfo(req->easy_handle, CURLINFO_CONTENT_TYPE, &content_type);
 626    if (err) {
 627        pw_set_status(PwCurlStatus(PwCurlError_Easy, err));
 628        return false;
 629    }
 630    if (content_type) {
 631        if (!pw_parse_content_type(content_type, &req->media_type, &req->media_subtype, &req->media_type_params)) {
 632            return false;
 633        }
 634    }
 635    return true;
 636}
 637
 638[[nodiscard]] static bool parse_content_disposition(CurlRequestData* req)
 639/*
 640 * Parse content-disposition header
 641 */
 642{
 643    if (pw_is_string(&req->disposition_type) && pw_strlen(&req->disposition_type) != 0) {
 644        // already parsed
 645        return true;
 646    }
 647    char* content_disposition = get_response_header(req->easy_handle, "Content-Disposition");
 648    if (content_disposition) {
 649        if (!pw_parse_content_disposition(content_disposition, &req->disposition_type, &req->disposition_params)) {
 650            return false;
 651        }
 652    }
 653    return true;
 654}
 655
 656static bool curl_request_attachment_filename(PwMethod_CurlRequest_attachment_filename* mthis, PwValuePtr self, PwValuePtr result)
 657{
 658    CurlRequestData* req = pw_this_data(self);
 659
 660    if (pw_is_string(&req->attachment_filename) && pw_strlen(&req->attachment_filename) != 0) {
 661        // return cached value
 662        pw_clone2(result, &req->attachment_filename);
 663        return true;
 664    }
 665    // ensure necessary headers are parsed
 666    if (!parse_content_type(req)) {
 667        return false;
 668    }
 669    if (!parse_content_disposition(req)) {
 670        return false;
 671    }
 672    // try to get filename from Content-Disposition header
 673    if (pw_equal(&req->disposition_type, "attachment")) {
 674        PwValue extval = PW_NULL;
 675        if (pw_map_get(&req->disposition_params, "filename*", &extval)) {
 676            PwValue charset = PW_NULL;
 677            PwValue filename = PW_NULL;
 678            if (!pw_map_get(&extval, "charset", &charset)) {
 679                return false;
 680            }
 681            if (!pw_map_get(&extval, "value", &filename)) {
 682                return false;
 683            }
 684            if (!decode(&filename, &charset, &req->attachment_filename)) {
 685                return false;
 686            }
 687        } else if (!pw_map_get(&req->disposition_params, "filename", &req->attachment_filename)) {
 688            // make sure it's Null:
 689            pw_destroy(&req->attachment_filename);
 690        }
 691    }
 692    if (pw_is_null(&req->attachment_filename)) {
 693        // try to get filename from Location header
 694        PwValue location = PwString("");  // empty string will result in empty filename in case of failure
 695        char* last_location = get_response_header(req->easy_handle, "Location");
 696        if (last_location) {
 697            if (!pw_create_string(&location, last_location)) {
 698                return false;
 699            }
 700        } else {
 701            // try to get filename from the effective URL
 702            char* url = nullptr;
 703            curl_easy_getinfo(req->easy_handle, CURLINFO_EFFECTIVE_URL, &url);
 704            if (url) {
 705                if (!pw_create_string(&location, url)) {
 706                    return false;
 707                }
 708            }
 709        }
 710
 711        // could use curl_url_get here, but...
 712
 713        // strip scheme
 714        unsigned scheme_sep;
 715        if (pw_strstr(&location, "://", 0, &scheme_sep)) {
 716            PwValue tmp = PW_NULL;
 717            if (!pw_substr(&location, scheme_sep + 3, UINT_MAX, &tmp)) {
 718                return false;
 719            }
 720            pw_move(&location, &tmp);
 721        }
 722        // strip trailing slash
 723        if (!pw_string_rstrip_chars(&location, U"/")) {
 724            return false;
 725        }
 726        // cut last part of the path
 727        PwValue parts = PW_NULL;
 728        if (!pw_string_split_chr(&location, '/', 1, &parts)) {
 729            return false;
 730        }
 731        if (pw_array_length(&parts) == 1) {
 732            // no path
 733            pw_destroy(&req->attachment_filename);
 734            req->attachment_filename = PwString("");
 735        } else {
 736            if (!pw_array_item(&parts, -1, &req->attachment_filename)) {
 737                return false;
 738            }
 739        }
 740    }
 741    pw_clone2(result, &req->attachment_filename);
 742    return true;
 743}
 744
 745static bool curl_request_content_disposition(PwMethod_CurlRequest_content_disposition* mthis, PwValuePtr self,
 746                                             PwValuePtr disposition_type, PwValuePtr disposition_params)
 747{
 748    CurlRequestData* req = pw_this_data(self);
 749
 750    if (!parse_content_disposition(req)) {
 751        return false;
 752    }
 753    if (disposition_type) {
 754        pw_clone2(disposition_type, &req->disposition_type);
 755    }
 756    if (disposition_params) {
 757        pw_clone2(disposition_params, &req->disposition_params);
 758    }
 759    return true;
 760}
 761
 762static bool curl_request_content_type(PwMethod_CurlRequest_content_type* mthis, PwValuePtr self,
 763                                      PwValuePtr media_type, PwValuePtr media_subtype, PwValuePtr media_type_params)
 764{
 765    CurlRequestData* req = pw_this_data(self);
 766
 767    if (!parse_content_type(req)) {
 768        return false;
 769    }
 770    if (media_type) {
 771        pw_clone2(media_type, &req->media_type);
 772    }
 773    if (media_subtype) {
 774        pw_clone2(media_subtype, &req->media_subtype);
 775    }
 776    if (media_type_params) {
 777        pw_clone2(media_type_params, &req->media_type_params);
 778    }
 779    return true;
 780}
 781
 782static bool curl_request_response_header(PwMethod_CurlRequest_response_header* mthis, PwValuePtr self,
 783                                         PwValuePtr name, PwValuePtr result)
 784{
 785    CurlRequestData* req = pw_this_data(self);
 786
 787    char* last_value = nullptr;
 788    int last_request = 0;
 789    int last_amount = 1;
 790
 791    struct curl_header* hdr;
 792    PW_CSTRING(name_cstr, name);
 793
 794    // iterate over issued requests to get the last header
 795    for (int i = 0;; i++) {
 796        CURLHcode err = curl_easy_header(req->easy_handle, name_cstr, 0, CURLH_HEADER, i, &hdr);
 797        if (err) {
 798            break;
 799        }
 800        last_value = hdr->value;
 801        last_amount = hdr->amount;
 802        last_request = i;
 803    }
 804    if (last_value == nullptr) {
 805        // make sure result is Null
 806        pw_destroy(result);
 807        return true;
 808    }
 809    if (last_amount == 1) {
 810        CURLHcode err = curl_easy_header(req->easy_handle, name_cstr, 0, CURLH_HEADER, last_request, &hdr);
 811        if (err) {
 812            pw_set_status(PwCurlStatus(PwCurlError_Easy, err));
 813            return false;
 814        }
 815        return pw_create_string(result, hdr->value);
 816    }
 817    if (!pw_create_array(result)) {
 818        return false;
 819    }
 820    for (int i = 0; i < last_amount; i++) {
 821        CURLHcode err = curl_easy_header(req->easy_handle, name_cstr, i, CURLH_HEADER, last_request, &hdr);
 822        if (err) {
 823            pw_set_status(PwCurlStatus(PwCurlError_Easy, err));
 824            return false;
 825        }
 826        PwValue header = PW_NULL;
 827        if (!pw_create_string(&header, hdr->value)) {
 828            return false;
 829        }
 830        if (!pw_array_append(result, &header)) {
 831            return false;
 832        }
 833    }
 834    return true;
 835}
 836
 837static bool curl_request_on_content_start(PwMethod_CurlRequest_on_content_start* mthis, PwValuePtr self,
 838                                          long status, curl_off_t length, PwValuePtr final_url)
 839{
 840    return true;
 841}
 842
 843static bool curl_request_on_content(PwMethod_CurlRequest_on_content* mthis, PwValuePtr self, void* data, size_t size)
 844{
 845    return true;
 846}
 847
 848static bool curl_request_on_complete(PwMethod_CurlRequest_on_complete* mthis, PwValuePtr self, PwValuePtr error)
 849{
 850    return true;
 851}
 852
 853static PwInterface_CurlRequest curl_request_interface = {
 854#define X(name, ...) .name = { .func = curl_request_##name } __VA_OPT__(,)
 855    PW_CURL_REQUEST_INTERFACE_METHODS
 856#undef X
 857};
 858
 859/*
 860 * Callbacks are set by add_request
 861 */
 862
 863static size_t write_callback(char* data, size_t always_1, size_t size, void* userdata)
 864{
 865    PwValuePtr request = userdata;
 866    CurlRequestData* req = _pw_get_struct_ptr(request, PwTypeId_CurlRequest);
 867
 868    if (!req->content_started) {
 869
 870        // first invocation
 871        // get request info and call on_content_start
 872
 873        long status;
 874        CURLcode err = curl_easy_getinfo(req->easy_handle, CURLINFO_RESPONSE_CODE, &status);
 875        if (err) {
 876            fprintf(stderr, "%s:%s: %s\n", __FILE__, __func__, curl_easy_strerror(err));
 877            return 0;
 878        }
 879        curl_off_t content_length;
 880        err = curl_easy_getinfo(req->easy_handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &content_length);
 881        if (err) {
 882            fprintf(stderr, "%s:%s: %s\n", __FILE__, __func__, curl_easy_strerror(err));
 883            return 0;
 884        }
 885        PwValue url = PW_STRING("");
 886        char* url_ptr = nullptr;
 887        err = curl_easy_getinfo(req->easy_handle, CURLINFO_EFFECTIVE_URL, &url_ptr);
 888        if (err) {
 889            fprintf(stderr, "%s:%s: %s\n", __FILE__, __func__, curl_easy_strerror(err));
 890            return 0;
 891        }
 892        if (url_ptr) {
 893            if (!pw_create_string(&url, url_ptr)) {
 894                fprintf(stderr, "%s:%s: out of memory\n", __FILE__, __func__);
 895                return 0;
 896            }
 897        }
 898        if (!pw_call(CurlRequest, on_content_start, request, status, content_length, &url)) {
 899            pw_print_status(stderr, &current_task->status);
 900            return 0;
 901        }
 902        req->content_started = true;
 903    }
 904
 905    if (size) {
 906        if (!pw_call(CurlRequest, on_content, request, data, size)) {
 907            pw_print_status(stderr, &current_task->status);
 908            return 0;
 909        }
 910    }
 911    return size;
 912}
 913
 914/****************************************************************
 915 * Wrapper type for Curl multi handle
 916 */
 917
 918uint16_t PwTypeId_CurlSession;
 919uint16_t PwInterfaceId_CurlSession;
 920
 921typedef struct {
 922
 923    CURLM* multi_handle;
 924    PwValuePtr requests;  // pointer to the first value in the linked list
 925
 926} CurlSessionData;
 927
 928
 929static bool append_request_to_list(CurlSessionData* session, PwValuePtr request, CurlRequestData* req)
 930/*
 931 * Clone request and add to the list of requests
 932 */
 933{
 934    PwValuePtr item = _pw_types[request->type_id]->allocator->allocate(sizeof(_PwValue), false);
 935    if (!item) {
 936        pw_set_status(PwStatus(PW_ERROR_OOM));
 937        return false;
 938    }
 939    __pw_clone(item, request);  // no need to destroy item, use __pw_clone here
 940
 941    if (session->requests) {
 942        req->next = session->requests;
 943        CurlRequestData* next = _pw_get_struct_ptr(req->next, PwTypeId_CurlRequest);
 944
 945        req->prev = next->prev;
 946        CurlRequestData* prev = _pw_get_struct_ptr(req->prev, PwTypeId_CurlRequest);
 947
 948        next->prev = item;
 949        prev->next = item;
 950    } else {
 951        // first item in the list
 952        req->next = item;
 953        req->prev = item;
 954    }
 955    session->requests = item;
 956    req->self = item;
 957    return true;
 958}
 959
 960static void remove_request_from_list(CurlSessionData* session, CurlRequestData* req)
 961/*
 962 * Remove request from the list and destroy.
 963 * The request should be out of Curl multi handle when this function is called.
 964 */
 965{
 966    if (req->next == req->prev) {
 967        // last item in the list
 968        session->requests = nullptr;
 969    } else {
 970        CurlRequestData* next = _pw_get_struct_ptr(req->next, PwTypeId_CurlRequest);
 971        CurlRequestData* prev = _pw_get_struct_ptr(req->prev, PwTypeId_CurlRequest);
 972
 973        session->requests = req->next;
 974        next->prev = req->prev;
 975        prev->next = req->next;
 976    }
 977    req->next = nullptr;
 978    req->prev = nullptr;
 979
 980    uint16_t type_id = req->self->type_id;
 981    pw_destroy(req->self);
 982    _pw_types[type_id]->allocator->release((void**) &req->self, sizeof(_PwValue));
 983}
 984
 985/*
 986 * Basic interface
 987 */
 988
 989static bool curl_session_create(PwMethod_Basic_create* mthis, PwValuePtr result, PwCtorArgs* ctor_args)
 990{
 991    if (!pw_super(mthis, result, ctor_args)) {
 992        return false;
 993    }
 994
 995    CurlSessionData* session = pw_this_data(result);
 996
 997    session->multi_handle = curl_multi_init();
 998    if (session->multi_handle) {
 999#       ifdef CURLPIPE_MULTIPLEX
1000            // enables http/2
1001            curl_multi_setopt(session->multi_handle, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
1002#       endif
1003        return true;
1004    }
1005
1006    pw_set_status(PwCurlStatus(PwCurlError_Easy, CURLE_FAILED_INIT));
1007    if (!pw_super_call(destroy, mthis, result, nullptr)) { /* no op */ }
1008    return false;
1009}
1010
1011static void remove_request(CurlSessionData* session, CurlRequestData* req)
1012{
1013    curl_easy_setopt(req->easy_handle, CURLOPT_PRIVATE, nullptr);
1014    curl_easy_setopt(req->easy_handle, CURLOPT_WRITEFUNCTION, nullptr);
1015    curl_easy_setopt(req->easy_handle, CURLOPT_WRITEDATA, nullptr);
1016
1017    CURLMcode err = curl_multi_remove_handle(session->multi_handle, req->easy_handle);
1018    if (err) {
1019        fprintf(stderr, "%s:%s: %s\n", __FILE__, __func__, curl_multi_strerror(err));
1020    }
1021
1022    req->session = nullptr;
1023
1024    // remove request from the internal storage
1025    remove_request_from_list(session, req);
1026}
1027
1028static bool curl_session_destroy(PwMethod_Basic_destroy* mthis, PwValuePtr self, _PwCompoundChain* tail)
1029{
1030    CurlSessionData* session = pw_this_data(self);
1031
1032    // forcibly remove all requests
1033    while (session->requests) {
1034        CurlRequestData* req = _pw_get_struct_ptr(session->requests, PwTypeId_CurlRequest);
1035        remove_request(session, req);
1036    }
1037
1038    CURLMcode err = curl_multi_cleanup(session->multi_handle);
1039    if (err) {
1040        // destructor can't fail, print warning
1041        fprintf(stderr, "%s:%s: %s\n", __FILE__, __func__, curl_multi_strerror(err));
1042    }
1043    return pw_super(mthis, self, tail);
1044}
1045
1046static PwInterface_Basic curl_session_basic_interface = {
1047    .create  = { .func = curl_session_create },
1048    .destroy = { .func = curl_session_destroy }
1049};
1050
1051/*
1052 * Curl interface
1053 */
1054
1055static bool curl_session_add_request(PwMethod_CurlSession_add_request* mthis, PwValuePtr self, PwValuePtr request)
1056{
1057    CurlSessionData* session = pw_this_data(self);
1058    CurlRequestData* req = _pw_get_struct_ptr(request, PwTypeId_CurlRequest);
1059
1060    if (!req) {
1061        pw_set_status(PwStatus(PW_ERROR_INCOMPATIBLE_TYPE));
1062        return false;
1063    }
1064
1065    if (req->session) {
1066        pw_set_status(PwStatus(PW_ERROR_REQUEST_ALREADY_ADDED));
1067        return false;
1068    }
1069
1070    // add request to the internal list
1071    if (!append_request_to_list(session, request, req)) {
1072        return false;
1073    }
1074
1075    CURLMcode m_err = curl_multi_add_handle(session->multi_handle, req->easy_handle);
1076    if (m_err) {
1077        remove_request_from_list(session, req);
1078        pw_set_status(PwCurlStatus(PwCurlError_Multi, m_err));
1079        return false;
1080    }
1081
1082    // set request as a private data for easy_handle
1083    // use PwValue stored in dynamically allocated block, i.e. req->self
1084
1085    CURLcode err = curl_easy_setopt(req->easy_handle, CURLOPT_PRIVATE, req->self);
1086    if (err) {
1087        goto err_out;
1088    }
1089
1090    // set write function
1091    err = curl_easy_setopt(req->easy_handle, CURLOPT_WRITEFUNCTION, write_callback);
1092    if (err) {
1093        goto err_out;
1094    }
1095    err = curl_easy_setopt(req->easy_handle, CURLOPT_WRITEDATA, req->self);
1096    if (err) {
1097        goto err_out;
1098    }
1099
1100    // python leftovers to do someday:
1101    //
1102    // if method == 'POST':
1103    //     if form_data is not None:
1104    //         post_data = urlencode(form_data)
1105    //     c.setopt(c.POSTFIELDS, post_data)
1106
1107    req->session = self->struct_data;
1108    return true;
1109
1110err_out:
1111    remove_request_from_list(session, req);
1112    pw_set_status(PwCurlStatus(PwCurlError_Easy, err));
1113    return false;
1114}
1115
1116static bool curl_session_remove_request(PwMethod_CurlSession_remove_request* mthis, PwValuePtr self, PwValuePtr request)
1117{
1118    CurlSessionData* session = pw_this_data(self);
1119    CurlRequestData* req = _pw_get_struct_ptr(request, PwTypeId_CurlRequest);
1120
1121    if (!req) {
1122        pw_set_status(PwStatus(PW_ERROR_INCOMPATIBLE_TYPE));
1123        return false;
1124    }
1125
1126    if (!req->session) {
1127        // already removed
1128        return true;
1129    }
1130
1131    remove_request(session, req);
1132    return true;
1133}
1134
1135static bool check_transfers(CurlSessionData* session)
1136{
1137    for(;;) {
1138        // check transfers
1139        int msgs_left;
1140        CURLMsg *m = curl_multi_info_read(session->multi_handle, &msgs_left);
1141        if (!m) {
1142            return true;
1143        }
1144        if(m->msg != CURLMSG_DONE) {
1145            continue;
1146        }
1147
1148        // get pointer to the internally stored request and then clone it
1149        // because internally stored value will be destroyed by remove_request;
1150        // we just need it alive a little bit bit longer to call on_complete
1151        PwValuePtr r = nullptr;
1152        CURLcode err = curl_easy_getinfo(m->easy_handle, CURLINFO_PRIVATE, (char**) &r);
1153        if (err) {
1154            pw_set_status(PwCurlStatus(PwCurlError_Easy, err));
1155            return false;
1156        }
1157        PwValue request = pw_clone(r);
1158
1159        // get completion status
1160
1161        PwValue error = PW_NULL;
1162        if(m->data.result != CURLE_OK) {
1163            error = PwCurlStatus(PwCurlError_Easy, m->data.result);
1164        }
1165
1166        // remove request
1167
1168        CurlRequestData* req = _pw_get_struct_ptr(&request, PwTypeId_CurlRequest);
1169        remove_request(session, req);
1170
1171        // call completion method
1172        if (!pw_call(CurlRequest, on_complete, &request, &error)) {
1173            pw_print_status(stderr, &current_task->status);
1174        }
1175    }
1176}
1177
1178static bool curl_session_perform(PwMethod_CurlSession_perform* mthis, PwValuePtr self,
1179                                 PwValuePtr timeout, int* running_transfers)
1180{
1181    CurlSessionData* session = pw_this_data(self);
1182    CURLMcode m_err;
1183
1184    m_err = curl_multi_perform(session->multi_handle, running_transfers);
1185    if (m_err) {
1186        pw_set_status(PwCurlStatus(PwCurlError_Multi, m_err));
1187        return false;
1188    }
1189    if (!*running_transfers) {
1190        // no transfers running
1191        // check completed requests
1192        return check_transfers(session);
1193    }
1194
1195    // wait for something to happen
1196
1197    int timeout_ms = INT_MAX;
1198
1199    if (pw_is_timestamp(timeout)) {
1200        if (timeout->ts_seconds < INT_MAX / 1000) {
1201            timeout_ms = timeout->ts_seconds * 1000 + timeout->ts_nanoseconds / 1000000;
1202        }
1203    } else if (pw_is_int(timeout)) {
1204        if (timeout->signed_value < INT_MAX / 1000) {
1205            timeout_ms = timeout->signed_value * 1000;
1206        }
1207    } else if (pw_is_null(timeout)) {
1208        timeout_ms = 1000;
1209    } else {
1210        pw_set_status(PwStatus(PW_ERROR_INCOMPATIBLE_TYPE), "Bad timeout");
1211        return false;
1212    }
1213
1214    m_err = curl_multi_wait(session->multi_handle, NULL, 0, timeout_ms, NULL);
1215    if (m_err) {
1216        pw_set_status(PwCurlStatus(PwCurlError_Multi, m_err));
1217        return false;
1218    }
1219
1220    return check_transfers(session);
1221}
1222
1223
1224static PwInterface_CurlSession curl_session_interface = {
1225#define X(name, ...) .name = { .func = curl_session_##name } __VA_OPT__(,)
1226    PW_CURL_SESSION_INTERFACE_METHODS
1227#undef X
1228};
1229
1230/****************************************************************
1231 * Initialization
1232 */
1233
1234[[ gnu::constructor ]]
1235static void init()
1236{
1237    if (PwInterfaceId_CurlStatus) {
1238        return;
1239    }
1240
1241    _pw_init_types();
1242
1243    PW_ERROR_CURL                  = pw_define_status("ERROR_CURL");
1244    PW_ERROR_REQUEST_ALREADY_ADDED = pw_define_status("REQUEST_ALREADY_ADDED");
1245
1246#   define X(name, ...) #name __VA_OPT__(,)
1247    PwInterfaceId_CurlStatus  = pw_register_interface("CurlStatus",  PW_CURL_STATUS_INTERFACE_METHODS, nullptr);
1248    PwInterfaceId_CurlRequest = pw_register_interface("CurlRequest", PW_CURL_REQUEST_INTERFACE_METHODS, nullptr);
1249    PwInterfaceId_CurlSession = pw_register_interface("CurlSession", PW_CURL_SESSION_INTERFACE_METHODS, nullptr);
1250#   undef X
1251
1252    PwTypeId_CurlStatus = pw_add_type2(
1253        "CurlStatus", PwCurlStatusData,
1254        PW_PARENTS,
1255            PwTypeId_Status,
1256        PW_INTERFACES,
1257            PwInterfaceId_CurlStatus, &curl_status_interface,
1258            PwInterfaceId_Basic,      &curl_status_basic_interface
1259    );
1260
1261    PwTypeId_CurlRequest = pw_add_type2(
1262        "CurlRequest", CurlRequestData,
1263        PW_PARENTS,
1264            PwTypeId_Struct,
1265        PW_INTERFACES,
1266            PwInterfaceId_CurlRequest, &curl_request_interface,
1267            PwInterfaceId_Basic,       &curl_request_basic_interface
1268    );
1269
1270    PwTypeId_CurlSession = pw_add_type2(
1271        "CurlSession", CurlSessionData,
1272        PW_PARENTS,
1273            PwTypeId_Struct,
1274        PW_INTERFACES,
1275            PwInterfaceId_CurlSession, &curl_session_interface,
1276            PwInterfaceId_Basic,       &curl_session_basic_interface
1277    );
1278}