changeset 5656:a3c244090f67 find-refactoring

integration mainline->find-refactoring
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 06 Jun 2024 13:24:04 +0200
parents 3f13db27b399 (current diff) 65a509cac161 (diff)
children
files NEWS OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp
diffstat 20 files changed, 428 insertions(+), 133 deletions(-) [+]
line wrap: on
line diff
--- a/CITATION.cff	Thu May 30 21:27:48 2024 +0200
+++ b/CITATION.cff	Thu Jun 06 13:24:04 2024 +0200
@@ -10,5 +10,5 @@
 doi: "10.1007/s10278-018-0082-y"
 license: "GPL-3.0-or-later"
 repository-code: "https://orthanc.uclouvain.be/hg/orthanc/"
-version: 1.12.3
-date-released: 2024-01-31
+version: 1.12.4
+date-released: 2024-06-05
--- a/NEWS	Thu May 30 21:27:48 2024 +0200
+++ b/NEWS	Thu Jun 06 13:24:04 2024 +0200
@@ -1,6 +1,13 @@
 Pending changes in the mainline
 ===============================
 
+* TODO-FIND: complete the list of updated routes:
+  /studies?expand and sibbling routes now also return "Metadata" (if the DB implements 'extended-api-v1')
+
+
+Version 1.12.4 (2024-06-05)
+===========================
+
 REST API
 --------
 
@@ -9,27 +16,24 @@
 * Added a new "LimitToThisLevelMainDicomTags" field in the payload of 
   /patients|studies|series/instances/../reconstruct to speed up the reconstruction
   in case you just want to update the MainDicomTags of that resource level only 
-  e.g. after you have updated the 'ExtraMainDicomTags' for this level.
-* The "requestedTags" GET argument was deprecated in favor of "requested-tags".
-* Fixed broken /instances/../tags route after the calling
-  /studies/../reconstruct when changing the "IngestTranscoding". 
-* TODO-FIND: complete the list of updated routes:
-  /studies?expand and sibbling routes now also return "Metadata" (if the DB implements 'extended-api-v1')
+  (e.g., after you have updated the "ExtraMainDicomTags" for this level)
+* The "requestedTags" GET argument is deprecated in favor of "requested-tags"
+* Added "?whole" option to "/instances/{id}/tags" to access tags stored after pixel data
 
 Plugins
 -------
 
-* Multitenant DICOM plugin: added support for locales
+* Multitenant DICOM plugin: added support for locales.
 * Housekeeper plugin: 
   - Added an option "LimitMainDicomTagsReconstructLevel"
-    (allowed values: "Patient", "Study", "Series", "Instance").  This can greatly speed
-    up the housekeeper process e.g. if you have only update the Study level ExtraMainDicomTags.
+    (allowed values: "Patient", "Study", "Series", "Instance"). This can greatly speed
+    up the housekeeper process, e.g. if you have only update the Study level ExtraMainDicomTags.
   - Fixed broken /instances/../tags route after running the Housekeeper
-    after having changed the "IngestTranscoding". 
-* SDK: added OrthancPluginLogMessage() that is a new primitive for
-  plugins to log messages.  This new primitive will display the plugin
-  name, the plugin file name, and the plugin line number in the
-  logs. If they are not using the LOG() facilities provided by the
+    after having changed the "IngestTranscoding".
+* SDK: added OrthancPluginLogMessage() as a new primitive for plugins
+  to log messages. This new primitive will display the plugin name,
+  the plugin file name, and the plugin line number in the logs. If
+  they are not using the LOG() facilities provided by the
   OrthancFramework, plugins should now use ORTHANC_PLUGINS_LOG_INFO(),
   ORTHANC_PLUGINS_LOG_WARNING(), and ORTHANC_PLUGINS_LOG_ERROR().
 
@@ -37,13 +41,13 @@
 -----------
 
 * C-Find queries:
-  - In C-Find queries including GenericGroupLength tags, Orthanc was still
+  - In C-Find queries including "GenericGroupLength" tags, Orthanc was still
     extracting these tags from the storage although they were already ignored
     and not returned in the response.
     They are now removed from the query earlier to avoid this disk access that
-    could slow down the response time.  Note that this seems to happen mainly
+    could slow down the response time. Note that this seems to happen mainly
     when the query originates from some GE devices (AWS).
-  - TimezoneOffsetFromUTC is now ignored for matching.
+  - "TimezoneOffsetFromUTC" is now ignored for matching.
 * The 0x0111 DIMSE Status is now considered as a warning instead of an error
   when received as a response to a C-Store.
   See https://discourse.orthanc-server.org/t/ignore-dimse-status-0x0111-when-sending-partial-duplicate-studies/4555/3
@@ -53,13 +57,18 @@
   resource type, not only the resource identifier identifier.
 * DICOM TLS:
   - In prior versions, when "DicomTlsRemoteCertificateRequired" was set to false, Orthanc
-    was still sending a client certificate request during the TLS handshake but was not triggering
-    and error if the client certificate was not trusted (equivalent to the --verify-peer-cert DCMTK option)
-    From this version, if this option is set to false, Orthanc will not send a 
-    client certificate request during the TLS handshake anymore (equivalent to the --ignore-peer-cert 
-    DCMTK option).
+    was still sending a client certificate request during the TLS handshake but was not
+    triggering and error if the client certificate was not trusted (equivalent to the
+    "--verify-peer-cert" DCMTK option). Starting with Orthanc 1.12.4, if this option is
+    set to "false", Orthanc will not send a client certificate request during the TLS
+    handshake anymore (equivalent to the "--ignore-peer-cert" DCMTK option).
   - When working with "DicomTlsEnabled": true and "DicomTlsRemoteCertificateRequired": false,
     Orthanc was refusing to start if no "DicomTlsTrustedCertificates" was provided.
