changeset 2800:dc7330089736

"OrthancPeers" configuration option now allows to specify HTTP headers
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 23 Aug 2018 13:11:48 +0200
parents 6e3a60b85da6
children 3ee82c7313e7
files Core/HttpClient.cpp Core/WebServiceParameters.cpp Core/WebServiceParameters.h NEWS OrthancServer/OrthancInitialization.cpp OrthancServer/OrthancRestApi/OrthancRestModalities.cpp OrthancServer/ServerJobs/Operations/StorePeerOperation.cpp OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp Resources/Configuration.json UnitTestsSources/MultiThreadingTests.cpp UnitTestsSources/RestApiTests.cpp
diffstat 11 files changed, 420 insertions(+), 166 deletions(-) [+]
line wrap: on
line diff
--- a/Core/HttpClient.cpp	Wed Aug 22 16:55:07 2018 +0200
+++ b/Core/HttpClient.cpp	Thu Aug 23 13:11:48 2018 +0200
@@ -400,6 +400,13 @@
     SetPkcs11Enabled(service.IsPkcs11Enabled());
 
     SetUrl(service.GetUrl() + uri);
+
+    for (WebServiceParameters::HttpHeaders::const_iterator 
+           it = service.GetHttpHeaders().begin();
+         it != service.GetHttpHeaders().end(); ++it)
+    {
+      AddHeader(it->first, it->second);
+    }
   }
 
 
--- a/Core/WebServiceParameters.cpp	Wed Aug 22 16:55:07 2018 +0200
+++ b/Core/WebServiceParameters.cpp	Thu Aug 23 13:11:48 2018 +0200
@@ -37,20 +37,31 @@
 #include "Logging.h"
 #include "OrthancException.h"
 #include "SerializationToolbox.h"
+#include "Toolbox.h"
 
 #if ORTHANC_SANDBOXED == 0
-#  include "../Core/SystemToolbox.h"
+#  include "SystemToolbox.h"
 #endif
 
 #include <cassert>
 
 namespace Orthanc
 {
+  static const char* KEY_CERTIFICATE_FILE = "CertificateFile";
+  static const char* KEY_CERTIFICATE_KEY_FILE = "CertificateKeyFile";
+  static const char* KEY_CERTIFICATE_KEY_PASSWORD = "CertificateKeyPassword";
+  static const char* KEY_HTTP_HEADERS = "HttpHeaders";
+  static const char* KEY_PASSWORD = "Password";
+  static const char* KEY_PKCS11 = "Pkcs11";
+  static const char* KEY_URL = "Url";
+  static const char* KEY_URL_2 = "URL";
+  static const char* KEY_USERNAME = "Username";
+
+
   WebServiceParameters::WebServiceParameters() : 
-    advancedFormat_(false),
-    url_("http://127.0.0.1:8042/"),
     pkcs11Enabled_(false)
   {
+    SetUrl("http://127.0.0.1:8042/");
   }
 
 
@@ -62,7 +73,50 @@
   }
 
 
