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
+  }
+}