+  - New configuration options:
+    - "DicomTlsMinimumProtocolVersion" to select the minimum TLS protocol version
+    - "DicomTlsCiphersAccepted" to fine tune the list of accepted ciphers
+* Fixed broken /instances/../tags route after calling of
+  /studies/../reconstruct after having changed the "IngestTranscoding".
 * Upgraded dependencies for static builds:
   - boost 1.85.0
 
--- a/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake	Thu May 30 21:27:48 2024 +0200
+++ b/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake	Thu Jun 06 13:24:04 2024 +0200
@@ -163,6 +163,8 @@
         set(ORTHANC_FRAMEWORK_MD5 "d2476b9e796e339ac320b5333489bdb3")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.3")
         set(ORTHANC_FRAMEWORK_MD5 "975f5bf2142c22cb1777b4f6a0a614c5")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.4")
+        set(ORTHANC_FRAMEWORK_MD5 "1e61779ea4a7cd705720bdcfed8a6a73")
 
       # Below this point are development snapshots that were used to
       # release some plugin, before an official release of the Orthanc
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp	Thu May 30 21:27:48 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp	Thu Jun 06 13:24:04 2024 +0200
@@ -300,11 +300,12 @@
       {
         assert(net_ != NULL &&
                params_ != NULL);
-        
         tls_.reset(Internals::InitializeDicomTls(net_, NET_REQUESTOR, parameters.GetOwnPrivateKeyPath(),
                                                  parameters.GetOwnCertificatePath(),
                                                  parameters.GetTrustedCertificatesPath(),
-                                                 parameters.IsRemoteCertificateRequired()));
+                                                 parameters.IsRemoteCertificateRequired(),
+                                                 parameters.GetMinimumTlsVersion(),
+                                                 parameters.GetAcceptedCiphers()));
       }
       catch (OrthancException&)
       {
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.cpp	Thu May 30 21:27:48 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.cpp	Thu Jun 06 13:24:04 2024 +0200
@@ -44,7 +44,8 @@
 static std::string   defaultTrustedCertificatesPath_;
 static unsigned int  defaultMaximumPduLength_ = ASC_DEFAULTMAXPDU;
 static bool          defaultRemoteCertificateRequired_ = true;
-
+static unsigned int  minimumTlsVersion_ = 0;
+static std::set<std::string> acceptedCiphers_;
 
 namespace Orthanc
 {
@@ -252,7 +253,26 @@
     return remoteCertificateRequired_;
   }
 
+  unsigned int DicomAssociationParameters::GetMinimumTlsVersion()
+  {
+    return minimumTlsVersion_;
+  }
   
+  void DicomAssociationParameters::SetMinimumTlsVersion(unsigned int version)
+  {
+    minimumTlsVersion_ = version;
+  }
+
+  void DicomAssociationParameters::SetAcceptedCiphers(const std::set<std::string>& acceptedCiphers)
+  {
+    acceptedCiphers_ = acceptedCiphers;
+  }
+
+  const std::set<std::string>& DicomAssociationParameters::GetAcceptedCiphers()
+  {
+    return acceptedCiphers_;
+  }
+
 
   static const char* const LOCAL_AET = "LocalAet";
   static const char* const REMOTE = "Remote";
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h	Thu May 30 21:27:48 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h	Thu Jun 06 13:24:04 2024 +0200
@@ -128,5 +128,13 @@
     static void SetDefaultRemoteCertificateRequired(bool required);
 
     static bool GetDefaultRemoteCertificateRequired();
+
+    static void SetMinimumTlsVersion(unsigned int version);
+
+    static unsigned int GetMinimumTlsVersion();
+
+    static void SetAcceptedCiphers(const std::set<std::string>& acceptedCiphers);
+
+    static const std::set<std::string>& GetAcceptedCiphers();
   };
 }
--- a/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp	Thu May 30 21:27:48 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp	Thu Jun 06 13:24:04 2024 +0200
@@ -107,7 +107,8 @@
     applicationEntityFilter_(NULL),
     useDicomTls_(false),
     maximumPduLength_(ASC_DEFAULTMAXPDU),
-    remoteCertificateRequired_(true)
+    remoteCertificateRequired_(true),
+    minimumTlsVersion_(0)
   {
   }
 
@@ -411,7 +412,7 @@
       {
         pimpl_->tls_.reset(Internals::InitializeDicomTls(
                              pimpl_->network_, NET_ACCEPTOR, ownPrivateKeyPath_, ownCertificatePath_,
-                             trustedCertificatesPath_, remoteCertificateRequired_));
+                             trustedCertificatesPath_, remoteCertificateRequired_, minimumTlsVersion_, acceptedCiphers_));
       }
       catch (OrthancException&)
       {
@@ -494,6 +495,18 @@
     return useDicomTls_;
   }
 
