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