274 size_t max_pooled_connections = kDefaultMaxPooledHandles)
275 : io_(io), timer_(io), max_pooled_handles_(max_pooled_connections) {
276 curl_global_init(CURL_GLOBAL_ALL);
277 multi_ = curl_multi_init();
278 curl_multi_setopt(multi_, CURLMOPT_SOCKETFUNCTION, socket_cb);
279 curl_multi_setopt(multi_, CURLMOPT_SOCKETDATA,
this);
280 curl_multi_setopt(multi_, CURLMOPT_TIMERFUNCTION, timer_cb);
281 curl_multi_setopt(multi_, CURLMOPT_TIMERDATA,
this);
295 for (
const auto& [fd, state] : sockets_) {
297 state->socket.close(ignored);
299 curl_multi_cleanup(multi_);
300 for (CURL* eh : pool_) {
301 curl_easy_cleanup(eh);
303 curl_global_cleanup();
317 template <
typename CompletionToken>
319 return net::async_initiate<CompletionToken, void(error_code, Response)>(
320 Initiation{
this}, token, std::move(req));
323 template <
typename CompletionToken>
324 auto async_get(std::string url, CompletionToken&& token) {
326 std::forward<CompletionToken>(token));
329 template <
typename CompletionToken>
330 auto async_post(std::string url, std::string body, CompletionToken&& token) {
333 .body(std::move(body)),
334 std::forward<CompletionToken>(token));
348 : client_(client), req_(std::move(url)) {}
355 req_.
body(std::move(b));
367 req_.
curl(std::move(fn));
371 template <
typename CompletionToken>
372 auto get(CompletionToken&& token) {
374 std::forward<CompletionToken>(token));
376 template <
typename CompletionToken>
377 auto post(CompletionToken&& token) {
379 std::forward<CompletionToken>(token));
381 template <
typename CompletionToken>
382 auto put(CompletionToken&& token) {
384 std::forward<CompletionToken>(token));
386 template <
typename CompletionToken>
387 auto patch(CompletionToken&& token) {
389 std::forward<CompletionToken>(token));
391 template <
typename CompletionToken>
392 auto del(CompletionToken&& token) {
394 std::forward<CompletionToken>(token));
398 template <
typename CompletionToken>
402 std::forward<CompletionToken>(token));
426 using executor_type = net::io_context::executor_type;
427 executor_type get_executor() const noexcept {
428 return self->io_.get_executor();
430 template <
typename H>
431 void operator()(H handler, Request r)
const {
432 self->start(std::move(r), Handler(std::move(handler)));
439 static constexpr size_t kDefaultMaxPooledHandles = 64;
442 Transfer(CURL* e, Handler h) : eh(e), handler(std::move(h)) {}
444 curl_slist* headers =
nullptr;
447 std::string header_buffer;
449 std::list<Transfer>::iterator self;
450 std::uint64_t
id = 0;
454 bool scrub_on_done =
false;
457 struct SocketState : std::enable_shared_from_this<SocketState> {
458 explicit SocketState(net::io_context& io) : socket(io) {}
459 net::ip::tcp::socket socket;
461 bool read_armed =
false;
462 bool write_armed =
false;
465 void start(Request r, Handler h) {
468 eh = curl_easy_init();
469 configure_handle(eh);
476 transfers_.emplace_front(eh, std::move(h));
477 const auto it = transfers_.begin();
479 it->body = std::move(r.body_);
481 curl_easy_setopt(eh, CURLOPT_URL, r.url_.c_str());
482 curl_easy_setopt(eh, CURLOPT_WRITEDATA, &*it);
483 curl_easy_setopt(eh, CURLOPT_HEADERDATA, &it->header_buffer);
484 curl_easy_setopt(eh, CURLOPT_PRIVATE, &*it);
485 curl_easy_setopt(eh, CURLOPT_TIMEOUT_MS,
486 static_cast<long>(r.timeout_.count()));
488 curl_easy_setopt(eh, CURLOPT_FOLLOWLOCATION,
489 r.max_redirects_ != 0 ? 1L : 0L);
490 curl_easy_setopt(eh, CURLOPT_MAXREDIRS, r.max_redirects_);
492 curl_slist* chunk =
nullptr;
493 for (
const auto& header : r.headers_) {
494 chunk = curl_slist_append(chunk, header.c_str());
497 curl_easy_setopt(eh, CURLOPT_HTTPHEADER, chunk);
502 curl_easy_setopt(eh, CURLOPT_CUSTOMREQUEST,
nullptr);
503 curl_easy_setopt(eh, CURLOPT_HTTPGET, 1L);
506 curl_easy_setopt(eh, CURLOPT_CUSTOMREQUEST,
nullptr);
507 curl_easy_setopt(eh, CURLOPT_POST, 1L);
511 curl_easy_setopt(eh, CURLOPT_CUSTOMREQUEST,
"PUT");
512 curl_easy_setopt(eh, CURLOPT_POST, 1L);
516 curl_easy_setopt(eh, CURLOPT_CUSTOMREQUEST,
"PATCH");
517 curl_easy_setopt(eh, CURLOPT_POST, 1L);
521 curl_easy_setopt(eh, CURLOPT_CUSTOMREQUEST,
"DELETE");
522 curl_easy_setopt(eh, CURLOPT_POST, 1L);
529 it->scrub_on_done =
true;
533 it->id = ++next_transfer_id_;
534 auto slot = net::get_associated_cancellation_slot(it->handler);
535 if (slot.is_connected()) {
541 [
this, alive = alive_,
id = it->id](net::cancellation_type_t) {
542 if (*alive) cancel(id);
547 curl_multi_add_handle(multi_, eh);
554 void cancel(std::uint64_t
id) {
555 for (
auto& t : transfers_) {
556 if (t.id !=
id)
continue;
558 curl_multi_remove_handle(multi_, t.eh);
559 if (running_ > 0) --running_;
560 Handler handler = std::move(t.handler);
561 curl_slist_free_all(t.headers);
563 curl_easy_reset(t.eh);
564 configure_handle(t.eh);
565 if (pool_.size() < max_pooled_handles_) {
566 pool_.emplace_back(t.eh);
568 curl_easy_cleanup(t.eh);
570 transfers_.erase(t.self);
575 net::post(io_, net::bind_allocator(
576 net::recycling_allocator<void>(),
577 [h = std::move(handler)]()
mutable {
580 Response{CURLE_ABORTED_BY_CALLBACK, 0, {}, {}});
589 void configure_handle(CURL* eh) {
590 curl_easy_setopt(eh, CURLOPT_WRITEFUNCTION, body_write_cb);
591 curl_easy_setopt(eh, CURLOPT_HEADERFUNCTION, write_cb);
592 curl_easy_setopt(eh, CURLOPT_NOSIGNAL, 1L);
593#if LIBCURL_VERSION_NUM >= 0x075000
596 curl_easy_setopt(eh, CURLOPT_MAXLIFETIME_CONN, 30L);
599 curl_easy_setopt(eh, CURLOPT_ACCEPT_ENCODING,
"");
602 curl_easy_setopt(eh, CURLOPT_PIPEWAIT, 1L);
603 curl_easy_setopt(eh, CURLOPT_BUFFERSIZE, 512L * 1024L);
604 curl_easy_setopt(eh, CURLOPT_OPENSOCKETFUNCTION, open_socket_cb);
605 curl_easy_setopt(eh, CURLOPT_OPENSOCKETDATA,
this);
606 curl_easy_setopt(eh, CURLOPT_CLOSESOCKETFUNCTION, close_socket_cb);
607 curl_easy_setopt(eh, CURLOPT_CLOSESOCKETDATA,
this);
610 void set_body(
const Transfer& t) {
611 curl_easy_setopt(t.eh, CURLOPT_POSTFIELDSIZE,
612 static_cast<long>(t.body.size()));
613 curl_easy_setopt(t.eh, CURLOPT_POSTFIELDS, t.body.c_str());
616 static size_t write_cb(
char* data,
size_t n,
size_t l, std::string* buf) {
617 buf->append(data, n * l);
621 static size_t body_write_cb(
char* data,
size_t n,
size_t l, Transfer* t) {
622 if (t->buffer.empty()) {
624 if (curl_easy_getinfo(t->eh, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &len) ==
627 t->buffer.reserve(
static_cast<size_t>(len));
630 t->buffer.append(data, n * l);
636 static curl_socket_t open_socket_cb(
void* clientp, curlsocktype purpose,
637 curl_sockaddr* address) {
638 auto*
const self =
static_cast<Client*
>(clientp);
639 if (purpose != CURLSOCKTYPE_IPCXN)
return CURL_SOCKET_BAD;
640 net::ip::tcp protocol = net::ip::tcp::v4();
641 if (address->family == AF_INET6) {
642 protocol = net::ip::tcp::v6();
643 }
else if (address->family != AF_INET) {
644 return CURL_SOCKET_BAD;
646 auto state = std::make_shared<SocketState>(self->io_);
648 state->socket.open(protocol, ec);
649 if (ec)
return CURL_SOCKET_BAD;
650 const curl_socket_t fd = state->socket.native_handle();
651 self->sockets_[fd] = std::move(state);
655 static int close_socket_cb(
void* clientp, curl_socket_t fd) {
656 auto*
const self =
static_cast<Client*
>(clientp);
657 const auto it = self->sockets_.find(fd);
658 if (it == self->sockets_.end())
return 1;
659 it->second->watch = 0;
661 it->second->socket.close(ignored);
662 self->sockets_.erase(it);
666 static int socket_cb(CURL*, curl_socket_t fd,
int what,
void* userp,
668 auto*
const self =
static_cast<Client*
>(userp);
669 auto* state =
static_cast<SocketState*
>(socketp);
670 if (state ==
nullptr) {
673 const auto it = self->sockets_.find(fd);
674 if (it == self->sockets_.end())
return 0;
675 state = it->second.get();
676 curl_multi_assign(self->multi_, fd, state);
678 state->watch = (what == CURL_POLL_REMOVE) ? 0 : what;
679 if (state->watch != 0) self->arm(state->shared_from_this());
683 void arm(
const std::shared_ptr<SocketState>& state) {
684 const curl_socket_t fd = state->socket.native_handle();
685 if ((state->watch & CURL_POLL_IN) && !state->read_armed) {
686 state->read_armed =
true;
687 state->socket.async_wait(
688 net::ip::tcp::socket::wait_read,
690 net::recycling_allocator<void>(),
691 [
this, w = std::weak_ptr<SocketState>(state), fd](
error_code ec) {
692 on_event(w, fd, CURL_CSELECT_IN, ec);
695 if ((state->watch & CURL_POLL_OUT) && !state->write_armed) {
696 state->write_armed =
true;
697 state->socket.async_wait(
698 net::ip::tcp::socket::wait_write,
700 net::recycling_allocator<void>(),
701 [
this, w = std::weak_ptr<SocketState>(state), fd](
error_code ec) {
702 on_event(w, fd, CURL_CSELECT_OUT, ec);
707 void on_event(
const std::weak_ptr<SocketState>& weak, curl_socket_t fd,
711 const auto state = weak.lock();
713 (flag == CURL_CSELECT_IN ? state->read_armed : state->write_armed) =
false;
714 if (ec == net::error::operation_aborted)
return;
715 curl_multi_socket_action(multi_, fd, ec ? CURL_CSELECT_ERR : flag,
718 if (state->socket.is_open() && state->watch != 0) arm(state);
721 static int timer_cb(CURLM*,
long timeout_ms,
void* userp) {
722 auto*
const self =
static_cast<Client*
>(userp);
723 if (timeout_ms < 0) {
724 self->timer_.cancel();
725 self->timer_armed_ =
false;
728 if (timeout_ms == 0) {
732 if (!self->kick_pending_) {
733 self->kick_pending_ =
true;
735 net::bind_allocator(net::recycling_allocator<void>(),
736 [self, alive = self->alive_] {
738 self->kick_pending_ = false;
744 const auto deadline = std::chrono::steady_clock::now() +
745 std::chrono::milliseconds(timeout_ms);
749 if (self->timer_armed_ && self->timer_.expiry() <= deadline)
return 0;
750 self->timer_.expires_at(deadline);
751 self->timer_armed_ =
true;
752 self->timer_.async_wait(
753 net::bind_allocator(net::recycling_allocator<void>(),
755 if (ec || !*alive) return;
756 self->timer_armed_ = false;
763 curl_multi_socket_action(multi_, CURL_SOCKET_TIMEOUT, 0, &running_);
767 void check_completions() {
769 while (CURLMsg* msg = curl_multi_info_read(multi_, &msgs_left)) {
770 if (msg->msg != CURLMSG_DONE)
continue;
771 Transfer* t =
nullptr;
774 const CURLcode curl_code = msg->data.result;
775 CURL*
const eh = msg->easy_handle;
776 curl_easy_getinfo(eh, CURLINFO_PRIVATE, &t);
777 curl_easy_getinfo(eh, CURLINFO_RESPONSE_CODE, &http_code);
778 curl_multi_remove_handle(multi_, eh);
780 Response res{curl_code, http_code, std::move(t->buffer),
781 std::move(t->header_buffer)};
782 Handler handler = std::move(t->handler);
783 curl_slist_free_all(t->headers);
784 if (t->scrub_on_done) {
786 configure_handle(eh);
788 if (pool_.size() < max_pooled_handles_) {
789 pool_.emplace_back(t->eh);
791 curl_easy_cleanup(t->eh);
793 transfers_.erase(t->self);
800 std::move(handler)(ec, std::move(res));
804 net::io_context& io_;
806 net::steady_timer timer_;
807 std::unordered_map<curl_socket_t, std::shared_ptr<SocketState>> sockets_;
808 std::list<Transfer> transfers_;
809 std::vector<CURL*> pool_;
810 const size_t max_pooled_handles_;
812 std::uint64_t next_transfer_id_ = 0;
813 bool kick_pending_ =
false;
814 bool timer_armed_ =
false;
817 std::shared_ptr<bool> alive_ = std::make_shared<bool>(
true);