+  void DicomServer::SetMinimumTlsVersion(unsigned int version)
+  {
+    minimumTlsVersion_ = version;
+    DicomAssociationParameters::SetMinimumTlsVersion(version);
+  }
+
+  void DicomServer::SetAcceptedCiphers(const std::set<std::string>& ciphers)
+  {
+    acceptedCiphers_ = ciphers;
+    DicomAssociationParameters::SetAcceptedCiphers(ciphers);
+  }
+
   void DicomServer::SetOwnCertificatePath(const std::string& privateKeyPath,
                                           const std::string& certificatePath)
   {
--- a/OrthancFramework/Sources/DicomNetworking/DicomServer.h	Thu May 30 21:27:48 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomServer.h	Thu Jun 06 13:24:04 2024 +0200
@@ -91,6 +91,8 @@
     std::string  trustedCertificatesPath_;
     unsigned int maximumPduLength_;
     bool         remoteCertificateRequired_;  // New in 1.9.3
+    unsigned int minimumTlsVersion_;          // New in 1.12.4
+    std::set<std::string> acceptedCiphers_;   // New in 1.12.4
 
 
     static void ServerThread(DicomServer* server,
@@ -154,6 +156,9 @@
     void SetDicomTlsEnabled(bool enabled);
     bool IsDicomTlsEnabled() const;
 
+    void SetMinimumTlsVersion(unsigned int version);
+    void SetAcceptedCiphers(const std::set<std::string>& ciphers);
+
     void SetOwnCertificatePath(const std::string& privateKeyPath,
                                const std::string& certificatePath);
     const std::string& GetOwnPrivateKeyPath() const;    
--- a/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.cpp	Thu May 30 21:27:48 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.cpp	Thu Jun 06 13:24:04 2024 +0200
@@ -28,6 +28,9 @@
 #include "../../Logging.h"
 #include "../../OrthancException.h"
 #include "../../SystemToolbox.h"
+#include "../../Toolbox.h"
+#include <openssl/ssl.h>
+#include <openssl/err.h>
 
 #if DCMTK_VERSION_NUMBER < 364
 #  define DCF_Filetype_PEM  SSL_FILETYPE_PEM
@@ -58,12 +61,47 @@
 #endif
 
 
+#if DCMTK_VERSION_NUMBER >= 367
+    static OFCondition MyConvertOpenSSLError(unsigned long errorCode, OFBool logAsError)
+    {
+      return DcmTLSTransportLayer::convertOpenSSLError(errorCode, logAsError);
+    }
+#else
+    static OFCondition MyConvertOpenSSLError(unsigned long errorCode, OFBool logAsError)
+    {
+      if (errorCode == 0)
+      {
+        return EC_Normal;
+      }
+      else
+      {
+        const char *err = ERR_reason_error_string(errorCode);
+        if (err == NULL)
+        {
+          err = "OpenSSL error";
+        }
+
+        if (logAsError)
+        {
+          DCMTLS_ERROR("OpenSSL error " << STD_NAMESPACE hex << STD_NAMESPACE setfill('0')
+                       << STD_NAMESPACE setw(8) << errorCode << ": " << err);
+        }
+
+        // The "2" below corresponds to the same error code as "DCMTLS_EC_FailedToSetCiphersuites"
+        return OFCondition(OFM_dcmtls, 2, OF_error, err);
+      }
+    }
+#endif
+
+
     DcmTLSTransportLayer* InitializeDicomTls(T_ASC_Network *network,
                                              T_ASC_NetworkRole role,
                                              const std::string& ownPrivateKeyPath,
                                              const std::string& ownCertificatePath,
                                              const std::string& trustedCertificatesPath,
-                                             bool requireRemoteCertificate)
+                                             bool requireRemoteCertificate,
+                                             unsigned int minimalTlsVersion,
+                                             const std::set<std::string>& ciphers)
     {
       if (network == NULL)
       {
@@ -156,14 +194,94 @@
       }
 
 #if DCMTK_VERSION_NUMBER >= 364
-      if (IsFailure(tls->setTLSProfile(TSP_Profile_BCP195 /*opt_tlsProfile*/)))
+      if (minimalTlsVersion == 0) // use the default values (same behavior as before 1.12.4)
       {
-        throw OrthancException(ErrorCode_InternalError, "Cannot set the DICOM TLS profile");
+        if (ciphers.size() > 0)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat, "The cipher suites can not be specified when using the default BCP profile");
+        }
+
+        if (IsFailure(tls->setTLSProfile(TSP_Profile_BCP195 /*opt_tlsProfile*/)))
+        {
+          throw OrthancException(ErrorCode_InternalError, "Cannot set the DICOM TLS profile");
+        }
+      
+        if (IsFailure(tls->activateCipherSuites()))
+        {
+          throw OrthancException(ErrorCode_InternalError, "Cannot activate the cipher suites for DICOM TLS");
+        }
       }