-#if ORTHANC_SANDBOXED == 0
+  void WebServiceParameters::SetUrl(const std::string& url)
+  {
+    if (!Toolbox::StartsWith(url, "http://") &&
+        !Toolbox::StartsWith(url, "https://"))
+    {
+      LOG(ERROR) << "Bad URL: " << url;
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    // Add trailing slash if needed
+    if (url[url.size() - 1] == '/')
+    {
+      url_ = url;
+    }
+    else
+    {
+      url_ = url + '/';
+    }
+  }
+
+
+  void WebServiceParameters::ClearCredentials()
+  {
+    username_.clear();
+    password_.clear();
+  }
+
+
+  void WebServiceParameters::SetCredentials(const std::string& username,
+                                            const std::string& password)
+  {
+    if (username.empty() && 
+        !password.empty())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+    else
+    {
+      username_ = username;
+      password_ = password;
+    }
+  }
+
+
   void WebServiceParameters::SetClientCertificate(const std::string& certificateFile,
                                                   const std::string& certificateKeyFile,
                                                   const std::string& certificateKeyPassword)
@@ -72,43 +126,24 @@
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
 
-    if (!SystemToolbox::IsRegularFile(certificateFile))
+    if (certificateKeyPassword.empty())
     {
-      LOG(ERROR) << "Cannot open certificate file: " << certificateFile;
-      throw OrthancException(ErrorCode_InexistentFile);
+      LOG(ERROR) << "The password for the HTTPS certificate is not provided: " << certificateFile;
+      throw OrthancException(ErrorCode_BadFileFormat);      
     }
 
-    if (!certificateKeyFile.empty() && 
-        !SystemToolbox::IsRegularFile(certificateKeyFile))
-    {
-      LOG(ERROR) << "Cannot open key file: " << certificateKeyFile;
-      throw OrthancException(ErrorCode_InexistentFile);
-    }
-
-    advancedFormat_ = true;
     certificateFile_ = certificateFile;
     certificateKeyFile_ = certificateKeyFile;
     certificateKeyPassword_ = certificateKeyPassword;
   }
-#endif
 
 
-  static void AddTrailingSlash(std::string& url)
-  {
-    if (url.size() != 0 && 
-        url[url.size() - 1] != '/')
-    {
-      url += '/';
-    }
-  }
-
-
-  void WebServiceParameters::FromJsonArray(const Json::Value& peer)
+  void WebServiceParameters::FromSimpleFormat(const Json::Value& peer)
   {
     assert(peer.isArray());
 
-    advancedFormat_ = false;
     pkcs11Enabled_ = false;
+    ClearClientCertificate();
 
     if (peer.size() != 1 && 
         peer.size() != 3)
@@ -116,19 +151,11 @@
       throw OrthancException(ErrorCode_BadFileFormat);
     }
 
-    std::string url = peer.get(0u, "").asString();
-    if (url.empty())
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    AddTrailingSlash(url);
-    SetUrl(url);
+    SetUrl(peer.get(0u, "").asString());
 
     if (peer.size() == 1)
     {
-      SetUsername("");
-      SetPassword("");
+      ClearCredentials();
     }
     else if (peer.size() == 2)
     {
@@ -137,8 +164,8 @@
     }
     else if (peer.size() == 3)
     {
-      SetUsername(peer.get(1u, "").asString());
-      SetPassword(peer.get(2u, "").asString());
+      SetCredentials(peer.get(1u, "").asString(),
+                     peer.get(2u, "").asString());
     }
     else
     {
@@ -166,70 +193,90 @@
   }
 
 
