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, ¤t_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, ¤t_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, ¤t_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, ¤t_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}