-    
-      if (IsFailure(tls->activateCipherSuites()))
+      else
       {
-        throw OrthancException(ErrorCode_InternalError, "Cannot activate the cipher suites for DICOM TLS");
+        // Fine tune the SSL context
+        if (IsFailure(tls->setTLSProfile(TSP_Profile_None)))
+        {
+          throw OrthancException(ErrorCode_InternalError, "Cannot set the DICOM TLS profile");
+        }
+
+        DcmTLSTransportLayer::native_handle_type sslNativeHandle = tls->getNativeHandle();
+        SSL_CTX_clear_options(sslNativeHandle, SSL_OP_NO_SSL_MASK);
+        if (minimalTlsVersion > 1) 
+        {
+          SSL_CTX_set_options(sslNativeHandle, SSL_OP_NO_SSLv3);
+        }
+        if (minimalTlsVersion > 2) 
+        {
+          SSL_CTX_set_options(sslNativeHandle, SSL_OP_NO_TLSv1);
+        }
+        if (minimalTlsVersion > 3) 
+        {
+          SSL_CTX_set_options(sslNativeHandle, SSL_OP_NO_TLSv1_1);
+        }
+        if (minimalTlsVersion > 4) 
+        {
+          SSL_CTX_set_options(sslNativeHandle, SSL_OP_NO_TLSv1_2);
+        }
+
+        std::set<std::string> ciphersTls;
+        std::set<std::string> ciphersTls13;
+
+        // DCMTK 3.8 is missing a method to add TLS13 cipher suite in the DcmTLSTransportLayer interface.
+        // And, anyway, since we do not run dcmtkPrepare.cmake, DCMTK is not aware of TLS v1.3 cipher suite names.
+        for (std::set<std::string>::const_iterator it = ciphers.begin(); it != ciphers.end(); ++it)
+        {
+          bool isValid = false;
+          if (DcmTLSCiphersuiteHandler::lookupCiphersuiteByOpenSSLName(it->c_str()) != DcmTLSCiphersuiteHandler::unknownCipherSuiteIndex)
+          {
+            ciphersTls.insert(it->c_str());
+            isValid = true;
+          }
+          
+          // list of TLS v1.3 ciphers according to https://www.openssl.org/docs/man3.3/man1/openssl-ciphers.html
+          if (strstr("TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_CCM_SHA256:TLS_AES_128_CCM_8_SHA256", it->c_str()) != NULL)
+          {
+            ciphersTls13.insert(it->c_str());
+            isValid = true;
+          }
+
+          if (!isValid)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat, "The cipher suite " + *it + " is not recognized as valid cipher suite by OpenSSL ");
+          }
+        }
+
+        std::string joinedCiphersTls;
+        std::string joinedCiphersTls13;
+        Toolbox::JoinStrings(joinedCiphersTls, ciphersTls, ":");
+        Toolbox::JoinStrings(joinedCiphersTls13, ciphersTls13, ":");
+
+        if (joinedCiphersTls.size() > 0 && SSL_CTX_set_cipher_list(sslNativeHandle, joinedCiphersTls.c_str()) != 1)
+        {
+          OFCondition cond = MyConvertOpenSSLError(ERR_get_error(), OFTrue);
+          throw OrthancException(ErrorCode_InternalError, "Unable to configure cipher suite.  OpenSSL error: " + boost::lexical_cast<std::string>(cond.code()) + " - " + cond.text());
+        }
+
+        if (joinedCiphersTls13.size() > 0 && SSL_CTX_set_ciphersuites(sslNativeHandle, joinedCiphersTls13.c_str()) != 1)
+        {
+          OFCondition cond = MyConvertOpenSSLError(ERR_get_error(), OFTrue);
+          throw OrthancException(ErrorCode_InternalError, "Unable to configure cipher suite for TLS 1.3.  OpenSSL error: " + boost::lexical_cast<std::string>(cond.code()) + " - " + cond.text());
+        }
+
       }
 #else
       CLOG(INFO, DICOM) << "Using the following cipher suites for DICOM TLS: " << opt_ciphersuites;
--- a/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.h	Thu May 30 21:27:48 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.h	Thu Jun 06 13:24:04 2024 +0200
@@ -39,7 +39,7 @@
 
 #include <dcmtk/dcmnet/dimse.h>
 #include <dcmtk/dcmtls/tlslayer.h>
-
+#include <set>
 
 namespace Orthanc
 {
@@ -51,6 +51,9 @@
       const std::string& ownPrivateKeyPath,        // This is the first argument of "+tls" option from DCMTK command-line tools
       const std::string& ownCertificatePath,       // This is the second argument of "+tls" option
       const std::string& trustedCertificatesPath,  // This is the "--add-cert-file" ("+cf") option
-      bool requireRemoteCertificate);              // "true" means "--require-peer-cert", "false" means "--verify-peer-cert"
+      bool requireRemoteCertificate,               // "true" means "--require-peer-cert", "false" means "--ignore-peer-cert"
+      unsigned int minimalTlsVersion,              // 0 = default BCP195, 5 = TLS1.3 only
+      const std::set<std::string>& acceptedCiphers
+    );
   }
 }
--- a/OrthancServer/Resources/Configuration.json	Thu May 30 21:27:48 2024 +0200
+++ b/OrthancServer/Resources/Configuration.json	Thu Jun 06 13:24:04 2024 +0200
@@ -309,10 +309,39 @@
   // Whether Orthanc rejects DICOM TLS connections to/from remote
   // modalities that do not provide a certificate. Setting this option
   // to "true" (resp. "false") corresponds to "--require-peer-cert"
-  // (resp. "--verify-peer-cert") in the DCMTK command-line
+  // (resp. "--ignore-peer-cert") in the DCMTK command-line
   // tools. (new in Orthanc 1.9.3)
   "DicomTlsRemoteCertificateRequired" : true,
 
+  // Sets the minimum accepted TLS protocol version for the DICOM server
+  // By default, require TLS 1.2 or 1.3. This option is only meaningful 
+  // if "DicomTlsEnabled" is true (new in Orthanc 1.12.4).
+  // Note that, internally, Orthanc is configured to use the BCP195 profile
+  // by default.  As soon as you switch to another protocol version, you
+  // must also provide the list of supported cipher suites.
+  // This configuration applies to Orthanc acting both as SCU and SCP.
+  // Value => Protocols
+  //   0      use default BCP 195 profile and default cipher suites
+  //   1      SSL3+TLS1.0+TLS1.1+TLS1.2+TLS1.3
+  //   2      TLS1.0+TLS1.1+TLS1.2+TLS1.3
+  //   3      TLS1.1+TLS1.2+TLS1.3
+  //   4      TLS1.2+TLS1.3
+  //   5      TLS1.3
+  "DicomTlsMinimumProtocolVersion" : 0,
+
+  // Set the accepted ciphers for TLS connections for the DICOM server. 
+  // The ciphers must be provided as a list of strings. If not set, 
+  // this will default to BCP195 ciphers if DicomTlsMinimumProtocolVersion is 0
+  // or to an empty list for other values. This option is only 
+  // meaningful if "DicomTlsEnabled" is true. (new in Orthanc 1.12.4).
+  // This configuration must be provided if DicomTlsMinimumProtocolVersion != 0.
+  // The list of valid cipher names are available in 
+  // https://www.openssl.org/docs/man3.3/man1/openssl-ciphers.html
+  // The OpenSSL names are used.
+  /**
+     "DicomTlsCiphersAccepted" : []
+  **/
+  
   // Whether the Orthanc SCP allows incoming C-ECHO requests, even
   // from SCU modalities it does not know about (i.e. that are not
   // listed in the "DicomModalities" option above). Orthanc 1.3.0