-  void WebServiceParameters::FromJsonObject(const Json::Value& peer)
+  void WebServiceParameters::FromAdvancedFormat(const Json::Value& peer)
   {
     assert(peer.isObject());
-    advancedFormat_ = true;
 
-    std::string url = GetStringMember(peer, "Url", "");
+    std::string url = GetStringMember(peer, KEY_URL, "");
     if (url.empty())
     {
-      throw OrthancException(ErrorCode_BadFileFormat);
+      SetUrl(GetStringMember(peer, KEY_URL_2, ""));
+    }
+    else
+    {
+      SetUrl(url);
     }
 
-    AddTrailingSlash(url);
-    SetUrl(url);
-
-    SetUsername(GetStringMember(peer, "Username", ""));
-    SetPassword(GetStringMember(peer, "Password", ""));
+    SetCredentials(GetStringMember(peer, KEY_USERNAME, ""),
+                   GetStringMember(peer, KEY_PASSWORD, ""));
 
-    if (!username_.empty() &&
-        !peer.isMember("Password"))
+    std::string file = GetStringMember(peer, KEY_CERTIFICATE_FILE, "");
+    if (!file.empty())
     {
-      LOG(ERROR) << "The HTTP password is not provided";
-      throw OrthancException(ErrorCode_BadFileFormat);      
+      SetClientCertificate(file, GetStringMember(peer, KEY_CERTIFICATE_KEY_FILE, ""),
+                           GetStringMember(peer, KEY_CERTIFICATE_KEY_PASSWORD, ""));
+    }
+    else
+    {
+      ClearClientCertificate();
     }
 
-#if ORTHANC_SANDBOXED == 0
-    if (peer.isMember("CertificateFile"))
+    if (peer.isMember(KEY_PKCS11))
     {
-      SetClientCertificate(GetStringMember(peer, "CertificateFile", ""),
-                           GetStringMember(peer, "CertificateKeyFile", ""),
-                           GetStringMember(peer, "CertificateKeyPassword", ""));
-
-      if (!peer.isMember("CertificateKeyPassword"))
+      if (peer[KEY_PKCS11].type() == Json::booleanValue)
       {
-        LOG(ERROR) << "The password for the HTTPS certificate is not provided";
-        throw OrthancException(ErrorCode_BadFileFormat);      
-      }
-    }
-#endif
-
-    if (peer.isMember("Pkcs11"))
-    {
-      if (peer["Pkcs11"].type() == Json::booleanValue)
-      {
-        pkcs11Enabled_ = peer["Pkcs11"].asBool();
+        pkcs11Enabled_ = peer[KEY_PKCS11].asBool();
       }
       else
       {
         throw OrthancException(ErrorCode_BadFileFormat);
       }
     }
+    else
+    {
+      pkcs11Enabled_ = false;
+    }
+
+    headers_.clear();
+
+    if (peer.isMember(KEY_HTTP_HEADERS))
+    {
+      const Json::Value& h = peer[KEY_HTTP_HEADERS];
+      if (h.type() != Json::objectValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        Json::Value::Members keys = h.getMemberNames();
+        for (size_t i = 0; i < keys.size(); i++)
+        {
+          const Json::Value& value = h[keys[i]];
+          if (value.type() != Json::stringValue)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+          else
+          {
+            headers_[keys[i]] = value.asString();
+          }
+        }
+      }
+    }
   }
 
 
-  void WebServiceParameters::FromJson(const Json::Value& peer)
+  void WebServiceParameters::Unserialize(const Json::Value& peer)
   {
     try
     {
       if (peer.isArray())
       {
-        FromJsonArray(peer);
+        FromSimpleFormat(peer);
       }
       else if (peer.isObject())
       {
-        FromJsonObject(peer);
+        FromAdvancedFormat(peer);
       }
       else
       {
@@ -247,39 +294,89 @@
   }
 
 
-  void WebServiceParameters::ToJson(Json::Value& value,
-                                    bool includePasswords) const
+  void WebServiceParameters::ListHttpHeaders(std::set<std::string>& target) const
+  {
+    target.clear();
+
+    for (HttpHeaders::const_iterator it = headers_.begin();
+         it != headers_.end(); ++it)
+    {
+      target.insert(it->first);
+    }
+  }
+
+
+  bool WebServiceParameters::LookupHttpHeader(std::string& value,
+                                              const std::string& key) const
   {
-    if (advancedFormat_)
+    HttpHeaders::const_iterator found = headers_.find(key);
+
+    if (found == headers_.end())
+    {
+      return false;
+    }
+    else
+    {
+      value = found->second;
+      return true;
+    }
+  }
+
+
+  bool WebServiceParameters::IsAdvancedFormatNeeded() const
+  {
+    return (!certificateFile_.empty() ||
+            !certificateKeyFile_.empty() ||
+            !certificateKeyPassword_.empty() ||
+            pkcs11Enabled_ ||
+            !headers_.empty());
+  }
+
+
+  void WebServiceParameters::Serialize(Json::Value& value,
+                                       bool forceAdvancedFormat,
+                                       bool includePasswords) const
+  {
+    if (forceAdvancedFormat ||
+        IsAdvancedFormatNeeded())
     {
       value = Json::objectValue;
-      value["Url"] = url_;
+      value[KEY_URL] = url_;
 
       if (!username_.empty() ||
           !password_.empty())
       {
-        value["Username"] = username_;
+        value[KEY_USERNAME] = username_;
 
         if (includePasswords)
         {
-          value["Password"] = password_;
+          value[KEY_PASSWORD] = password_;
         }
       }
 
       if (!certificateFile_.empty())
       {
-        value["CertificateFile"] = certificateFile_;
+        value[KEY_CERTIFICATE_FILE] = certificateFile_;
       }
 
       if (!certificateKeyFile_.empty())
       {
-        value["CertificateKeyFile"] = certificateKeyFile_;
+        value[KEY_CERTIFICATE_KEY_FILE] = certificateKeyFile_;
       }
 
       if (!certificateKeyPassword_.empty() &&
           includePasswords)
       {
-        value["CertificateKeyPassword"] = certificateKeyPassword_;
+        value[KEY_CERTIFICATE_KEY_PASSWORD] = certificateKeyPassword_;
+      }
+
+      value[KEY_PKCS11] = pkcs11Enabled_;
+
+      value[KEY_HTTP_HEADERS] = Json::objectValue;
+      for (HttpHeaders::const_iterator it = headers_.begin();
+           it != headers_.end(); ++it)
+      {
+        value[KEY_HTTP_HEADERS][it->first] = it->second;
       }
     }
     else
@@ -291,50 +388,30 @@
           !password_.empty())
       {
         value.append(username_);
-
-        if (includePasswords)
-        {
-          value.append(password_);
-        }
+        value.append(includePasswords ? password_ : "");
       }
     }
   }
 
