Mercurial > hg > orthanc
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 } |