@@ -476,11 +505,15 @@
 
   // Number of threads that are used by the embedded DICOM server.
   // This defines the number of concurrent DICOM operations that can
-  // be run. Note: This is not limiting the number of concurrent
-  // connections. With a single thread, if a C-Find is received during
-  // e.g the transcoding of an incoming C-Store, it will have to wait
-  // until the end of the C-Store before being processed. (new in
-  // Orthanc 1.10.0, before this version, the value was fixed to 4)
+  // be run when Orthanc is acting as SCP. 
+  // Note: This is not limiting the number of concurrent connections
+  // but the number of concurrent DICOM operations.
+  // E.g, with a single thread, if a C-Find is received during
+  // e.g the transcoding of an incoming C-Store, the C-Find will 
+  // be processed only at the end of the C-Store operation but both
+  // DICOM assocations will remain active. 
+  // (new in Orthanc 1.10.0, before this version, the value was 
+  // fixed to 4)
   "DicomThreadsCount" : 4,
 
   // The list of the known Orthanc peers. This option is ignored if
--- a/OrthancServer/Resources/RunCppCheck.sh	Thu May 30 21:27:48 2024 +0200
+++ b/OrthancServer/Resources/RunCppCheck.sh	Thu Jun 06 13:24:04 2024 +0200
@@ -12,33 +12,33 @@
 constParameter:../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp
 knownArgument:../../OrthancFramework/UnitTestsSources/ImageTests.cpp
 knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp
-nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:315
-stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1476
-stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:165
-stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:73
-stlFindInsert:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:373
-stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:377
-stlFindInsert:../../OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp:40
-stlFindInsert:../../OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp:190
-stlFindInsert:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:360
-syntaxError:../../OrthancFramework/Sources/SQLite/FunctionContext.h:52
-syntaxError:../../OrthancFramework/UnitTestsSources/DicomMapTests.cpp:73
-syntaxError:../../OrthancFramework/UnitTestsSources/ZipTests.cpp:132
-syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:310
-uninitMemberVar:../../OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp:416
+nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:316
+stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1477
+stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:166
+stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:74
+stlFindInsert:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:374
+stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:378
+stlFindInsert:../../OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp:41
+stlFindInsert:../../OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp:191
+stlFindInsert:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:361
+syntaxError:../../OrthancFramework/Sources/SQLite/FunctionContext.h:53
+syntaxError:../../OrthancFramework/UnitTestsSources/DicomMapTests.cpp:74
+syntaxError:../../OrthancFramework/UnitTestsSources/ZipTests.cpp:133
+syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:322
+uninitMemberVar:../../OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp:417
 unreadVariable:../../OrthancFramework/Sources/FileStorage/StorageAccessor.cpp
-unreadVariable:../../OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp:1118
+unreadVariable:../../OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp:1123
 unusedFunction
-useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:90
-useInitializationList:../../OrthancFramework/Sources/Images/PngWriter.cpp:98
-useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:274
-assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:276
-assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:1025
-assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:289
-assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:388
-assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3630
-assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:285
-assertWithSideEffect:../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp:453
+useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:91
+useInitializationList:../../OrthancFramework/Sources/Images/PngWriter.cpp:99
+useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:275
+assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:277
+assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:1026
+assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:290
+assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:389
+assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3663
+assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:286
+assertWithSideEffect:../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp:454
 EOF
 
 ${CPPCHECK} --enable=all --quiet --std=c++11 \
--- a/OrthancServer/Sources/OrthancConfiguration.cpp	Thu May 30 21:27:48 2024 +0200
+++ b/OrthancServer/Sources/OrthancConfiguration.cpp	Thu Jun 06 13:24:04 2024 +0200
@@ -751,7 +751,7 @@
 
     if (lst.type() != Json::arrayValue)
     {
-      throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted list of strings");
+      throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted list of strings: " + key);
     }
 
     for (Json::Value::ArrayIndex i = 0; i < lst.size(); i++)
@@ -760,7 +760,31 @@
     }    
   }
 
-    
+
+  void OrthancConfiguration::GetSetOfStringsParameter(std::set<std::string>& target,
+                                                      const std::string& key) const
+  {
+    target.clear();
+  
+    if (!json_.isMember(key))
+    {
+      return;
+    }
+
+    const Json::Value& lst = json_[key];
+
+    if (lst.type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted set of strings: " + key);
+    }
+
+    for (Json::Value::ArrayIndex i = 0; i < lst.size(); i++)
+    {
+      target.insert(lst[i].asString());
+    }    
+  }
+
+
   bool OrthancConfiguration::IsSameAETitle(const std::string& aet1,
                                            const std::string& aet2) const
   {
--- a/OrthancServer/Sources/OrthancConfiguration.h	Thu May 30 21:27:48 2024 +0200
+++ b/OrthancServer/Sources/OrthancConfiguration.h	Thu Jun 06 13:24:04 2024 +0200
@@ -204,7 +204,10 @@
     
     void GetListOfStringsParameter(std::list<std::string>& target,
                                    const std::string& key) const;
-    
+
+    void GetSetOfStringsParameter(std::set<std::string>& target,
+                                  const std::string& key) const;
+
     bool IsSameAETitle(const std::string& aet1,
                        const std::string& aet2) const;
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp	Thu May 30 21:27:48 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp	Thu Jun 06 13:24:04 2024 +0200
@@ -200,8 +200,6 @@
   static void DocumentEchoShared(RestApiPostCall& call)
   {
     call.GetDocumentation()
-      .SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number,
-                       "Timeout for the C-ECHO command, in seconds", false)
       .SetRequestField(KEY_CHECK_FIND, RestApiCallDocumentation::Type_Boolean,
                        "Issue a dummy C-FIND command after the C-GET SCU, in order to check whether the remote "
                        "modality knows about Orthanc. This field defaults to the value of the `DicomEchoChecksFind` "
@@ -219,6 +217,8 @@
         .SetSummary("Trigger C-ECHO SCU")
         .SetDescription("Trigger C-ECHO SCU command against the DICOM modality whose identifier is provided in URL: "
                         "https://orthanc.uclouvain.be/book/users/rest.html#performing-c-echo")
+        .SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number,
+                         "Timeout for the C-ECHO command, in seconds", false)
         .SetUriArgument("id", "Identifier of the modality of interest");
       return;
     }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Thu May 30 21:27:48 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Thu Jun 06 13:24:04 2024 +0200