-  
-  void WebServiceParameters::Serialize(Json::Value& target) const
-  {
-    target = Json::objectValue;
-    target["URL"] = url_;
-    target["Username"] = username_;
-    target["Password"] = password_;
-    target["CertificateFile"] = certificateFile_;
-    target["CertificateKeyFile"] = certificateKeyFile_;
-    target["CertificateKeyPassword"] = certificateKeyPassword_;
-    target["PKCS11"] = pkcs11Enabled_;
-    target["AdvancedFormat"] = advancedFormat_;
-  }
 
-  
 #if ORTHANC_SANDBOXED == 0
-  WebServiceParameters::WebServiceParameters(const Json::Value& serialized) :
-    advancedFormat_(true)
+  void WebServiceParameters::CheckClientCertificate() const
   {
-    url_ = SerializationToolbox::ReadString(serialized, "URL");
-    username_ = SerializationToolbox::ReadString(serialized, "Username");
-    password_ = SerializationToolbox::ReadString(serialized, "Password");
+    if (!certificateFile_.empty())
+    {
+      if (!SystemToolbox::IsRegularFile(certificateFile_))
+      {
+        LOG(ERROR) << "Cannot open certificate file: " << certificateFile_;
+        throw OrthancException(ErrorCode_InexistentFile);
+      }
 
-    std::string a, b, c;
-    a = SerializationToolbox::ReadString(serialized, "CertificateFile");
-    b = SerializationToolbox::ReadString(serialized, "CertificateKeyFile");
-    c = SerializationToolbox::ReadString(serialized, "CertificateKeyPassword");
-
-    if (!a.empty())
-    {
-      SetClientCertificate(a, b, c);
+      if (!certificateKeyFile_.empty() && 
+          !SystemToolbox::IsRegularFile(certificateKeyFile_))
+      {
+        LOG(ERROR) << "Cannot open key file: " << certificateKeyFile_;
+        throw OrthancException(ErrorCode_InexistentFile);
+      }
     }
-    
-    pkcs11Enabled_ = SerializationToolbox::ReadBoolean(serialized, "PKCS11");
-    advancedFormat_ = SerializationToolbox::ReadBoolean(serialized, "AdvancedFormat");
   }
 #endif
 }
--- a/Core/WebServiceParameters.h	Wed Aug 22 16:55:07 2018 +0200
+++ b/Core/WebServiceParameters.h	Thu Aug 23 13:11:48 2018 +0200
@@ -37,6 +37,8 @@
 #  error The macro ORTHANC_SANDBOXED must be defined
 #endif
 
