1 /**
2 * Orthanc - A Lightweight, RESTful DICOM Store
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
4 * Department, University Hospital of Liege, Belgium
5 * Copyright (C) 2017-2021 Osimis S.A., Belgium
6 *
7 * This program is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU Lesser General Public License
9 * as published by the Free Software Foundation, either version 3 of
10 * the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Lesser General Public License for more details.
16 *
17 * You should have received a copy of the GNU Lesser General Public
18 * License along with this program. If not, see
19 * <http://www.gnu.org/licenses/>.
20 **/
21
22
23 #include "PrecompiledHeaders.h"
24 #include "HttpClient.h"
25
26 #include "Toolbox.h"
27 #include "OrthancException.h"
28 #include "Logging.h"
29 #include "ChunkedBuffer.h"
30 #include "SystemToolbox.h"
31
32 #include <string.h>
33 #include <curl/curl.h>
34 #include <boost/algorithm/string/predicate.hpp>
35 #include <boost/thread/mutex.hpp>
36
37 // Default timeout = 60 seconds (in Orthanc <= 1.5.6, it was 10 seconds)
38 static const unsigned int DEFAULT_HTTP_TIMEOUT = 60;
39
40
41 #if ORTHANC_ENABLE_PKCS11 == 1
42 # include "Pkcs11.h"
43 #endif
44
45
46 extern "C"
47 {
GetHttpStatus(CURLcode code,CURL * curl,long * status)48 static CURLcode GetHttpStatus(CURLcode code, CURL* curl, long* status)
49 {
50 if (code == CURLE_OK)
51 {
52 code = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, status);
53 return code;
54 }
55 else
56 {
57 LOG(ERROR) << "Error code " << static_cast<int>(code)
58 << " in libcurl: " << curl_easy_strerror(code);
59 *status = 0;
60 return code;
61 }
62 }
63 }
64
65 // This is a dummy wrapper function to suppress any OpenSSL-related
66 // problem in valgrind. Inlining is prevented.
67 #if defined(__GNUC__) || defined(__clang__)
68 __attribute__((noinline))
69 #endif
OrthancHttpClientPerformSSL(CURL * curl,long * status)70 static CURLcode OrthancHttpClientPerformSSL(CURL* curl, long* status)
71 {
72 #if ORTHANC_ENABLE_SSL == 1
73 return GetHttpStatus(curl_easy_perform(curl), curl, status);
74 #else
75 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
76 "Orthanc was compiled without SSL support, "
77 "cannot make HTTPS request");
78 #endif
79 }
80
81
82
83 namespace Orthanc
84 {
CheckCode(CURLcode code)85 static CURLcode CheckCode(CURLcode code)
86 {
87 if (code == CURLE_NOT_BUILT_IN)
88 {
89 throw OrthancException(ErrorCode_InternalError,
90 "Your libcurl does not contain a required feature, "
91 "please recompile Orthanc with -DUSE_SYSTEM_CURL=OFF");
92 }
93
94 if (code != CURLE_OK)
95 {
96 throw OrthancException(ErrorCode_NetworkProtocol,
97 "libCURL error: " + std::string(curl_easy_strerror(code)));
98 }
99
100 return code;
101 }
102
103
104 // RAII pattern around a "curl_slist"
105 class HttpClient::CurlHeaders : public boost::noncopyable
106 {
107 private:
108 struct curl_slist *content_;
109 bool isChunkedTransfer_;
110 bool hasExpect_;
111
112 public:
CurlHeaders()113 CurlHeaders() :
114 content_(NULL),
115 isChunkedTransfer_(false),
116 hasExpect_(false)
117 {
118 }
119
CurlHeaders(const HttpClient::HttpHeaders & headers)120 explicit CurlHeaders(const HttpClient::HttpHeaders& headers)
121 {
122 for (HttpClient::HttpHeaders::const_iterator
123 it = headers.begin(); it != headers.end(); ++it)
124 {
125 AddHeader(it->first, it->second);
126 }
127 }
128
~CurlHeaders()129 ~CurlHeaders()
130 {
131 Clear();
132 }
133
IsEmpty() const134 bool IsEmpty() const
135 {
136 return content_ == NULL;
137 }
138
Clear()139 void Clear()
140 {
141 if (content_ != NULL)
142 {
143 curl_slist_free_all(content_);
144 content_ = NULL;
145 }
146
147 isChunkedTransfer_ = false;
148 hasExpect_ = false;
149 }
150
AddHeader(const std::string & key,const std::string & value)151 void AddHeader(const std::string& key,
152 const std::string& value)
153 {
154 if (boost::iequals(key, "Expect"))
155 {
156 hasExpect_ = true;
157 }
158
159 if (boost::iequals(key, "Transfer-Encoding") &&
160 value == "chunked")
161 {
162 isChunkedTransfer_ = true;
163 }
164
165 std::string item = key + ": " + value;
166
167 struct curl_slist *tmp = curl_slist_append(content_, item.c_str());
168
169 if (tmp == NULL)
170 {
171 throw OrthancException(ErrorCode_NotEnoughMemory);
172 }
173 else
174 {
175 content_ = tmp;
176 }
177 }
178
Assign(CURL * curl) const179 void Assign(CURL* curl) const
180 {
181 CheckCode(curl_easy_setopt(curl, CURLOPT_HTTPHEADER, content_));
182 }
183
HasExpect() const184 bool HasExpect() const
185 {
186 return hasExpect_;
187 }
188
IsChunkedTransfer() const189 bool IsChunkedTransfer() const
190 {
191 return isChunkedTransfer_;
192 }
193 };
194
195
196 class HttpClient::CurlRequestBody : public boost::noncopyable
197 {
198 private:
199 HttpClient::IRequestBody* body_;
200 std::string pending_;
201 size_t pendingPos_;
202
CallbackInternal(char * curlBuffer,size_t curlBufferSize)203 size_t CallbackInternal(char* curlBuffer,
204 size_t curlBufferSize)
205 {
206 if (body_ == NULL)
207 {
208 throw OrthancException(ErrorCode_BadSequenceOfCalls);
209 }
210
211 if (curlBufferSize == 0)
212 {
213 throw OrthancException(ErrorCode_InternalError);
214 }
215
216 if (pendingPos_ + curlBufferSize <= pending_.size())
217 {
218 assert(sizeof(char) == 1);
219 memcpy(curlBuffer, &pending_[pendingPos_], curlBufferSize);
220 pendingPos_ += curlBufferSize;
221 return curlBufferSize;
222 }
223 else
224 {
225 ChunkedBuffer buffer;
226 buffer.SetPendingBufferSize(curlBufferSize);
227
228 if (pendingPos_ < pending_.size())
229 {
230 buffer.AddChunk(&pending_[pendingPos_], pending_.size() - pendingPos_);
231 }
232
233 // Read chunks from the body stream so as to fill the target buffer
234 std::string chunk;
235
236 while (buffer.GetNumBytes() < curlBufferSize &&
237 body_->ReadNextChunk(chunk))
238 {
239 buffer.AddChunk(chunk);
240 }
241
242 buffer.Flatten(pending_);
243 pendingPos_ = std::min(pending_.size(), curlBufferSize);
244
245 if (pendingPos_ != 0)
246 {
247 memcpy(curlBuffer, pending_.c_str(), pendingPos_);
248 }
249
250 return pendingPos_;
251 }
252 }
253
254 public:
CurlRequestBody()255 CurlRequestBody() :
256 body_(NULL),
257 pendingPos_(0)
258 {
259 }
260
SetBody(HttpClient::IRequestBody & body)261 void SetBody(HttpClient::IRequestBody& body)
262 {
263 body_ = &body;
264 pending_.clear();
265 pendingPos_ = 0;
266 }
267
Clear()268 void Clear()
269 {
270 body_ = NULL;
271 pending_.clear();
272 pendingPos_ = 0;
273 }
274
IsValid() const275 bool IsValid() const
276 {
277 return body_ != NULL;
278 }
279
Callback(char * buffer,size_t size,size_t nitems,void * userdata)280 static size_t Callback(char *buffer, size_t size, size_t nitems, void *userdata)
281 {
282 try
283 {
284 assert(userdata != NULL);
285 return reinterpret_cast<HttpClient::CurlRequestBody*>(userdata)->
286 CallbackInternal(buffer, size * nitems);
287 }
288 catch (OrthancException& e)
289 {
290 LOG(ERROR) << "Exception while streaming HTTP body: " << e.What();
291 return CURL_READFUNC_ABORT;
292 }
293 catch (...)
294 {
295 LOG(ERROR) << "Native exception while streaming HTTP body";
296 return CURL_READFUNC_ABORT;
297 }
298 }
299 };
300
301
302 class HttpClient::CurlAnswer : public boost::noncopyable
303 {
304 private:
305 HttpClient::IAnswer& answer_;
306 bool headersLowerCase_;
307
308 public:
CurlAnswer(HttpClient::IAnswer & answer,bool headersLowerCase)309 CurlAnswer(HttpClient::IAnswer& answer,
310 bool headersLowerCase) :
311 answer_(answer),
312 headersLowerCase_(headersLowerCase)
313 {
314 }
315
HeaderCallback(void * buffer,size_t size,size_t nmemb,void * userdata)316 static size_t HeaderCallback(void *buffer, size_t size, size_t nmemb, void *userdata)
317 {
318 try
319 {
320 assert(userdata != NULL);
321
322 size_t length = size * nmemb;
323 if (length == 0)
324 {
325 return 0;
326 }
327 else
328 {
329 std::string s(reinterpret_cast<const char*>(buffer), length);
330 std::size_t colon = s.find(':');
331 std::size_t eol = s.find("\r\n");
332 if (colon != std::string::npos &&
333 eol != std::string::npos)
334 {
335 CurlAnswer& that = *(static_cast<CurlAnswer*>(userdata));
336 std::string tmp(s.substr(0, colon));
337
338 if (that.headersLowerCase_)
339 {
340 Toolbox::ToLowerCase(tmp);
341 }
342
343 std::string key = Toolbox::StripSpaces(tmp);
344
345 if (!key.empty())
346 {
347 std::string value = Toolbox::StripSpaces(s.substr(colon + 1, eol));
348
349 that.answer_.AddHeader(key, value);
350 }
351 }
352
353 return length;
354 }
355 }
356 catch (OrthancException& e)
357 {
358 LOG(ERROR) << "Exception while streaming HTTP body: " << e.What();
359 return CURL_READFUNC_ABORT;
360 }
361 catch (...)
362 {
363 LOG(ERROR) << "Native exception while streaming HTTP body";
364 return CURL_READFUNC_ABORT;
365 }
366 }
367
BodyCallback(void * buffer,size_t size,size_t nmemb,void * userdata)368 static size_t BodyCallback(void *buffer, size_t size, size_t nmemb, void *userdata)
369 {
370 try
371 {
372 assert(userdata != NULL);
373
374 size_t length = size * nmemb;
375 if (length == 0)
376 {
377 return 0;
378 }
379 else
380 {
381 CurlAnswer& that = *(static_cast<CurlAnswer*>(userdata));
382 that.answer_.AddChunk(buffer, length);
383 return length;
384 }
385 }
386 catch (OrthancException& e)
387 {
388 LOG(ERROR) << "Exception while streaming HTTP body: " << e.What();
389 return CURL_READFUNC_ABORT;
390 }
391 catch (...)
392 {
393 LOG(ERROR) << "Native exception while streaming HTTP body";
394 return CURL_READFUNC_ABORT;
395 }
396 }
397 };
398
399
400 class HttpClient::DefaultAnswer : public HttpClient::IAnswer
401 {
402 private:
403 ChunkedBuffer answer_;
404 HttpHeaders* headers_;
405
406 public:
DefaultAnswer()407 DefaultAnswer() : headers_(NULL)
408 {
409 }
410
SetHeaders(HttpHeaders & headers)411 void SetHeaders(HttpHeaders& headers)
412 {
413 headers_ = &headers;
414 headers_->clear();
415 }
416
FlattenBody(std::string & target)417 void FlattenBody(std::string& target)
418 {
419 answer_.Flatten(target);
420 }
421
AddHeader(const std::string & key,const std::string & value)422 virtual void AddHeader(const std::string& key,
423 const std::string& value) ORTHANC_OVERRIDE
424 {
425 if (headers_ != NULL)
426 {
427 (*headers_) [key] = value;
428 }
429 }
430
AddChunk(const void * data,size_t size)431 virtual void AddChunk(const void* data,
432 size_t size) ORTHANC_OVERRIDE
433 {
434 answer_.AddChunk(data, size);
435 }
436 };
437
438
439 class HttpClient::GlobalParameters
440 {
441 private:
442 boost::mutex mutex_;
443 bool httpsVerifyPeers_;
444 std::string httpsCACertificates_;
445 std::string proxy_;
446 long timeout_;
447 bool verbose_;
448
GlobalParameters()449 GlobalParameters() :
450 httpsVerifyPeers_(true),
451 timeout_(0),
452 verbose_(false)
453 {
454 }
455
456 public:
457 // Singleton pattern
GetInstance()458 static GlobalParameters& GetInstance()
459 {
460 static GlobalParameters parameters;
461 return parameters;
462 }
463
ConfigureSsl(bool httpsVerifyPeers,const std::string & httpsCACertificates)464 void ConfigureSsl(bool httpsVerifyPeers,
465 const std::string& httpsCACertificates)
466 {
467 boost::mutex::scoped_lock lock(mutex_);
468 httpsVerifyPeers_ = httpsVerifyPeers;
469 httpsCACertificates_ = httpsCACertificates;
470 }
471
GetSslConfiguration(bool & httpsVerifyPeers,std::string & httpsCACertificates)472 void GetSslConfiguration(bool& httpsVerifyPeers,
473 std::string& httpsCACertificates)
474 {
475 boost::mutex::scoped_lock lock(mutex_);
476 httpsVerifyPeers = httpsVerifyPeers_;
477 httpsCACertificates = httpsCACertificates_;
478 }
479
SetDefaultProxy(const std::string & proxy)480 void SetDefaultProxy(const std::string& proxy)
481 {
482 CLOG(INFO, HTTP) << "Setting the default proxy for HTTP client connections: " << proxy;
483
484 {
485 boost::mutex::scoped_lock lock(mutex_);
486 proxy_ = proxy;
487 }
488 }
489
GetDefaultProxy(std::string & target)490 void GetDefaultProxy(std::string& target)
491 {
492 boost::mutex::scoped_lock lock(mutex_);
493 target = proxy_;
494 }
495
SetDefaultTimeout(long seconds)496 void SetDefaultTimeout(long seconds)
497 {
498 CLOG(INFO, HTTP) << "Setting the default timeout for HTTP client connections: " << seconds << " seconds";
499
500 {
501 boost::mutex::scoped_lock lock(mutex_);
502 timeout_ = seconds;
503 }
504 }
505
GetDefaultTimeout()506 long GetDefaultTimeout()
507 {
508 boost::mutex::scoped_lock lock(mutex_);
509 return timeout_;
510 }
511
512 #if ORTHANC_ENABLE_PKCS11 == 1
IsPkcs11Initialized()513 bool IsPkcs11Initialized()
514 {
515 boost::mutex::scoped_lock lock(mutex_);
516 return Pkcs11::IsInitialized();
517 }
518
InitializePkcs11(const std::string & module,const std::string & pin,bool verbose)519 void InitializePkcs11(const std::string& module,
520 const std::string& pin,
521 bool verbose)
522 {
523 boost::mutex::scoped_lock lock(mutex_);
524 Pkcs11::Initialize(module, pin, verbose);
525 }
526 #endif
527
IsDefaultVerbose() const528 bool IsDefaultVerbose() const
529 {
530 return verbose_;
531 }
532
SetDefaultVerbose(bool verbose)533 void SetDefaultVerbose(bool verbose)
534 {
535 verbose_ = verbose;
536 }
537 };
538
539
540 struct HttpClient::PImpl
541 {
542 CURL* curl_;
543 CurlHeaders defaultPostHeaders_;
544 CurlHeaders defaultChunkedHeaders_;
545 CurlHeaders userHeaders_;
546 CurlRequestBody requestBody_;
547 };
548
549
ThrowException(HttpStatus status)550 void HttpClient::ThrowException(HttpStatus status)
551 {
552 switch (status)
553 {
554 case HttpStatus_400_BadRequest:
555 throw OrthancException(ErrorCode_BadRequest);
556
557 case HttpStatus_401_Unauthorized:
558 case HttpStatus_403_Forbidden:
559 throw OrthancException(ErrorCode_Unauthorized);
560
561 case HttpStatus_404_NotFound:
562 throw OrthancException(ErrorCode_UnknownResource);
563
564 default:
565 throw OrthancException(ErrorCode_NetworkProtocol);
566 }
567 }
568
569
570 /*static int CurlDebugCallback(CURL *handle,
571 curl_infotype type,
572 char *data,
573 size_t size,
574 void *userptr)
575 {
576 switch (type)
577 {
578 case CURLINFO_TEXT:
579 case CURLINFO_HEADER_IN:
580 case CURLINFO_HEADER_OUT:
581 case CURLINFO_SSL_DATA_IN:
582 case CURLINFO_SSL_DATA_OUT:
583 case CURLINFO_END:
584 case CURLINFO_DATA_IN:
585 case CURLINFO_DATA_OUT:
586 {
587 std::string s(data, size);
588 CLOG(INFO, INFO) << "libcurl: " << s;
589 break;
590 }
591
592 default:
593 break;
594 }
595
596 return 0;
597 }*/
598
599
Setup()600 void HttpClient::Setup()
601 {
602 pimpl_->defaultPostHeaders_.AddHeader("Expect", "");
603 pimpl_->defaultChunkedHeaders_.AddHeader("Expect", "");
604 pimpl_->defaultChunkedHeaders_.AddHeader("Transfer-Encoding", "chunked");
605
606 pimpl_->curl_ = curl_easy_init();
607
608 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERFUNCTION, &CurlAnswer::HeaderCallback));
609 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEFUNCTION, &CurlAnswer::BodyCallback));
610 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADER, 0));
611 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_FOLLOWLOCATION, 1));
612
613 // This fixes the "longjmp causes uninitialized stack frame" crash
614 // that happens on modern Linux versions.
615 // http://stackoverflow.com/questions/9191668/error-longjmp-causes-uninitialized-stack-frame
616 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOSIGNAL, 1));
617
618 url_ = "";
619 method_ = HttpMethod_Get;
620 lastStatus_ = HttpStatus_None;
621 SetVerbose(GlobalParameters::GetInstance().IsDefaultVerbose());
622 timeout_ = GlobalParameters::GetInstance().GetDefaultTimeout();
623 GlobalParameters::GetInstance().GetDefaultProxy(proxy_);
624 GlobalParameters::GetInstance().GetSslConfiguration(verifyPeers_, caCertificates_);
625
626 hasExternalBody_ = false;
627 externalBodyData_ = NULL;
628 externalBodySize_ = 0;
629 }
630
631
HttpClient()632 HttpClient::HttpClient() :
633 pimpl_(new PImpl),
634 verifyPeers_(true),
635 pkcs11Enabled_(false),
636 headersToLowerCase_(true),
637 redirectionFollowed_(true)
638 {
639 Setup();
640 }
641
642
HttpClient(const WebServiceParameters & service,const std::string & uri)643 HttpClient::HttpClient(const WebServiceParameters& service,
644 const std::string& uri) :
645 pimpl_(new PImpl),
646 verifyPeers_(true),
647 headersToLowerCase_(true),
648 redirectionFollowed_(true)
649 {
650 Setup();
651
652 if (service.GetUsername().size() != 0 &&
653 service.GetPassword().size() != 0)
654 {
655 SetCredentials(service.GetUsername().c_str(),
656 service.GetPassword().c_str());
657 }
658
659 if (!service.GetCertificateFile().empty())
660 {
661 SetClientCertificate(service.GetCertificateFile(),
662 service.GetCertificateKeyFile(),
663 service.GetCertificateKeyPassword());
664 }
665
666 SetPkcs11Enabled(service.IsPkcs11Enabled());
667
668 SetUrl(service.GetUrl() + uri);
669
670 for (WebServiceParameters::Dictionary::const_iterator
671 it = service.GetHttpHeaders().begin();
672 it != service.GetHttpHeaders().end(); ++it)
673 {
674 AddHeader(it->first, it->second);
675 }
676
677 if (service.HasTimeout())
678 {
679 SetTimeout(service.GetTimeout());
680 }
681 }
682
683
~HttpClient()684 HttpClient::~HttpClient()
685 {
686 curl_easy_cleanup(pimpl_->curl_);
687 }
688
SetUrl(const char * url)689 void HttpClient::SetUrl(const char *url)
690 {
691 url_ = std::string(url);
692 }
693
SetUrl(const std::string & url)694 void HttpClient::SetUrl(const std::string &url)
695 {
696 url_ = url;
697 }
698
GetUrl() const699 const std::string &HttpClient::GetUrl() const
700 {
701 return url_;
702 }
703
SetMethod(HttpMethod method)704 void HttpClient::SetMethod(HttpMethod method)
705 {
706 method_ = method;
707 }
708
GetMethod() const709 HttpMethod HttpClient::GetMethod() const
710 {
711 return method_;
712 }
713
SetTimeout(long seconds)714 void HttpClient::SetTimeout(long seconds)
715 {
716 timeout_ = seconds;
717 }
718
GetTimeout() const719 long HttpClient::GetTimeout() const
720 {
721 return timeout_;
722 }
723
724
AssignBody(const std::string & data)725 void HttpClient::AssignBody(const std::string& data)
726 {
727 body_ = data;
728 pimpl_->requestBody_.Clear();
729 hasExternalBody_ = false;
730 }
731
732
AssignBody(const void * data,size_t size)733 void HttpClient::AssignBody(const void* data,
734 size_t size)
735 {
736 if (size != 0 &&
737 data == NULL)
738 {
739 throw OrthancException(ErrorCode_NullPointer);
740 }
741 else
742 {
743 body_.assign(reinterpret_cast<const char*>(data), size);
744 pimpl_->requestBody_.Clear();
745 hasExternalBody_ = false;
746 }
747 }
748
749
SetBody(IRequestBody & body)750 void HttpClient::SetBody(IRequestBody& body)
751 {
752 body_.clear();
753 pimpl_->requestBody_.SetBody(body);
754 hasExternalBody_ = false;
755 }
756
757
SetExternalBody(const void * data,size_t size)758 void HttpClient::SetExternalBody(const void* data,
759 size_t size)
760 {
761 if (size != 0 &&
762 data == NULL)
763 {
764 throw OrthancException(ErrorCode_NullPointer);
765 }
766 else
767 {
768 body_.clear();
769 pimpl_->requestBody_.Clear();
770 hasExternalBody_ = true;
771 externalBodyData_ = data;
772 externalBodySize_ = size;
773 }
774 }
775
776
SetExternalBody(const std::string & data)777 void HttpClient::SetExternalBody(const std::string& data)
778 {
779 SetExternalBody(data.empty() ? NULL : data.c_str(), data.size());
780 }
781
782
ClearBody()783 void HttpClient::ClearBody()
784 {
785 body_.clear();
786 pimpl_->requestBody_.Clear();
787 hasExternalBody_ = false;
788 }
789
790
SetVerbose(bool isVerbose)791 void HttpClient::SetVerbose(bool isVerbose)
792 {
793 isVerbose_ = isVerbose;
794
795 if (isVerbose_)
796 {
797 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_VERBOSE, 1));
798 //CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_DEBUGFUNCTION, &CurlDebugCallback));
799 }
800 else
801 {
802 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_VERBOSE, 0));
803 }
804 }
805
IsVerbose() const806 bool HttpClient::IsVerbose() const
807 {
808 return isVerbose_;
809 }
810
811
AddHeader(const std::string & key,const std::string & value)812 void HttpClient::AddHeader(const std::string& key,
813 const std::string& value)
814 {
815 if (key.empty())
816 {
817 throw OrthancException(ErrorCode_ParameterOutOfRange);
818 }
819 else
820 {
821 pimpl_->userHeaders_.AddHeader(key, value);
822 }
823 }
824
825
ClearHeaders()826 void HttpClient::ClearHeaders()
827 {
828 pimpl_->userHeaders_.Clear();
829 }
830
831
ApplyInternal(CurlAnswer & answer)832 bool HttpClient::ApplyInternal(CurlAnswer& answer)
833 {
834 CLOG(INFO, HTTP) << "New HTTP request to: " << url_ << " (timeout: "
835 << boost::lexical_cast<std::string>(timeout_ <= 0 ? DEFAULT_HTTP_TIMEOUT : timeout_) << "s)";
836
837 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_URL, url_.c_str()));
838 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERDATA, &answer));
839
840 #if ORTHANC_ENABLE_SSL == 1
841 // Setup HTTPS-related options
842
843 if (verifyPeers_)
844 {
845 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CAINFO, caCertificates_.c_str()));
846 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYHOST, 2)); // libcurl default is strict verifyhost
847 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYPEER, 1));
848 }
849 else
850 {
851 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYHOST, 0));
852 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYPEER, 0));
853 }
854 #endif
855
856 // Setup the HTTPS client certificate
857 if (!clientCertificateFile_.empty() &&
858 pkcs11Enabled_)
859 {
860 throw OrthancException(ErrorCode_ParameterOutOfRange,
861 "Cannot enable both client certificates and PKCS#11 authentication");
862 }
863
864 if (pkcs11Enabled_)
865 {
866 #if ORTHANC_ENABLE_PKCS11 == 1
867 if (GlobalParameters::GetInstance().IsPkcs11Initialized())
868 {
869 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLENGINE, Pkcs11::GetEngineIdentifier()));
870 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLKEYTYPE, "ENG"));
871 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLCERTTYPE, "ENG"));
872 }
873 else
874 {
875 throw OrthancException(ErrorCode_BadSequenceOfCalls,
876 "Cannot use PKCS#11 for a HTTPS request, "
877 "because it has not been initialized");
878 }
879 #else
880 throw OrthancException(ErrorCode_InternalError,
881 "This version of Orthanc is compiled without support for PKCS#11");
882 #endif
883 }
884 else if (!clientCertificateFile_.empty())
885 {
886 #if ORTHANC_ENABLE_SSL == 1
887 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLCERTTYPE, "PEM"));
888 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLCERT, clientCertificateFile_.c_str()));
889 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_KEYPASSWD, clientCertificateKeyPassword_.c_str()));
890
891 // NB: If no "clientKeyFile_" is provided, the key must be
892 // prepended to the certificate file
893 if (!clientCertificateKeyFile_.empty())
894 {
895 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLKEYTYPE, "PEM"));
896 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLKEY, clientCertificateKeyFile_.c_str()));
897 }
898 #else
899 throw OrthancException(ErrorCode_InternalError,
900 "This version of Orthanc is compiled without OpenSSL support, "
901 "cannot use HTTPS client authentication");
902 #endif
903 }
904
905 // Reset the parameters from previous calls to Apply()
906 pimpl_->userHeaders_.Assign(pimpl_->curl_);
907 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPGET, 0L));
908 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 0L));
909 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOBODY, 0L));
910 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, NULL));
911 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, NULL));
912 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, 0L));
913 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PROXY, NULL));
914
915 if (redirectionFollowed_)
916 {
917 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_FOLLOWLOCATION, 1L));
918 }
919 else
920 {
921 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_FOLLOWLOCATION, 0L));
922 }
923
924 // Set timeouts
925 if (timeout_ <= 0)
926 {
927 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_TIMEOUT, DEFAULT_HTTP_TIMEOUT));
928 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CONNECTTIMEOUT, DEFAULT_HTTP_TIMEOUT));
929 }
930 else
931 {
932 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_TIMEOUT, timeout_));
933 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CONNECTTIMEOUT, timeout_));
934 }
935
936 if (credentials_.size() != 0)
937 {
938 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_USERPWD, credentials_.c_str()));
939 }
940
941 if (proxy_.size() != 0)
942 {
943 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PROXY, proxy_.c_str()));
944 }
945
946 switch (method_)
947 {
948 case HttpMethod_Get:
949 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPGET, 1L));
950 break;
951
952 case HttpMethod_Post:
953 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 1L));
954
955 break;
956
957 case HttpMethod_Delete:
958 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOBODY, 1L));
959 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, "DELETE"));
960 break;
961
962 case HttpMethod_Put:
963 // http://stackoverflow.com/a/7570281/881731: Don't use
964 // CURLOPT_PUT if there is a body
965
966 // CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PUT, 1L));
967
968 curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, "PUT"); /* !!! */
969 break;
970
971 default:
972 throw OrthancException(ErrorCode_InternalError);
973 }
974
975 if (method_ == HttpMethod_Post ||
976 method_ == HttpMethod_Put)
977 {
978 if (!pimpl_->userHeaders_.IsEmpty() &&
979 !pimpl_->userHeaders_.HasExpect())
980 {
981 CLOG(INFO, HTTP) << "For performance, the HTTP header \"Expect\" should be set to empty string in POST/PUT requests";
982 }
983
984 if (pimpl_->requestBody_.IsValid())
985 {
986 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_READFUNCTION, CurlRequestBody::Callback));
987 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_READDATA, &pimpl_->requestBody_));
988 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 1L));
989 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, -1L));
990
991 if (pimpl_->userHeaders_.IsEmpty())
992 {
993 pimpl_->defaultChunkedHeaders_.Assign(pimpl_->curl_);
994 }
995 else if (!pimpl_->userHeaders_.IsChunkedTransfer())
996 {
997 LOG(WARNING) << "The HTTP header \"Transfer-Encoding\" must be set to \"chunked\" "
998 << "if streaming a chunked body in POST/PUT requests";
999 }
1000 }
1001 else
1002 {
1003 // Disable possible previous stream transfers
1004 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_READFUNCTION, NULL));
1005 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_UPLOAD, 0));
1006
1007 if (pimpl_->userHeaders_.IsChunkedTransfer())
1008 {
1009 LOG(WARNING) << "The HTTP header \"Transfer-Encoding\" must only be set "
1010 << "if streaming a chunked body in POST/PUT requests";
1011 }
1012
1013 if (pimpl_->userHeaders_.IsEmpty())
1014 {
1015 pimpl_->defaultPostHeaders_.Assign(pimpl_->curl_);
1016 }
1017
1018 if (hasExternalBody_)
1019 {
1020 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, externalBodyData_));
1021 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, externalBodySize_));
1022 }
1023 else if (body_.size() > 0)
1024 {
1025 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, body_.c_str()));
1026 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, body_.size()));
1027 }
1028 else
1029 {
1030 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, NULL));
1031 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, 0));
1032 }
1033 }
1034 }
1035
1036
1037 // Do the actual request
1038 CURLcode code;
1039 long status = 0;
1040
1041 CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEDATA, &answer));
1042
1043 const boost::posix_time::ptime start = boost::posix_time::microsec_clock::universal_time();
1044
1045 if (boost::starts_with(url_, "https://"))
1046 {
1047 code = OrthancHttpClientPerformSSL(pimpl_->curl_, &status);
1048 }
1049 else
1050 {
1051 code = GetHttpStatus(curl_easy_perform(pimpl_->curl_), pimpl_->curl_, &status);
1052 }
1053
1054 const boost::posix_time::ptime end = boost::posix_time::microsec_clock::universal_time();
1055
1056 CLOG(INFO, HTTP) << "HTTP status code " << status << " in "
1057 << ((end - start).total_milliseconds()) << " ms after "
1058 << EnumerationToString(method_) << " request on: " << url_;
1059
1060 if (isVerbose_)
1061 {
1062 CLOG(INFO, HTTP) << "cURL status code: " << code;
1063 }
1064
1065 CheckCode(code);
1066
1067 if (status == 0)
1068 {
1069 // This corresponds to a call to an inexistent host
1070 lastStatus_ = HttpStatus_500_InternalServerError;
1071 }
1072 else
1073 {
1074 lastStatus_ = static_cast<HttpStatus>(status);
1075 }
1076
1077 if (status >= 200 && status < 300)
1078 {
1079 return true; // Success
1080 }
1081 else
1082 {
1083 LOG(ERROR) << "Error in HTTP request, received HTTP status " << status
1084 << " (" << EnumerationToString(lastStatus_) << ")";
1085 return false;
1086 }
1087 }
1088
1089
ApplyInternal(std::string & answerBody,HttpHeaders * answerHeaders)1090 bool HttpClient::ApplyInternal(std::string& answerBody,
1091 HttpHeaders* answerHeaders)
1092 {
1093 answerBody.clear();
1094
1095 DefaultAnswer answer;
1096
1097 if (answerHeaders != NULL)
1098 {
1099 answer.SetHeaders(*answerHeaders);
1100 }
1101
1102 CurlAnswer wrapper(answer, headersToLowerCase_);
1103
1104 if (ApplyInternal(wrapper))
1105 {
1106 answer.FlattenBody(answerBody);
1107 return true;
1108 }
1109 else
1110 {
1111 return false;
1112 }
1113 }
1114
1115
ApplyInternal(Json::Value & answerBody,HttpClient::HttpHeaders * answerHeaders)1116 bool HttpClient::ApplyInternal(Json::Value& answerBody,
1117 HttpClient::HttpHeaders* answerHeaders)
1118 {
1119 std::string s;
1120 if (ApplyInternal(s, answerHeaders))
1121 {
1122 return Toolbox::ReadJson(answerBody, s);
1123 }
1124 else
1125 {
1126 return false;
1127 }
1128 }
1129
1130
SetCredentials(const char * username,const char * password)1131 void HttpClient::SetCredentials(const char* username,
1132 const char* password)
1133 {
1134 credentials_ = std::string(username) + ":" + std::string(password);
1135 }
1136
SetProxy(const std::string & proxy)1137 void HttpClient::SetProxy(const std::string &proxy)
1138 {
1139 proxy_ = proxy;
1140 }
1141
SetHttpsVerifyPeers(bool verify)1142 void HttpClient::SetHttpsVerifyPeers(bool verify)
1143 {
1144 verifyPeers_ = verify;
1145 }
1146
IsHttpsVerifyPeers() const1147 bool HttpClient::IsHttpsVerifyPeers() const
1148 {
1149 return verifyPeers_;
1150 }
1151
SetHttpsCACertificates(const std::string & certificates)1152 void HttpClient::SetHttpsCACertificates(const std::string &certificates)
1153 {
1154 caCertificates_ = certificates;
1155 }
1156
GetHttpsCACertificates() const1157 const std::string &HttpClient::GetHttpsCACertificates() const
1158 {
1159 return caCertificates_;
1160 }
1161
1162
ConfigureSsl(bool httpsVerifyPeers,const std::string & httpsVerifyCertificates)1163 void HttpClient::ConfigureSsl(bool httpsVerifyPeers,
1164 const std::string& httpsVerifyCertificates)
1165 {
1166 #if ORTHANC_ENABLE_SSL == 1
1167 if (httpsVerifyPeers)
1168 {
1169 if (httpsVerifyCertificates.empty())
1170 {
1171 LOG(WARNING) << "No certificates are provided to validate peers, "
1172 << "set \"HttpsCACertificates\" if you need to do HTTPS requests";
1173 }
1174 else
1175 {
1176 LOG(WARNING) << "HTTPS will use the CA certificates from this file: " << httpsVerifyCertificates;
1177 }
1178 }
1179 else
1180 {
1181 LOG(WARNING) << "The verification of the peers in HTTPS requests is disabled";
1182 }
1183 #endif
1184
1185 GlobalParameters::GetInstance().ConfigureSsl(httpsVerifyPeers, httpsVerifyCertificates);
1186 }
1187
1188
GlobalInitialize()1189 void HttpClient::GlobalInitialize()
1190 {
1191 #if ORTHANC_ENABLE_SSL == 1
1192 CheckCode(curl_global_init(CURL_GLOBAL_ALL));
1193 #else
1194 CheckCode(curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_SSL));
1195 #endif
1196 }
1197
1198
GlobalFinalize()1199 void HttpClient::GlobalFinalize()
1200 {
1201 curl_global_cleanup();
1202
1203 #if ORTHANC_ENABLE_PKCS11 == 1
1204 Pkcs11::Finalize();
1205 #endif
1206 }
1207
1208
SetDefaultVerbose(bool verbose)1209 void HttpClient::SetDefaultVerbose(bool verbose)
1210 {
1211 GlobalParameters::GetInstance().SetDefaultVerbose(verbose);
1212 }
1213
1214
SetDefaultProxy(const std::string & proxy)1215 void HttpClient::SetDefaultProxy(const std::string& proxy)
1216 {
1217 GlobalParameters::GetInstance().SetDefaultProxy(proxy);
1218 }
1219
1220
SetDefaultTimeout(long timeout)1221 void HttpClient::SetDefaultTimeout(long timeout)
1222 {
1223 GlobalParameters::GetInstance().SetDefaultTimeout(timeout);
1224 }
1225
1226
Apply(IAnswer & answer)1227 bool HttpClient::Apply(IAnswer& answer)
1228 {
1229 CurlAnswer wrapper(answer, headersToLowerCase_);
1230 return ApplyInternal(wrapper);
1231 }
1232
Apply(std::string & answerBody)1233 bool HttpClient::Apply(std::string &answerBody)
1234 {
1235 return ApplyInternal(answerBody, NULL);
1236 }
1237
Apply(Json::Value & answerBody)1238 bool HttpClient::Apply(Json::Value &answerBody)
1239 {
1240 return ApplyInternal(answerBody, NULL);
1241 }
1242
Apply(std::string & answerBody,HttpClient::HttpHeaders & answerHeaders)1243 bool HttpClient::Apply(std::string &answerBody,
1244 HttpClient::HttpHeaders &answerHeaders)
1245 {
1246 return ApplyInternal(answerBody, &answerHeaders);
1247 }
1248
Apply(Json::Value & answerBody,HttpClient::HttpHeaders & answerHeaders)1249 bool HttpClient::Apply(Json::Value &answerBody,
1250 HttpClient::HttpHeaders &answerHeaders)
1251 {
1252 return ApplyInternal(answerBody, &answerHeaders);
1253 }
1254
GetLastStatus() const1255 HttpStatus HttpClient::GetLastStatus() const
1256 {
1257 return lastStatus_;
1258 }
1259
1260
ApplyAndThrowException(IAnswer & answer)1261 void HttpClient::ApplyAndThrowException(IAnswer& answer)
1262 {
1263 CurlAnswer wrapper(answer, headersToLowerCase_);
1264
1265 if (!ApplyInternal(wrapper))
1266 {
1267 ThrowException(GetLastStatus());
1268 }
1269 }
1270
1271
ApplyAndThrowException(std::string & answerBody)1272 void HttpClient::ApplyAndThrowException(std::string& answerBody)
1273 {
1274 if (!Apply(answerBody))
1275 {
1276 ThrowException(GetLastStatus());
1277 }
1278 }
1279
1280
ApplyAndThrowException(Json::Value & answerBody)1281 void HttpClient::ApplyAndThrowException(Json::Value& answerBody)
1282 {
1283 if (!Apply(answerBody))
1284 {
1285 ThrowException(GetLastStatus());
1286 }
1287 }
1288
1289
ApplyAndThrowException(std::string & answerBody,HttpHeaders & answerHeaders)1290 void HttpClient::ApplyAndThrowException(std::string& answerBody,
1291 HttpHeaders& answerHeaders)
1292 {
1293 if (!Apply(answerBody, answerHeaders))
1294 {
1295 ThrowException(GetLastStatus());
1296 }
1297 }
1298
1299
ApplyAndThrowException(Json::Value & answerBody,HttpHeaders & answerHeaders)1300 void HttpClient::ApplyAndThrowException(Json::Value& answerBody,
1301 HttpHeaders& answerHeaders)
1302 {
1303 if (!Apply(answerBody, answerHeaders))
1304 {
1305 ThrowException(GetLastStatus());
1306 }
1307 }
1308
1309
SetClientCertificate(const std::string & certificateFile,const std::string & certificateKeyFile,const std::string & certificateKeyPassword)1310 void HttpClient::SetClientCertificate(const std::string& certificateFile,
1311 const std::string& certificateKeyFile,
1312 const std::string& certificateKeyPassword)
1313 {
1314 if (certificateFile.empty())
1315 {
1316 throw OrthancException(ErrorCode_ParameterOutOfRange);
1317 }
1318
1319 if (!SystemToolbox::IsRegularFile(certificateFile))
1320 {
1321 throw OrthancException(ErrorCode_InexistentFile,
1322 "Cannot open certificate file: " + certificateFile);
1323 }
1324
1325 if (!certificateKeyFile.empty() &&
1326 !SystemToolbox::IsRegularFile(certificateKeyFile))
1327 {
1328 throw OrthancException(ErrorCode_InexistentFile,
1329 "Cannot open key file: " + certificateKeyFile);
1330 }
1331
1332 clientCertificateFile_ = certificateFile;
1333 clientCertificateKeyFile_ = certificateKeyFile;
1334 clientCertificateKeyPassword_ = certificateKeyPassword;
1335 }
1336
SetPkcs11Enabled(bool enabled)1337 void HttpClient::SetPkcs11Enabled(bool enabled)
1338 {
1339 pkcs11Enabled_ = enabled;
1340 }
1341
IsPkcs11Enabled() const1342 bool HttpClient::IsPkcs11Enabled() const
1343 {
1344 return pkcs11Enabled_;
1345 }
1346
GetClientCertificateFile() const1347 const std::string &HttpClient::GetClientCertificateFile() const
1348 {
1349 return clientCertificateFile_;
1350 }
1351
GetClientCertificateKeyFile() const1352 const std::string &HttpClient::GetClientCertificateKeyFile() const
1353 {
1354 return clientCertificateKeyFile_;
1355 }
1356
GetClientCertificateKeyPassword() const1357 const std::string &HttpClient::GetClientCertificateKeyPassword() const
1358 {
1359 return clientCertificateKeyPassword_;
1360 }
1361
SetConvertHeadersToLowerCase(bool lowerCase)1362 void HttpClient::SetConvertHeadersToLowerCase(bool lowerCase)
1363 {
1364 headersToLowerCase_ = lowerCase;
1365 }
1366
IsConvertHeadersToLowerCase() const1367 bool HttpClient::IsConvertHeadersToLowerCase() const
1368 {
1369 return headersToLowerCase_;
1370 }
1371
SetRedirectionFollowed(bool follow)1372 void HttpClient::SetRedirectionFollowed(bool follow)
1373 {
1374 redirectionFollowed_ = follow;
1375 }
1376
IsRedirectionFollowed() const1377 bool HttpClient::IsRedirectionFollowed() const
1378 {
1379 return redirectionFollowed_;
1380 }
1381
1382
InitializePkcs11(const std::string & module,const std::string & pin,bool verbose)1383 void HttpClient::InitializePkcs11(const std::string& module,
1384 const std::string& pin,
1385 bool verbose)
1386 {
1387 #if ORTHANC_ENABLE_PKCS11 == 1
1388 CLOG(INFO, HTTP) << "Initializing PKCS#11 using " << module
1389 << (pin.empty() ? " (no PIN provided)" : " (PIN is provided)");
1390 GlobalParameters::GetInstance().InitializePkcs11(module, pin, verbose);
1391 #else
1392 throw OrthancException(ErrorCode_InternalError,
1393 "This version of Orthanc is compiled without support for PKCS#11");
1394 #endif
1395 }
1396 }
1397