37 #include <XrdVersion.hh>
39 #include <curl/curl.h>
40 #include <openssl/bio.h>
41 #include <openssl/evp.h>
48 #include <sys/syscall.h>
49 #include <sys/types.h>
59 #include <system_error>
64 thread_local std::vector<CURL*> HandlerQueue::m_handles;
65 std::atomic<unsigned> CurlWorker::m_maintenance_period = 5;
66 std::vector<std::unique_ptr<XrdClHttp::CurlWorker>> CurlWorker::m_workers;
67 std::mutex CurlWorker::m_workers_mutex;
70 std::atomic<uint64_t> CurlWorker::m_conncall_errors = 0;
71 std::atomic<uint64_t> CurlWorker::m_conncall_req = 0;
72 std::atomic<uint64_t> CurlWorker::m_conncall_success = 0;
73 std::atomic<uint64_t> CurlWorker::m_conncall_timeout = 0;
74 decltype(CurlWorker::m_ops) CurlWorker::m_ops = {};
75 std::vector<std::atomic<std::chrono::system_clock::rep>*> CurlWorker::m_workers_last_completed_cycle;
76 std::vector<std::atomic<std::chrono::system_clock::rep>*> CurlWorker::m_workers_oldest_op;
77 std::mutex CurlWorker::m_worker_stats_mutex;
80 std::atomic<uint64_t> HandlerQueue::m_ops_consumed = 0;
81 std::atomic<uint64_t> HandlerQueue::m_ops_produced = 0;
82 std::atomic<uint64_t> HandlerQueue::m_ops_rejected = 0;
85 CurlWorker::initcontrol CurlWorker::m_initcontrol;
95 #if defined(__APPLE__)
96 uint64_t pth_threadid;
97 pthread_threadid_np(pthread_self(), &pth_threadid);
99 #elif defined(__linux__)
104 return syscall(SYS_gettid);
113 return (status < 100) || (status >= 400);
122 if (!env->GetString(
"BearerToken", token) || token.empty()) {
123 env->ImportString(
"BearerToken",
"BEARER_TOKEN");
124 env->GetString(
"BearerToken", token);
126 if (!token.empty()) {
131 std::string token_file;
132 if (!env->GetString(
"BearerTokenFile", token_file) || token_file.empty()) {
133 env->ImportString(
"BearerTokenFile",
"BEARER_TOKEN_FILE");
134 env->GetString(
"BearerTokenFile", token_file);
136 if (token_file.empty())
return {};
138 std::ifstream input(token_file);
139 if (!input)
return {};
146 bool hasX509Credential,
149 if(protocols.empty())
return hasBearerToken;
151 std::vector<std::string> requested;
153 for(
auto protocol : requested)
156 std::transform(protocol.begin(), protocol.end(), protocol.begin(),
157 [](
unsigned char c) { return std::tolower(c); });
158 if(protocol ==
"gsi" && hasX509Credential)
return false;
159 if(protocol ==
"ztn" && hasBearerToken)
return true;
166 std::vector<std::pair<std::string, std::string>> &headers,
169 if(url.
GetParams().count(
"authz"))
return;
170 for(
const auto &header : headers)
172 if(header.first ==
"Authorization")
return;
176 if(token.empty())
return;
178 bool hasX509Credential =
false;
183 std::string certificate;
185 env->GetInt(
"HttpDisableX509", disableX509)
187 && env->GetString(
"HttpClientCertFile", certificate)
188 && !certificate.empty();
191 const char *configuredProtocols = std::getenv(
"XrdSecPROTOCOL");
193 hasX509Credential,
true))
201 "Injecting bearer token from environment for %s",
204 headers.emplace_back(
"Authorization",
"Bearer " + token);
279 case CURLE_COULDNT_RESOLVE_PROXY:
280 case CURLE_COULDNT_RESOLVE_HOST:
282 case CURLE_LOGIN_DENIED:
287 case CURLE_REMOTE_ACCESS_DENIED:
289 case CURLE_SSL_CONNECT_ERROR:
290 case CURLE_SSL_ENGINE_NOTFOUND:
291 case CURLE_SSL_ENGINE_SETFAILED:
292 case CURLE_SSL_CERTPROBLEM:
293 case CURLE_SSL_CIPHER:
295 case CURLE_SSL_SHUTDOWN_FAILED:
296 case CURLE_SSL_CRL_BADFILE:
297 case CURLE_SSL_ISSUER_ERROR:
298 case CURLE_SSL_CACERT:
302 case CURLE_SEND_ERROR:
303 case CURLE_RECV_ERROR:
305 case CURLE_COULDNT_CONNECT:
306 case CURLE_GOT_NOTHING:
308 case CURLE_OPERATION_TIMEDOUT:
309 #ifdef HAVE_XPROTOCOL_TIMEREXPIRED
314 case CURLE_UNSUPPORTED_PROTOCOL:
315 case CURLE_NOT_BUILT_IN:
317 case CURLE_FAILED_INIT:
319 case CURLE_URL_MALFORMAT:
325 case CURLE_PARTIAL_FILE:
330 case CURLE_READ_ERROR:
331 case CURLE_WRITE_ERROR:
333 case CURLE_RANGE_ERROR:
334 case CURLE_BAD_CONTENT_ENCODING:
336 case CURLE_TOO_MANY_REDIRECTS:
344 std::string_view input,
345 std::array<unsigned char, g_max_checksum_length> &output) {
346 if (input.size() > 4 * ((output.size() + 2) / 3)
347 || input.size() % 4 != 0)
349 if (input.size() == 0)
return true;
351 std::unique_ptr<BIO, decltype(&BIO_free_all)> b64(BIO_new(BIO_f_base64()), &BIO_free_all);
352 BIO_set_flags(b64.get(), BIO_FLAGS_BASE64_NO_NL);
353 std::unique_ptr<BIO, decltype(&BIO_free_all)> bmem(
354 BIO_new_mem_buf(
const_cast<char *
>(input.data()), input.size()), &BIO_free_all);
355 bmem.reset(BIO_push(b64.release(), bmem.release()));
358 size_t expectedLen =
static_cast<size_t>(input.size() * 0.75);
359 if (input[input.size() - 1] ==
'=') {
361 if (input[input.size() - 2] ==
'=') {
366 auto len = BIO_read(bmem.get(), &output[0], output.size());
368 if (len == -1 ||
static_cast<size_t>(len) != expectedLen)
return false;
380 if (m_recv_all_headers) {
381 m_recv_all_headers =
false;
382 m_recv_status_line =
false;
385 if (!m_recv_status_line) {
386 m_recv_status_line =
true;
388 std::stringstream ss(header_line);
391 m_resp_protocol = item;
394 m_status_code = std::stol(item);
398 if (m_status_code < 100 || m_status_code >= 600) {
402 auto cr_loc = item.find(
'\r');
403 if (cr_loc != std::string::npos) {
404 m_resp_message = item.substr(0, cr_loc);
406 m_resp_message = item;
411 if (header_line.empty() || header_line ==
"\n" || header_line ==
"\r\n") {
412 m_recv_all_headers =
true;
416 auto found = header_line.find(
":");
417 if (found == std::string::npos) {
421 std::string header_name = header_line.substr(0, found);
427 while (found < header_line.size()) {
428 if (header_line[found] !=
' ') {
break;}
431 std::string header_value = header_line.substr(found);
434 header_value.erase(header_value.find_last_not_of(
" \r\n\t") + 1);
438 auto iter = m_headers.find(header_name);
439 if (iter == m_headers.end()) {
440 m_headers.insert(iter, {header_name, {header_value}});
442 iter->second.push_back(header_value);
445 if (header_name ==
"Allow") {
446 std::string_view val(header_value);
447 while (!val.empty()) {
448 auto found = val.find(
',');
449 auto method = val.substr(0, found);
450 if (method ==
"PROPFIND") {
454 if (found == std::string_view::npos)
break;
455 val = val.substr(found + 1);
460 }
else if (header_name ==
"Content-Length") {
462 m_content_length = std::stoll(header_value);
467 else if (header_name ==
"Content-Type") {
468 std::string_view val(header_value);
469 auto found = val.find(
";");
470 auto first_type = val.substr(0, found);
471 m_multipart_byteranges = first_type ==
"multipart/byteranges";
472 if (m_multipart_byteranges) {
473 auto remainder = val.substr(found + 1);
474 found = remainder.find(
"boundary=");
475 if (found != std::string_view::npos) {
480 else if (header_name ==
"Content-Range") {
481 auto found = header_value.find(
" ");
482 if (found == std::string::npos) {
485 std::string range_unit = header_value.substr(0, found);
486 if (range_unit !=
"bytes") {
489 auto range_resp = header_value.substr(found + 1);
490 found = range_resp.find(
"/");
491 if (found == std::string::npos) {
494 auto incl_range = range_resp.substr(0, found);
495 found = incl_range.find(
"-");
496 if (found == std::string::npos) {
499 auto first_pos = incl_range.substr(0, found);
501 m_response_offset = std::stoll(first_pos);
505 auto last_pos = incl_range.substr(found + 1);
508 last_byte = std::stoll(last_pos);
512 m_content_length = last_byte - m_response_offset + 1;
514 else if (header_name ==
"Location") {
515 m_location = header_value;
516 }
else if (header_name ==
"Digest") {
519 else if (header_name ==
"Etag")
523 m_etag = header_value;
524 m_etag.erase(remove(m_etag.begin(), m_etag.end(),
'\"'), m_etag.end());
526 else if (header_name ==
"Cache-Control")
528 m_cache_control = header_value;
538 std::string_view view(digest);
539 std::array<unsigned char, g_max_checksum_length> checksum_value{};
540 std::string digest_lower;
541 while (!view.empty()) {
542 auto nextsep = view.find(
',');
543 auto entry = view.substr(0, nextsep);
544 if (nextsep == std::string_view::npos) {
547 view = view.substr(nextsep + 1);
549 nextsep = entry.find(
'=');
550 if (nextsep == std::string_view::npos)
continue;
551 auto name =
trim_view(entry.substr(0, nextsep));
552 auto value =
trim_view(entry.substr(nextsep + 1));
553 digest_lower.clear();
554 digest_lower.resize(name.size());
555 std::transform(name.begin(), name.end(), digest_lower.begin(), [](
unsigned char c) {
556 return std::tolower(c);
559 if (value.empty() || value.size() > 8)
return;
560 std::string padded(8 - value.size(),
'0');
561 padded.append(value.data(), value.size());
563 if (!checksum.
Set(padded.c_str(), padded.size()))
return;
565 reinterpret_cast<const unsigned char *
>(checksum.
Value),
567 info.
Set(type, checksum_value);
571 const size_t encodedSize = 4 * ((decodedSize + 2) / 3);
572 if (value.size() == encodedSize
575 info.
Set(type, checksum_value);
578 if (digest_lower ==
"adler" || digest_lower ==
"adler32") {
579 setHex32(ChecksumType::kADLER32);
580 }
else if (digest_lower ==
"crc32") {
581 setHex32(ChecksumType::kCRC32);
582 }
else if (digest_lower ==
"md5") {
583 setBase64(ChecksumType::kMD5);
584 }
else if (digest_lower ==
"sha" || digest_lower ==
"sha1") {
585 setBase64(ChecksumType::kSHA1);
586 }
else if (digest_lower ==
"sha-256" || digest_lower ==
"sha256") {
587 setBase64(ChecksumType::kSHA256);
588 }
else if (digest_lower ==
"crc32c") {
593 if (value.size() == 8 && value[6] ==
'=' && value[7] ==
'=') {
594 setBase64(ChecksumType::kCRC32C);
597 setHex32(ChecksumType::kCRC32C);
606 case XrdClHttp::ChecksumType::kADLER32:
608 case XrdClHttp::ChecksumType::kCRC32:
610 case XrdClHttp::ChecksumType::kMD5:
612 case XrdClHttp::ChecksumType::kCRC32C:
614 case XrdClHttp::ChecksumType::kSHA1:
616 case XrdClHttp::ChecksumType::kSHA256:
618 case XrdClHttp::ChecksumType::kAll: {
621 value < static_cast<int>(XrdClHttp::ChecksumType::kAll);
623 if (!result.empty()) result +=
',';
635 bool HeaderParser::validHeaderByte(
unsigned char c)
637 const static uint64_t mask_lower = 0 |
638 uint64_t((1<<10)-1) <<
'0' |
644 uint64_t(1) <<
'\'' |
650 const static uint64_t mask_upper = 0 |
651 uint64_t((1<<26)-1) << (
'a'-64) |
652 uint64_t((1<<26)-1) << (
'A'-64) |
653 uint64_t(1) << (
'^'-64) |
654 uint64_t(1) << (
'_'-64) |
655 uint64_t(1) << (
'`'-64) |
656 uint64_t(1) << (
'|'-64) |
657 uint64_t(1) << (
'~'-64);
659 if (c >= 128)
return false;
660 if (c >= 64)
return (uint64_t(1)<<(c-64)) & mask_upper;
661 return (uint64_t(1) << c) & mask_lower;
667 const static int toLower =
'a' -
'A';
668 for (
size_t idx=0; idx<headerName.size(); idx++) {
669 char c = headerName[idx];
670 if (!validHeaderByte(c)) {
673 if (upper &&
'a' <= c && c <=
'z') {
675 }
else if (!upper &&
'A' <= c && c <=
'Z') {
685 m_max_pending_ops(max_pending_ops)
688 auto result = pipe(filedes);
690 throw std::system_error(errno, std::generic_category(),
691 "failed to create HTTP worker pipe");
693 if (fcntl(filedes[0], F_SETFL, O_NONBLOCK | O_CLOEXEC) == -1 || fcntl(filedes[1], F_SETFL, O_NONBLOCK | O_CLOEXEC) == -1) {
694 const int error = errno;
697 throw std::system_error(error, std::generic_category(),
698 "failed to configure HTTP worker pipe");
700 m_read_fd = filedes[0];
701 m_write_fd = filedes[1];
706 bool EnableCurlHeaderDump() {
715 int DumpHeader(
CURL *handle, curl_infotype type,
char *data,
size_t size,
void *clientp) {
717 auto *logger =
static_cast<XrdCl::Log *
>(clientp);
718 if (!logger || !data || size == 0) {
722 const char *direction =
nullptr;
724 case CURLINFO_HEADER_OUT:
727 case CURLINFO_HEADER_IN:
734 const std::string redacted =
obfuscateAuth(std::string(data, size));
735 logger->Debug(
kLogXrdClHttp,
"%s %s", direction, redacted.c_str());
744 for (
size_t idx = 0; idx < input_view.size(); idx++) {
745 if (!isspace(view[view.size() - 1 - idx])) {
746 return view.substr(0, view.size() - idx);
754 for (
size_t idx = 0; idx < input_view.size(); idx++) {
755 if (!isspace(input_view[idx])) {
756 return input_view.substr(idx);
764 curl_easy_setopt(curl, CURLOPT_USERAGENT,
"xrdcl-http/" XrdVERSION);
765 curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, DumpHeader);
768 curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
772 if (!env->GetString(
"HttpCertFile", ca_file) || ca_file.empty()) {
773 char *x509_ca_file = getenv(
"X509_CERT_FILE");
775 ca_file = std::string(x509_ca_file);
778 if (!ca_file.empty()) {
779 curl_easy_setopt(curl, CURLOPT_CAINFO, ca_file.c_str());
782 if (!env->GetString(
"HttpCertDir", ca_dir) || ca_dir.empty()) {
783 char *x509_ca_dir = getenv(
"X509_CERT_DIR");
785 ca_dir = std::string(x509_ca_dir);
788 if (!ca_dir.empty()) {
789 curl_easy_setopt(curl, CURLOPT_CAPATH, ca_dir.c_str());
792 curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 32*1024);
797 auto result = curl_easy_init();
798 if (result ==
nullptr) {
809 if (m_handles.size()) {
810 auto result = m_handles.back();
811 m_handles.pop_back();
820 m_handles.push_back(curl);
826 std::unique_lock<std::mutex> lk(m_mutex);
827 auto now = std::chrono::steady_clock::now();
830 for (
auto &op : m_ops) {
831 if (!op->IsPaused())
continue;
833 if (op->TransferStalled(0, now)) {
834 op->ContinueHandle();
838 std::vector<decltype(m_ops)::value_type> expired_ops;
839 unsigned expired_count = 0;
840 auto it = std::remove_if(m_ops.begin(), m_ops.end(),
841 [&](
const std::shared_ptr<CurlOperation> &handler) {
842 auto expired = handler->GetOperationExpiry() < now;
844 expired_ops.push_back(handler);
849 m_ops.erase(it, m_ops.end());
854 unsigned bytes_to_read = expired_count;
855 while (bytes_to_read > 0) {
856 size_t chunk = std::min<size_t>(
sizeof(throwaway), bytes_to_read);
857 ssize_t n =
read(m_read_fd, throwaway, chunk);
860 }
else if (n == -1) {
861 if (errno == EINTR) {
881 for (
auto &handler : expired_ops) {
887 HandlerQueue::Produce(std::shared_ptr<CurlOperation> handler)
889 auto handler_expiry = handler->GetOperationExpiry();
890 std::unique_lock<std::mutex> lk{m_mutex};
891 m_producer_cv.wait_until(lk,
893 [&]{
return m_ops.size() < m_max_pending_ops;}
895 if (std::chrono::steady_clock::now() > handler_expiry) {
898 m_ops_rejected.fetch_add(1, std::memory_order_relaxed);
902 m_ops.push_back(handler);
905 auto result =
write(m_write_fd, ready, 1);
907 if (errno == EINTR) {
909 }
else if (errno == EAGAIN || errno == EWOULDBLOCK) {
914 throw std::system_error(errno, std::generic_category(),
915 "failed to notify HTTP worker");
921 m_consumer_cv.notify_one();
922 m_ops_produced.fetch_add(1, std::memory_order_relaxed);
925 std::shared_ptr<CurlOperation>
926 HandlerQueue::Consume(std::chrono::steady_clock::duration dur)
928 std::unique_lock<std::mutex> lk(m_mutex);
929 m_consumer_cv.wait_for(lk, dur, [&]{
return m_ops.size() > 0 || m_shutdown;});
930 if (m_shutdown || m_ops.empty()) {
934 std::shared_ptr<CurlOperation> result = m_ops.front();
939 auto result =
read(m_read_fd, ready, 1);
941 if (errno == EINTR) {
943 }
else if (errno == EAGAIN || errno == EWOULDBLOCK) {
948 throw std::system_error(errno, std::generic_category(),
949 "failed to consume HTTP worker notification");
955 m_producer_cv.notify_one();
956 m_ops_consumed.fetch_add(1, std::memory_order_relaxed);
962 HandlerQueue::GetMonitoringJson()
964 auto consumed = m_ops_consumed.load(std::memory_order_relaxed);
965 auto produced = m_ops_produced.load(std::memory_order_relaxed);
967 "\"produced\":" + std::to_string(produced) +
","
968 "\"consumed\":" + std::to_string(consumed) +
","
969 "\"pending\":" + std::to_string(produced - consumed) +
","
970 "\"rejected\":" + std::to_string(m_ops_rejected.load(std::memory_order_relaxed)) +
974 std::shared_ptr<CurlOperation>
975 HandlerQueue::TryConsume()
977 std::unique_lock<std::mutex> lk(m_mutex);
978 if (m_ops.size() == 0) {
979 std::shared_ptr<CurlOperation> result;
983 std::shared_ptr<CurlOperation> result = m_ops.front();
988 auto result =
read(m_read_fd, ready, 1);
990 if (errno == EINTR) {
992 }
else if (errno == EAGAIN || errno == EWOULDBLOCK) {
997 throw std::system_error(errno, std::generic_category(),
998 "failed to consume HTTP worker notification");
1004 m_producer_cv.notify_one();
1005 m_ops_consumed.fetch_add(1, std::memory_order_relaxed);
1011 HandlerQueue::Shutdown()
1013 std::unique_lock lock(m_mutex);
1015 m_consumer_cv.notify_all();
1019 HandlerQueue::ReleaseHandles()
1021 for (
auto handle : m_handles) {
1022 curl_easy_cleanup(handle);
1033 std::unique_lock lk(m_worker_stats_mutex);
1034 m_stats_offset = m_workers_last_completed_cycle.size();
1035 m_workers_last_completed_cycle.push_back(&m_last_completed_cycle);
1036 m_workers_oldest_op.push_back(&m_oldest_op);
1039 if ((pipe(pipeInfo) == -1) || (fcntl(pipeInfo[0], F_SETFD, FD_CLOEXEC)) || (fcntl(pipeInfo[1], F_SETFD, FD_CLOEXEC))) {
1040 throw std::runtime_error(
"Failed to create shutdown monitoring pipe for curl worker");
1042 m_shutdown_pipe_r = pipeInfo[0];
1043 m_shutdown_pipe_w = pipeInfo[1];
1047 env->GetString(
"HttpClientCertFile", m_x509_client_cert_file);
1048 env->GetString(
"HttpClientKeyFile", m_x509_client_key_file);
1053 return std::make_tuple(m_x509_client_cert_file, m_x509_client_key_file);
1059 auto now = std::chrono::system_clock::now().time_since_epoch().count();
1060 auto oldest_op = now;
1061 auto oldest_cycle = now;
1063 std::unique_lock lk(m_worker_stats_mutex);
1064 for (
const auto &entry : m_workers_last_completed_cycle) {
1065 if (!entry) {
continue;}
1066 auto cycle = entry->load(std::memory_order_relaxed);
1067 if (cycle < oldest_cycle) oldest_cycle = cycle;
1069 for (
const auto &entry : m_workers_oldest_op) {
1070 if (!entry) {
continue;}
1071 auto op = entry->load(std::memory_order_relaxed);
1072 if (op < oldest_op) oldest_op = op;
1075 auto oldest_op_dbl = std::chrono::duration<double>(std::chrono::system_clock::time_point(std::chrono::system_clock::duration(oldest_op)).time_since_epoch()).count();
1076 auto oldest_cycle_dbl = std::chrono::duration<double>(std::chrono::system_clock::time_point(std::chrono::system_clock::duration(oldest_cycle)).time_since_epoch()).count();
1077 std::string retval =
"{"
1078 "\"oldest_op\":" + std::to_string(oldest_op_dbl) +
","
1079 "\"oldest_cycle\":" + std::to_string(oldest_cycle_dbl) +
","
1082 for (
size_t verb_idx = 0; verb_idx < static_cast<int>(XrdClHttp::CurlOperation::HttpVerb::Count); verb_idx++) {
1084 for (
size_t op_idx = 0; op_idx < 402; op_idx++) {
1085 if (op_idx == 401)
continue;
1087 auto &op_stats = m_ops[verb_idx][op_idx];
1088 auto duration = op_stats.m_duration.load(std::memory_order_relaxed);
1089 if (duration == 0)
continue;
1091 std::string prefix =
"http_" + verb_str +
"_" + ((op_idx == 402) ?
"invalid" : std::to_string(200 + op_idx)) +
"_";
1093 auto duration_dbl = std::chrono::duration<double>(std::chrono::steady_clock::duration(duration)).count();
1094 retval +=
"\"" + prefix +
"duration\":" + std::to_string(duration_dbl) +
",";
1096 duration = op_stats.m_pause_duration.load(std::memory_order_relaxed);
1098 duration_dbl = std::chrono::duration<double>(std::chrono::steady_clock::duration(duration)).count();
1099 retval +=
"\"" + prefix +
"pause_duration\":" + std::to_string(duration_dbl) +
",";
1102 auto count = op_stats.m_bytes.load(std::memory_order_relaxed);
1103 if (count) retval +=
"\"" + prefix +
"bytes\":" + std::to_string(count) +
",";
1104 count = op_stats.m_error.load(std::memory_order_relaxed);
1105 if (count) retval +=
"\"" + prefix +
"error\":" + std::to_string(count) +
",";
1106 count = op_stats.m_finished.load(std::memory_order_relaxed);
1107 if (count) retval +=
"\"" + prefix +
"finished\":" + std::to_string(count) +
",";
1108 count = op_stats.m_client_timeout.load(std::memory_order_relaxed);
1109 if (count) retval +=
"\"" + prefix +
"client_timeout\":" + std::to_string(count) +
",";
1110 count = op_stats.m_server_timeout.load(std::memory_order_relaxed);
1111 if (count) retval +=
"\"" + prefix +
"server_timeout\":" + std::to_string(count) +
",";
1114 auto &op_stats = m_ops[verb_idx][401];
1115 auto duration = op_stats.m_duration.load(std::memory_order_relaxed);
1116 if (duration == 0)
continue;
1118 std::string prefix =
"http_" + verb_str +
"_";
1120 auto duration_dbl = std::chrono::duration<double>(std::chrono::steady_clock::duration(duration)).count();
1121 retval +=
"\"" + prefix +
"preheader_duration\":" + std::to_string(duration_dbl) +
",";
1123 auto count = op_stats.m_started.load(std::memory_order_relaxed);
1124 if (count) retval +=
"\"" + prefix +
"started\":" + std::to_string(count) +
",";
1125 count = op_stats.m_error.load(std::memory_order_relaxed);
1126 if (count) retval +=
"\"" + prefix +
"preheader_error\":" + std::to_string(count) +
",";
1127 count = op_stats.m_finished.load(std::memory_order_relaxed);
1128 if (count) retval +=
"\"" + prefix +
"preheader_finished\":" + std::to_string(count) +
",";
1129 count = op_stats.m_server_timeout.load(std::memory_order_relaxed);
1130 if (count) retval +=
"\"" + prefix +
"preheader_timeout\":" + std::to_string(count) +
",";
1131 count = op_stats.m_conncall_timeout.load(std::memory_order_relaxed);
1132 if (count) retval +=
"\"" + prefix +
"conncall_timeout\":" + std::to_string(count) +
",";
1137 "\"conncall_error\":" + std::to_string(m_conncall_errors.load(std::memory_order_relaxed)) +
","
1138 "\"conncall_started\":" + std::to_string(m_conncall_req.load(std::memory_order_relaxed)) +
","
1139 "\"conncall_success\":" + std::to_string(m_conncall_success.load(std::memory_order_relaxed)) +
","
1140 "\"conncall_timeout\":" + std::to_string(m_conncall_timeout.load(std::memory_order_relaxed)) +
1153 if (sc < 0 || kind == OpKind::Start || sc == 100) {
1155 }
else if (sc < 200 || sc >= 600) {
1160 auto [bytes, pre_headers, post_headers, pause_duration] = op.
StatisticsReset();
1161 auto &op_stats = m_ops[
static_cast<int>(op.
GetVerb())][sc];
1162 op_stats.m_bytes.fetch_add(bytes, std::memory_order_relaxed);
1163 op_stats.m_duration.fetch_add((sc == 401) ? pre_headers.count() : post_headers.count(), std::memory_order_relaxed);
1164 op_stats.m_pause_duration.fetch_add(pause_duration.count(), std::memory_order_relaxed);
1165 if (pre_headers != std::chrono::steady_clock::duration::zero() && sc != 401) {
1166 auto &old_stats = m_ops[
static_cast<int>(op.
GetVerb())][401];
1167 old_stats.m_duration.fetch_add(pre_headers.count(), std::memory_order_relaxed);
1170 case OpKind::ConncallTimeout:
1171 op_stats.m_conncall_timeout.fetch_add(1, std::memory_order_relaxed);
1173 case OpKind::ClientTimeout:
1174 op_stats.m_client_timeout.fetch_add(1, std::memory_order_relaxed);
1177 op_stats.m_error.fetch_add(1, std::memory_order_relaxed);
1179 case OpKind::Finish:
1180 op_stats.m_finished.fetch_add(1, std::memory_order_relaxed);
1183 op_stats.m_started.fetch_add(1, std::memory_order_relaxed);
1185 case OpKind::ServerTimeout:
1186 op_stats.m_server_timeout.fetch_add(1, std::memory_order_relaxed);
1188 case OpKind::Update:
1197 std::unique_lock lock(m_workers_mutex);
1198 m_workers.emplace_back(std::move(
self));
1199 m_self_tid = std::move(tid);
1201 std::unique_lock lock(m_start_lock);
1202 m_start_complete =
true;
1203 m_start_complete_cv.notify_one();
1210 std::unique_lock lock(myself->m_start_lock);
1211 myself->m_start_complete_cv.wait(lock, [&]{
return myself->m_start_complete;});
1218 std::unique_lock lock(m_workers_mutex);
1219 auto iter = std::remove_if(m_workers.begin(), m_workers.end(), [&](std::unique_ptr<XrdClHttp::CurlWorker> &worker){return worker.get() == myself;});
1220 m_workers.erase(iter);
1227 int max_pending = 50;
1230 auto &queue = *m_queue.get();
1233 CURLM *multi_handle = curl_multi_init();
1234 if (multi_handle ==
nullptr) {
1235 throw std::runtime_error(
"Failed to create curl multi-handle");
1238 int running_handles = 0;
1239 time_t last_maintenance = time(NULL);
1240 CURLMcode mres = CURLM_OK;
1244 std::unordered_map<int, WaitingForBroker> broker_reqs;
1245 std::vector<struct curl_waitfd> waitfds;
1247 bool want_shutdown =
false;
1248 while (!want_shutdown) {
1249 m_last_completed_cycle.store(std::chrono::system_clock::now().time_since_epoch().count());
1250 auto oldest_op = std::chrono::system_clock::now();
1251 for (
const auto &entry : m_op_map) {
1252 OpRecord(*entry.second.first, OpKind::Update);
1253 if (entry.second.second < oldest_op) {
1254 oldest_op = entry.second.second;
1257 m_oldest_op.store(oldest_op.time_since_epoch().count());
1261 auto op = m_continue_queue->TryConsume();
1268 m_logger->
Debug(
kLogXrdClHttp,
"Ignoring continuation of operation that has already completed");
1271 m_logger->
Debug(
kLogXrdClHttp,
"Continuing the curl handle from op %p on thread %d", op.get(), getthreadid());
1278 curl_multi_remove_handle(multi_handle, curl);
1279 curl_easy_cleanup(curl);
1280 m_op_map.erase(curl);
1282 running_handles -= 1;
1285 auto iter = m_op_map.find(curl);
1286 if (iter != m_op_map.end()) iter->second.second = std::chrono::system_clock::now();
1290 while (running_handles <
static_cast<int>(m_max_ops)) {
1291 auto op = running_handles == 0 ? queue.Consume(std::chrono::seconds(1)) : queue.TryConsume();
1295 auto curl = queue.GetHandle();
1296 if (curl ==
nullptr) {
1302 auto rv = op->
Setup(curl, *
this);
1322 queue.RecycleHandle(curl);
1325 m_op_map[curl] = {op, std::chrono::system_clock::now()};
1330 std::string modified_url;
1331 std::shared_ptr<CurlOptionsOp> options_op(
1343 curl = queue.GetHandle();
1344 if (curl ==
nullptr) {
1350 auto rv = options_op->Setup(curl, *
this);
1355 m_op_map[curl] = {options_op, std::chrono::system_clock::now()};
1356 OpRecord(*options_op, OpKind::Start);
1357 running_handles += 1;
1359 OpRecord(*op, OpKind::Start);
1362 auto mres = curl_multi_add_handle(multi_handle, curl);
1363 if (mres != CURLM_OK) {
1370 running_handles += 1;
1375 time_t now = time(NULL);
1376 time_t next_maintenance = last_maintenance + m_maintenance_period.load(std::memory_order_relaxed);
1377 if (now >= next_maintenance) {
1379 m_continue_queue->Expire();
1381 getthreadid(), running_handles);
1382 last_maintenance = now;
1385 std::vector<std::pair<int, CURL *>> expired_ops;
1386 for (
const auto &entry : broker_reqs) {
1387 if (entry.second.expiry < now) {
1388 expired_ops.emplace_back(entry.first, entry.second.curl);
1391 for (
const auto &entry : expired_ops) {
1392 auto iter = m_op_map.find(entry.second);
1393 if (iter == m_op_map.end()) {
1394 m_logger->
Warning(
kLogXrdClHttp,
"Found an expired curl handle with no corresponding operation!");
1398 if ((options_op =
dynamic_cast<CurlOptionsOp*
>(iter->second.first.get())) !=
nullptr) {
1400 bool parent_op_failed =
false;
1401 if (parent_op->IsRedirect()) {
1403 if (parent_op->Redirect(target) == CurlOperation::RedirectAction::Fail) {
1405 if (iter != m_op_map.end()) {
1408 m_op_map.erase(iter);
1409 running_handles -= 1;
1411 parent_op_failed =
true;
1413 OpRecord(*parent_op, OpKind::Start);
1416 OpRecord(*parent_op, OpKind::Start);
1418 if (!parent_op_failed){
1424 iter->second.first->ReleaseHandle();
1425 OpRecord(*(iter->second.first), OpKind::ConncallTimeout);
1426 m_op_map.erase(entry.second);
1427 curl_easy_cleanup(entry.second);
1428 running_handles -= 1;
1430 broker_reqs.erase(entry.first);
1431 m_conncall_timeout.fetch_add(1, std::memory_order_relaxed);
1439 waitfds.resize(3 + broker_reqs.size());
1441 waitfds[0].fd = queue.PollFD();
1442 waitfds[0].events = CURL_WAIT_POLLIN;
1443 waitfds[0].revents = 0;
1444 waitfds[1].fd = m_continue_queue->PollFD();
1445 waitfds[1].events = CURL_WAIT_POLLIN;
1446 waitfds[1].revents = 0;
1447 waitfds[2].fd = m_shutdown_pipe_r;
1448 waitfds[2].revents = 0;
1449 waitfds[2].events = CURL_WAIT_POLLIN | CURL_WAIT_POLLPRI;
1452 for (
const auto &entry : broker_reqs) {
1453 waitfds[idx].fd = entry.first;
1454 waitfds[idx].events = CURL_WAIT_POLLIN|CURL_WAIT_POLLPRI;
1455 waitfds[idx].revents = 0;
1460 curl_multi_timeout(multi_handle, &timeo);
1464 if (running_handles && timeo == -1) {
1469 mres = curl_multi_wait(multi_handle, &waitfds[0], waitfds.size(), 50,
nullptr);
1475 mres = curl_multi_wait(multi_handle, &waitfds[0], waitfds.size(), 50,
nullptr);
1477 if (mres != CURLM_OK) {
1482 for (
const auto &entry : waitfds) {
1484 if (waitfds[0].fd == entry.fd || waitfds[1].fd == entry.fd) {
1488 if ((waitfds[2].fd == entry.fd) && entry.revents) {
1489 want_shutdown =
true;
1492 if ((entry.revents & CURL_WAIT_POLLIN) != CURL_WAIT_POLLIN) {
1495 auto handle = broker_reqs[entry.fd].curl;
1496 auto iter = m_op_map.find(handle);
1497 if (iter == m_op_map.end()) {
1498 m_logger->
Warning(
kLogXrdClHttp,
"Internal error: broker responded on FD %d but no corresponding curl operation", entry.fd);
1499 broker_reqs.erase(entry.fd);
1500 m_conncall_errors.fetch_add(1, std::memory_order_relaxed);
1504 auto result = iter->second.first->WaitSocketCallback(err);
1509 if ((options_op =
dynamic_cast<CurlOptionsOp*
>(iter->second.first.get())) !=
nullptr) {
1511 bool parent_op_failed =
false;
1512 if (parent_op->IsRedirect()) {
1514 if (parent_op->Redirect(target) == CurlOperation::RedirectAction::Fail) {
1516 if (iter != m_op_map.end()) {
1519 m_op_map.erase(iter);
1520 running_handles -= 1;
1522 parent_op_failed =
true;
1524 OpRecord(*parent_op, OpKind::Start);
1527 OpRecord(*parent_op, OpKind::Start);
1529 if (!parent_op_failed){
1536 m_op_map.erase(handle);
1537 broker_reqs.erase(entry.fd);
1538 m_conncall_errors.fetch_add(1, std::memory_order_relaxed);
1539 running_handles -= 1;
1541 broker_reqs.erase(entry.fd);
1542 curl_multi_add_handle(multi_handle, handle);
1543 m_conncall_success.fetch_add(1, std::memory_order_relaxed);
1549 auto mres = curl_multi_perform(multi_handle, &still_running);
1550 if (mres == CURLM_CALL_MULTI_PERFORM) {
1552 }
else if (mres != CURLM_OK) {
1560 msg = curl_multi_info_read(multi_handle, &msgq);
1561 if (msg && (msg->msg == CURLMSG_DONE)) {
1562 if (!msg->easy_handle) {
1564 mres = CURLM_BAD_EASY_HANDLE;
1567 auto iter = m_op_map.find(msg->easy_handle);
1568 if (iter == m_op_map.end()) {
1569 m_logger->
Error(
kLogXrdClHttp,
"Logic error: got a callback for an entry that doesn't exist");
1570 mres = CURLM_BAD_EASY_HANDLE;
1573 auto op = iter->second.first;
1574 auto res = msg->data.result;
1575 bool keep_handle =
false;
1576 bool waiting_on_callout =
false;
1577 if (res == CURLE_OK) {
1579 OpRecord(*op, OpKind::Finish);
1588 if ((options_op =
dynamic_cast<CurlOptionsOp*
>(op.get())) !=
nullptr) {
1590 bool parent_op_failed =
false;
1591 if (parent_op->IsRedirect()) {
1593 if (parent_op->Redirect(target) == CurlOperation::RedirectAction::Fail) {
1596 running_handles -= 1;
1597 parent_op_failed =
true;
1599 OpRecord(*parent_op, OpKind::Start);
1602 OpRecord(*parent_op, OpKind::Start);
1605 if (!parent_op_failed) {
1610 queue.RecycleHandle(iter->first);
1614 if ((options_op =
dynamic_cast<CurlOptionsOp*
>(op.get()))) {
1620 OpRecord(*op, OpKind::Start);
1622 curl_multi_remove_handle(multi_handle, iter->first);
1623 queue.RecycleHandle(iter->first);
1631 case CurlOperation::RedirectAction::Fail:
1640 keep_handle =
false;
1642 case CurlOperation::RedirectAction::Reinvoke:
1648 OpRecord(*op, OpKind::Start);
1651 case CurlOperation::RedirectAction::ReinvokeAfterAllow:
1658 std::string modified_url;
1661 std::shared_ptr<CurlOperation> new_op(options_op);
1662 auto curl = queue.GetHandle();
1663 if (curl ==
nullptr) {
1666 keep_handle =
false;
1667 options_op =
nullptr;
1670 OpRecord(*new_op, OpKind::Start);
1672 auto rv = new_op->Setup(curl, *
this);
1675 keep_handle =
false;
1676 options_op =
nullptr;
1680 m_logger->
Debug(
kLogXrdClHttp,
"Unable to setup the curl handle for the OPTIONS operation");
1681 new_op->Fail(
XrdCl::errInternal, ENOMEM,
"Failed to setup the curl handle for the OPTIONS operation");
1683 keep_handle =
false;
1686 new_op->SetContinueQueue(m_continue_queue);
1687 m_op_map[curl] = {new_op, std::chrono::system_clock::now()};
1688 auto mres = curl_multi_add_handle(multi_handle, curl);
1689 if (mres != CURLM_OK) {
1690 m_logger->
Debug(
kLogXrdClHttp,
"Unable to add OPTIONS operation to the curl multi-handle: %s", curl_multi_strerror(mres));
1695 running_handles += 1;
1696 m_logger->
Debug(
kLogXrdClHttp,
"Invoking the OPTIONS operation before redirect to %s", target.c_str());
1703 if ((waiting_on_callout = callout_socket >= 0)) {
1704 auto expiry = time(
nullptr) + 20;
1705 m_logger->
Debug(
kLogXrdClHttp,
"Creating a callout wait request on socket %d", callout_socket);
1706 broker_reqs[callout_socket] = {iter->first, expiry};
1707 m_conncall_req.fetch_add(1, std::memory_order_relaxed);
1709 }
else if (options_op) {
1714 curl_multi_remove_handle(multi_handle, iter->first);
1715 if (!waiting_on_callout && !options_op) {
1716 curl_multi_add_handle(multi_handle, iter->first);
1718 }
else if (!options_op) {
1723 curl_multi_remove_handle(multi_handle, iter->first);
1728 queue.RecycleHandle(iter->first);
1734 auto next_res = curl_multi_add_handle(multi_handle, iter->first);
1735 if (next_res == CURLM_OK) {
1737 OpRecord(*op, OpKind::Start);
1740 "Unable to add the next operation request to the curl multi-handle");
1743 queue.RecycleHandle(iter->first);
1754 int wait_socket = -1;
1756 m_logger->
Error(
kLogXrdClHttp,
"Failed to start broker-based connection: %s", err.c_str());
1758 keep_handle =
false;
1760 curl_multi_remove_handle(multi_handle, iter->first);
1761 auto expiry = time(
nullptr) + 20;
1762 m_logger->
Debug(
kLogXrdClHttp,
"Curl operation requires a new TCP socket; waiting for callout to respond on socket %d", wait_socket);
1763 broker_reqs[wait_socket] = {iter->first, expiry};
1764 m_conncall_req.fetch_add(1, std::memory_order_relaxed);
1767 if (res == CURLE_ABORTED_BY_CALLBACK || res == CURLE_WRITE_ERROR) {
1771 case CurlOperation::OpError::ErrHeaderTimeout:
1772 #ifdef HAVE_XPROTOCOL_TIMEREXPIRED
1779 case CurlOperation::OpError::ErrCallback: {
1785 case CurlOperation::OpError::ErrOperationTimeout:
1787 OpRecord(*op, op->
IsPaused() ? OpKind::ClientTimeout : OpKind::ServerTimeout);
1789 case CurlOperation::OpError::ErrTransferSlow:
1791 OpRecord(*op, OpKind::ServerTimeout);
1793 case CurlOperation::OpError::ErrTransferClientStall:
1795 OpRecord(*op, OpKind::ClientTimeout);
1797 case CurlOperation::OpError::ErrTransferStall:
1799 OpRecord(*op, OpKind::ServerTimeout);
1801 case CurlOperation::OpError::ErrNone:
1807 if ((options_op =
dynamic_cast<CurlOptionsOp*
>(op.get())) !=
nullptr) {
1809 bool parent_op_failed =
false;
1810 if (parent_op->IsRedirect()) {
1812 if (parent_op->Redirect(target) == CurlOperation::RedirectAction::Fail) {
1814 if (iter != m_op_map.end()) {
1817 m_op_map.erase(iter);
1818 running_handles -= 1;
1820 parent_op_failed =
true;
1822 OpRecord(*parent_op, OpKind::Start);
1825 OpRecord(*parent_op, OpKind::Start);
1827 if (!parent_op_failed){
1834 const char *curl_easy_err = curl_easy_strerror(res);
1835 const std::string fail_err = !curl_err.empty() ? curl_err : curl_easy_err;
1836 m_logger->
Debug(
kLogXrdClHttp,
"Curl generated an error: %s (%d)", fail_err.c_str(), res);
1837 op->
Fail(xrdCode.first, xrdCode.second, fail_err);
1840 if ((options_op =
dynamic_cast<CurlOptionsOp*
>(op.get())) !=
nullptr) {
1842 bool parent_op_failed =
false;
1843 if (parent_op->IsRedirect()) {
1845 if (parent_op->Redirect(target) == CurlOperation::RedirectAction::Fail) {
1847 if (iter != m_op_map.end()) {
1850 m_op_map.erase(iter);
1851 running_handles -= 1;
1853 parent_op_failed =
true;
1856 if (!parent_op_failed){
1864 curl_multi_remove_handle(multi_handle, iter->first);
1865 if (res != CURLE_OK) {
1866 curl_easy_cleanup(iter->first);
1868 for (
auto &req : broker_reqs) {
1869 if (req.second.curl == iter->first) {
1870 m_logger->
Warning(
kLogXrdClHttp,
"Curl handle finished while a broker operation was outstanding");
1871 m_conncall_errors.fetch_add(1, std::memory_order_relaxed);
1874 m_op_map.erase(iter);
1875 running_handles -= 1;
1881 for (
auto map_entry : m_op_map) {
1886 if (multi_handle && map_entry.first) curl_multi_remove_handle(multi_handle, map_entry.first);
1889 m_queue->ReleaseHandles();
1890 curl_multi_cleanup(multi_handle);
1894 CurlWorker::Shutdown()
1896 m_queue->Shutdown();
1897 if (m_shutdown_pipe_w == -1) {
1901 close(m_shutdown_pipe_w);
1902 m_shutdown_pipe_w = -1;
1908 std::unique_lock lk(m_worker_stats_mutex);
1909 m_workers_last_completed_cycle[m_stats_offset] =
nullptr;
1910 m_workers_oldest_op[m_stats_offset] =
nullptr;
1916 CurlWorker::ShutdownAll()
1918 std::unique_lock lock(m_workers_mutex);
1919 for (
auto &worker : m_workers) {
1924 CurlWorker::initcontrol::initcontrol()
1926 curl_global_init(CURL_GLOBAL_DEFAULT);
1929 CurlWorker::initcontrol::~initcontrol()
1932 curl_global_cleanup();
std::pair< uint16_t, uint32_t > CurlCodeConvert(CURLcode res)
std::string obfuscateAuth(const std::string &input)
ssize_t read(int fildes, void *buf, size_t nbyte)
void getline(uchar *buff, int blen)
int emsg(int rc, char *msg)
int Set(const char *csName)
bool Set(ChecksumType ctype, const std::array< unsigned char, g_max_checksum_length > &value)
virtual void OptionsDone()
int GetStatusCode() const
bool FinishSetup(CURL *curl)
const std::string & GetUrl() const
std::pair< XErrorCode, std::string > GetCallbackError() const
virtual void Fail(uint16_t errCode, uint32_t errNum, const std::string &)
static const std::string GetVerbString(HttpVerb)
bool UseConnectionCallout()
virtual HttpVerb GetVerb() const =0
std::string GetCurlErrorMessage() const
virtual void ReleaseHandle()
virtual bool RequiresOptions() const
static void CleanupDnsCache()
bool GetTriedBoker() const
std::tuple< uint64_t, std::chrono::steady_clock::duration, std::chrono::steady_clock::duration, std::chrono::steady_clock::duration > StatisticsReset()
virtual bool ContinueHandle()
std::string GetStatusMessage() const
CURL * GetCurlHandle() const
CreateConnCalloutType GetConnCalloutFunc() const
XrdClHttp::HttpVerb HttpVerb
virtual RedirectAction Redirect(std::string &target)
virtual void SetContinueQueue(std::shared_ptr< XrdClHttp::HandlerQueue > queue)
bool StartConnectionCallout(std::string &err)
virtual bool Setup(CURL *curl, CurlWorker &)
CURL * GetParentCurlHandle() const
void ReleaseHandle() override
std::shared_ptr< CurlOperation > GetOperation() const
std::tuple< std::string, std::string > ClientX509CertKeyFile() const
static void RunStatic(CurlWorker *myself)
void Start(std::unique_ptr< XrdClHttp::CurlWorker > self, std::thread tid)
static std::string GetMonitoringJson()
HandlerQueue(unsigned max_pending_ops)
void RecycleHandle(CURL *)
static std::string_view GetUrlKey(const std::string &url, std::string &modified_url)
static Log * GetLog()
Get default log.
static Env * GetEnv()
Get default client environment.
bool GetInt(const std::string &key, int &value)
@ DumpMsg
print details of the request and responses
void Error(uint64_t topic, const char *format,...)
Report an error.
void Warning(uint64_t topic, const char *format,...)
Report a warning.
void Debug(uint64_t topic, const char *format,...)
Print a debug message.
std::string GetURL() const
Get the URL.
const ParamsMap & GetParams() const
Get the URL params.
static void splitString(Container &result, const std::string &input, const std::string &delimiter)
Split a string.
static void Trim(std::string &str)
Trim a string.
std::pair< uint16_t, uint32_t > HTTPStatusConvert(unsigned status)
size_t GetChecksumLength(ChecksumType ctype)
void InjectBearerToken(const XrdCl::URL &url, std::vector< std::pair< std::string, std::string >> &headers, XrdCl::Log *logger=nullptr)
CURL * GetHandle(bool verbose)
bool HTTPStatusIsError(unsigned status)
bool ShouldUseBearerToken(const std::string &protocols, bool hasX509Credential, bool hasBearerToken)
std::string_view ltrim_view(const std::string_view &input_view)
std::string GetBearerToken(XrdCl::Log *logger=nullptr)
void ConfigureHandle(CURL *curl, bool verbose)
std::string_view trim_view(const std::string_view &input_view)
const uint16_t errUnknown
Unknown error.
const uint16_t errInvalidAddr
const uint16_t errRedirectLimit
const uint16_t errErrorResponse
const uint16_t errTlsError
const uint16_t errOperationExpired
const uint16_t errLoginFailed
const uint16_t errDataError
data is corrupted
const uint16_t errInternal
Internal error.
const uint16_t errInvalidArgs
const uint16_t errConnectionError
const uint16_t errNotSupported
const uint16_t errSocketError
const uint16_t errCorruptedHeader
const uint16_t errNone
No error.
const uint64_t kLogXrdClHttp