+#include <map>
+#include <set>
 #include <string>
 #include <json/json.h>
 
@@ -44,64 +46,58 @@
 {
   class WebServiceParameters
   {
+  public:
+    typedef std::map<std::string, std::string>  HttpHeaders;
+
   private:
-    bool        advancedFormat_;
-    std::string url_;
-    std::string username_;
-    std::string password_;
-    std::string certificateFile_;
-    std::string certificateKeyFile_;
-    std::string certificateKeyPassword_;
-    bool        pkcs11Enabled_;
+    std::string  url_;
+    std::string  username_;
+    std::string  password_;
+    std::string  certificateFile_;
+    std::string  certificateKeyFile_;
+    std::string  certificateKeyPassword_;
+    bool         pkcs11Enabled_;
+    HttpHeaders  headers_;
 
-    void FromJsonArray(const Json::Value& peer);
+    void FromSimpleFormat(const Json::Value& peer);
 
-    void FromJsonObject(const Json::Value& peer);
+    void FromAdvancedFormat(const Json::Value& peer);
 
   public:
     WebServiceParameters();
 
-#if ORTHANC_SANDBOXED == 0
-    WebServiceParameters(const Json::Value& serialized);
-#endif
+    WebServiceParameters(const Json::Value& serialized)
+    {
+      Unserialize(serialized);
+    }
 
     const std::string& GetUrl() const
     {
       return url_;
     }
 
-    void SetUrl(const std::string& url)
-    {
-      url_ = url;
-    }
+    void SetUrl(const std::string& url);
+
+    void ClearCredentials();
 
+    void SetCredentials(const std::string& username,
+                        const std::string& password);
+    
     const std::string& GetUsername() const
     {
       return username_;
     }
 
-    void SetUsername(const std::string& username)
-    {
-      username_ = username;
-    }
-    
     const std::string& GetPassword() const
     {
       return password_;
     }
 
-    void SetPassword(const std::string& password)
-    {
-      password_ = password;
-    }
-
     void ClearClientCertificate();
 
-#if ORTHANC_SANDBOXED == 0
     void SetClientCertificate(const std::string& certificateFile,
                               const std::string& certificateKeyFile,
                               const std::string& certificateKeyPassword);
-#endif
 
     const std::string& GetCertificateFile() const
     {
@@ -118,9 +114,9 @@
       return certificateKeyPassword_;
     }
 
-    void SetPkcs11Enabled(bool pkcs11Enabled)
+    void SetPkcs11Enabled(bool enabled)
     {
-      pkcs11Enabled_ = pkcs11Enabled;
+      pkcs11Enabled_ = enabled;
     }
 
     bool IsPkcs11Enabled() const
@@ -128,11 +124,37 @@
       return pkcs11Enabled_;
     }
 
-    void FromJson(const Json::Value& peer);
+    void AddHttpHeader(const std::string& key,
+                       const std::string& value)
+    {
+      headers_[key] = value;
+    }
+
+    void ClearHttpHeaders()
+    {
+      headers_.clear();
+    }
+
+    const HttpHeaders& GetHttpHeaders() const
+    {
+      return headers_;
+    }
 
-    void ToJson(Json::Value& value,
-                bool includePasswords) const;
+    void ListHttpHeaders(std::set<std::string>& target) const; 
+
+    bool LookupHttpHeader(std::string& value,
+                          const std::string& key) const; 
+
+    bool IsAdvancedFormatNeeded() const;
 
-    void Serialize(Json::Value& target) const;
+    void Unserialize(const Json::Value& peer);
+
+    void Serialize(Json::Value& value,
+                   bool forceAdvancedFormat,
+                   bool includePasswords) const;
+
+#if ORTHANC_SANDBOXED == 0
+    void CheckClientCertificate() const;
+#endif
   };
 }
