view OrthancFramework/Sources/DicomNetworking/DicomServer.cpp @ 5911:bfae0fc2ea1b get-scu-test

Started to work on handling errors as warnings when trying to store instances whose SOPClassUID has not been accepted during the negotiation. Work to be finalized later
author Alain Mazy <am@orthanc.team>
date Mon, 09 Dec 2024 10:07:19 +0100
parents 9d27024a431f
children
line wrap: on
line source

/**
 * Orthanc - A Lightweight, RESTful DICOM Store
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2023 Osimis S.A., Belgium
 * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
 * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program. If not, see
 * <http://www.gnu.org/licenses/>.
 **/


#include "../PrecompiledHeaders.h"
#include "DicomServer.h"

#include "../Logging.h"
#include "../MultiThreading/RunnableWorkersPool.h"
#include "../OrthancException.h"
#include "../SystemToolbox.h"
#include "../Toolbox.h"
#include "DicomAssociationParameters.h"
#include "Internals/CommandDispatcher.h"

#include <boost/thread.hpp>

#if ORTHANC_ENABLE_SSL == 1
#  include "Internals/DicomTls.h"
#endif

#if defined(__linux__)
#  include <cstdlib>
#endif


namespace Orthanc
{
  struct DicomServer::PImpl
  {
    boost::thread  thread_;
    T_ASC_Network *network_;
    std::unique_ptr<RunnableWorkersPool>  workers_;

#if ORTHANC_ENABLE_SSL == 1
    std::unique_ptr<DcmTLSTransportLayer> tls_;
#endif
  };


  void DicomServer::ServerThread(DicomServer* server,
                                 unsigned int maximumPduLength,
                                 bool useDicomTls)
  {
    Logging::SetCurrentThreadName("DICOM-SERVER");
    CLOG(INFO, DICOM) << "DICOM server started";

    while (server->continue_)
    {
      /* receive an association and acknowledge or reject it. If the association was */
      /* acknowledged, offer corresponding services and invoke one or more if required. */
      std::unique_ptr<Internals::CommandDispatcher> dispatcher(
        Internals::AcceptAssociation(*server, server->pimpl_->network_, maximumPduLength, useDicomTls));

      try
      {
        if (dispatcher.get() != NULL)
        {
          server->pimpl_->workers_->Add(dispatcher.release());
        }
      }
      catch (OrthancException& e)
      {
        CLOG(ERROR, DICOM) << "Exception in the DICOM server thread: " << e.What();
      }
    }

    CLOG(INFO, DICOM) << "DICOM server stopping";
  }


  DicomServer::DicomServer() : 
    pimpl_(new PImpl),
    checkCalledAet_(true),
    aet_("ANY-SCP"),
    port_(104),
    continue_(false),
    associationTimeout_(30),
    threadsCount_(4),
    modalities_(NULL),
    findRequestHandlerFactory_(NULL),
    moveRequestHandlerFactory_(NULL),
    getRequestHandlerFactory_(NULL),
    storeRequestHandlerFactory_(NULL),
    worklistRequestHandlerFactory_(NULL),
    storageCommitmentFactory_(NULL),
    applicationEntityFilter_(NULL),
    useDicomTls_(false),
    maximumPduLength_(ASC_DEFAULTMAXPDU),
    remoteCertificateRequired_(true),
    minimumTlsVersion_(0)
  {
  }

  DicomServer::~DicomServer()
  {
    if (continue_)
    {
      CLOG(ERROR, DICOM) << "INTERNAL ERROR: DicomServer::Stop() should be invoked manually to avoid mess in the destruction order!";
      Stop();
    }
  }

  void DicomServer::SetPortNumber(uint16_t port)
  {
    Stop();
    port_ = port;
  }

  uint16_t DicomServer::GetPortNumber() const
  {
    return port_;
  }

  void DicomServer::SetAssociationTimeout(uint32_t seconds)
  {
    CLOG(INFO, DICOM) << "Setting timeout for DICOM connections if Orthanc acts as SCP (server): " 
                      << seconds << " seconds (0 = no timeout)";

    Stop();
    associationTimeout_ = seconds;
  }

  uint32_t DicomServer::GetAssociationTimeout() const
  {
    return associationTimeout_;
  }