@@ -62,6 +62,7 @@
 static const char* const IGNORE_LENGTH = "ignore-length";
 static const char* const RECONSTRUCT_FILES = "ReconstructFiles";
 static const char* const LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS = "LimitToThisLevelMainDicomTags";
+static const char* const ARG_WHOLE = "whole";
 
 
 namespace Orthanc
@@ -563,7 +564,8 @@
 
 
   template <DicomToJsonFormat format>
-  static void GetInstanceTagsInternal(RestApiGetCall& call)
+  static void GetInstanceTagsInternal(RestApiGetCall& call,
+                                      bool whole)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
@@ -571,56 +573,90 @@
 
     std::set<DicomTag> ignoreTagLength;
     ParseSetOfTags(ignoreTagLength, call, IGNORE_LENGTH);
-    
-    if (format != DicomToJsonFormat_Full ||
-        !ignoreTagLength.empty())
+
+    if (whole)
     {
-      Json::Value full;
-      context.ReadDicomAsJson(full, publicId, ignoreTagLength);
-      AnswerDicomAsJson(call, full, format);
+      // This is new in Orthanc 1.12.4. Reference:
+      // https://discourse.orthanc-server.org/t/private-tags-with-group-7fe0-are-not-provided-via-rest-api/4744
+      const DicomToJsonFlags flags = static_cast<DicomToJsonFlags>(DicomToJsonFlags_Default & ~DicomToJsonFlags_StopAfterPixelData);
+
+      Json::Value answer;
+
+      {
+        ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId);
+        locker.GetDicom().DatasetToJson(answer, format, flags,
+                                        ORTHANC_MAXIMUM_TAG_LENGTH, ignoreTagLength);
+      }
+
+      call.GetOutput().AnswerJson(answer);
     }
     else
     {
-      // This path allows one to avoid the JSON decoding if no
-      // simplification is asked, and if no "ignore-length" argument
-      // is present
-      Json::Value full;
-      context.ReadDicomAsJson(full, publicId);
-      call.GetOutput().AnswerJson(full);
+      if (format != DicomToJsonFormat_Full ||
+          !ignoreTagLength.empty())
+      {
+        Json::Value full;
+        context.ReadDicomAsJson(full, publicId, ignoreTagLength);
+        AnswerDicomAsJson(call, full, format);
+      }
+      else
+      {
+        // This path allows one to avoid the JSON decoding if no
+        // simplification is asked, and if no "ignore-length" argument
+        // is present
+        Json::Value full;
+        context.ReadDicomAsJson(full, publicId);
+        call.GetOutput().AnswerJson(full);
+      }
     }
   }
 
 
+  static void DocumentGetInstanceTags(RestApiGetCall& call)
+  {
+    call.GetDocumentation()
+      .SetTag("Instances")
+      .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest")
+      .SetHttpGetArgument(
+        IGNORE_LENGTH, RestApiCallDocumentation::Type_JsonListOfStrings,
+        "Also include the DICOM tags that are provided in this list, even if their associated value is long", false)
+      .SetHttpGetArgument(
+        ARG_WHOLE, RestApiCallDocumentation::Type_Boolean, "Whether to read the whole DICOM file from the "
+        "storage area (new in Orthanc 1.12.4). If set to \"false\" (default value), the DICOM file is read "
+        "until the pixel data tag (7fe0,0010) to optimize access to storage. Setting the option "
+        "to \"true\" provides access to the DICOM tags stored after the pixel data tag.", false)
+      .AddAnswerType(MimeType_Json, "JSON object containing the DICOM tags and their associated value");
+  }
+
+
   static void GetInstanceTags(RestApiGetCall& call)
   {
     if (call.IsDocumentation())
     {
       OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Full);
+      DocumentGetInstanceTags(call);
       call.GetDocumentation()
-        .SetTag("Instances")
         .SetSummary("Get DICOM tags")
         .SetDescription("Get the DICOM tags in the specified format. By default, the `full` format is used, which "
                         "combines hexadecimal tags with human-readable description.")
-        .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest")
-        .SetHttpGetArgument(IGNORE_LENGTH, RestApiCallDocumentation::Type_JsonListOfStrings,
-                            "Also include the DICOM tags that are provided in this list, even if their associated value is long", false)
-        .AddAnswerType(MimeType_Json, "JSON object containing the DICOM tags and their associated value")
         .SetTruncatedJsonHttpGetSample("https://orthanc.uclouvain.be/demo/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/tags", 10);
       return;
     }
 