--- a/NEWS	Wed Aug 22 16:55:07 2018 +0200
+++ b/NEWS	Thu Aug 23 13:11:48 2018 +0200
@@ -1,6 +1,10 @@
 Pending changes in the mainline
 ===============================
 
+General
+-------
+
+* "OrthancPeers" configuration option now allows to specify HTTP headers
 
 Plugins
 -------
--- a/OrthancServer/OrthancInitialization.cpp	Wed Aug 22 16:55:07 2018 +0200
+++ b/OrthancServer/OrthancInitialization.cpp	Thu Aug 23 13:11:48 2018 +0200
@@ -277,6 +277,7 @@
     {
       WebServiceParameters peer;
       Configuration::GetOrthancPeer(peer, *it);
+      peer.CheckClientCertificate();
     }
 
     Configuration::GetListOfDicomModalities(ids);
@@ -628,7 +629,7 @@
       }
       else
       {
-        peer.FromJson(modalities[name]);
+        peer.Unserialize(modalities[name]);
         return true;
       }
     }
@@ -942,6 +943,8 @@
   void Configuration::UpdatePeer(const std::string& symbolicName,
                                  const WebServiceParameters& peer)
   {
+    peer.CheckClientCertificate();
+
     boost::recursive_mutex::scoped_lock lock(globalMutex_);
 
     if (!configuration_.isMember("OrthancPeers"))
@@ -960,7 +963,9 @@
     peers.removeMember(symbolicName);
 
     Json::Value v;
-    peer.ToJson(v, true);
+    peer.Serialize(v, 
+                   false /* use simple format if possible */, 
+                   true  /* include passwords */);
     peers[symbolicName] = v;
   }
   
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Wed Aug 22 16:55:07 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Thu Aug 23 13:11:48 2018 +0200
@@ -983,7 +983,7 @@
     if (reader.parse(call.GetBodyData(), call.GetBodyData() + call.GetBodySize(), json))
     {
       WebServiceParameters peer;
-      peer.FromJson(json);
+      peer.Unserialize(json);
       Configuration::UpdatePeer(call.GetUriComponent("id", ""), peer);
       call.GetOutput().AnswerBuffer("", "text/plain");
     }
--- a/OrthancServer/ServerJobs/Operations/StorePeerOperation.cpp	Wed Aug 22 16:55:07 2018 +0200
+++ b/OrthancServer/ServerJobs/Operations/StorePeerOperation.cpp	Thu Aug 23 13:11:48 2018 +0200
@@ -87,7 +87,9 @@
   {
     result = Json::objectValue;
     result["Type"] = "StorePeer";
-    peer_.Serialize(result["Peer"]);
+    peer_.Serialize(result["Peer"], 
+                    true /* force advanced format */,
+                    true /* include passwords */);
   }
 
 
--- a/OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp	Wed Aug 22 16:55:07 2018 +0200
+++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp	Thu Aug 23 13:11:48 2018 +0200
@@ -98,7 +98,9 @@
     SetOfInstancesJob::GetPublicContent(value);
     
     Json::Value v;
-    peer_.ToJson(v, false /* don't include passwords */);
+    peer_.Serialize(v, 
+                    false /* allow simple format if possible */,
+                    false /* don't include passwords */);
     value["Peer"] = v;
   }
 
@@ -122,7 +124,9 @@
     }
     else
     {
-      peer_.Serialize(target[PEER]);
+      peer_.Serialize(target[PEER],
+                      true /* force advanced format */,
+                      true /* include passwords */);
       return true;
     }
   }  
--- a/Resources/Configuration.json	Wed Aug 22 16:55:07 2018 +0200
+++ b/Resources/Configuration.json	Thu Aug 23 13:11:48 2018 +0200
@@ -210,14 +210,15 @@
 
     /**
      * This is another, more advanced format to define Orthanc
-     * peers. It notably allows to specify a HTTPS client certificate
-     * in the PEM format (as in the "--cert" option of curl), or to
-     * enable PKCS#11 authentication for smart cards.
+     * peers. It notably allows to specify HTTP headers, a HTTPS
+     * client certificate in the PEM format (as in the "--cert" option
+     * of curl), or to enable PKCS#11 authentication for smart cards.
      **/
     // "peer" : {
     //   "Url" : "http://127.0.0.1:8043/",
     //   "Username" : "alice",
     //   "Password" : "alicePassword",
