Stroika Library 3.0d22
 
Loading...
Searching...
No Matches
Connection_libcurl.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2025. All rights reserved
3 */
4#include "Stroika/Foundation/StroikaPreComp.h"
5
6#include <csignal>
7
8#if qStroika_HasComponent_libcurl
9#include <curl/curl.h>
10#endif
11
15#include "Stroika/Foundation/Execution/Throw.h"
16#include "Stroika/Foundation/IO/Network/HTTP/Headers.h"
17#include "Stroika/Foundation/IO/Network/HTTP/Methods.h"
19
20#include "Connection_libcurl.h"
21
22using namespace Stroika::Foundation;
24using namespace Stroika::Foundation::Execution;
25using namespace Stroika::Foundation::IO;
28using namespace Stroika::Foundation::Time;
29
32using Stroika::Foundation::IO::Network::Transfer::Connection::IRep;
33using Stroika::Foundation::IO::Network::Transfer::Connection::Options;
34
35// Comment this in to turn on aggressive noisy DbgTrace in this module
36//#define USE_NOISY_TRACE_IN_THIS_MODULE_ 1
37
38// Uncomment this line to enable libcurl to print diagnostics to stderr
39//#define USE_LIBCURL_VERBOSE_ 1
40
41CompileTimeFlagChecker_SOURCE (Stroika::Foundation::IO::Network::Transfer, qStroika_HasComponent_libcurl, qStroika_HasComponent_libcurl);
42
43#if qStroika_HasComponent_libcurl
44using namespace Stroika::Foundation::IO::Network::Transfer::LibCurl;
45
46namespace {
47 void AssureLibCurlInitialized_ ()
48 {
49 static once_flag sOnceFlag_;
50 call_once (sOnceFlag_, [] () {
51 atexit ([] () { ::curl_global_cleanup (); });
52 /*
53 * @todo review CURL_GLOBAL_SSL
54 *
55 * @see http://curl.haxx.se/libcurl/c/curl_global_init.html
56 * not 100% sure what flags to send
57 * CURL_GLOBAL_SSL needed for now, but could interfere with other openssl uses
58 */
59 Verify (::curl_global_init (CURL_GLOBAL_SSL | CURL_GLOBAL_ACK_EINTR) == 0);
60 });
61 }
62}
63
64/*
65 ********************************************************************************
66 *********************** Transfer::LibCurl::error_category **********************
67 ********************************************************************************
68 */
69const std::error_category& Transfer::LibCurl::error_category () noexcept
70{
71 class LibCurl_error_category_ : public error_category {
72 public:
73 virtual const char* name () const noexcept override
74 {
75 return "LibCurl error";
76 }
77 virtual error_condition default_error_condition ([[maybe_unused]] int ev) const noexcept override
78 {
79 switch (ev) {
80 case CURLE_OUT_OF_MEMORY:
81 return errc::not_enough_memory;
82 case CURLE_OPERATION_TIMEDOUT:
83 return errc::timed_out;
84 case CURLE_LOGIN_DENIED:
85 return errc::permission_denied;
86 case CURLE_SEND_ERROR:
87 return errc::io_error;
88 case CURLE_RECV_ERROR:
89 return errc::io_error;
90 }
91 // @todo - not sure how todo this - except by defining new conditions
92 //switch (ev) {
93 //}
94 return error_condition{errc::bad_message}; // no idea what to return here
95 }
96 virtual string message (int ccode) const override
97 {
98 return ::curl_easy_strerror (static_cast<CURLcode> (ccode));
99 }
100 };
101 return Common::Immortalize<LibCurl_error_category_> ();
102}
103
104namespace {
105 class Rep_ : public Transfer::LibCurl::Connection::IRep {
106 private:
107 Options fOptions_;
108
109 public:
110 Rep_ (const Options& options)
111 : fOptions_{options}
112 {
113 AssureLibCurlInitialized_ ();
114 }
115 Rep_ (const Rep_&) = delete;
116 virtual ~Rep_ ()
117 {
118 if (fCurlHandle_ != nullptr) {
119 ::curl_easy_cleanup (fCurlHandle_);
120 }
121 if (fSavedHeaders_ != nullptr) {
122 ::curl_slist_free_all (fSavedHeaders_);
123 fSavedHeaders_ = nullptr;
124 }
125 }
126
127 public:
128 nonvirtual Rep_& operator= (const Rep_&) = delete;
129
130 public:
131 virtual Options GetOptions () const override
132 {
133 return fOptions_;
134 }
135 virtual Time::DurationSeconds GetTimeout () const override
136 {
138 return 0s;
139 }
140 virtual void SetTimeout (Time::DurationSeconds timeout) override
141 {
142 MakeHandleIfNeeded_ ();
143 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_TIMEOUT_MS, static_cast<int> (timeout.count () * 1000)));
144 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_CONNECTTIMEOUT_MS, static_cast<int> (timeout.count () * 1000)));
145 }
146 virtual URI GetSchemeAndAuthority () const override
147 {
148 return fURL_.GetSchemeAndAuthority ();
149 }
150 virtual void SetSchemeAndAuthority (const URI& schemeAndAuthority) override
151 {
152#if USE_NOISY_TRACE_IN_THIS_MODULE_
153 DbgTrace ("Connection_LibCurl::Rep_::SetSchemeAndAuthority ('{}')"_f, schemeAndAuthority);
154#endif
155 fURL_.SetScheme (schemeAndAuthority.GetScheme ());
156 fURL_.SetAuthority (schemeAndAuthority.GetAuthority ());
157 if (fCurlHandle_ != nullptr) {
158 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_URL, fURL_.As<string> ().c_str ()));
159 }
160 }
161 virtual void Close () override
162 {
163 if (fCurlHandle_ != nullptr) {
164 ::curl_easy_cleanup (fCurlHandle_);
165 fCurlHandle_ = nullptr;
166 }
167 }
168 virtual Response Send (const Request& request) override
169 {
170#if USE_NOISY_TRACE_IN_THIS_MODULE_
171 Debug::TraceContextBumper ctx{L"Connection_LibCurl::Rep_::Send", L"method='%s'", request.fMethod.c_str ()};
172#endif
173 Request useRequest = request;
174
175 SetAuthorityRelativeURL_ (useRequest.fAuthorityRelativeURL);
176
178 if (fOptions_.fCache != nullptr) {
179 if (auto r = fOptions_.fCache->OnBeforeFetch (&cacheContext, fURL_.GetSchemeAndAuthority (), &useRequest)) {
180 // shortcut - we already have a cached answer
181 return *r;
182 }
183 }
184
185 MakeHandleIfNeeded_ ();
186 fUploadData_ = useRequest.fData.As<vector<byte>> ();
187 fUploadDataCursor_ = 0;
188 fResponseData_.clear ();
189 fResponseHeaders_.clear ();
190
191 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_USERAGENT, fOptions_.fUserAgent.AsUTF8 ().c_str ()));
192
193 if (fOptions_.fSupportSessionCookies) {
194 //@todo horrible kludge, but I couldnt find a better way. Will need to use soemthing else for windows, probably!
195 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_COOKIEFILE, "/dev/null"));
196 }
197
198 Mapping<String, String> overrideHeaders = useRequest.fOverrideHeaders;
199 if (fOptions_.fAssumeLowestCommonDenominatorHTTPServer) {
200 static const Mapping<String, String> kSilenceTheseHeaders_{
201 {pair<String, String>{"Expect"sv, {}}, pair<String, String>{"Transfer-Encoding"sv, {}}}};
202 overrideHeaders = kSilenceTheseHeaders_ + overrideHeaders;
203 }
204 if (fOptions_.fAuthentication and
205 fOptions_.fAuthentication->GetOptions () == Connection::Options::Authentication::Options::eProactivelySendAuthentication) {
206 overrideHeaders.Add (String::FromStringConstant (HeaderName::kAuthorization), fOptions_.fAuthentication->GetAuthToken ());
207 }
208 {
209 constexpr bool kDefault_FailConnectionIfSSLCertificateInvalid{false};
210 // ignore error if compiled without ssl
211 (void)::curl_easy_setopt (
212 fCurlHandle_, CURLOPT_SSL_VERIFYPEER,
213 fOptions_.fFailConnectionIfSSLCertificateInvalid.value_or (kDefault_FailConnectionIfSSLCertificateInvalid) ? 1L : 0L);
214 (void)::curl_easy_setopt (
215 fCurlHandle_, CURLOPT_SSL_VERIFYHOST,
216 fOptions_.fFailConnectionIfSSLCertificateInvalid.value_or (kDefault_FailConnectionIfSSLCertificateInvalid) ? 2L : 0L);
217 }
218
219 if (useRequest.fMethod == HTTP::Methods::kGet) {
220 if (fDidCustomMethod_) {
221 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_CUSTOMREQUEST, nullptr));
222 fDidCustomMethod_ = false;
223 }
224 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_HTTPGET, 1));
225 }
226 else if (useRequest.fMethod == HTTP::Methods::kPost) {
227 if (fDidCustomMethod_) {
228 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_CUSTOMREQUEST, nullptr));
229 fDidCustomMethod_ = false;
230 }
231 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_POST, 1));
232 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_POSTFIELDSIZE, fUploadData_.size ()));
233 }
234 else if (useRequest.fMethod == HTTP::Methods::kPut) {
235 if (fDidCustomMethod_) {
236 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_CUSTOMREQUEST, nullptr));
237 fDidCustomMethod_ = false;
238 }
239 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_UPLOAD, fUploadData_.empty () ? 0 : 1));
240 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_INFILESIZE, fUploadData_.size ()));
241 }
242 else {
243 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_HTTPGET, 0));
244 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_POST, 0));
245 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_CUSTOMREQUEST, useRequest.fMethod.AsUTF8 ().c_str ()));
246 }
247
248 if (fOptions_.fAuthentication and
249 fOptions_.fAuthentication->GetOptions () == Connection::Options::Authentication::Options::eRespondToWWWAuthenticate) {
250 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_HTTPAUTH, (long)CURLAUTH_ANY)); // tell libcurl we can use "any" auth, which lets the lib pick one, but it also costs one extra round-trip and possibly sending of all the PUT data twice!
251 auto nameAndPassword = *fOptions_.fAuthentication->GetUsernameAndPassword (); // if eRespondToWWWAuthenticate we must have username/password (Options CTOR requirement)
252 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_USERNAME, nameAndPassword.first.AsUTF8 ().c_str ()));
253 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_PASSWORD, nameAndPassword.second.AsUTF8 ().c_str ()));
254 }
255
256 // grab initial headers and do POST/etc based on args in request...
257 curl_slist* tmpH = nullptr;
258 for (const auto& i : overrideHeaders) {
259 tmpH = ::curl_slist_append (tmpH, (i.fKey + ": "sv + i.fValue).AsUTF8<string> ().c_str ());
260 }
261 AssertNotNull (fCurlHandle_);
262 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_HTTPHEADER, tmpH));
263 if (fSavedHeaders_ != nullptr) {
264 ::curl_slist_free_all (fSavedHeaders_);
265 fSavedHeaders_ = nullptr;
266 }
267 fSavedHeaders_ = tmpH;
268
269 ThrowIfError (::curl_easy_perform (fCurlHandle_));
270
271 long resultCode = 0;
272 ThrowIfError (::curl_easy_getinfo (fCurlHandle_, CURLINFO_RESPONSE_CODE, &resultCode));
273 Response result{fResponseData_, static_cast<HTTP::Status> (resultCode), fResponseHeaders_};
274 if (fOptions_.fCache != nullptr) {
275 fOptions_.fCache->OnAfterFetch (cacheContext, &result);
276 }
277#if USE_NOISY_TRACE_IN_THIS_MODULE_
278 DbgTrace ("returning status = {}, dataLen = {}"_f, result.GetStatus (), result.GetData ().size ());
279#endif
280 return result;
281 }
282
283 private:
284 nonvirtual void SetAuthorityRelativeURL_ (const URI& url)
285 {
286#if USE_NOISY_TRACE_IN_THIS_MODULE_
287 DbgTrace ("Connection_LibCurl::Rep_::SetAuthorityRelativeURL_ ({})"_f, url);
288#endif
289 URI newURL = url; // almost but not quite the same as fURL_.Combine (url)
290 newURL.SetScheme (fURL_.GetScheme ());
291 newURL.SetAuthority (fURL_.GetAuthority ());
292 if (fCurlHandle_ != nullptr) {
293 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_URL, newURL.As<string> ().c_str ()));
294 }
295 fURL_ = newURL;
296 }
297
298 private:
299 nonvirtual void MakeHandleIfNeeded_ ()
300 {
301 if (fCurlHandle_ == nullptr) {
302 ThrowIfNull (fCurlHandle_ = ::curl_easy_init ());
303
304 /*
305 * Now setup COMMON options we ALWAYS set.
306 */
307 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_URL, fURL_.As<string> ().c_str ()));
308
309#if USE_LIBCURL_VERBOSE_
310 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_VERBOSE, 1));
311#endif
312
313 /*
314 * We may want this to be optional? Or use ares, for resolution (may want that anyhow).
315 * But for now, this is most likely to avoid untoward interactions with other libraries (guess)
316 * --LGP 2015-11-12
317 */
318 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_NOSIGNAL, 1));
319
320#if qStroika_Foundation_Debug_AssertionsChecked && qStroika_Foundation_Common_Platform_POSIX
321 {
322 struct sigaction oldact;
323 (void)::sigaction (SIGPIPE, NULL, &oldact);
324 if (oldact.sa_handler == SIG_DFL) {
325 DbgTrace ("Warning: no override of SIGPIPE. This is a risk factor with curl. Often just ignore"_f);
326 }
327 }
328#endif
329
330 if (fOptions_.fMaxAutomaticRedirects == 0) {
331 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_FOLLOWLOCATION, 0L));
332 }
333 else {
334 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_FOLLOWLOCATION, 1L));
335 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_MAXREDIRS, fOptions_.fMaxAutomaticRedirects));
336 // violate dejure standard but follow defacto standard and only senisble behavior
337 // https://curl.haxx.se/libcurl/c/CURLOPT_POSTREDIR.html
338 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_POSTREDIR, CURL_REDIR_POST_301)); // could have used CURL_REDIR_POST_ALL?
339 }
340
341 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_READFUNCTION, &s_RequestPayloadReadHandler_));
342 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_READDATA, this));
343 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_WRITEFUNCTION, &s_ResponseWriteHandler_));
344 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_WRITEDATA, this));
345 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_HEADERFUNCTION, &s_ResponseHeaderWriteHandler_));
346 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_WRITEHEADER, this));
347
348 if (fOptions_.fTCPKeepAlives) {
349 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_TCP_KEEPALIVE, fOptions_.fTCPKeepAlives->fEnabled));
350#if qStroika_Foundation_Common_Platform_Linux
351 if (fOptions_.fTCPKeepAlives->fTimeIdleBeforeSendingKeepalives) {
352 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_TCP_KEEPIDLE, *fOptions_.fTCPKeepAlives->fTimeIdleBeforeSendingKeepalives));
353 }
354 if (fOptions_.fTCPKeepAlives->fTimeBetweenIndividualKeepaliveProbes) {
355 ThrowIfError (::curl_easy_setopt (fCurlHandle_, CURLOPT_TCP_KEEPINTVL,
356 *fOptions_.fTCPKeepAlives->fTimeBetweenIndividualKeepaliveProbes));
357 }
358#endif
359 }
360 }
361 }
362
363 private:
364 static size_t s_RequestPayloadReadHandler_ (char* buffer, size_t size, size_t nitems, void* userP)
365 {
366 return reinterpret_cast<Rep_*> (userP)->RequestPayloadReadHandler_ (reinterpret_cast<byte*> (buffer), size * nitems);
367 }
368
369 nonvirtual size_t RequestPayloadReadHandler_ (byte* buffer, size_t bufSize)
370 {
371#if USE_NOISY_TRACE_IN_THIS_MODULE_
372 Debug::TraceContextBumper ctx{"Connection_LibCurl::Rep_::RequestPayloadReadHandler_"};
373#endif
374 size_t bytes2Copy = fUploadData_.size () - fUploadDataCursor_;
375 bytes2Copy = min (bytes2Copy, bufSize);
376 ::memcpy (buffer, Traversal::Iterator2Pointer (begin (fUploadData_)) + fUploadDataCursor_, bytes2Copy);
377 fUploadDataCursor_ += bytes2Copy;
378#if USE_NOISY_TRACE_IN_THIS_MODULE_
379 DbgTrace ("bufSize = {}, bytes2Copy={}"_f, bufSize, bytes2Copy);
380#endif
381 return bytes2Copy;
382 }
383
384 private:
385 static size_t s_ResponseWriteHandler_ (char* ptr, size_t size, size_t nmemb, void* userP)
386 {
387#if qStroika_FeatureSupported_Valgrind
388 VALGRIND_MAKE_MEM_DEFINED (ptr, size * nmemb); // Handle OpenSSL if not built with purify
389#endif
390 return reinterpret_cast<Rep_*> (userP)->ResponseWriteHandler_ (reinterpret_cast<const byte*> (ptr), size * nmemb);
391 }
392
393 nonvirtual size_t ResponseWriteHandler_ (const byte* ptr, size_t nBytes)
394 {
395 fResponseData_.insert (fResponseData_.end (), ptr, ptr + nBytes);
396 return nBytes;
397 }
398
399 private:
400 static size_t s_ResponseHeaderWriteHandler_ (char* ptr, size_t size, size_t nmemb, void* userP)
401 {
402#if qStroika_FeatureSupported_Valgrind
403 VALGRIND_MAKE_MEM_DEFINED (ptr, size * nmemb); // Handle OpenSSL if not built with purify
404#endif
405 return reinterpret_cast<Rep_*> (userP)->ResponseHeaderWriteHandler_ (reinterpret_cast<const byte*> (ptr), size * nmemb);
406 }
407
408 nonvirtual size_t ResponseHeaderWriteHandler_ (const byte* ptr, size_t nBytes)
409 {
410 String from = String::FromUTF8 (span{reinterpret_cast<const char*> (ptr), nBytes});
411 String to;
412 size_t i = from.find (':');
413 if (i != string::npos) {
414 to = from.SubString (i + 1);
415 from = from.SubString (0, i);
416 }
417 from = from.Trim ();
418 to = to.Trim ();
419 fResponseHeaders_.Add (from, to);
420 return nBytes;
421 }
422
423 private:
424 void* fCurlHandle_{nullptr};
425 URI fURL_;
426 bool fDidCustomMethod_{false};
427 vector<byte> fUploadData_;
428 size_t fUploadDataCursor_{};
429 vector<byte> fResponseData_;
430 Mapping<String, String> fResponseHeaders_;
431 curl_slist* fSavedHeaders_{nullptr};
432 };
433}
434
435/*
436 ********************************************************************************
437 ****************** Transfer::LibCurl::Connection::New **************************
438 ********************************************************************************
439 */
440Connection::Ptr Transfer::LibCurl::Connection::New (const Options& options)
441{
442 return Connection::Ptr{Memory::MakeSharedPtr<Rep_> (options)};
443}
444#endif
#define AssertNotNull(p)
Definition Assertions.h:333
#define AssertNotImplemented()
Definition Assertions.h:401
#define Verify(c)
Definition Assertions.h:419
#define CompileTimeFlagChecker_SOURCE(NS_PREFIX, NAME, VALUE)
chrono::duration< double > DurationSeconds
chrono::duration<double> - a time span (length of time) measured in seconds, but high precision.
Definition Realtime.h:57
#define DbgTrace
Definition Trace.h:309
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
nonvirtual String SubString(SZ from) const
nonvirtual String Trim(bool(*shouldBeTrimmed)(Character)=Character::IsWhitespace) const
Definition String.cpp:1593
nonvirtual size_t find(Character c, size_t startAt=0) const
Definition String.inl:1067
nonvirtual bool Add(ArgByValueType< key_type > key, ArgByValueType< mapped_type > newElt, AddReplaceMode addReplaceMode=AddReplaceMode::eAddReplaces)
Definition Mapping.inl:188
nonvirtual URI GetSchemeAndAuthority() const
Definition URI.inl:90
nonvirtual T As(optional< StringPCTEncodedFlag > pctEncoded={}) const
nonvirtual optional< SchemeType > GetScheme() const
Definition URI.inl:36
nonvirtual optional< Authority > GetAuthority() const
Definition URI.inl:55
nonvirtual void SetScheme(const optional< SchemeType > &scheme)
Definition URI.inl:41
void ThrowIfNull(const Private_::ConstVoidStar &p, const HRESULT &hr)
Template specialization for ThrowIfNull (), for thing being thrown HRESULT - really throw HRESULTErro...