  void DicomServer::SetCalledApplicationEntityTitleCheck(bool check)
  {
    Stop();
    checkCalledAet_ = check;
  }

  bool DicomServer::HasCalledApplicationEntityTitleCheck() const
  {
    return checkCalledAet_;
  }

  void DicomServer::SetApplicationEntityTitle(const std::string& aet)
  {
    if (aet.size() == 0)
    {
      throw OrthancException(ErrorCode_BadApplicationEntityTitle);
    }

    if (aet.size() > 16)
    {
      throw OrthancException(ErrorCode_BadApplicationEntityTitle);
    }

    for (size_t i = 0; i < aet.size(); i++)
    {
      if (!(aet[i] == '-' ||
            aet[i] == '_' ||
            isdigit(aet[i]) ||
            (aet[i] >= 'A' && aet[i] <= 'Z')))
      {
        CLOG(WARNING, DICOM) << "For best interoperability, only upper case, alphanumeric characters should be present in AET: \"" << aet << "\"";
        break;
      }
    }

    Stop();
    aet_ = aet;
  }

  const std::string& DicomServer::GetApplicationEntityTitle() const
  {
    return aet_;
  }

  void DicomServer::SetRemoteModalities(IRemoteModalities& modalities)
  {
    Stop();
    modalities_ = &modalities;
  }
  
  DicomServer::IRemoteModalities& DicomServer::GetRemoteModalities() const
  {
    if (modalities_ == NULL)
    {
      throw OrthancException(ErrorCode_BadSequenceOfCalls);
    }
    else
    {
      return *modalities_;
    }
  }
    
  void DicomServer::SetFindRequestHandlerFactory(IFindRequestHandlerFactory& factory)
  {
    Stop();
    findRequestHandlerFactory_ = &factory;
  }

  bool DicomServer::HasFindRequestHandlerFactory() const
  {
    return (findRequestHandlerFactory_ != NULL);
  }

  IFindRequestHandlerFactory& DicomServer::GetFindRequestHandlerFactory() const
  {
    if (HasFindRequestHandlerFactory())
    {
      return *findRequestHandlerFactory_;
    }
    else
    {
      throw OrthancException(ErrorCode_NoCFindHandler);
    }
  }

  void DicomServer::SetMoveRequestHandlerFactory(IMoveRequestHandlerFactory& factory)
  {
    Stop();
    moveRequestHandlerFactory_ = &factory;
  }

  bool DicomServer::HasMoveRequestHandlerFactory() const
  {
    return (moveRequestHandlerFactory_ != NULL);
  }

  IMoveRequestHandlerFactory& DicomServer::GetMoveRequestHandlerFactory() const
  {
    if (HasMoveRequestHandlerFactory())
    {
      return *moveRequestHandlerFactory_;
    }
    else
    {
      throw OrthancException(ErrorCode_NoCMoveHandler);
    }
  }

  void DicomServer::SetGetRequestHandlerFactory(IGetRequestHandlerFactory& factory)
  {
    Stop();
    getRequestHandlerFactory_ = &factory;
  }

  bool DicomServer::HasGetRequestHandlerFactory() const
  {
    return (getRequestHandlerFactory_ != NULL);
  }

  IGetRequestHandlerFactory& DicomServer::GetGetRequestHandlerFactory() const
  {
    if (HasGetRequestHandlerFactory())
    {
      return *getRequestHandlerFactory_;
    }
    else
    {
      throw OrthancException(ErrorCode_NoCGetHandler);
    }
  }

  void DicomServer::SetStoreRequestHandlerFactory(IStoreRequestHandlerFactory& factory)
  {
    Stop();
    storeRequestHandlerFactory_ = &factory;
  }

  bool DicomServer::HasStoreRequestHandlerFactory() const
  {
    return (storeRequestHandlerFactory_ != NULL);
  }

  IStoreRequestHandlerFactory& DicomServer::GetStoreRequestHandlerFactory() const
  {
    if (HasStoreRequestHandlerFactory())
    {
      return *storeRequestHandlerFactory_;
    }
    else
    {
      throw OrthancException(ErrorCode_NoCStoreHandler);
    }
  }

  void DicomServer::SetWorklistRequestHandlerFactory(IWorklistRequestHandlerFactory& factory)
  {
    Stop();
    worklistRequestHandlerFactory_ = &factory;
  }