+    //   "HttpHeaders" : { "Token" : "Hello world" },
     //   "CertificateFile" : "client.crt",
     //   "CertificateKeyFile" : "client.key",
     //   "CertificateKeyPassword" : "certpass",
--- a/UnitTestsSources/MultiThreadingTests.cpp	Wed Aug 22 16:55:07 2018 +0200
+++ b/UnitTestsSources/MultiThreadingTests.cpp	Thu Aug 23 13:11:48 2018 +0200
@@ -1299,8 +1299,7 @@
   {
     WebServiceParameters peer;
     peer.SetUrl("http://localhost/");
-    peer.SetUsername("username");
-    peer.SetPassword("password");
+    peer.SetCredentials("username", "password");
     peer.SetPkcs11Enabled(true);
 
     StorePeerOperation operation(peer);
@@ -1443,8 +1442,7 @@
   {
     WebServiceParameters peer;
     peer.SetUrl("http://localhost/");
-    peer.SetUsername("username");
-    peer.SetPassword("password");
+    peer.SetCredentials("username", "password");
     peer.SetPkcs11Enabled(true);
 
     OrthancPeerStoreJob job(GetContext());
--- a/UnitTestsSources/RestApiTests.cpp	Wed Aug 22 16:55:07 2018 +0200
+++ b/UnitTestsSources/RestApiTests.cpp	Thu Aug 23 13:11:48 2018 +0200
@@ -469,3 +469,137 @@
     ASSERT_EQ("plain", h.GetSubType());
   }
 }
