Mercurial > hg > orthanc
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/HttpClient.cpp Wed Jun 10 20:30:34 2020 +0200 @@ -0,0 +1,1214 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "PrecompiledHeaders.h" +#include "HttpClient.h" + +#include "Toolbox.h" +#include "OrthancException.h" +#include "Logging.h" +#include "ChunkedBuffer.h" +#include "SystemToolbox.h" + +#include <string.h> +#include <curl/curl.h> +#include <boost/algorithm/string/predicate.hpp> +#include <boost/thread/mutex.hpp> + +// Default timeout = 60 seconds (in Orthanc <= 1.5.6, it was 10 seconds) +static const unsigned int DEFAULT_HTTP_TIMEOUT = 60; + + +#if ORTHANC_ENABLE_PKCS11 == 1 +# include "Pkcs11.h" +#endif + + +extern "C" +{ + static CURLcode GetHttpStatus(CURLcode code, CURL* curl, long* status) + { + if (code == CURLE_OK) + { + code = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, status); + return code; + } + else + { + LOG(ERROR) << "Error code " << static_cast<int>(code) + << " in libcurl: " << curl_easy_strerror(code); + *status = 0; + return code; + } + } +} + +// This is a dummy wrapper function to suppress any OpenSSL-related +// problem in valgrind. Inlining is prevented. +#if defined(__GNUC__) || defined(__clang__) + __attribute__((noinline)) +#endif +static CURLcode OrthancHttpClientPerformSSL(CURL* curl, long* status) +{ +#if ORTHANC_ENABLE_SSL == 1 + return GetHttpStatus(curl_easy_perform(curl), curl, status); +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "Orthanc was compiled without SSL support, " + "cannot make HTTPS request"); +#endif +} + + + +namespace Orthanc +{ + static CURLcode CheckCode(CURLcode code) + { + if (code == CURLE_NOT_BUILT_IN) + { + throw OrthancException(ErrorCode_InternalError, + "Your libcurl does not contain a required feature, " + "please recompile Orthanc with -DUSE_SYSTEM_CURL=OFF"); + } + + if (code != CURLE_OK) + { + throw OrthancException(ErrorCode_NetworkProtocol, + "libCURL error: " + std::string(curl_easy_strerror(code))); + } + + return code; + } + + + // RAII pattern around a "curl_slist" + class HttpClient::CurlHeaders : public boost::noncopyable + { + private: + struct curl_slist *content_; + bool isChunkedTransfer_; + bool hasExpect_; + + public: + CurlHeaders() : + content_(NULL), + isChunkedTransfer_(false), + hasExpect_(false) + { + } + + CurlHeaders(const HttpClient::HttpHeaders& headers) + { + for (HttpClient::HttpHeaders::const_iterator + it = headers.begin(); it != headers.end(); ++it) + { + AddHeader(it->first, it->second); + } + } + + ~CurlHeaders() + { + Clear(); + } + + bool IsEmpty() const + { + return content_ == NULL; + } + + void Clear() + { + if (content_ != NULL) + { + curl_slist_free_all(content_); + content_ = NULL; + } + + isChunkedTransfer_ = false; + hasExpect_ = false; + } + + void AddHeader(const std::string& key, + const std::string& value) + { + if (boost::iequals(key, "Expect")) + { + hasExpect_ = true; + } + + if (boost::iequals(key, "Transfer-Encoding") && + value == "chunked") + { + isChunkedTransfer_ = true; + } + + std::string item = key + ": " + value; + + struct curl_slist *tmp = curl_slist_append(content_, item.c_str()); + + if (tmp == NULL) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + else + { + content_ = tmp; + } + } + + void Assign(CURL* curl) const + { + CheckCode(curl_easy_setopt(curl, CURLOPT_HTTPHEADER, content_)); + } + + bool HasExpect() const + { + return hasExpect_; + } + + bool IsChunkedTransfer() const + { + return isChunkedTransfer_; + } + }; + + + class HttpClient::CurlRequestBody : public boost::noncopyable + { + private: + HttpClient::IRequestBody* body_; + std::string sourceBuffer_; + size_t sourceBufferTransmittedSize_; + + size_t CallbackInternal(char* curlBuffer, + size_t curlBufferSize) + { + if (body_ == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (curlBufferSize == 0) + { + throw OrthancException(ErrorCode_InternalError); + } + + // Read chunks from the body stream so as to fill the target buffer + size_t curlBufferFilledSize = 0; + size_t sourceRemainingSize = sourceBuffer_.size() - sourceBufferTransmittedSize_; + bool hasMore = true; + + while (sourceRemainingSize < curlBufferSize && hasMore) + { + if (sourceRemainingSize > 0) + { + // transmit the end of current source buffer + memcpy(curlBuffer + curlBufferFilledSize, + sourceBuffer_.data() + sourceBufferTransmittedSize_, sourceRemainingSize); + + curlBufferFilledSize += sourceRemainingSize; + } + + // start filling a new source buffer + sourceBufferTransmittedSize_ = 0; + sourceBuffer_.clear(); + + hasMore = body_->ReadNextChunk(sourceBuffer_); + + sourceRemainingSize = sourceBuffer_.size(); + } + + if (sourceRemainingSize > 0 && + curlBufferSize > curlBufferFilledSize) + { + size_t s = std::min(sourceRemainingSize, curlBufferSize - curlBufferFilledSize); + + memcpy(curlBuffer + curlBufferFilledSize, + sourceBuffer_.data() + sourceBufferTransmittedSize_, s); + + sourceBufferTransmittedSize_ += s; + curlBufferFilledSize += s; + } + + return curlBufferFilledSize; + } + + public: + CurlRequestBody() : + body_(NULL), + sourceBufferTransmittedSize_(0) + { + } + + void SetBody(HttpClient::IRequestBody& body) + { + body_ = &body; + sourceBufferTransmittedSize_ = 0; + sourceBuffer_.clear(); + } + + void Clear() + { + body_ = NULL; + sourceBufferTransmittedSize_ = 0; + sourceBuffer_.clear(); + } + + bool IsValid() const + { + return body_ != NULL; + } + + static size_t Callback(char *buffer, size_t size, size_t nitems, void *userdata) + { + try + { + assert(userdata != NULL); + return reinterpret_cast<HttpClient::CurlRequestBody*>(userdata)-> + CallbackInternal(buffer, size * nitems); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Exception while streaming HTTP body: " << e.What(); + return CURL_READFUNC_ABORT; + } + catch (...) + { + LOG(ERROR) << "Native exception while streaming HTTP body"; + return CURL_READFUNC_ABORT; + } + } + }; + + + class HttpClient::CurlAnswer : public boost::noncopyable + { + private: + HttpClient::IAnswer& answer_; + bool headersLowerCase_; + + public: + CurlAnswer(HttpClient::IAnswer& answer, + bool headersLowerCase) : + answer_(answer), + headersLowerCase_(headersLowerCase) + { + } + + static size_t HeaderCallback(void *buffer, size_t size, size_t nmemb, void *userdata) + { + try + { + assert(userdata != NULL); + CurlAnswer& that = *(static_cast<CurlAnswer*>(userdata)); + + size_t length = size * nmemb; + if (length == 0) + { + return 0; + } + else + { + std::string s(reinterpret_cast<const char*>(buffer), length); + std::size_t colon = s.find(':'); + std::size_t eol = s.find("\r\n"); + if (colon != std::string::npos && + eol != std::string::npos) + { + std::string tmp(s.substr(0, colon)); + + if (that.headersLowerCase_) + { + Toolbox::ToLowerCase(tmp); + } + + std::string key = Toolbox::StripSpaces(tmp); + + if (!key.empty()) + { + std::string value = Toolbox::StripSpaces(s.substr(colon + 1, eol)); + that.answer_.AddHeader(key, value); + } + } + + return length; + } + } + catch (OrthancException& e) + { + LOG(ERROR) << "Exception while streaming HTTP body: " << e.What(); + return CURL_READFUNC_ABORT; + } + catch (...) + { + LOG(ERROR) << "Native exception while streaming HTTP body"; + return CURL_READFUNC_ABORT; + } + } + + static size_t BodyCallback(void *buffer, size_t size, size_t nmemb, void *userdata) + { + try + { + assert(userdata != NULL); + CurlAnswer& that = *(static_cast<CurlAnswer*>(userdata)); + + size_t length = size * nmemb; + if (length == 0) + { + return 0; + } + else + { + that.answer_.AddChunk(buffer, length); + return length; + } + } + catch (OrthancException& e) + { + LOG(ERROR) << "Exception while streaming HTTP body: " << e.What(); + return CURL_READFUNC_ABORT; + } + catch (...) + { + LOG(ERROR) << "Native exception while streaming HTTP body"; + return CURL_READFUNC_ABORT; + } + } + }; + + + class HttpClient::DefaultAnswer : public HttpClient::IAnswer + { + private: + ChunkedBuffer answer_; + HttpHeaders* headers_; + + public: + DefaultAnswer() : headers_(NULL) + { + } + + void SetHeaders(HttpHeaders& headers) + { + headers_ = &headers; + headers_->clear(); + } + + void FlattenBody(std::string& target) + { + answer_.Flatten(target); + } + + virtual void AddHeader(const std::string& key, + const std::string& value) + { + if (headers_ != NULL) + { + (*headers_) [key] = value; + } + } + + virtual void AddChunk(const void* data, + size_t size) + { + answer_.AddChunk(data, size); + } + }; + + + class HttpClient::GlobalParameters + { + private: + boost::mutex mutex_; + bool httpsVerifyPeers_; + std::string httpsCACertificates_; + std::string proxy_; + long timeout_; + bool verbose_; + + GlobalParameters() : + httpsVerifyPeers_(true), + timeout_(0), + verbose_(false) + { + } + + public: + // Singleton pattern + static GlobalParameters& GetInstance() + { + static GlobalParameters parameters; + return parameters; + } + + void ConfigureSsl(bool httpsVerifyPeers, + const std::string& httpsCACertificates) + { + boost::mutex::scoped_lock lock(mutex_); + httpsVerifyPeers_ = httpsVerifyPeers; + httpsCACertificates_ = httpsCACertificates; + } + + void GetSslConfiguration(bool& httpsVerifyPeers, + std::string& httpsCACertificates) + { + boost::mutex::scoped_lock lock(mutex_); + httpsVerifyPeers = httpsVerifyPeers_; + httpsCACertificates = httpsCACertificates_; + } + + void SetDefaultProxy(const std::string& proxy) + { + LOG(INFO) << "Setting the default proxy for HTTP client connections: " << proxy; + + { + boost::mutex::scoped_lock lock(mutex_); + proxy_ = proxy; + } + } + + void GetDefaultProxy(std::string& target) + { + boost::mutex::scoped_lock lock(mutex_); + target = proxy_; + } + + void SetDefaultTimeout(long seconds) + { + LOG(INFO) << "Setting the default timeout for HTTP client connections: " << seconds << " seconds"; + + { + boost::mutex::scoped_lock lock(mutex_); + timeout_ = seconds; + } + } + + long GetDefaultTimeout() + { + boost::mutex::scoped_lock lock(mutex_); + return timeout_; + } + +#if ORTHANC_ENABLE_PKCS11 == 1 + bool IsPkcs11Initialized() + { + boost::mutex::scoped_lock lock(mutex_); + return Pkcs11::IsInitialized(); + } + + void InitializePkcs11(const std::string& module, + const std::string& pin, + bool verbose) + { + boost::mutex::scoped_lock lock(mutex_); + Pkcs11::Initialize(module, pin, verbose); + } +#endif + + bool IsDefaultVerbose() const + { + return verbose_; + } + + void SetDefaultVerbose(bool verbose) + { + verbose_ = verbose; + } + }; + + + struct HttpClient::PImpl + { + CURL* curl_; + CurlHeaders defaultPostHeaders_; + CurlHeaders defaultChunkedHeaders_; + CurlHeaders userHeaders_; + CurlRequestBody requestBody_; + }; + + + void HttpClient::ThrowException(HttpStatus status) + { + switch (status) + { + case HttpStatus_400_BadRequest: + throw OrthancException(ErrorCode_BadRequest); + + case HttpStatus_401_Unauthorized: + case HttpStatus_403_Forbidden: + throw OrthancException(ErrorCode_Unauthorized); + + case HttpStatus_404_NotFound: + throw OrthancException(ErrorCode_UnknownResource); + + default: + throw OrthancException(ErrorCode_NetworkProtocol); + } + } + + + /*static int CurlDebugCallback(CURL *handle, + curl_infotype type, + char *data, + size_t size, + void *userptr) + { + switch (type) + { + case CURLINFO_TEXT: + case CURLINFO_HEADER_IN: + case CURLINFO_HEADER_OUT: + case CURLINFO_SSL_DATA_IN: + case CURLINFO_SSL_DATA_OUT: + case CURLINFO_END: + case CURLINFO_DATA_IN: + case CURLINFO_DATA_OUT: + { + std::string s(data, size); + LOG(INFO) << "libcurl: " << s; + break; + } + + default: + break; + } + + return 0; + }*/ + + + void HttpClient::Setup() + { + pimpl_->defaultPostHeaders_.AddHeader("Expect", ""); + pimpl_->defaultChunkedHeaders_.AddHeader("Expect", ""); + pimpl_->defaultChunkedHeaders_.AddHeader("Transfer-Encoding", "chunked"); + + pimpl_->curl_ = curl_easy_init(); + + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERFUNCTION, &CurlAnswer::HeaderCallback)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEFUNCTION, &CurlAnswer::BodyCallback)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADER, 0)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_FOLLOWLOCATION, 1)); + + // This fixes the "longjmp causes uninitialized stack frame" crash + // that happens on modern Linux versions. + // http://stackoverflow.com/questions/9191668/error-longjmp-causes-uninitialized-stack-frame + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOSIGNAL, 1)); + + url_ = ""; + method_ = HttpMethod_Get; + lastStatus_ = HttpStatus_None; + SetVerbose(GlobalParameters::GetInstance().IsDefaultVerbose()); + timeout_ = GlobalParameters::GetInstance().GetDefaultTimeout(); + GlobalParameters::GetInstance().GetDefaultProxy(proxy_); + GlobalParameters::GetInstance().GetSslConfiguration(verifyPeers_, caCertificates_); + } + + + HttpClient::HttpClient() : + pimpl_(new PImpl), + verifyPeers_(true), + pkcs11Enabled_(false), + headersToLowerCase_(true), + redirectionFollowed_(true) + { + Setup(); + } + + + HttpClient::HttpClient(const WebServiceParameters& service, + const std::string& uri) : + pimpl_(new PImpl), + verifyPeers_(true), + headersToLowerCase_(true), + redirectionFollowed_(true) + { + Setup(); + + if (service.GetUsername().size() != 0 && + service.GetPassword().size() != 0) + { + SetCredentials(service.GetUsername().c_str(), + service.GetPassword().c_str()); + } + + if (!service.GetCertificateFile().empty()) + { + SetClientCertificate(service.GetCertificateFile(), + service.GetCertificateKeyFile(), + service.GetCertificateKeyPassword()); + } + + SetPkcs11Enabled(service.IsPkcs11Enabled()); + + SetUrl(service.GetUrl() + uri); + + for (WebServiceParameters::Dictionary::const_iterator + it = service.GetHttpHeaders().begin(); + it != service.GetHttpHeaders().end(); ++it) + { + AddHeader(it->first, it->second); + } + } + + + HttpClient::~HttpClient() + { + curl_easy_cleanup(pimpl_->curl_); + } + + + void HttpClient::SetBody(const std::string& data) + { + body_ = data; + pimpl_->requestBody_.Clear(); + } + + + void HttpClient::SetBody(IRequestBody& body) + { + body_.clear(); + pimpl_->requestBody_.SetBody(body); + } + + + void HttpClient::ClearBody() + { + body_.clear(); + pimpl_->requestBody_.Clear(); + } + + + void HttpClient::SetVerbose(bool isVerbose) + { + isVerbose_ = isVerbose; + + if (isVerbose_) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_VERBOSE, 1)); + //CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_DEBUGFUNCTION, &CurlDebugCallback)); + } + else + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_VERBOSE, 0)); + } + } + + + void HttpClient::AddHeader(const std::string& key, + const std::string& value) + { + if (key.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + pimpl_->userHeaders_.AddHeader(key, value); + } + } + + + void HttpClient::ClearHeaders() + { + pimpl_->userHeaders_.Clear(); + } + + + bool HttpClient::ApplyInternal(CurlAnswer& answer) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_URL, url_.c_str())); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERDATA, &answer)); + +#if ORTHANC_ENABLE_SSL == 1 + // Setup HTTPS-related options + + if (verifyPeers_) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CAINFO, caCertificates_.c_str())); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYHOST, 2)); // libcurl default is strict verifyhost + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYPEER, 1)); + } + else + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYHOST, 0)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYPEER, 0)); + } +#endif + + // Setup the HTTPS client certificate + if (!clientCertificateFile_.empty() && + pkcs11Enabled_) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Cannot enable both client certificates and PKCS#11 authentication"); + } + + if (pkcs11Enabled_) + { +#if ORTHANC_ENABLE_PKCS11 == 1 + if (GlobalParameters::GetInstance().IsPkcs11Initialized()) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLENGINE, Pkcs11::GetEngineIdentifier())); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLKEYTYPE, "ENG")); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLCERTTYPE, "ENG")); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "Cannot use PKCS#11 for a HTTPS request, " + "because it has not been initialized"); + } +#else + throw OrthancException(ErrorCode_InternalError, + "This version of Orthanc is compiled without support for PKCS#11"); +#endif + } + else if (!clientCertificateFile_.empty()) + { +#if ORTHANC_ENABLE_SSL == 1 + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLCERTTYPE, "PEM")); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLCERT, clientCertificateFile_.c_str())); + + if (!clientCertificateKeyPassword_.empty()) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_KEYPASSWD, clientCertificateKeyPassword_.c_str())); + } + + // NB: If no "clientKeyFile_" is provided, the key must be + // prepended to the certificate file + if (!clientCertificateKeyFile_.empty()) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLKEYTYPE, "PEM")); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLKEY, clientCertificateKeyFile_.c_str())); + } +#else + throw OrthancException(ErrorCode_InternalError, + "This version of Orthanc is compiled without OpenSSL support, " + "cannot use HTTPS client authentication"); +#endif + } + + // Reset the parameters from previous calls to Apply() + pimpl_->userHeaders_.Assign(pimpl_->curl_); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPGET, 0L)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 0L)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOBODY, 0L)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, NULL)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, NULL)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, 0L)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PROXY, NULL)); + + if (redirectionFollowed_) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_FOLLOWLOCATION, 1L)); + } + else + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_FOLLOWLOCATION, 0L)); + } + + // Set timeouts + if (timeout_ <= 0) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_TIMEOUT, DEFAULT_HTTP_TIMEOUT)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CONNECTTIMEOUT, DEFAULT_HTTP_TIMEOUT)); + } + else + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_TIMEOUT, timeout_)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CONNECTTIMEOUT, timeout_)); + } + + if (credentials_.size() != 0) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_USERPWD, credentials_.c_str())); + } + + if (proxy_.size() != 0) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PROXY, proxy_.c_str())); + } + + switch (method_) + { + case HttpMethod_Get: + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPGET, 1L)); + break; + + case HttpMethod_Post: + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 1L)); + + break; + + case HttpMethod_Delete: + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOBODY, 1L)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, "DELETE")); + break; + + case HttpMethod_Put: + // http://stackoverflow.com/a/7570281/881731: Don't use + // CURLOPT_PUT if there is a body + + // CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PUT, 1L)); + + curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, "PUT"); /* !!! */ + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + if (method_ == HttpMethod_Post || + method_ == HttpMethod_Put) + { + if (!pimpl_->userHeaders_.IsEmpty() && + !pimpl_->userHeaders_.HasExpect()) + { + LOG(INFO) << "For performance, the HTTP header \"Expect\" should be set to empty string in POST/PUT requests"; + } + + if (pimpl_->requestBody_.IsValid()) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_READFUNCTION, CurlRequestBody::Callback)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_READDATA, &pimpl_->requestBody_)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 1L)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, -1L)); + + if (pimpl_->userHeaders_.IsEmpty()) + { + pimpl_->defaultChunkedHeaders_.Assign(pimpl_->curl_); + } + else if (!pimpl_->userHeaders_.IsChunkedTransfer()) + { + LOG(WARNING) << "The HTTP header \"Transfer-Encoding\" must be set to \"chunked\" " + << "if streaming a chunked body in POST/PUT requests"; + } + } + else + { + // Disable possible previous stream transfers + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_READFUNCTION, NULL)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_UPLOAD, 0)); + + if (pimpl_->userHeaders_.IsChunkedTransfer()) + { + LOG(WARNING) << "The HTTP header \"Transfer-Encoding\" must only be set " + << "if streaming a chunked body in POST/PUT requests"; + } + + if (pimpl_->userHeaders_.IsEmpty()) + { + pimpl_->defaultPostHeaders_.Assign(pimpl_->curl_); + } + + if (body_.size() > 0) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, body_.c_str())); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, body_.size())); + } + else + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, NULL)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, 0)); + } + } + } + + + // Do the actual request + CURLcode code; + long status = 0; + + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEDATA, &answer)); + + const boost::posix_time::ptime start = boost::posix_time::microsec_clock::universal_time(); + + if (boost::starts_with(url_, "https://")) + { + code = OrthancHttpClientPerformSSL(pimpl_->curl_, &status); + } + else + { + code = GetHttpStatus(curl_easy_perform(pimpl_->curl_), pimpl_->curl_, &status); + } + + const boost::posix_time::ptime end = boost::posix_time::microsec_clock::universal_time(); + + LOG(INFO) << "HTTP status code " << status << " in " + << ((end - start).total_milliseconds()) << " ms after " + << EnumerationToString(method_) << " request on: " << url_; + + if (isVerbose_) + { + LOG(INFO) << "cURL status code: " << code; + } + + CheckCode(code); + + if (status == 0) + { + // This corresponds to a call to an inexistent host + lastStatus_ = HttpStatus_500_InternalServerError; + } + else + { + lastStatus_ = static_cast<HttpStatus>(status); + } + + if (status >= 200 && status < 300) + { + return true; // Success + } + else + { + LOG(ERROR) << "Error in HTTP request, received HTTP status " << status + << " (" << EnumerationToString(lastStatus_) << ")"; + return false; + } + } + + + bool HttpClient::ApplyInternal(std::string& answerBody, + HttpHeaders* answerHeaders) + { + answerBody.clear(); + + DefaultAnswer answer; + + if (answerHeaders != NULL) + { + answer.SetHeaders(*answerHeaders); + } + + CurlAnswer wrapper(answer, headersToLowerCase_); + + if (ApplyInternal(wrapper)) + { + answer.FlattenBody(answerBody); + return true; + } + else + { + return false; + } + } + + + bool HttpClient::ApplyInternal(Json::Value& answerBody, + HttpClient::HttpHeaders* answerHeaders) + { + std::string s; + if (ApplyInternal(s, answerHeaders)) + { + Json::Reader reader; + return reader.parse(s, answerBody); + } + else + { + return false; + } + } + + + void HttpClient::SetCredentials(const char* username, + const char* password) + { + credentials_ = std::string(username) + ":" + std::string(password); + } + + + void HttpClient::ConfigureSsl(bool httpsVerifyPeers, + const std::string& httpsVerifyCertificates) + { +#if ORTHANC_ENABLE_SSL == 1 + if (httpsVerifyPeers) + { + if (httpsVerifyCertificates.empty()) + { + LOG(WARNING) << "No certificates are provided to validate peers, " + << "set \"HttpsCACertificates\" if you need to do HTTPS requests"; + } + else + { + LOG(WARNING) << "HTTPS will use the CA certificates from this file: " << httpsVerifyCertificates; + } + } + else + { + LOG(WARNING) << "The verification of the peers in HTTPS requests is disabled"; + } +#endif + + GlobalParameters::GetInstance().ConfigureSsl(httpsVerifyPeers, httpsVerifyCertificates); + } + + + void HttpClient::GlobalInitialize() + { +#if ORTHANC_ENABLE_SSL == 1 + CheckCode(curl_global_init(CURL_GLOBAL_ALL)); +#else + CheckCode(curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_SSL)); +#endif + } + + + void HttpClient::GlobalFinalize() + { + curl_global_cleanup(); + +#if ORTHANC_ENABLE_PKCS11 == 1 + Pkcs11::Finalize(); +#endif + } + + + void HttpClient::SetDefaultVerbose(bool verbose) + { + GlobalParameters::GetInstance().SetDefaultVerbose(verbose); + } + + + void HttpClient::SetDefaultProxy(const std::string& proxy) + { + GlobalParameters::GetInstance().SetDefaultProxy(proxy); + } + + + void HttpClient::SetDefaultTimeout(long timeout) + { + GlobalParameters::GetInstance().SetDefaultTimeout(timeout); + } + + + bool HttpClient::Apply(IAnswer& answer) + { + CurlAnswer wrapper(answer, headersToLowerCase_); + return ApplyInternal(wrapper); + } + + + void HttpClient::ApplyAndThrowException(IAnswer& answer) + { + CurlAnswer wrapper(answer, headersToLowerCase_); + + if (!ApplyInternal(wrapper)) + { + ThrowException(GetLastStatus()); + } + } + + + void HttpClient::ApplyAndThrowException(std::string& answerBody) + { + if (!Apply(answerBody)) + { + ThrowException(GetLastStatus()); + } + } + + + void HttpClient::ApplyAndThrowException(Json::Value& answerBody) + { + if (!Apply(answerBody)) + { + ThrowException(GetLastStatus()); + } + } + + + void HttpClient::ApplyAndThrowException(std::string& answerBody, + HttpHeaders& answerHeaders) + { + if (!Apply(answerBody, answerHeaders)) + { + ThrowException(GetLastStatus()); + } + } + + + void HttpClient::ApplyAndThrowException(Json::Value& answerBody, + HttpHeaders& answerHeaders) + { + if (!Apply(answerBody, answerHeaders)) + { + ThrowException(GetLastStatus()); + } + } + + + void HttpClient::SetClientCertificate(const std::string& certificateFile, + const std::string& certificateKeyFile, + const std::string& certificateKeyPassword) + { + if (certificateFile.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (!SystemToolbox::IsRegularFile(certificateFile)) + { + throw OrthancException(ErrorCode_InexistentFile, + "Cannot open certificate file: " + certificateFile); + } + + if (!certificateKeyFile.empty() && + !SystemToolbox::IsRegularFile(certificateKeyFile)) + { + throw OrthancException(ErrorCode_InexistentFile, + "Cannot open key file: " + certificateKeyFile); + } + + clientCertificateFile_ = certificateFile; + clientCertificateKeyFile_ = certificateKeyFile; + clientCertificateKeyPassword_ = certificateKeyPassword; + } + + + void HttpClient::InitializePkcs11(const std::string& module, + const std::string& pin, + bool verbose) + { +#if ORTHANC_ENABLE_PKCS11 == 1 + LOG(INFO) << "Initializing PKCS#11 using " << module + << (pin.empty() ? " (no PIN provided)" : " (PIN is provided)"); + GlobalParameters::GetInstance().InitializePkcs11(module, pin, verbose); +#else + throw OrthancException(ErrorCode_InternalError, + "This version of Orthanc is compiled without support for PKCS#11"); +#endif + } +}