  bool DicomServer::HasWorklistRequestHandlerFactory() const
  {
    return (worklistRequestHandlerFactory_ != NULL);
  }

  IWorklistRequestHandlerFactory& DicomServer::GetWorklistRequestHandlerFactory() const
  {
    if (HasWorklistRequestHandlerFactory())
    {
      return *worklistRequestHandlerFactory_;
    }
    else
    {
      throw OrthancException(ErrorCode_NoWorklistHandler);
    }
  }

  void DicomServer::SetStorageCommitmentRequestHandlerFactory(IStorageCommitmentRequestHandlerFactory& factory)
  {
    Stop();
    storageCommitmentFactory_ = &factory;
  }

  bool DicomServer::HasStorageCommitmentRequestHandlerFactory() const
  {
    return (storageCommitmentFactory_ != NULL);
  }

  IStorageCommitmentRequestHandlerFactory& DicomServer::GetStorageCommitmentRequestHandlerFactory() const
  {
    if (HasStorageCommitmentRequestHandlerFactory())
    {
      return *storageCommitmentFactory_;
    }
    else
    {
      throw OrthancException(ErrorCode_NoStorageCommitmentHandler);
    }
  }

  void DicomServer::SetApplicationEntityFilter(IApplicationEntityFilter& factory)
  {
    Stop();
    applicationEntityFilter_ = &factory;
  }

  bool DicomServer::HasApplicationEntityFilter() const
  {
    return (applicationEntityFilter_ != NULL);
  }

  IApplicationEntityFilter& DicomServer::GetApplicationEntityFilter() const
  {
    if (HasApplicationEntityFilter())
    {
      return *applicationEntityFilter_;
    }
    else
    {
      throw OrthancException(ErrorCode_NoApplicationEntityFilter);
    }
  }


  void DicomServer::Start()
  {
    if (modalities_ == NULL)
    {
      throw OrthancException(ErrorCode_BadSequenceOfCalls,
                             "No list of modalities was provided to the DICOM server");
    }

    if (useDicomTls_)
    {
      if (ownCertificatePath_.empty() ||
          ownPrivateKeyPath_.empty())
      {
        throw OrthancException(ErrorCode_ParameterOutOfRange,
                               "DICOM TLS is enabled in Orthanc SCP, but no certificate was provided");
      }
    }
    
    Stop();

    /* initialize network, i.e. create an instance of T_ASC_Network*. */
    OFCondition cond = ASC_initializeNetwork
      (NET_ACCEPTOR, OFstatic_cast(int, port_), /*opt_acse_timeout*/ 30, &pimpl_->network_);
    if (cond.bad())
    {
      throw OrthancException(ErrorCode_DicomPortInUse,
                             " (port = " + boost::lexical_cast<std::string>(port_) +
                             ") cannot create network: " + std::string(cond.text()));
    }

#if ORTHANC_ENABLE_SSL == 1
    assert(pimpl_->tls_.get() == NULL);

    if (useDicomTls_)
    {
      CLOG(INFO, DICOM) << "Orthanc SCP will use DICOM TLS";

      try
      {
        pimpl_->tls_.reset(Internals::InitializeDicomTls(
                             pimpl_->network_, NET_ACCEPTOR, ownPrivateKeyPath_, ownCertificatePath_,
                             trustedCertificatesPath_, remoteCertificateRequired_, minimumTlsVersion_, acceptedCiphers_));
      }
      catch (OrthancException&)
      {
        ASC_dropNetwork(&pimpl_->network_);
        throw;
      }
    }
    else
    {
      CLOG(INFO, DICOM) << "Orthanc SCP will *not* use DICOM TLS";
    }
#else
    CLOG(INFO, DICOM) << "Orthanc SCP will *not* use DICOM TLS";
#endif

    continue_ = true;

    CLOG(INFO, DICOM) << "The embedded DICOM server will use " << threadsCount_ << " threads";

    pimpl_->workers_.reset(new RunnableWorkersPool(threadsCount_, "DICOM-"));
    pimpl_->thread_ = boost::thread(ServerThread, this, maximumPduLength_, useDicomTls_);
  }