+
+
+TEST(WebServiceParameters, Serialization)
+{
+  {
+    Json::Value v = Json::arrayValue;
+    v.append("http://localhost:8042/");
+
+    WebServiceParameters p(v);
+    ASSERT_FALSE(p.IsAdvancedFormatNeeded());
+
+    Json::Value v2;
+    p.Serialize(v2, false, true);
+    ASSERT_EQ(v, v2);
+
+    WebServiceParameters p2(v2);
+    ASSERT_EQ("http://localhost:8042/", p2.GetUrl());
+    ASSERT_TRUE(p2.GetUsername().empty());
+    ASSERT_TRUE(p2.GetPassword().empty());
+    ASSERT_TRUE(p2.GetCertificateFile().empty());
+    ASSERT_TRUE(p2.GetCertificateKeyFile().empty());
+    ASSERT_TRUE(p2.GetCertificateKeyPassword().empty());
+    ASSERT_FALSE(p2.IsPkcs11Enabled());
+  }
+
+  {
+    Json::Value v = Json::arrayValue;
+    v.append("http://localhost:8042/");
+    v.append("user");
+    v.append("pass");
+
+    WebServiceParameters p(v);
+    ASSERT_FALSE(p.IsAdvancedFormatNeeded());
+    ASSERT_EQ("http://localhost:8042/", p.GetUrl());
+    ASSERT_EQ("user", p.GetUsername());
+    ASSERT_EQ("pass", p.GetPassword());
+    ASSERT_TRUE(p.GetCertificateFile().empty());
+    ASSERT_TRUE(p.GetCertificateKeyFile().empty());
+    ASSERT_TRUE(p.GetCertificateKeyPassword().empty());
+    ASSERT_FALSE(p.IsPkcs11Enabled());
+
+    Json::Value v2;
+    p.Serialize(v2, false, true);
+    ASSERT_EQ(v, v2);
+
+    p.Serialize(v2, false, false /* no password */);
+    WebServiceParameters p2(v2);
+    ASSERT_EQ(Json::arrayValue, v2.type());
+    ASSERT_EQ(3u, v2.size());
+    ASSERT_EQ("http://localhost:8042/", v2[0u].asString());
+    ASSERT_EQ("user", v2[1u].asString());
+    ASSERT_TRUE(v2[2u].asString().empty());
+  }
+
+  {
+    Json::Value v = Json::arrayValue;
+    v.append("http://localhost:8042/");
+
+    WebServiceParameters p(v);
+    ASSERT_FALSE(p.IsAdvancedFormatNeeded());
+    p.SetPkcs11Enabled(true);
+    ASSERT_TRUE(p.IsAdvancedFormatNeeded());
+
+    Json::Value v2;
+    p.Serialize(v2, false, true);
+    WebServiceParameters p2(v2);
+
+    ASSERT_EQ(Json::objectValue, v2.type());
+    ASSERT_EQ(3u, v2.size());
+    ASSERT_EQ("http://localhost:8042/", v2["Url"].asString());
+    ASSERT_TRUE(v2["Pkcs11"].asBool());
+    ASSERT_EQ(Json::objectValue, v2["HttpHeaders"].type());
+    ASSERT_EQ(0u, v2["HttpHeaders"].size());
+  }
+
+  {
+    Json::Value v = Json::arrayValue;
+    v.append("http://localhost:8042/");
+
+    WebServiceParameters p(v);
+    ASSERT_FALSE(p.IsAdvancedFormatNeeded());
+    p.SetClientCertificate("a", "b", "c");
+    ASSERT_TRUE(p.IsAdvancedFormatNeeded());
+
+    Json::Value v2;
+    p.Serialize(v2, false, true);
+    WebServiceParameters p2(v2);
+
+    ASSERT_EQ(Json::objectValue, v2.type());
+    ASSERT_EQ(6u, v2.size());
+    ASSERT_EQ("http://localhost:8042/", v2["Url"].asString());
+    ASSERT_EQ("a", v2["CertificateFile"].asString());
+    ASSERT_EQ("b", v2["CertificateKeyFile"].asString());
+    ASSERT_EQ("c", v2["CertificateKeyPassword"].asString());
+    ASSERT_FALSE(v2["Pkcs11"].asBool());
+    ASSERT_EQ(Json::objectValue, v2["HttpHeaders"].type());
+    ASSERT_EQ(0u, v2["HttpHeaders"].size());
+  }
+
+  {
+    Json::Value v = Json::arrayValue;
+    v.append("http://localhost:8042/");
+
+    WebServiceParameters p(v);
+    ASSERT_FALSE(p.IsAdvancedFormatNeeded());
+    p.AddHttpHeader("a", "b");
+    p.AddHttpHeader("c", "d");
+    ASSERT_TRUE(p.IsAdvancedFormatNeeded());
+
+    Json::Value v2;
+    p.Serialize(v2, false, true);
+    WebServiceParameters p2(v2);
+
+    ASSERT_EQ(Json::objectValue, v2.type());
+    ASSERT_EQ(3u, v2.size());
+    ASSERT_EQ("http://localhost:8042/", v2["Url"].asString());
+    ASSERT_FALSE(v2["Pkcs11"].asBool());
+    ASSERT_EQ(Json::objectValue, v2["HttpHeaders"].type());
+    ASSERT_EQ(2u, v2["HttpHeaders"].size());
+    ASSERT_EQ("b", v2["HttpHeaders"]["a"].asString());
+    ASSERT_EQ("d", v2["HttpHeaders"]["c"].asString());
+
+    std::set<std::string> a;
+    p2.ListHttpHeaders(a);
+    ASSERT_EQ(2u, a.size());
+    ASSERT_TRUE(a.find("a") != a.end());
+    ASSERT_TRUE(a.find("c") != a.end());
+
+    std::string s;
+    ASSERT_TRUE(p2.LookupHttpHeader(s, "a")); ASSERT_EQ("b", s);
+    ASSERT_TRUE(p2.LookupHttpHeader(s, "c")); ASSERT_EQ("d", s);
+    ASSERT_FALSE(p2.LookupHttpHeader(s, "nope"));
+  }
+}