+    const bool whole = call.GetBooleanArgument(ARG_WHOLE, false);
+
     switch (OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Full))
     {
       case DicomToJsonFormat_Human:
-        GetInstanceTagsInternal<DicomToJsonFormat_Human>(call);
+        GetInstanceTagsInternal<DicomToJsonFormat_Human>(call, whole);
         break;
 
       case DicomToJsonFormat_Short:
-        GetInstanceTagsInternal<DicomToJsonFormat_Short>(call);
+        GetInstanceTagsInternal<DicomToJsonFormat_Short>(call, whole);
         break;
 
       case DicomToJsonFormat_Full:
-        GetInstanceTagsInternal<DicomToJsonFormat_Full>(call);
+        GetInstanceTagsInternal<DicomToJsonFormat_Full>(call, whole);
         break;
 
       default:
@@ -633,20 +669,16 @@
   {
     if (call.IsDocumentation())
     {
+      DocumentGetInstanceTags(call);
       call.GetDocumentation()
-        .SetTag("Instances")
         .SetSummary("Get human-readable tags")
         .SetDescription("Get the DICOM tags in human-readable format (same as the `/instances/{id}/tags?simplify` route)")
-        .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest")
-        .SetHttpGetArgument(IGNORE_LENGTH, RestApiCallDocumentation::Type_JsonListOfStrings,
-                            "Also include the DICOM tags that are provided in this list, even if their associated value is long", false)
-        .AddAnswerType(MimeType_Json, "JSON object containing the DICOM tags and their associated value")
         .SetTruncatedJsonHttpGetSample("https://orthanc.uclouvain.be/demo/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/simplified-tags", 10);
       return;
     }
     else
     {
-      GetInstanceTagsInternal<DicomToJsonFormat_Human>(call);
+      GetInstanceTagsInternal<DicomToJsonFormat_Human>(call, call.GetBooleanArgument(ARG_WHOLE, false));
     }
   }
 
@@ -3757,7 +3789,7 @@
       call.GetDocumentation()
         .SetRequestField(LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS, RestApiCallDocumentation::Type_Boolean,
                         "Only reconstruct this level MainDicomTags by re-reading them from a random child instance of the resource. "
-                        "This option is much faster than a full reconstruct and is usefull e.g. if you have modified the "
+                        "This option is much faster than a full reconstruct and is useful e.g. if you have modified the "
                         "'ExtraMainDicomTags' at the Study level to optimize the speed of some C-Find. "
                         "'false' by default. (New in Orthanc 1.12.4)", false);
     }
--- a/OrthancServer/Sources/main.cpp	Thu May 30 21:27:48 2024 +0200
+++ b/OrthancServer/Sources/main.cpp	Thu Jun 06 13:24:04 2024 +0200
@@ -59,8 +59,10 @@
 static const char* const KEY_DICOM_TLS_ENABLED = "DicomTlsEnabled";
 static const char* const KEY_DICOM_TLS_CERTIFICATE = "DicomTlsCertificate";
 static const char* const KEY_DICOM_TLS_TRUSTED_CERTIFICATES = "DicomTlsTrustedCertificates";
+static const char* const KEY_DICOM_TLS_REMOTE_CERTIFICATE_REQUIRED = "DicomTlsRemoteCertificateRequired";
+static const char* const KEY_DICOM_TLS_MINIMUM_PROTOCOL_VERSION = "DicomTlsMinimumProtocolVersion";
+static const char* const KEY_DICOM_TLS_ACCEPTED_CIPHERS = "DicomTlsCiphersAccepted";
 static const char* const KEY_MAXIMUM_PDU_LENGTH = "MaximumPduLength";
-static const char* const KEY_DICOM_TLS_REMOTE_CERTIFICATE_REQUIRED = "DicomTlsRemoteCertificateRequired";
 
 
 class OrthancStoreRequestHandler : public IStoreRequestHandler
@@ -1279,6 +1281,12 @@
           lock.GetConfiguration().GetStringParameter(KEY_DICOM_TLS_CERTIFICATE, ""));
         dicomServer.SetTrustedCertificatesPath(
           lock.GetConfiguration().GetStringParameter(KEY_DICOM_TLS_TRUSTED_CERTIFICATES, ""));
+        dicomServer.SetMinimumTlsVersion(
+          lock.GetConfiguration().GetUnsignedIntegerParameter(KEY_DICOM_TLS_MINIMUM_PROTOCOL_VERSION, 0));
+        
+        std::set<std::string> acceptedCiphers;
+        lock.GetConfiguration().GetSetOfStringsParameter(acceptedCiphers, KEY_DICOM_TLS_ACCEPTED_CIPHERS);
+        dicomServer.SetAcceptedCiphers(acceptedCiphers);
       }
 
       dicomServer.SetMaximumPduLength(lock.GetConfiguration().GetUnsignedIntegerParameter(KEY_MAXIMUM_PDU_LENGTH, 16384));
--- a/OrthancServer/UnitTestsSources/UnitTestsMain.cpp	Thu May 30 21:27:48 2024 +0200
+++ b/OrthancServer/UnitTestsSources/UnitTestsMain.cpp	Thu Jun 06 13:24:04 2024 +0200
@@ -242,42 +242,53 @@
   Json::Value dicomAsJson;
   OrthancConfiguration::DefaultDicomDatasetToJson(dicomAsJson, toStore->GetParsedDicomFile());
   
-  DicomMap m;
-  m.FromDicomAsJson(dicomAsJson);
+  { // without parsing sequences
+    DicomMap m;
+    m.FromDicomAsJson(dicomAsJson);
 
-  ASSERT_EQ("ISO_IR 100", m.GetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET).GetContent());
-  
-  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).IsBinary());
-  ASSERT_EQ("Hello", m.GetValue(DICOM_TAG_PATIENT_NAME).GetContent());
-  
-  ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_DESCRIPTION).IsBinary());
-  ASSERT_EQ(utf8, m.GetValue(DICOM_TAG_STUDY_DESCRIPTION).GetContent());
+    ASSERT_EQ("ISO_IR 100", m.GetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET).GetContent());
+    
+    ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).IsBinary());
+    ASSERT_EQ("Hello", m.GetValue(DICOM_TAG_PATIENT_NAME).GetContent());
+    
+    ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_DESCRIPTION).IsBinary());
+    ASSERT_EQ(utf8, m.GetValue(DICOM_TAG_STUDY_DESCRIPTION).GetContent());
 
