comparison OrthancFramework/Sources/HttpClient.cpp @ 4044:d25f4c0fa160 framework

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