XRootD
Loading...
Searching...
No Matches
XrdClHttpUtil.cc
Go to the documentation of this file.
1/******************************************************************************/
2/* Copyright (C) 2025, Pelican Project, Morgridge Institute for Research */
3/* */
4/* This file is part of the XrdClHttp client plugin for XRootD. */
5/* */
6/* XRootD is free software: you can redistribute it and/or modify it under */
7/* the terms of the GNU Lesser General Public License as published by the */
8/* Free Software Foundation, either version 3 of the License, or (at your */
9/* option) any later version. */
10/* */
11/* XRootD is distributed in the hope that it will be useful, but WITHOUT */
12/* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or */
13/* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public */
14/* License for more details. */
15/* */
16/* The copyright holder's institutional names and contributor's names may not */
17/* be used to endorse or promote products derived from this software without */
18/* specific prior written permission of the institution or contributor. */
19/******************************************************************************/
20
21#include "XrdClHttpFile.hh"
22#include "XrdClHttpOps.hh"
24#include "XrdClHttpUtil.hh"
25#include "XrdClHttpWorker.hh"
26
29#include <XrdCl/XrdClLog.hh>
30#include <XrdCl/XrdClURL.hh>
31#include <XrdCl/XrdClUtils.hh>
33#include <XrdCks/XrdCksData.hh>
34#include <XrdOuc/XrdOucCRC.hh>
37#include <XrdVersion.hh>
38
39#include <curl/curl.h>
40#include <openssl/bio.h>
41#include <openssl/evp.h>
42
43#include <fcntl.h>
44#include <fstream>
45#ifdef __APPLE__
46#include <pthread.h>
47#else
48#include <sys/syscall.h>
49#include <sys/types.h>
50#endif
51#include <unistd.h>
52
53#include <algorithm>
54#include <cerrno>
55#include <cctype>
56#include <cstdlib>
57#include <ctime>
58#include <sstream>
59#include <stdexcept>
60#include <system_error>
61#include <utility>
62
63using namespace XrdClHttp;
64
65thread_local std::vector<CURL*> HandlerQueue::m_handles;
66std::atomic<unsigned> CurlWorker::m_maintenance_period = 5;
67std::vector<std::unique_ptr<XrdClHttp::CurlWorker>> CurlWorker::m_workers;
68std::mutex CurlWorker::m_workers_mutex;
69
70// Performance statistics for the worker
71std::atomic<uint64_t> CurlWorker::m_conncall_errors = 0;
72std::atomic<uint64_t> CurlWorker::m_conncall_req = 0;
73std::atomic<uint64_t> CurlWorker::m_conncall_success = 0;
74std::atomic<uint64_t> CurlWorker::m_conncall_timeout = 0;
75decltype(CurlWorker::m_ops) CurlWorker::m_ops = {};
76std::vector<std::atomic<std::chrono::system_clock::rep>*> CurlWorker::m_workers_last_completed_cycle;
77std::vector<std::atomic<std::chrono::system_clock::rep>*> CurlWorker::m_workers_oldest_op;
78std::mutex CurlWorker::m_worker_stats_mutex;
79
80// Performance statistics for the queue
81std::atomic<uint64_t> HandlerQueue::m_ops_consumed = 0; // Count of operations consumed from the queue.
82std::atomic<uint64_t> HandlerQueue::m_ops_produced = 0; // Count of operations added to the queue.
83std::atomic<uint64_t> HandlerQueue::m_ops_rejected = 0; // Count of operations rejected by the queue.
84
85// shutdown + init trigger, must be last of the static members
86CurlWorker::initcontrol CurlWorker::m_initcontrol;
87
89 CURL *curl{nullptr};
90 time_t expiry{0};
91};
92
93namespace {
94
95pid_t getthreadid() {
96#if defined(__APPLE__)
97 uint64_t pth_threadid;
98 pthread_threadid_np(pthread_self(), &pth_threadid);
99 return pth_threadid;
100#elif defined(__linux__)
101 // NOTE: glibc 2.30 finally provides a gettid() wrapper; however,
102 // we currently support RHEL 8, which is based on glibc 2.28. Until
103 // we drop that platform, it's easier to do the syscall directly on Linux
104 // instead of additional ifdef calls.
105 return syscall(SYS_gettid);
106#else
107 return getpid();
108#endif
109}
110
111}
112
113bool XrdClHttp::HTTPStatusIsError(unsigned status) {
114 return (status < 100) || (status >= 400);
115}
116
118{
119 auto env = XrdCl::DefaultEnv::GetEnv();
120 if (!env) return {};
121
122 std::string token;
123 if (!env->GetString("BearerToken", token) || token.empty()) {
124 env->ImportString("BearerToken", "BEARER_TOKEN");
125 env->GetString("BearerToken", token);
126 }
127 if (!token.empty()) {
128 XrdCl::Utils::Trim(token);
129 return token;
130 }
131
132 std::string token_file;
133 if (!env->GetString("BearerTokenFile", token_file) || token_file.empty()) {
134 env->ImportString("BearerTokenFile", "BEARER_TOKEN_FILE");
135 env->GetString("BearerTokenFile", token_file);
136 }
137 if (token_file.empty()) return {};
138
139 std::ifstream input(token_file);
140 if (!input) return {};
141 std::getline(input, token);
142 XrdCl::Utils::Trim(token);
143 return token;
144}
145
146bool XrdClHttp::ShouldUseBearerToken(const std::string &protocols,
147 bool hasX509Credential,
148 bool hasBearerToken)
149{
150 if(protocols.empty()) return hasBearerToken;
151
152 std::vector<std::string> requested;
153 XrdCl::Utils::splitString(requested, protocols, ",");
154 for(auto protocol : requested)
155 {
156 XrdCl::Utils::Trim(protocol);
157 std::transform(protocol.begin(), protocol.end(), protocol.begin(),
158 [](unsigned char c) { return std::tolower(c); });
159 if(protocol == "gsi" && hasX509Credential) return false;
160 if(protocol == "ztn" && hasBearerToken) return true;
161 }
162 return false;
163}
164
166 const XrdCl::URL &url,
167 std::vector<std::pair<std::string, std::string>> &headers,
168 XrdCl::Log *logger)
169{
170 if(url.GetParams().count("authz")) return;
171 for(const auto &header : headers)
172 {
173 if(header.first == "Authorization") return;
174 }
175
176 const std::string token = GetBearerToken(logger);
177 if(token.empty()) return;
178
179 bool hasX509Credential = false;
180 auto env = XrdCl::DefaultEnv::GetEnv();
181 if(env)
182 {
183 int disableX509 = 0;
184 std::string certificate;
185 hasX509Credential =
186 env->GetInt("HttpDisableX509", disableX509)
187 && !disableX509
188 && env->GetString("HttpClientCertFile", certificate)
189 && !certificate.empty();
190 }
191
192 const char *configuredProtocols = std::getenv("XrdSecPROTOCOL");
193 if(!ShouldUseBearerToken(configuredProtocols ? configuredProtocols : "",
194 hasX509Credential, true))
195 {
196 return;
197 }
198
199 if(logger)
200 {
201 logger->Debug(kLogXrdClHttp,
202 "Injecting bearer token from environment for %s",
203 url.GetURL().c_str());
204 }
205 headers.emplace_back("Authorization", "Bearer " + token);
206}
207
208std::pair<uint16_t, uint32_t> XrdClHttp::HTTPStatusConvert(unsigned status) {
209 switch (status) {
210 case 400: // Bad Request
211 return std::make_pair(XrdCl::errErrorResponse, kXR_InvalidRequest);
212 case 401: // Unauthorized (needs authentication)
213 return std::make_pair(XrdCl::errErrorResponse, kXR_NotAuthorized);
214 case 402: // Payment Required
215 case 403: // Forbidden (failed authorization)
216 return std::make_pair(XrdCl::errErrorResponse, kXR_NotAuthorized);
217 case 404:
218 return std::make_pair(XrdCl::errErrorResponse, kXR_NotFound);
219 case 405: // Method not allowed
220 return std::make_pair(XrdCl::errErrorResponse, kXR_Unsupported);
221 case 406: // Not acceptable
222 return std::make_pair(XrdCl::errErrorResponse, kXR_InvalidRequest);
223 case 407: // Proxy Authentication Required
224 return std::make_pair(XrdCl::errErrorResponse, kXR_NotAuthorized);
225 case 408: // Request timeout
226 return std::make_pair(XrdCl::errErrorResponse, kXR_ReqTimedOut);
227 case 409: // Conflict
228 return std::make_pair(XrdCl::errErrorResponse, kXR_Conflict);
229 case 410: // Gone
230 return std::make_pair(XrdCl::errErrorResponse, kXR_NotFound);
231 case 411: // Length required
232 case 412: // Precondition failed
233 case 413: // Payload too large
234 case 414: // URI too long
235 case 415: // Unsupported Media Type
236 case 416: // Range Not Satisfiable
237 case 417: // Expectation Failed
238 case 418: // I'm a teapot
239 return std::make_pair(XrdCl::errErrorResponse, kXR_InvalidRequest);
240 case 421: // Misdirected Request
241 case 422: // Unprocessable Content
242 return std::make_pair(XrdCl::errErrorResponse, kXR_InvalidRequest);
243 case 423: // Locked
244 return std::make_pair(XrdCl::errErrorResponse, kXR_FileLocked);
245 case 424: // Failed Dependency
246 case 425: // Too Early
247 case 426: // Upgrade Required
248 case 428: // Precondition Required
249 return std::make_pair(XrdCl::errErrorResponse, kXR_InvalidRequest);
250 case 429: // Too Many Requests
251 return std::make_pair(XrdCl::errErrorResponse, kXR_Overloaded);
252 case 431: // Request Header Fields Too Large
253 return std::make_pair(XrdCl::errErrorResponse, kXR_InvalidRequest);
254 case 451: // Unavailable For Legal Reasons
255 return std::make_pair(XrdCl::errErrorResponse, kXR_Impossible);
256 case 500: // Internal Server Error
257 return std::make_pair(XrdCl::errErrorResponse, kXR_ServerError);
258 case 501: // Not Implemented
259 return std::make_pair(XrdCl::errErrorResponse, kXR_Unsupported);
260 case 502: // Bad Gateway
261 return std::make_pair(XrdCl::errErrorResponse, kXR_ServerError);
262 case 503: // Service Unavailable
263 return std::make_pair(XrdCl::errErrorResponse, kXR_Overloaded);
264 case 504: // Gateway Timeout
265 return std::make_pair(XrdCl::errErrorResponse, kXR_ReqTimedOut);
266 case 507: // Insufficient Storage
267 return std::make_pair(XrdCl::errErrorResponse, kXR_overQuota);
268 case 508: // Loop Detected
269 case 510: // Not Extended
270 case 511: // Network Authentication Required
271 return std::make_pair(XrdCl::errErrorResponse, kXR_ServerError);
272 }
273 return std::make_pair(XrdCl::errUnknown, status);
274}
275
276std::pair<uint16_t, uint32_t> CurlCodeConvert(CURLcode res) {
277 switch (res) {
278 case CURLE_OK:
279 return std::make_pair(XrdCl::errNone, 0);
280 case CURLE_COULDNT_RESOLVE_PROXY:
281 case CURLE_COULDNT_RESOLVE_HOST:
282 return std::make_pair(XrdCl::errInvalidAddr, 0);
283 case CURLE_LOGIN_DENIED:
284 // Commented-out cases are for platforms (RHEL7) where the error
285 // codes are undefined.
286 //case CURLE_AUTH_ERROR:
287 //case CURLE_SSL_CLIENTCERT:
288 case CURLE_REMOTE_ACCESS_DENIED:
289 return std::make_pair(XrdCl::errLoginFailed, EACCES);
290 case CURLE_SSL_CONNECT_ERROR:
291 case CURLE_SSL_ENGINE_NOTFOUND:
292 case CURLE_SSL_ENGINE_SETFAILED:
293 case CURLE_SSL_CERTPROBLEM:
294 case CURLE_SSL_CIPHER:
295 case 51: // In old curl versions, this is CURLE_PEER_FAILED_VERIFICATION; that constant was changed to be 60 / CURLE_SSL_CACERT
296 case CURLE_SSL_SHUTDOWN_FAILED:
297 case CURLE_SSL_CRL_BADFILE:
298 case CURLE_SSL_ISSUER_ERROR:
299 case CURLE_SSL_CACERT: // value is 60; merged with CURLE_PEER_FAILED_VERIFICATION
300 //case CURLE_SSL_PINNEDPUBKEYNOTMATCH:
301 //case CURLE_SSL_INVALIDCERTSTATUS:
302 return std::make_pair(XrdCl::errTlsError, 0);
303 case CURLE_SEND_ERROR:
304 case CURLE_RECV_ERROR:
305 return std::make_pair(XrdCl::errSocketError, EIO);
306 case CURLE_COULDNT_CONNECT:
307 case CURLE_GOT_NOTHING:
308 return std::make_pair(XrdCl::errConnectionError, ECONNREFUSED);
309 case CURLE_OPERATION_TIMEDOUT:
310#ifdef HAVE_XPROTOCOL_TIMEREXPIRED
312#else
313 return std::make_pair(XrdCl::errOperationExpired, ESTALE);
314#endif
315 case CURLE_UNSUPPORTED_PROTOCOL:
316 case CURLE_NOT_BUILT_IN:
317 return std::make_pair(XrdCl::errNotSupported, ENOSYS);
318 case CURLE_FAILED_INIT:
319 return std::make_pair(XrdCl::errInternal, 0);
320 case CURLE_URL_MALFORMAT:
321 return std::make_pair(XrdCl::errInvalidArgs, res);
322 //case CURLE_WEIRD_SERVER_REPLY:
323 //case CURLE_HTTP2:
324 //case CURLE_HTTP2_STREAM:
325 return std::make_pair(XrdCl::errCorruptedHeader, res);
326 case CURLE_PARTIAL_FILE:
327 return std::make_pair(XrdCl::errDataError, res);
328 // These two errors indicate a failure in the callback. That
329 // should generate their own failures, meaning this should never
330 // get use.
331 case CURLE_READ_ERROR:
332 case CURLE_WRITE_ERROR:
333 return std::make_pair(XrdCl::errInternal, res);
334 case CURLE_RANGE_ERROR:
335 case CURLE_BAD_CONTENT_ENCODING:
336 return std::make_pair(XrdCl::errNotSupported, res);
337 case CURLE_TOO_MANY_REDIRECTS:
338 return std::make_pair(XrdCl::errRedirectLimit, res);
339 default:
340 return std::make_pair(XrdCl::errUnknown, res);
341 }
342}
343
345 std::string_view input,
346 std::array<unsigned char, g_max_checksum_length> &output) {
347 if (input.size() > 4 * ((output.size() + 2) / 3)
348 || input.size() % 4 != 0)
349 return false;
350 if (input.size() == 0) return true;
351
352 std::unique_ptr<BIO, decltype(&BIO_free_all)> b64(BIO_new(BIO_f_base64()), &BIO_free_all);
353 BIO_set_flags(b64.get(), BIO_FLAGS_BASE64_NO_NL);
354 std::unique_ptr<BIO, decltype(&BIO_free_all)> bmem(
355 BIO_new_mem_buf(const_cast<char *>(input.data()), input.size()), &BIO_free_all);
356 bmem.reset(BIO_push(b64.release(), bmem.release()));
357
358 // Compute expected length of output; used to verify BIO_read consumes all input
359 size_t expectedLen = static_cast<size_t>(input.size() * 0.75);
360 if (input[input.size() - 1] == '=') {
361 expectedLen -= 1;
362 if (input[input.size() - 2] == '=') {
363 expectedLen -= 1;
364 }
365 }
366
367 auto len = BIO_read(bmem.get(), &output[0], output.size());
368
369 if (len == -1 || static_cast<size_t>(len) != expectedLen) return false;
370
371 return true;
372}
373
374// Parse a single header line.
375//
376// Curl promises for its callbacks "The header callback is
377// called once for each header and only complete header lines
378// are passed on to the callback".
379bool HeaderParser::Parse(const std::string &header_line)
380{
381 if (m_recv_all_headers) {
382 m_recv_all_headers = false;
383 m_recv_status_line = false;
384 }
385
386 if (!m_recv_status_line) {
387 m_recv_status_line = true;
388
389 std::stringstream ss(header_line);
390 std::string item;
391 if (!std::getline(ss, item, ' ')) return false;
392 m_resp_protocol = item;
393 if (!std::getline(ss, item, ' ')) return false;
394 try {
395 m_status_code = std::stol(item);
396 } catch (...) {
397 return false;
398 }
399 if (m_status_code < 100 || m_status_code >= 600) {
400 return false;
401 }
402 if (!std::getline(ss, item, '\n')) return false;
403 auto cr_loc = item.find('\r');
404 if (cr_loc != std::string::npos) {
405 m_resp_message = item.substr(0, cr_loc);
406 } else {
407 m_resp_message = item;
408 }
409 return true;
410 }
411
412 if (header_line.empty() || header_line == "\n" || header_line == "\r\n") {
413 m_recv_all_headers = true;
414 return true;
415 }
416
417 auto found = header_line.find(":");
418 if (found == std::string::npos) {
419 return false;
420 }
421
422 std::string header_name = header_line.substr(0, found);
423 if (!Canonicalize(header_name)) {
424 return false;
425 }
426
427 found += 1;
428 while (found < header_line.size()) {
429 if (header_line[found] != ' ') {break;}
430 found += 1;
431 }
432 std::string header_value = header_line.substr(found);
433 // Note: ignoring the fact headers are only supposed to contain ASCII.
434 // We should trim out UTF-8.
435 header_value.erase(header_value.find_last_not_of(" \r\n\t") + 1);
436
437 // Record the line in our header structure. Will be returned as part
438 // of the response info object.
439 auto iter = m_headers.find(header_name);
440 if (iter == m_headers.end()) {
441 m_headers.insert(iter, {header_name, {header_value}});
442 } else {
443 iter->second.push_back(header_value);
444 }
445
446 if (header_name == "Allow") {
447 std::string_view val(header_value);
448 while (!val.empty()) {
449 auto found = val.find(',');
450 auto method = val.substr(0, found);
451 if (method == "PROPFIND") {
452 auto new_verbs = static_cast<unsigned>(m_allow_verbs) | static_cast<unsigned>(VerbsCache::HttpVerb::kPROPFIND);
453 m_allow_verbs = static_cast<VerbsCache::HttpVerb>(new_verbs);
454 }
455 if (found == std::string_view::npos) break;
456 val = val.substr(found + 1);
457 }
458 if (static_cast<unsigned>(m_allow_verbs) & ~static_cast<unsigned>(VerbsCache::HttpVerb::kUnknown)) {
459 m_allow_verbs = static_cast<VerbsCache::HttpVerb>(static_cast<unsigned>(m_allow_verbs) & ~static_cast<unsigned>(VerbsCache::HttpVerb::kUnknown));
460 }
461 } else if (header_name == "Last-Modified") {
462 struct tm tm;
463 if (strptime(header_value.c_str(), "%a, %d %b %Y %H:%M:%S", &tm) != nullptr) {
464 m_modtime = timegm(&tm);
465 }
466 } else if (header_name == "Content-Length") {
467 try {
468 m_content_length = std::stoll(header_value);
469 } catch (...) {
470 return false;
471 }
472 }
473 else if (header_name == "Content-Type") {
474 std::string_view val(header_value);
475 auto found = val.find(";");
476 auto first_type = val.substr(0, found);
477 m_multipart_byteranges = first_type == "multipart/byteranges";
478 if (m_multipart_byteranges) {
479 auto remainder = val.substr(found + 1);
480 found = remainder.find("boundary=");
481 if (found != std::string_view::npos) {
482 SetMultipartSeparator(remainder.substr(found + 9));
483 }
484 }
485 }
486 else if (header_name == "Content-Range") {
487 auto found = header_value.find(" ");
488 if (found == std::string::npos) {
489 return false;
490 }
491 std::string range_unit = header_value.substr(0, found);
492 if (range_unit != "bytes") {
493 return false;
494 }
495 auto range_resp = header_value.substr(found + 1);
496 found = range_resp.find("/");
497 if (found == std::string::npos) {
498 return false;
499 }
500 auto incl_range = range_resp.substr(0, found);
501 found = incl_range.find("-");
502 if (found == std::string::npos) {
503 return false;
504 }
505 auto first_pos = incl_range.substr(0, found);
506 try {
507 m_response_offset = std::stoll(first_pos);
508 } catch (...) {
509 return false;
510 }
511 auto last_pos = incl_range.substr(found + 1);
512 size_t last_byte;
513 try {
514 last_byte = std::stoll(last_pos);
515 } catch (...) {
516 return false;
517 }
518 m_content_length = last_byte - m_response_offset + 1;
519 }
520 else if (header_name == "Location") {
521 m_location = header_value;
522 } else if (header_name == "Digest") {
523 ParseDigest(header_value, m_checksums);
524 }
525 else if (header_name == "Etag")
526 {
527 // Note, the original hader name is ETag, renamed to Etag in parsing
528 // remove additional quotes
529 m_etag = header_value;
530 m_etag.erase(remove(m_etag.begin(), m_etag.end(), '\"'), m_etag.end());
531 }
532 else if (header_name == "Cache-Control")
533 {
534 m_cache_control = header_value;
535 }
536
537 return true;
538}
539
540// Parse a RFC 3230 header into the checksum info structure
541//
542// If the parsing fails, the second element of the tuple will be false.
543void HeaderParser::ParseDigest(const std::string &digest, XrdClHttp::ChecksumInfo &info) {
544 std::string_view view(digest);
545 std::array<unsigned char, g_max_checksum_length> checksum_value{};
546 std::string digest_lower;
547 while (!view.empty()) {
548 auto nextsep = view.find(',');
549 auto entry = view.substr(0, nextsep);
550 if (nextsep == std::string_view::npos) {
551 view = "";
552 } else {
553 view = view.substr(nextsep + 1);
554 }
555 nextsep = entry.find('=');
556 if (nextsep == std::string_view::npos) continue;
557 auto name = trim_view(entry.substr(0, nextsep));
558 auto value = trim_view(entry.substr(nextsep + 1));
559 digest_lower.clear();
560 digest_lower.resize(name.size());
561 std::transform(name.begin(), name.end(), digest_lower.begin(), [](unsigned char c) {
562 return std::tolower(c);
563 });
564 const auto setHex32 = [&](ChecksumType type) {
565 if (value.empty() || value.size() > 8) return;
566 std::string padded(8 - value.size(), '0');
567 padded.append(value.data(), value.size());
568 XrdCksData checksum;
569 if (!checksum.Set(padded.c_str(), padded.size())) return;
570 std::copy_n(
571 reinterpret_cast<const unsigned char *>(checksum.Value),
572 GetChecksumLength(type), checksum_value.begin());
573 info.Set(type, checksum_value);
574 };
575 const auto setBase64 = [&](ChecksumType type) {
576 const size_t decodedSize = GetChecksumLength(type);
577 const size_t encodedSize = 4 * ((decodedSize + 2) / 3);
578 if (value.size() == encodedSize
579 && Base64Decode(value, checksum_value))
580 {
581 info.Set(type, checksum_value);
582 }
583 };
584 if (digest_lower == "adler" || digest_lower == "adler32") {
585 setHex32(ChecksumType::kADLER32);
586 } else if (digest_lower == "crc32") {
587 // Accept padded base64 alongside the existing hexadecimal form.
588 // Require "==" to distinguish a four-byte base64 checksum from
589 // short hexadecimal values, which may also be valid unpadded base64.
590 if (value.size() == 8 && value[6] == '=' && value[7] == '=') {
591 setBase64(ChecksumType::kCRC32);
592 continue;
593 }
594 setHex32(ChecksumType::kCRC32);
595 } else if (digest_lower == "md5") {
596 setBase64(ChecksumType::kMD5);
597 } else if (digest_lower == "sha" || digest_lower == "sha1") {
598 setBase64(ChecksumType::kSHA1);
599 } else if (digest_lower == "sha-256" || digest_lower == "sha256") {
600 setBase64(ChecksumType::kSHA256);
601 } else if (digest_lower == "crc32c") {
602 // XRootD currently incorrectly base64-encodes crc32c checksums; see
603 // https://github.com/xrootd/xrootd/issues/2456
604 // For backward comaptibility, if this looks like base64 encoded (8
605 // bytes long and last two bytes are padding), then we base64 decode.
606 if (value.size() == 8 && value[6] == '=' && value[7] == '=') {
607 setBase64(ChecksumType::kCRC32C);
608 continue;
609 }
610 setHex32(ChecksumType::kCRC32C);
611 }
612 }
613}
614
615// Convert the checksum type to a RFC 3230 digest name as recorded by IANA here:
616// https://www.iana.org/assignments/http-dig-alg/http-dig-alg.xhtml
618 switch (type) {
620 return "adler32";
622 return "crc32";
624 return "MD5";
626 return "CRC32c";
628 return "SHA";
630 return "SHA-256";
632 std::string result;
633 for (int value = 0;
634 value < static_cast<int>(XrdClHttp::ChecksumType::kAll);
635 ++value) {
636 if (!result.empty()) result += ',';
637 result += ChecksumTypeToDigestName(
638 static_cast<XrdClHttp::ChecksumType>(value));
639 }
640 return result;
641 }
642 default:
643 return "";
644 }
645}
646
647// This clever approach was inspired by golang's net/textproto
648bool HeaderParser::validHeaderByte(unsigned char c)
649{
650 const static uint64_t mask_lower = 0 |
651 uint64_t((1<<10)-1) << '0' |
652 uint64_t(1) << '!' |
653 uint64_t(1) << '#' |
654 uint64_t(1) << '$' |
655 uint64_t(1) << '%' |
656 uint64_t(1) << '&' |
657 uint64_t(1) << '\'' |
658 uint64_t(1) << '*' |
659 uint64_t(1) << '+' |
660 uint64_t(1) << '-' |
661 uint64_t(1) << '.';
662
663 const static uint64_t mask_upper = 0 |
664 uint64_t((1<<26)-1) << ('a'-64) |
665 uint64_t((1<<26)-1) << ('A'-64) |
666 uint64_t(1) << ('^'-64) |
667 uint64_t(1) << ('_'-64) |
668 uint64_t(1) << ('`'-64) |
669 uint64_t(1) << ('|'-64) |
670 uint64_t(1) << ('~'-64);
671
672 if (c >= 128) return false;
673 if (c >= 64) return (uint64_t(1)<<(c-64)) & mask_upper;
674 return (uint64_t(1) << c) & mask_lower;
675}
676
677bool HeaderParser::Canonicalize(std::string &headerName)
678{
679 auto upper = true;
680 const static int toLower = 'a' - 'A';
681 for (size_t idx=0; idx<headerName.size(); idx++) {
682 char c = headerName[idx];
683 if (!validHeaderByte(c)) {
684 return false;
685 }
686 if (upper && 'a' <= c && c <= 'z') {
687 c -= toLower;
688 } else if (!upper && 'A' <= c && c <= 'Z') {
689 c += toLower;
690 }
691 headerName[idx] = c;
692 upper = c == '-';
693 }
694 return true;
695}
696
697HandlerQueue::HandlerQueue(unsigned max_pending_ops) :
698 m_max_pending_ops(max_pending_ops)
699{
700 int filedes[2];
701 auto result = pipe(filedes);
702 if (result == -1) {
703 throw std::system_error(errno, std::generic_category(),
704 "failed to create HTTP worker pipe");
705 }
706 if (fcntl(filedes[0], F_SETFL, O_NONBLOCK | O_CLOEXEC) == -1 || fcntl(filedes[1], F_SETFL, O_NONBLOCK | O_CLOEXEC) == -1) {
707 const int error = errno;
708 close(filedes[0]);
709 close(filedes[1]);
710 throw std::system_error(error, std::generic_category(),
711 "failed to configure HTTP worker pipe");
712 }
713 m_read_fd = filedes[0];
714 m_write_fd = filedes[1];
715};
716
717namespace {
718
719bool EnableCurlHeaderDump() {
720 auto *log = XrdCl::DefaultEnv::GetLog();
721 if (log && log->GetLevel() >= XrdCl::Log::DumpMsg)
722 return true;
723
724 return false;
725}
726
727// Debug callback for libcurl headers; enabled with XRD_LOGLEVEL=Dump
728int DumpHeader(CURL *handle, curl_infotype type, char *data, size_t size, void *clientp) {
729 (void)handle;
730 auto *logger = static_cast<XrdCl::Log *>(clientp);
731 if (!logger || !data || size == 0) {
732 return 0;
733 }
734
735 const char *direction = nullptr;
736 switch (type) {
737 case CURLINFO_HEADER_OUT:
738 direction = ">";
739 break;
740 case CURLINFO_HEADER_IN:
741 direction = "<";
742 break;
743 default:
744 return 0;
745 }
746
747 const std::string redacted = obfuscateAuth(std::string(data, size));
748 logger->Debug(kLogXrdClHttp, "%s %s", direction, redacted.c_str());
749 return 0;
750}
751
752}
753
754// Trim left and right side of a string_view for space characters
755std::string_view XrdClHttp::trim_view(const std::string_view &input_view) {
756 auto view = XrdClHttp::ltrim_view(input_view);
757 for (size_t idx = 0; idx < input_view.size(); idx++) {
758 if (!isspace(view[view.size() - 1 - idx])) {
759 return view.substr(0, view.size() - idx);
760 }
761 }
762 return "";
763}
764
765// Trim the left side of a string_view for space
766std::string_view XrdClHttp::ltrim_view(const std::string_view &input_view) {
767 for (size_t idx = 0; idx < input_view.size(); idx++) {
768 if (!isspace(input_view[idx])) {
769 return input_view.substr(idx);
770 }
771 }
772 return "";
773}
774
775void
776XrdClHttp::ConfigureHandle(CURL *curl, bool verbose) {
777 curl_easy_setopt(curl, CURLOPT_USERAGENT, "xrdcl-http/" XrdVERSION);
778 curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, DumpHeader);
779 curl_easy_setopt(curl, CURLOPT_DEBUGDATA, XrdCl::DefaultEnv::GetLog());
780 if (verbose)
781 curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
782
783 auto env = XrdCl::DefaultEnv::GetEnv();
784 std::string ca_file;
785 if (!env->GetString("HttpCertFile", ca_file) || ca_file.empty()) {
786 char *x509_ca_file = getenv("X509_CERT_FILE");
787 if (x509_ca_file) {
788 ca_file = std::string(x509_ca_file);
789 }
790 }
791 if (!ca_file.empty()) {
792 curl_easy_setopt(curl, CURLOPT_CAINFO, ca_file.c_str());
793 }
794 std::string ca_dir;
795 if (!env->GetString("HttpCertDir", ca_dir) || ca_dir.empty()) {
796 char *x509_ca_dir = getenv("X509_CERT_DIR");
797 if (x509_ca_dir) {
798 ca_dir = std::string(x509_ca_dir);
799 }
800 }
801 if (!ca_dir.empty()) {
802 curl_easy_setopt(curl, CURLOPT_CAPATH, ca_dir.c_str());
803 }
804
805 curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 32*1024);
806}
807
808CURL *
809XrdClHttp::GetHandle(bool verbose) {
810 auto result = curl_easy_init();
811 if (result == nullptr) {
812 return result;
813 }
814
815 XrdClHttp::ConfigureHandle(result, verbose);
816
817 return result;
818}
819
820CURL *
822 if (m_handles.size()) {
823 auto result = m_handles.back();
824 m_handles.pop_back();
825 return result;
826 }
827
828 return ::GetHandle(EnableCurlHeaderDump());
829}
830
831void
833 m_handles.push_back(curl);
834}
835
836void
838{
839 std::unique_lock<std::mutex> lk(m_mutex);
840 auto now = std::chrono::steady_clock::now();
841
842 // Iterate through the paused transfers, checking if they are done.
843 for (auto &op : m_ops) {
844 if (!op->IsPaused()) continue;
845
846 if (op->TransferStalled(0, now)) {
847 op->ContinueHandle();
848 }
849 }
850
851 std::vector<decltype(m_ops)::value_type> expired_ops;
852 unsigned expired_count = 0;
853 auto it = std::remove_if(m_ops.begin(), m_ops.end(),
854 [&](const std::shared_ptr<CurlOperation> &handler) {
855 auto expired = handler->GetOperationExpiry() < now;
856 if (expired) {
857 expired_ops.push_back(handler);
858 expired_count++;
859 }
860 return expired;
861 });
862 m_ops.erase(it, m_ops.end());
863
864 // The contents of our pipe and the in-memory queue are now off by expired_count.
865 // Read exactly that many bytes from the pipe and throw them away.
866 char throwaway[64];
867 unsigned bytes_to_read = expired_count;
868 while (bytes_to_read > 0) {
869 size_t chunk = std::min<size_t>(sizeof(throwaway), bytes_to_read);
870 ssize_t n = read(m_read_fd, throwaway, chunk);
871 if (n > 0) {
872 bytes_to_read -= n;
873 } else if (n == -1) {
874 if (errno == EINTR) {
875 continue;
876 } else {
877 // EWOULDBLOCK is a possibility if there's a synchronization error;
878 // for now, just continue on as if we were successful in reading out
879 // the missing bytes
880 break;
881 }
882 } else {
883 break;
884 }
885 }
886
887 // Note: the failure handler may trigger new operations submitted to the queue
888 // (which requires the lock to be held) such as a prefetch operation that gets split
889 // into multiple sub-operations.
890 //
891 // Thus, we must unlock the mutex protecting the queue and avoid touching the shared state of
892 // m_ops.
893 lk.unlock();
894 for (auto &handler : expired_ops) {
895 if (handler) handler->Fail(XrdCl::errOperationExpired, 0, "Operation expired while in queue");
896 }
897}
898
899void
900HandlerQueue::Produce(std::shared_ptr<CurlOperation> handler)
901{
902 auto handler_expiry = handler->GetOperationExpiry();
903 std::unique_lock<std::mutex> lk{m_mutex};
904 m_producer_cv.wait_until(lk,
905 handler_expiry,
906 [&]{return m_ops.size() < m_max_pending_ops;}
907 );
908 if (std::chrono::steady_clock::now() > handler_expiry) {
909 lk.unlock();
910 handler->Fail(XrdCl::errOperationExpired, 0, "Operation expired while waiting for worker");
911 m_ops_rejected.fetch_add(1, std::memory_order_relaxed);
912 return;
913 }
914
915 m_ops.push_back(handler);
916 char ready[] = "1";
917 while (true) {
918 auto result = write(m_write_fd, ready, 1);
919 if (result == -1) {
920 if (errno == EINTR) {
921 continue;
922 } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
923 // This should never happen, but if it does, just continue
924 // as if we successfully wrote the notification to the pipe.
925 break;
926 }
927 throw std::system_error(errno, std::generic_category(),
928 "failed to notify HTTP worker");
929 }
930 break;
931 }
932
933 lk.unlock();
934 m_consumer_cv.notify_one();
935 m_ops_produced.fetch_add(1, std::memory_order_relaxed);
936}
937
938std::shared_ptr<CurlOperation>
939HandlerQueue::Consume(std::chrono::steady_clock::duration dur)
940{
941 std::unique_lock<std::mutex> lk(m_mutex);
942 m_consumer_cv.wait_for(lk, dur, [&]{return m_ops.size() > 0 || m_shutdown;});
943 if (m_shutdown || m_ops.empty()) {
944 return {};
945 }
946
947 std::shared_ptr<CurlOperation> result = m_ops.front();
948 m_ops.pop_front();
949
950 char ready[1];
951 while (true) {
952 auto result = read(m_read_fd, ready, 1);
953 if (result == -1) {
954 if (errno == EINTR) {
955 continue;
956 } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
957 // This should never happen, but if it does, just continue
958 // as if we successfully read the byte.
959 break;
960 }
961 throw std::system_error(errno, std::generic_category(),
962 "failed to consume HTTP worker notification");
963 }
964 break;
965 }
966
967 lk.unlock();
968 m_producer_cv.notify_one();
969 m_ops_consumed.fetch_add(1, std::memory_order_relaxed);
970
971 return result;
972}
973
974std::string
976{
977 auto consumed = m_ops_consumed.load(std::memory_order_relaxed);
978 auto produced = m_ops_produced.load(std::memory_order_relaxed);
979 return "{"
980 "\"produced\":" + std::to_string(produced) + ","
981 "\"consumed\":" + std::to_string(consumed) + ","
982 "\"pending\":" + std::to_string(produced - consumed) + ","
983 "\"rejected\":" + std::to_string(m_ops_rejected.load(std::memory_order_relaxed)) +
984 "}";
985}
986
987std::shared_ptr<CurlOperation>
989{
990 std::unique_lock<std::mutex> lk(m_mutex);
991 if (m_ops.size() == 0) {
992 std::shared_ptr<CurlOperation> result;
993 return result;
994 }
995
996 std::shared_ptr<CurlOperation> result = m_ops.front();
997 m_ops.pop_front();
998
999 char ready[1];
1000 while (true) {
1001 auto result = read(m_read_fd, ready, 1);
1002 if (result == -1) {
1003 if (errno == EINTR) {
1004 continue;
1005 } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
1006 // This should never happen, but if it does, just continue
1007 // as if we successfully read the byte.
1008 break;
1009 }
1010 throw std::system_error(errno, std::generic_category(),
1011 "failed to consume HTTP worker notification");
1012 }
1013 break;
1014 }
1015
1016 lk.unlock();
1017 m_producer_cv.notify_one();
1018 m_ops_consumed.fetch_add(1, std::memory_order_relaxed);
1019
1020 return result;
1021}
1022
1023void
1025{
1026 std::unique_lock lock(m_mutex);
1027 m_shutdown = true;
1028 m_consumer_cv.notify_all();
1029}
1030
1031void
1033{
1034 for (auto handle : m_handles) {
1035 curl_easy_cleanup(handle);
1036 }
1037 m_handles.clear();
1038}
1039
1040CurlWorker::CurlWorker(std::shared_ptr<HandlerQueue> queue, VerbsCache &cache, XrdCl::Log* logger) :
1041 m_cache(cache),
1042 m_queue(queue),
1043 m_logger(logger)
1044{
1045 {
1046 std::unique_lock lk(m_worker_stats_mutex);
1047 m_stats_offset = m_workers_last_completed_cycle.size();
1048 m_workers_last_completed_cycle.push_back(&m_last_completed_cycle);
1049 m_workers_oldest_op.push_back(&m_oldest_op);
1050 }
1051 int pipeInfo[2];
1052 if ((pipe(pipeInfo) == -1) || (fcntl(pipeInfo[0], F_SETFD, FD_CLOEXEC)) || (fcntl(pipeInfo[1], F_SETFD, FD_CLOEXEC))) {
1053 throw std::runtime_error("Failed to create shutdown monitoring pipe for curl worker");
1054 }
1055 m_shutdown_pipe_r = pipeInfo[0];
1056 m_shutdown_pipe_w = pipeInfo[1];
1057
1058 // Handle setup of the X509 authentication
1059 auto env = XrdCl::DefaultEnv::GetEnv();
1060 env->GetString("HttpClientCertFile", m_x509_client_cert_file);
1061 env->GetString("HttpClientKeyFile", m_x509_client_key_file);
1062}
1063
1064std::tuple<std::string, std::string> CurlWorker::ClientX509CertKeyFile() const
1065{
1066 return std::make_tuple(m_x509_client_cert_file, m_x509_client_key_file);
1067}
1068
1069std::string
1071{
1072 auto now = std::chrono::system_clock::now().time_since_epoch().count();
1073 auto oldest_op = now;
1074 auto oldest_cycle = now;
1075 {
1076 std::unique_lock lk(m_worker_stats_mutex);
1077 for (const auto &entry : m_workers_last_completed_cycle) {
1078 if (!entry) {continue;}
1079 auto cycle = entry->load(std::memory_order_relaxed);
1080 if (cycle < oldest_cycle) oldest_cycle = cycle;
1081 }
1082 for (const auto &entry : m_workers_oldest_op) {
1083 if (!entry) {continue;}
1084 auto op = entry->load(std::memory_order_relaxed);
1085 if (op < oldest_op) oldest_op = op;
1086 }
1087 }
1088 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();
1089 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();
1090 std::string retval = "{"
1091 "\"oldest_op\":" + std::to_string(oldest_op_dbl) + ","
1092 "\"oldest_cycle\":" + std::to_string(oldest_cycle_dbl) + ","
1093 ;
1094
1095 for (size_t verb_idx = 0; verb_idx < static_cast<int>(XrdClHttp::CurlOperation::HttpVerb::Count); verb_idx++) {
1096 const auto &verb_str = XrdClHttp::CurlOperation::GetVerbString(static_cast<XrdClHttp::CurlOperation::HttpVerb>(verb_idx));
1097 for (size_t op_idx = 0; op_idx < 402; op_idx++) {
1098 if (op_idx == 401) continue;
1099
1100 auto &op_stats = m_ops[verb_idx][op_idx];
1101 auto duration = op_stats.m_duration.load(std::memory_order_relaxed);
1102 if (duration == 0) continue;
1103
1104 std::string prefix = "http_" + verb_str + "_" + ((op_idx == 402) ? "invalid" : std::to_string(200 + op_idx)) + "_";
1105
1106 auto duration_dbl = std::chrono::duration<double>(std::chrono::steady_clock::duration(duration)).count();
1107 retval += "\"" + prefix + "duration\":" + std::to_string(duration_dbl) + ",";
1108
1109 duration = op_stats.m_pause_duration.load(std::memory_order_relaxed);
1110 if (duration > 0) {
1111 duration_dbl = std::chrono::duration<double>(std::chrono::steady_clock::duration(duration)).count();
1112 retval += "\"" + prefix + "pause_duration\":" + std::to_string(duration_dbl) + ",";
1113 }
1114
1115 auto count = op_stats.m_bytes.load(std::memory_order_relaxed);
1116 if (count) retval += "\"" + prefix + "bytes\":" + std::to_string(count) + ",";
1117 count = op_stats.m_error.load(std::memory_order_relaxed);
1118 if (count) retval += "\"" + prefix + "error\":" + std::to_string(count) + ",";
1119 count = op_stats.m_finished.load(std::memory_order_relaxed);
1120 if (count) retval += "\"" + prefix + "finished\":" + std::to_string(count) + ",";
1121 count = op_stats.m_client_timeout.load(std::memory_order_relaxed);
1122 if (count) retval += "\"" + prefix + "client_timeout\":" + std::to_string(count) + ",";
1123 count = op_stats.m_server_timeout.load(std::memory_order_relaxed);
1124 if (count) retval += "\"" + prefix + "server_timeout\":" + std::to_string(count) + ",";
1125 }
1126 {
1127 auto &op_stats = m_ops[verb_idx][401];
1128 auto duration = op_stats.m_duration.load(std::memory_order_relaxed);
1129 if (duration == 0) continue;
1130
1131 std::string prefix = "http_" + verb_str + "_";
1132
1133 auto duration_dbl = std::chrono::duration<double>(std::chrono::steady_clock::duration(duration)).count();
1134 retval += "\"" + prefix + "preheader_duration\":" + std::to_string(duration_dbl) + ",";
1135
1136 auto count = op_stats.m_started.load(std::memory_order_relaxed);
1137 if (count) retval += "\"" + prefix + "started\":" + std::to_string(count) + ",";
1138 count = op_stats.m_error.load(std::memory_order_relaxed);
1139 if (count) retval += "\"" + prefix + "preheader_error\":" + std::to_string(count) + ",";
1140 count = op_stats.m_finished.load(std::memory_order_relaxed);
1141 if (count) retval += "\"" + prefix + "preheader_finished\":" + std::to_string(count) + ",";
1142 count = op_stats.m_server_timeout.load(std::memory_order_relaxed);
1143 if (count) retval += "\"" + prefix + "preheader_timeout\":" + std::to_string(count) + ",";
1144 count = op_stats.m_conncall_timeout.load(std::memory_order_relaxed);
1145 if (count) retval += "\"" + prefix + "conncall_timeout\":" + std::to_string(count) + ",";
1146 }
1147 }
1148
1149 retval +=
1150 "\"conncall_error\":" + std::to_string(m_conncall_errors.load(std::memory_order_relaxed)) + ","
1151 "\"conncall_started\":" + std::to_string(m_conncall_req.load(std::memory_order_relaxed)) + ","
1152 "\"conncall_success\":" + std::to_string(m_conncall_success.load(std::memory_order_relaxed)) + ","
1153 "\"conncall_timeout\":" + std::to_string(m_conncall_timeout.load(std::memory_order_relaxed)) +
1154 "}";
1155
1156 return retval;
1157}
1158
1159void
1160CurlWorker::OpRecord(XrdClHttp::CurlOperation &op, OpKind kind)
1161{
1162 int sc = op.GetStatusCode();
1163 // - We encode everything pre-header as integer "401". We include a 100-continue request as "pre-header".
1164 // - Status codes out of the acceptable range are labeled "402"
1165 // - Otherwise, we store it in the array shifted by 200 (to avoid more sparsity)
1166 if (sc < 0 || kind == OpKind::Start || sc == 100) {
1167 sc = 401;
1168 } else if (sc < 200 || sc >= 600) {
1169 sc = 402;
1170 } else {
1171 sc -= 200;
1172 }
1173 auto [bytes, pre_headers, post_headers, pause_duration] = op.StatisticsReset();
1174 auto &op_stats = m_ops[static_cast<int>(op.GetVerb())][sc];
1175 op_stats.m_bytes.fetch_add(bytes, std::memory_order_relaxed);
1176 op_stats.m_duration.fetch_add((sc == 401) ? pre_headers.count() : post_headers.count(), std::memory_order_relaxed);
1177 op_stats.m_pause_duration.fetch_add(pause_duration.count(), std::memory_order_relaxed);
1178 if (pre_headers != std::chrono::steady_clock::duration::zero() && sc != 401) {
1179 auto &old_stats = m_ops[static_cast<int>(op.GetVerb())][401];
1180 old_stats.m_duration.fetch_add(pre_headers.count(), std::memory_order_relaxed);
1181 }
1182 switch (kind) {
1183 case OpKind::ConncallTimeout:
1184 op_stats.m_conncall_timeout.fetch_add(1, std::memory_order_relaxed);
1185 break;
1186 case OpKind::ClientTimeout:
1187 op_stats.m_client_timeout.fetch_add(1, std::memory_order_relaxed);
1188 break;
1189 case OpKind::Error:
1190 op_stats.m_error.fetch_add(1, std::memory_order_relaxed);
1191 break;
1192 case OpKind::Finish:
1193 op_stats.m_finished.fetch_add(1, std::memory_order_relaxed);
1194 break;
1195 case OpKind::Start:
1196 op_stats.m_started.fetch_add(1, std::memory_order_relaxed);
1197 break;
1198 case OpKind::ServerTimeout:
1199 op_stats.m_server_timeout.fetch_add(1, std::memory_order_relaxed);
1200 break;
1201 case OpKind::Update:
1202 break;
1203 }
1204}
1205
1206void
1207CurlWorker::Start(std::unique_ptr<XrdClHttp::CurlWorker> self, std::thread tid)
1208{
1209 {
1210 std::unique_lock lock(m_workers_mutex);
1211 m_workers.emplace_back(std::move(self));
1212 m_self_tid = std::move(tid);
1213 }
1214 std::unique_lock lock(m_start_lock);
1215 m_start_complete = true;
1216 m_start_complete_cv.notify_one();
1217}
1218
1219void
1221{
1222 {
1223 std::unique_lock lock(myself->m_start_lock);
1224 myself->m_start_complete_cv.wait(lock, [&]{return myself->m_start_complete;});
1225 }
1226 try {
1227 myself->Run();
1228 } catch (...) {
1229 myself->m_logger->Warning(kLogXrdClHttp, "Curl worker got an exception");
1230 {
1231 std::unique_lock lock(m_workers_mutex);
1232 auto iter = std::remove_if(m_workers.begin(), m_workers.end(), [&](std::unique_ptr<XrdClHttp::CurlWorker> &worker){return worker.get() == myself;});
1233 m_workers.erase(iter);
1234 }
1235 }
1236}
1237
1238void
1240 int max_pending = 50;
1241 XrdCl::DefaultEnv::GetEnv()->GetInt("HttpMaxPendingOps", max_pending);
1242 m_continue_queue.reset(new HandlerQueue(max_pending));
1243 auto &queue = *m_queue.get();
1244 m_logger->Debug(kLogXrdClHttp, "Started a curl worker");
1245
1246 CURLM *multi_handle = curl_multi_init();
1247 if (multi_handle == nullptr) {
1248 throw std::runtime_error("Failed to create curl multi-handle");
1249 }
1250
1251 int running_handles = 0;
1252 time_t last_maintenance = time(NULL);
1253 CURLMcode mres = CURLM_OK;
1254
1255 // Map from a file descriptor that has an outstanding broker request
1256 // to the corresponding CURL handle.
1257 std::unordered_map<int, WaitingForBroker> broker_reqs;
1258 std::vector<struct curl_waitfd> waitfds;
1259
1260 // Detach the handle before clearing the operation's curl callbacks. The
1261 // caller may still hold the operation after this worker stops using it.
1262 auto release_handle = [&](const std::shared_ptr<CurlOperation> &op, CURL *handle) {
1263 if (!handle) return;
1264 curl_multi_remove_handle(multi_handle, handle);
1265 if (op->GetCurlHandle()) op->ReleaseHandle();
1266 curl_easy_cleanup(handle);
1267 };
1268 auto remove_op = [&](CURL *handle) {
1269 auto iter = m_op_map.find(handle);
1270 if (iter == m_op_map.end()) return;
1271 const auto &[op, started_at] = iter->second;
1272 release_handle(op, handle);
1273 m_op_map.erase(iter);
1274 running_handles -= 1;
1275 for (auto req = broker_reqs.begin(); req != broker_reqs.end();) {
1276 if (req->second.curl == handle) {
1277 req = broker_reqs.erase(req);
1278 } else {
1279 ++req;
1280 }
1281 }
1282 };
1283 auto start_options = [&](CURL *parent_handle, const std::shared_ptr<CurlOperation> &parent_op,
1284 const std::string &target) {
1285 std::shared_ptr<CurlOptionsOp> options_op;
1286 CURL *options_handle = nullptr;
1287 try {
1288 std::string modified_url;
1289 options_op = std::make_shared<CurlOptionsOp>(parent_handle, parent_op,
1290 std::string(VerbsCache::GetUrlKey(target, modified_url)),
1291 m_logger, parent_op->GetConnCalloutFunc());
1292 options_handle = queue.GetHandle();
1293 if (!options_handle || !options_op->Setup(options_handle, *this)) {
1294 throw std::runtime_error("Failed to setup the curl handle for OPTIONS");
1295 }
1296 } catch (...) {
1297 m_logger->Debug(kLogXrdClHttp, "Failed to setup the curl handle for OPTIONS");
1298 if (options_op) {
1299 options_op->Fail(XrdCl::errInternal, ENOMEM,
1300 "Failed to setup the curl handle for the OPTIONS operation");
1301 release_handle(options_op, options_handle);
1302 }
1303 parent_op->Fail(XrdCl::errInternal, ENOMEM,
1304 "Failed to setup the curl handle for the OPTIONS operation");
1305 OpRecord(*parent_op, OpKind::Error);
1306 return false;
1307 }
1308 m_op_map[options_handle] = {options_op, std::chrono::system_clock::now()};
1309 OpRecord(*options_op, OpKind::Start);
1310 running_handles += 1;
1311 auto options_res = curl_multi_add_handle(multi_handle, options_handle);
1312 if (options_res == CURLM_OK) return true;
1313 options_op->Fail(XrdCl::errInternal, options_res,
1314 "Unable to add OPTIONS operation to the curl multi-handle");
1315 OpRecord(*options_op, OpKind::Error);
1316 parent_op->Fail(XrdCl::errInternal, options_res,
1317 "Unable to add OPTIONS operation to the curl multi-handle");
1318 OpRecord(*parent_op, OpKind::Error);
1319 remove_op(options_handle);
1320 return false;
1321 };
1322 auto reactivate_parent = [&](const std::shared_ptr<CurlOptionsOp> &options_op) {
1323 auto parent_op = options_op->GetOperation();
1324 auto parent_handle = options_op->GetParentCurlHandle();
1325 if (parent_op->IsDone()) {
1326 remove_op(parent_handle);
1327 return false;
1328 }
1329 // OPTIONS is advisory, including for redirect targets. Resolve a
1330 // pending redirect before adding the parent's handle back to libcurl.
1331 if (parent_op->IsRedirect()) {
1332 std::string target;
1333 auto action = parent_op->Redirect(target);
1335 OpRecord(*parent_op, OpKind::Error);
1336 remove_op(parent_handle);
1337 return false;
1338 }
1340 if (start_options(parent_handle, parent_op, target)) return true;
1341 remove_op(parent_handle);
1342 return false;
1343 }
1344 }
1345 OpRecord(*parent_op, OpKind::Start);
1346 auto callout_socket = parent_op->WaitSocket();
1347 if (callout_socket >= 0) {
1348 broker_reqs[callout_socket] = {parent_handle, time(nullptr) + 20};
1349 m_conncall_req.fetch_add(1, std::memory_order_relaxed);
1350 return true;
1351 }
1352 auto parent_res = curl_multi_add_handle(multi_handle, parent_handle);
1353 if (parent_res == CURLM_OK) return true;
1354 m_logger->Debug(kLogXrdClHttp,
1355 "Unable to re-add the parent operation to the curl multi-handle: %s",
1356 curl_multi_strerror(parent_res));
1357 parent_op->Fail(XrdCl::errInternal, parent_res,
1358 "Unable to re-add the parent operation to the curl multi-handle");
1359 OpRecord(*parent_op, OpKind::Error);
1360 remove_op(parent_handle);
1361 return false;
1362 };
1363
1364 bool want_shutdown = false;
1365 while (!want_shutdown) {
1366 m_last_completed_cycle.store(std::chrono::system_clock::now().time_since_epoch().count());
1367 auto oldest_op = std::chrono::system_clock::now();
1368 for (const auto &[handle, entry] : m_op_map) {
1369 const auto &[op, started_at] = entry;
1370 OpRecord(*op, OpKind::Update);
1371 if (started_at < oldest_op) {
1372 oldest_op = started_at;
1373 }
1374 }
1375 m_oldest_op.store(oldest_op.time_since_epoch().count());
1376
1377 // Try continuing any available handles that have more data
1378 while (true) {
1379 auto op = m_continue_queue->TryConsume();
1380 if (!op) {
1381 break;
1382 }
1383 // Avoid race condition where external thread added a continue operation to queue
1384 // while the curl worker thread failed the transfer.
1385 if (op->IsDone()) {
1386 m_logger->Debug(kLogXrdClHttp, "Ignoring continuation of operation that has already completed");
1387 continue;
1388 }
1389 m_logger->Debug(kLogXrdClHttp, "Continuing the curl handle from op %p on thread %d", op.get(), getthreadid());
1390 auto curl = op->GetCurlHandle();
1391 if (!op->ContinueHandle()) {
1392 op->Fail(XrdCl::errInternal, 0, "Failed to continue the curl handle for the operation");
1393 OpRecord(*op, OpKind::Error);
1394 remove_op(curl);
1395 continue;
1396 } else {
1397 auto iter = m_op_map.find(curl);
1398 if (iter != m_op_map.end()) {
1399 auto &[operation, started_at] = iter->second;
1400 started_at = std::chrono::system_clock::now();
1401 }
1402 }
1403 }
1404 // Consume from the shared new operation queue
1405 while (running_handles < static_cast<int>(m_max_ops)) {
1406 auto op = running_handles == 0 ? queue.Consume(std::chrono::seconds(1)) : queue.TryConsume();
1407 if (!op) {
1408 break;
1409 }
1410 auto curl = queue.GetHandle();
1411 if (curl == nullptr) {
1412 m_logger->Debug(kLogXrdClHttp, "Unable to allocate a curl handle");
1413 op->Fail(XrdCl::errInternal, ENOMEM, "Unable to get allocate a curl handle");
1414 continue;
1415 }
1416 try {
1417 auto rv = op->Setup(curl, *this);
1418 if (!rv) {
1419 m_logger->Debug(kLogXrdClHttp, "Failed to setup the curl handle");
1420 op->Fail(XrdCl::errInternal, ENOMEM, "Failed to setup the curl handle for the operation");
1421 release_handle(op, curl);
1422 continue;
1423 }
1424 if (!op->FinishSetup(curl)) {
1425 m_logger->Debug(kLogXrdClHttp, "Failed to finish setup of the curl handle");
1426 op->Fail(XrdCl::errInternal, ENOMEM, "Failed to finish setup of the curl handle for the operation");
1427 release_handle(op, curl);
1428 continue;
1429 }
1430 } catch (...) {
1431 m_logger->Debug(kLogXrdClHttp, "Unable to setup the curl handle");
1432 op->Fail(XrdCl::errInternal, ENOMEM, "Failed to setup the curl handle for the operation");
1433 release_handle(op, curl);
1434 continue;
1435 }
1436 op->SetContinueQueue(m_continue_queue);
1437
1438 if (op->IsDone()) {
1439 op->ReleaseHandle();
1440 queue.RecycleHandle(curl);
1441 continue;
1442 }
1443 m_op_map[curl] = {op, std::chrono::system_clock::now()};
1444 running_handles += 1;
1445
1446 // If the operation requires the result of the OPTIONS verb to function, then
1447 // we add that to the multi-handle instead, chaining the two calls together.
1448 if (op->RequiresOptions()) {
1449 if (!start_options(curl, op, op->GetUrl())) {
1450 remove_op(curl);
1451 continue;
1452 }
1453 } else {
1454 OpRecord(*op, OpKind::Start);
1455 auto add_res = curl_multi_add_handle(multi_handle, curl);
1456 if (add_res != CURLM_OK) {
1457 m_logger->Debug(kLogXrdClHttp, "Unable to add operation to the curl multi-handle");
1458 op->Fail(XrdCl::errInternal, add_res, "Unable to add operation to the curl multi-handle");
1459 OpRecord(*op, OpKind::Error);
1460 remove_op(curl);
1461 continue;
1462 }
1463 }
1464 m_logger->Debug(kLogXrdClHttp, "Added request for URL %s to worker thread for processing", op->GetUrl().c_str());
1465 }
1466
1467 // Maintain the periodic reporting of thread activity and fail any operations
1468 // that have expired / timed out.
1469 time_t now = time(NULL);
1470 time_t next_maintenance = last_maintenance + m_maintenance_period.load(std::memory_order_relaxed);
1471 if (now >= next_maintenance) {
1472 m_queue->Expire();
1473 m_continue_queue->Expire();
1474 m_logger->Debug(kLogXrdClHttp, "Curl worker thread %d is running %d operations",
1475 getthreadid(), running_handles);
1476 last_maintenance = now;
1477
1478 // Timeout all the pending broker requests.
1479 std::vector<std::pair<int, CURL *>> expired_ops;
1480 for (const auto &[fd, request] : broker_reqs) {
1481 if (request.expiry < now) {
1482 expired_ops.emplace_back(fd, request.curl);
1483 }
1484 }
1485 for (const auto &[fd, handle] : expired_ops) {
1486 auto iter = m_op_map.find(handle);
1487 if (iter == m_op_map.end()) {
1488 m_logger->Warning(kLogXrdClHttp, "Found an expired curl handle with no corresponding operation!");
1489 } else {
1490 auto [op, started_at] = iter->second;
1491 op->Fail(XrdCl::errConnectionError, 1, "Timeout: connection never provided for request");
1492 OpRecord(*op, OpKind::ConncallTimeout);
1493 if (auto options_op = std::dynamic_pointer_cast<CurlOptionsOp>(op))
1494 reactivate_parent(options_op);
1495 remove_op(handle);
1496 }
1497 broker_reqs.erase(fd);
1498 m_conncall_timeout.fetch_add(1, std::memory_order_relaxed);
1499 }
1500
1501 // Cleanup the fake connection cache entries.
1503 }
1504
1505 waitfds.clear();
1506 waitfds.resize(3 + broker_reqs.size());
1507
1508 waitfds[0].fd = queue.PollFD();
1509 waitfds[0].events = CURL_WAIT_POLLIN;
1510 waitfds[0].revents = 0;
1511 waitfds[1].fd = m_continue_queue->PollFD();
1512 waitfds[1].events = CURL_WAIT_POLLIN;
1513 waitfds[1].revents = 0;
1514 waitfds[2].fd = m_shutdown_pipe_r;
1515 waitfds[2].revents = 0;
1516 waitfds[2].events = CURL_WAIT_POLLIN | CURL_WAIT_POLLPRI;
1517
1518 int idx = 3;
1519 for (const auto &[fd, request] : broker_reqs) {
1520 waitfds[idx].fd = fd;
1521 waitfds[idx].events = CURL_WAIT_POLLIN|CURL_WAIT_POLLPRI;
1522 waitfds[idx].revents = 0;
1523 idx += 1;
1524 }
1525
1526 long timeo;
1527 curl_multi_timeout(multi_handle, &timeo);
1528 // These commented-out lines are purposely left; will need to revisit after the 0.9.1 release;
1529 // for now, they are too verbose on RHEL7.
1530 //m_logger->Debug(kLogXrdClHttp, "Curl advises a timeout of %ld ms", timeo);
1531 if (running_handles && timeo == -1) {
1532 // Bug workaround: we've seen RHEL7 libcurl have a race condition where it'll not
1533 // set a timeout while doing the DNS lookup; assume that if there are running handles
1534 // but no timeout, we've hit this bug.
1535 //m_logger->Debug(kLogXrdClHttp, "Will sleep for up to 50ms");
1536 mres = curl_multi_wait(multi_handle, &waitfds[0], waitfds.size(), 50, nullptr);
1537 } else {
1538 //m_logger->Debug(kLogXrdClHttp, "Will sleep for up to %d seconds", max_sleep_time);
1539 //mres = curl_multi_wait(multi_handle, &waitfds[0], waitfds.size(), max_sleep_time*1000, nullptr);
1540 // Temporary test: we've been seeing DNS lookups timeout on additional platforms. Switch to always
1541 // poll as curl_multi_wait doesn't seem to get notified when DNS lookups are done.
1542 mres = curl_multi_wait(multi_handle, &waitfds[0], waitfds.size(), 50, nullptr);
1543 }
1544 if (mres != CURLM_OK) {
1545 m_logger->Warning(kLogXrdClHttp, "Failed to wait on multi-handle: %d", mres);
1546 }
1547
1548 // Iterate through the waiting broker callbacks.
1549 for (const auto &entry : waitfds) {
1550 // Ignore the queue's poll fd.
1551 if (waitfds[0].fd == entry.fd || waitfds[1].fd == entry.fd) {
1552 continue;
1553 }
1554 // Handle shutdown requests
1555 if ((waitfds[2].fd == entry.fd) && entry.revents) {
1556 want_shutdown = true;
1557 break;
1558 }
1559 if ((entry.revents & CURL_WAIT_POLLIN) != CURL_WAIT_POLLIN) {
1560 continue;
1561 }
1562 auto request = broker_reqs.find(entry.fd);
1563 if (request == broker_reqs.end()) continue;
1564 auto handle = request->second.curl;
1565 auto iter = m_op_map.find(handle);
1566 if (iter == m_op_map.end()) {
1567 m_logger->Warning(kLogXrdClHttp, "Internal error: broker responded on FD %d but no corresponding curl operation", entry.fd);
1568 broker_reqs.erase(entry.fd);
1569 m_conncall_errors.fetch_add(1, std::memory_order_relaxed);
1570 continue;
1571 }
1572 std::string err;
1573 auto [op, started_at] = iter->second;
1574 auto result = op->WaitSocketCallback(err);
1575 if (result == -1) {
1576 m_logger->Warning(kLogXrdClHttp, "Error when invoking the broker callback: %s", err.c_str());
1577 op->Fail(XrdCl::errErrorResponse, 1, err);
1578 OpRecord(*op, OpKind::Error);
1579 if (auto options_op = std::dynamic_pointer_cast<CurlOptionsOp>(op))
1580 reactivate_parent(options_op);
1581 remove_op(handle);
1582 broker_reqs.erase(entry.fd);
1583 m_conncall_errors.fetch_add(1, std::memory_order_relaxed);
1584 } else {
1585 broker_reqs.erase(entry.fd);
1586 auto add_res = curl_multi_add_handle(multi_handle, handle);
1587 if (add_res != CURLM_OK) {
1588 op->Fail(XrdCl::errInternal, add_res,
1589 "Unable to add operation to the curl multi-handle after the broker callback");
1590 OpRecord(*op, OpKind::Error);
1591 if (auto options_op = std::dynamic_pointer_cast<CurlOptionsOp>(op))
1592 reactivate_parent(options_op);
1593 remove_op(handle);
1594 m_conncall_errors.fetch_add(1, std::memory_order_relaxed);
1595 continue;
1596 }
1597 m_conncall_success.fetch_add(1, std::memory_order_relaxed);
1598 }
1599 }
1600
1601 // Do maintenance on the multi-handle
1602 int still_running;
1603 mres = curl_multi_perform(multi_handle, &still_running);
1604 if (mres == CURLM_CALL_MULTI_PERFORM) {
1605 mres = CURLM_OK;
1606 continue;
1607 } else if (mres != CURLM_OK) {
1608 m_logger->Warning(kLogXrdClHttp, "Failed to perform multi-handle operation: %d", mres);
1609 break;
1610 }
1611
1612 CURLMsg *msg;
1613 do {
1614 int msgq = 0;
1615 msg = curl_multi_info_read(multi_handle, &msgq);
1616 if (msg && (msg->msg == CURLMSG_DONE)) {
1617 if (!msg->easy_handle) {
1618 m_logger->Warning(kLogXrdClHttp, "Logic error: got a callback for a null handle");
1619 mres = CURLM_BAD_EASY_HANDLE;
1620 break;
1621 }
1622 CURL *const easy_handle = msg->easy_handle;
1623 auto iter = m_op_map.find(easy_handle);
1624 if (iter == m_op_map.end()) {
1625 m_logger->Error(kLogXrdClHttp, "Logic error: got a callback for an entry that doesn't exist");
1626 mres = CURLM_BAD_EASY_HANDLE;
1627 break;
1628 }
1629 auto [op, started_at] = iter->second;
1630 auto res = msg->data.result;
1631 // Completion callbacks may reconfigure or release their handle.
1632 // Stop libcurl from owning it before invoking operation methods.
1633 curl_multi_remove_handle(multi_handle, easy_handle);
1634 bool keep_handle = false;
1635 bool waiting_on_callout = false;
1636 bool options_pending = false;
1637 if (res == CURLE_OK) {
1638 auto sc = op->GetStatusCode();
1639 OpRecord(*op, OpKind::Finish);
1640 if (HTTPStatusIsError(sc)) {
1641 auto [code, number] = HTTPStatusConvert(sc);
1642 op->Fail(code, number, op->GetStatusMessage());
1643 op->ReleaseHandle();
1644 // If this was a failed CurlOptionsOp, then we re-activate the parent handle.
1645 // If the parent handle was stopped at a redirect that now returns failure, then
1646 // we'll clean it up.
1647 if (auto options_op = std::dynamic_pointer_cast<CurlOptionsOp>(op))
1648 reactivate_parent(options_op);
1649 // The curl operation was successful, it's just the HTTP request failed; recycle the handle.
1650 queue.RecycleHandle(easy_handle);
1651 } else {
1652 // The child and suspended parent own distinct handles.
1653 // Complete the child, then resume or fail the parent without
1654 // treating the parent's redirect as a child request.
1655 if (auto options_op = std::dynamic_pointer_cast<CurlOptionsOp>(op)) {
1656 options_op->Success();
1657 options_op->ReleaseHandle();
1658 if (!options_op->GetOperation()->IsDone()) {
1659 options_op->GetOperation()->OptionsDone();
1660 }
1661 reactivate_parent(options_op);
1662 queue.RecycleHandle(easy_handle);
1663 } else {
1664 if (op->IsRedirect()) {
1665 std::string target;
1666 switch (op->Redirect(target)) {
1668 keep_handle = false;
1669 break;
1671 keep_handle = true;
1672 OpRecord(*op, OpKind::Start);
1673 break;
1675 {
1676 // The redirect resulted in a new endpoint where the cache lookup failed;
1677 // we need to know what HTTP verbs are in the server's Allow list before this
1678 // operation can continue. Inject a new CurlOptionsOp and chain it to the one
1679 // being processed. Once the OPTIONS request is done, then we'll restart this
1680 // operation.
1681 if (!start_options(easy_handle, op, target)) {
1682 keep_handle = false;
1683 break;
1684 }
1685 m_logger->Debug(kLogXrdClHttp, "Invoking the OPTIONS operation before redirect to %s", target.c_str());
1686 // The original curl operation needs to be kept around without re-adding its handle
1687 // to the multi-handle until the OPTIONS operation completes.
1688 options_pending = true;
1689 keep_handle = true;
1690 }
1691 }
1692 int callout_socket = op->WaitSocket();
1693 if ((waiting_on_callout = keep_handle && !options_pending && callout_socket >= 0)) {
1694 auto expiry = time(nullptr) + 20;
1695 m_logger->Debug(kLogXrdClHttp, "Creating a callout wait request on socket %d", callout_socket);
1696 broker_reqs[callout_socket] = {easy_handle, expiry};
1697 m_conncall_req.fetch_add(1, std::memory_order_relaxed);
1698 }
1699 }
1700 if (keep_handle) {
1701 if (!waiting_on_callout && !options_pending) {
1702 auto redirect_res = curl_multi_add_handle(multi_handle, easy_handle);
1703 if (redirect_res != CURLM_OK) {
1704 op->Fail(XrdCl::errInternal, redirect_res,
1705 "Unable to add redirected operation to the curl multi-handle");
1706 OpRecord(*op, OpKind::Error);
1707 op->ReleaseHandle();
1708 queue.RecycleHandle(easy_handle);
1709 keep_handle = false;
1710 }
1711 }
1712 } else {
1713 if (!op->IsDone()) {
1714 op->Success();
1715 }
1716 if (op->IsDone()) {
1717 op->ReleaseHandle();
1718 // If the handle was successful, then we can recycle it.
1719 queue.RecycleHandle(easy_handle);
1720 } else {
1721 // Multi-step operations may configure their easy handle
1722 // for another request from Success(). Keep ownership of
1723 // the handle and run the next request through this worker's
1724 // multi-handle like any other operation.
1725 auto next_res = curl_multi_add_handle(multi_handle, easy_handle);
1726 if (next_res == CURLM_OK) {
1727 keep_handle = true;
1728 OpRecord(*op, OpKind::Start);
1729 } else {
1730 op->Fail(XrdCl::errInternal, next_res,
1731 "Unable to add the next operation request to the curl multi-handle");
1732 OpRecord(*op, OpKind::Error);
1733 op->ReleaseHandle();
1734 queue.RecycleHandle(easy_handle);
1735 }
1736 }
1737 }
1738 }
1739 }
1740 } else if (res == CURLE_COULDNT_CONNECT && op->UseConnectionCallout() && !op->GetTriedBoker()) {
1741 // In this case, we need to use the broker and the curl handle couldn't reuse
1742 // an existing socket.
1743 keep_handle = true;
1744 op->SetTriedBoker(); // Flag to ensure we try a connection only once per operation.
1745 std::string err;
1746 int wait_socket = -1;
1747 if (!op->StartConnectionCallout(err) || (wait_socket=op->WaitSocket()) == -1) {
1748 m_logger->Error(kLogXrdClHttp, "Failed to start broker-based connection: %s", err.c_str());
1749 if (!op->IsDone())
1750 op->Fail(XrdCl::errConnectionError, 1, err);
1751 OpRecord(*op, OpKind::Error);
1752 if (auto options_op = std::dynamic_pointer_cast<CurlOptionsOp>(op))
1753 reactivate_parent(options_op);
1754 op->ReleaseHandle();
1755 keep_handle = false;
1756 } else {
1757 curl_multi_remove_handle(multi_handle, easy_handle);
1758 auto expiry = time(nullptr) + 20;
1759 m_logger->Debug(kLogXrdClHttp, "Curl operation requires a new TCP socket; waiting for callout to respond on socket %d", wait_socket);
1760 broker_reqs[wait_socket] = {easy_handle, expiry};
1761 m_conncall_req.fetch_add(1, std::memory_order_relaxed);
1762 }
1763 } else {
1764 if (res == CURLE_ABORTED_BY_CALLBACK || res == CURLE_WRITE_ERROR) {
1765 // We cannot invoke the failure from within a callback as the curl thread and
1766 // original thread of execution may fight over the ownership of the handle memory.
1767 switch (op->GetError()) {
1769#ifdef HAVE_XPROTOCOL_TIMEREXPIRED
1770 op->Fail(XrdCl::errOperationExpired, 0, "Origin did not respond with headers within timeout");
1771#else
1772 op->Fail(XrdCl::errOperationExpired, 0, "Origin did not respond within timeout");
1773#endif
1774 OpRecord(*op, OpKind::Error);
1775 break;
1777 auto [ecode, emsg] = op->GetCallbackError();
1778 op->Fail(XrdCl::errErrorResponse, ecode, emsg);
1779 OpRecord(*op, OpKind::Error);
1780 break;
1781 }
1783 op->Fail(XrdCl::errOperationExpired, 0, "Operation timed out");
1784 OpRecord(*op, op->IsPaused() ? OpKind::ClientTimeout : OpKind::ServerTimeout);
1785 break;
1787 op->Fail(XrdCl::errOperationExpired, 0, "Transfer speed below minimum threshold");
1788 OpRecord(*op, OpKind::ServerTimeout);
1789 break;
1791 op->Fail(XrdCl::errOperationExpired, 0, "Transfer stalled for too long");
1792 OpRecord(*op, OpKind::ClientTimeout);
1793 break;
1795 op->Fail(XrdCl::errOperationExpired, 0, "Transfer stalled for too long");
1796 OpRecord(*op, OpKind::ServerTimeout);
1797 break;
1799 op->Fail(XrdCl::errInternal, 0, "Operation was aborted without recording an abort reason");
1800 OpRecord(*op, OpKind::Error);
1801 break;
1802 };
1803 if (auto options_op = std::dynamic_pointer_cast<CurlOptionsOp>(op))
1804 reactivate_parent(options_op);
1805 } else {
1806 auto [code, number] = CurlCodeConvert(res);
1807 const auto curl_err = op->GetCurlErrorMessage();
1808 const char *curl_easy_err = curl_easy_strerror(res);
1809 const std::string fail_err = !curl_err.empty() ? curl_err : curl_easy_err;
1810 m_logger->Debug(kLogXrdClHttp, "Curl generated an error: %s (%d)", fail_err.c_str(), res);
1811 op->Fail(code, number, fail_err);
1812 OpRecord(*op, OpKind::Error);
1813 if (auto options_op = std::dynamic_pointer_cast<CurlOptionsOp>(op))
1814 reactivate_parent(options_op);
1815 }
1816 op->ReleaseHandle();
1817 }
1818 if (!keep_handle) {
1819 curl_multi_remove_handle(multi_handle, easy_handle);
1820 if (res != CURLE_OK) {
1821 curl_easy_cleanup(easy_handle);
1822 }
1823 for (auto req = broker_reqs.begin(); req != broker_reqs.end();) {
1824 if (req->second.curl == easy_handle) {
1825 m_logger->Warning(kLogXrdClHttp, "Curl handle finished while a broker operation was outstanding");
1826 m_conncall_errors.fetch_add(1, std::memory_order_relaxed);
1827 req = broker_reqs.erase(req);
1828 } else {
1829 ++req;
1830 }
1831 }
1832 m_op_map.erase(easy_handle);
1833 running_handles -= 1;
1834 }
1835 }
1836 } while (msg);
1837 if (mres != CURLM_OK) break;
1838 }
1839
1840 for (const auto &[handle, entry] : m_op_map) {
1841 const auto &[op, started_at] = entry;
1842 if (!op->IsDone()) {
1843 const auto err_num = mres == CURLM_OK ? ECANCELED : static_cast<int>(mres);
1844 const std::string err_msg = mres == CURLM_OK
1845 ? "Curl worker shut down before the operation completed"
1846 : curl_multi_strerror(mres);
1847 op->Fail(XrdCl::errInternal, err_num, err_msg);
1848 OpRecord(*op, OpKind::Error);
1849 }
1850 release_handle(op, handle);
1851 }
1852 m_op_map.clear();
1853
1854 m_queue->ReleaseHandles();
1855 curl_multi_cleanup(multi_handle);
1856}
1857
1858void
1859CurlWorker::Shutdown()
1860{
1861 m_queue->Shutdown();
1862 if (m_shutdown_pipe_w == -1) {
1863 m_logger->Debug(kLogXrdClHttp, "Curl worker shutdown prior to launch of thread");
1864 return;
1865 }
1866 close(m_shutdown_pipe_w);
1867 m_shutdown_pipe_w = -1;
1868
1869 // wait for worker thread to exit
1870 m_self_tid.join();
1871
1872 {
1873 std::unique_lock lk(m_worker_stats_mutex);
1874 m_workers_last_completed_cycle[m_stats_offset] = nullptr;
1875 m_workers_oldest_op[m_stats_offset] = nullptr;
1876 }
1877 m_logger->Debug(kLogXrdClHttp, "Curl worker thread shutdown has completed.");
1878}
1879
1880void
1881CurlWorker::ShutdownAll()
1882{
1883 std::unique_lock lock(m_workers_mutex);
1884 for (auto &worker : m_workers) {
1885 worker->Shutdown();
1886 }
1887}
1888
1889CurlWorker::initcontrol::initcontrol()
1890{
1891 curl_global_init(CURL_GLOBAL_DEFAULT);
1892}
1893
1894CurlWorker::initcontrol::~initcontrol()
1895{
1896 ShutdownAll();
1897 curl_global_cleanup();
1898}
@ kXR_InvalidRequest
@ kXR_Impossible
@ kXR_TimerExpired
@ kXR_NotAuthorized
@ kXR_NotFound
@ kXR_FileLocked
@ kXR_overQuota
@ kXR_Unsupported
@ kXR_Conflict
@ kXR_ServerError
@ kXR_Overloaded
@ kXR_ReqTimedOut
std::pair< uint16_t, uint32_t > CurlCodeConvert(CURLcode res)
void CURL
std::string obfuscateAuth(const std::string &input)
#define close(a)
Definition XrdPosix.hh:48
#define write(a, b, c)
Definition XrdPosix.hh:121
#define read(a, b, c)
Definition XrdPosix.hh:86
int emsg(int rc, char *msg)
char Value[ValuSize]
Definition XrdCksData.hh:53
int Set(const char *csName)
Definition XrdCksData.hh:81
bool Set(ChecksumType ctype, const std::array< unsigned char, g_max_checksum_length > &value)
static const std::string GetVerbString(HttpVerb)
virtual HttpVerb GetVerb() const =0
static void CleanupDnsCache()
std::tuple< uint64_t, std::chrono::steady_clock::duration, std::chrono::steady_clock::duration, std::chrono::steady_clock::duration > StatisticsReset()
XrdClHttp::HttpVerb HttpVerb
std::tuple< std::string, std::string > ClientX509CertKeyFile() const
CurlWorker(std::shared_ptr< HandlerQueue > queue, VerbsCache &cache, XrdCl::Log *logger)
static void RunStatic(CurlWorker *myself)
void Start(std::unique_ptr< XrdClHttp::CurlWorker > self, std::thread tid)
static std::string GetMonitoringJson()
std::shared_ptr< CurlOperation > Consume(std::chrono::steady_clock::duration)
HandlerQueue(unsigned max_pending_ops)
void Produce(std::shared_ptr< CurlOperation > handler)
static std::string GetMonitoringJson()
std::shared_ptr< CurlOperation > TryConsume()
void SetMultipartSeparator(const std::string_view &sep)
static void ParseDigest(const std::string &digest, XrdClHttp::ChecksumInfo &info)
static bool Canonicalize(std::string &headerName)
bool Parse(const std::string &headers)
static std::string ChecksumTypeToDigestName(XrdClHttp::ChecksumType type)
static bool Base64Decode(std::string_view input, std::array< unsigned char, g_max_checksum_length > &output)
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)
Definition XrdClEnv.cc:115
Handle diagnostics.
Definition XrdClLog.hh:101
@ DumpMsg
print details of the request and responses
Definition XrdClLog.hh:113
void Warning(uint64_t topic, const char *format,...)
Report a warning.
Definition XrdClLog.cc:248
void Debug(uint64_t topic, const char *format,...)
Print a debug message.
Definition XrdClLog.cc:282
URL representation.
Definition XrdClURL.hh:31
std::string GetURL() const
Get the URL.
Definition XrdClURL.hh:86
const ParamsMap & GetParams() const
Get the URL params.
Definition XrdClURL.hh:244
static void splitString(Container &result, const std::string &input, const std::string &delimiter)
Split a string.
Definition XrdClUtils.hh:56
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)
const uint64_t kLogXrdClHttp
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.