-  ASSERT_FALSE(m.HasTag(DICOM_TAG_MANUFACTURER));                // Too long
-  ASSERT_FALSE(m.HasTag(DICOM_TAG_PIXEL_DATA));                  // Pixel data
-  ASSERT_FALSE(m.HasTag(DICOM_TAG_REFERENCED_SERIES_SEQUENCE));  // Sequence
-  ASSERT_EQ(DICOM_TAG_REFERENCED_SERIES_SEQUENCE.GetGroup(), DCM_ReferencedSeriesSequence.getGroup());
-  ASSERT_EQ(DICOM_TAG_REFERENCED_SERIES_SEQUENCE.GetElement(), DCM_ReferencedSeriesSequence.getElement());
+    ASSERT_FALSE(m.HasTag(DICOM_TAG_MANUFACTURER));                // Too long
+    ASSERT_FALSE(m.HasTag(DICOM_TAG_PIXEL_DATA));                  // Pixel data
+    ASSERT_FALSE(m.HasTag(DICOM_TAG_REFERENCED_SERIES_SEQUENCE));  // Sequence
+    ASSERT_EQ(DICOM_TAG_REFERENCED_SERIES_SEQUENCE.GetGroup(), DCM_ReferencedSeriesSequence.getGroup());
+    ASSERT_EQ(DICOM_TAG_REFERENCED_SERIES_SEQUENCE.GetElement(), DCM_ReferencedSeriesSequence.getElement());
+
+    ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_DESCRIPTION));  // Maximum length
+    ASSERT_FALSE(m.GetValue(DICOM_TAG_SERIES_DESCRIPTION).IsBinary());
+    ASSERT_EQ(ORTHANC_MAXIMUM_TAG_LENGTH,
+              static_cast<int>(m.GetValue(DICOM_TAG_SERIES_DESCRIPTION).GetContent().length()));
 
-  ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_DESCRIPTION));  // Maximum length
-  ASSERT_FALSE(m.GetValue(DICOM_TAG_SERIES_DESCRIPTION).IsBinary());
-  ASSERT_EQ(ORTHANC_MAXIMUM_TAG_LENGTH,
-            static_cast<int>(m.GetValue(DICOM_TAG_SERIES_DESCRIPTION).GetContent().length()));
+    ASSERT_FALSE(m.GetValue(DICOM_TAG_ROWS).IsBinary());
+    ASSERT_EQ("512", m.GetValue(DICOM_TAG_ROWS).GetContent());
 
-  ASSERT_FALSE(m.GetValue(DICOM_TAG_ROWS).IsBinary());
-  ASSERT_EQ("512", m.GetValue(DICOM_TAG_ROWS).GetContent());
+    ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_ID).IsNull());
+    ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_ID).IsBinary());
+    ASSERT_EQ("", m.GetValue(DICOM_TAG_STUDY_ID).GetContent());
+
+    DicomArray a(m);
+    ASSERT_EQ(6u, a.GetSize());
 
-  ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_ID).IsNull());
-  ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_ID).IsBinary());
-  ASSERT_EQ("", m.GetValue(DICOM_TAG_STUDY_ID).GetContent());
+    
+    //dicom.SaveToFile("/tmp/test.dcm"); 
+    //std::cout << toStore.GetJson() << std::endl;
+    //a.Print(stdout);
+  }
 
-  DicomArray a(m);
-  ASSERT_EQ(6u, a.GetSize());
+  { // now parses sequences
+    // LOG(INFO) << dicomAsJson.toStyledString();
 
-  
-  //dicom.SaveToFile("/tmp/test.dcm"); 
-  //std::cout << toStore.GetJson() << std::endl;
-  //a.Print(stdout);
+    DicomMap m;
+    m.FromDicomAsJson(dicomAsJson, false, true /* parseSequences */);
+
+    ASSERT_TRUE(m.HasTag(DICOM_TAG_REFERENCED_SERIES_SEQUENCE));
+  }
 }
 
 
--- a/OrthancServer/UnitTestsSources/VersionsTests.cpp	Thu May 30 21:27:48 2024 +0200
+++ b/OrthancServer/UnitTestsSources/VersionsTests.cpp	Thu Jun 06 13:24:04 2024 +0200
@@ -113,7 +113,7 @@
 
 TEST(Versions, BoostStatic)
 {
-  ASSERT_TRUE(std::string(BOOST_LIB_VERSION) == "1_84" ||
+  ASSERT_TRUE(std::string(BOOST_LIB_VERSION) == "1_85" ||
               std::string(BOOST_LIB_VERSION) == "1_69" /* if USE_LEGACY_BOOST */);
 }
 
--- a/TODO	Thu May 30 21:27:48 2024 +0200
+++ b/TODO	Thu Jun 06 13:24:04 2024 +0200
@@ -126,7 +126,12 @@
   https://groups.google.com/g/orthanc-users/c/o15Dekecgds/m/xmPE2y3bAwAJ
 * Support Palette PNG in /tools/create-dicom:
   https://discourse.orthanc-server.org/t/404-on-tools-create-dicom-endpoint-with-specific-png/3562
-* Support creation of DICOM files from MP4 in /tools/create-dicom
+* Support creation of DICOM files from MP4 in /tools/create-dicom.
+  Sample python code: https://github.com/salimkanoun/OrthancGif/blob/new-organisation/python/create_dicom_video.py.
+  We would need to extract frame rate + dimension from the MP4 which would
+  require ffmpeg or a similar library -> can not be done in the Orthanc core.
+  -> keep it for a python plugin
+  -> or require the payload to include rows/columns/cinerate/...
 * (1) In the /studies/{id}/anonymize route, add an option to remove
   secondary captures.  They usually contains Patient info in the
   image. The SOPClassUID might be used to identify such secondary