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