  void DicomServer::Stop()
  {
    if (continue_)
    {
      continue_ = false;

      if (pimpl_->thread_.joinable())
      {
        pimpl_->thread_.join();
      }

      pimpl_->workers_.reset(NULL);

#if ORTHANC_ENABLE_SSL == 1
      pimpl_->tls_.reset(NULL);  // Transport layer must be destroyed before the association itself
#endif

      /* drop the network, i.e. free memory of T_ASC_Network* structure. This call */
      /* is the counterpart of ASC_initializeNetwork(...) which was called above. */
      OFCondition cond = ASC_dropNetwork(&pimpl_->network_);
      if (cond.bad())
      {
        CLOG(ERROR, DICOM) << "Error while dropping the network: " << cond.text();
      }
    }
  }


  bool DicomServer::IsMyAETitle(const std::string& aet) const
  {
    if (modalities_ == NULL)
    {
      throw OrthancException(ErrorCode_BadSequenceOfCalls);
    }
    
    if (!HasCalledApplicationEntityTitleCheck())
    {
      // OK, no check on the AET.
      return true;
    }
    else
    {
      return modalities_->IsSameAETitle(aet, GetApplicationEntityTitle());
    }
  }


  void DicomServer::SetDicomTlsEnabled(bool enabled)
  {
    Stop();
    useDicomTls_ = enabled;
  }
  
  bool DicomServer::IsDicomTlsEnabled() const
  {
    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)
  {
    Stop();

    if (!privateKeyPath.empty() &&
        !certificatePath.empty())
    {
      CLOG(INFO, DICOM) << "Setting the TLS certificate for DICOM SCP connections: " 
                        << privateKeyPath << " (key), " << certificatePath << " (certificate)";

      if (certificatePath.empty())
      {
        throw OrthancException(ErrorCode_ParameterOutOfRange, "No path to the default DICOM TLS certificate was provided");
      }
      
      if (privateKeyPath.empty())
      {
        throw OrthancException(ErrorCode_ParameterOutOfRange,
                               "No path to the private key for the default DICOM TLS certificate was provided");
      }
      
      if (!SystemToolbox::IsRegularFile(privateKeyPath))
      {
        throw OrthancException(ErrorCode_InexistentFile, "Inexistent file: " + privateKeyPath);
      }

      if (!SystemToolbox::IsRegularFile(certificatePath))
      {
        throw OrthancException(ErrorCode_InexistentFile, "Inexistent file: " + certificatePath);
      }
      
      ownPrivateKeyPath_ = privateKeyPath;
      ownCertificatePath_ = certificatePath;
    }
    else
    {
      ownPrivateKeyPath_.clear();
      ownCertificatePath_.clear();
    }
  }
  
  const std::string& DicomServer::GetOwnPrivateKeyPath() const
  {
    return ownPrivateKeyPath_;
  }
  
  const std::string& DicomServer::GetOwnCertificatePath() const
  {
    return ownCertificatePath_;
  }
    
  void DicomServer::SetTrustedCertificatesPath(const std::string& path)
  {
    Stop();

    if (!path.empty())
    {
      CLOG(INFO, DICOM) << "Setting the trusted certificates for DICOM SCP connections: " << path;

      if (!SystemToolbox::IsRegularFile(path))
      {
        throw OrthancException(ErrorCode_InexistentFile, "Inexistent file: " + path);
      }
      
      trustedCertificatesPath_ = path;
    }
    else
    {
      trustedCertificatesPath_.clear();
    }
  }
  
  const std::string& DicomServer::GetTrustedCertificatesPath() const
  {
    return trustedCertificatesPath_;
  }

  unsigned int DicomServer::GetMaximumPduLength() const
  {
    return maximumPduLength_;
  }

  void DicomServer::SetMaximumPduLength(unsigned int pdu)
  {
    DicomAssociationParameters::CheckMaximumPduLength(pdu);

    Stop();
    maximumPduLength_ = pdu;
  }

  void DicomServer::SetRemoteCertificateRequired(bool required)
  {
    Stop();
    remoteCertificateRequired_ = required;
  }
  
  bool DicomServer::IsRemoteCertificateRequired() const
  {
    return remoteCertificateRequired_;
  }

  void DicomServer::SetThreadsCount(unsigned int threads)
  {
    if (threads == 0)
    {
      throw OrthancException(ErrorCode_ParameterOutOfRange);
    }
    
    Stop();
    threadsCount_ = threads;
  }

}