1006 lines
34 KiB
C++
1006 lines
34 KiB
C++
/**
|
|
* 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-2025 Orthanc Team SRL, Belgium
|
|
* Copyright (C) 2021-2025 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 "DicomAssociation.h"
|
|
|
|
#if !defined(DCMTK_VERSION_NUMBER)
|
|
# error The macro DCMTK_VERSION_NUMBER must be defined
|
|
#endif
|
|
|
|
#include "../Compatibility.h"
|
|
#include "../Logging.h"
|
|
#include "../OrthancException.h"
|
|
#include "NetworkingCompatibility.h"
|
|
|
|
#ifdef _WIN32
|
|
# include <winsock.h>
|
|
#endif
|
|
|
|
#include <dcmtk/dcmnet/diutil.h> // For dcmConnectionTimeout()
|
|
#include <dcmtk/dcmdata/dcdeftag.h>
|
|
|
|
namespace Orthanc
|
|
{
|
|
static void FillSopSequence(DcmDataset& dataset,
|
|
const DcmTagKey& tag,
|
|
const std::vector<std::string>& sopClassUids,
|
|
const std::vector<std::string>& sopInstanceUids,
|
|
const std::vector<StorageCommitmentFailureReason>& failureReasons,
|
|
bool hasFailureReasons)
|
|
{
|
|
assert(sopClassUids.size() == sopInstanceUids.size() &&
|
|
(hasFailureReasons ?
|
|
failureReasons.size() == sopClassUids.size() :
|
|
failureReasons.empty()));
|
|
|
|
if (sopInstanceUids.empty())
|
|
{
|
|
// Add an empty sequence
|
|
if (!dataset.insertEmptyElement(tag).good())
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (size_t i = 0; i < sopClassUids.size(); i++)
|
|
{
|
|
std::unique_ptr<DcmItem> item(new DcmItem);
|
|
if (!item->putAndInsertString(DCM_ReferencedSOPClassUID, sopClassUids[i].c_str()).good() ||
|
|
!item->putAndInsertString(DCM_ReferencedSOPInstanceUID, sopInstanceUids[i].c_str()).good() ||
|
|
(hasFailureReasons &&
|
|
!item->putAndInsertUint16(DCM_FailureReason, failureReasons[i]).good()) ||
|
|
!dataset.insertSequenceItem(tag, item.release()).good())
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void DicomAssociation::CheckConnecting(const DicomAssociationParameters& parameters,
|
|
const OFCondition& cond)
|
|
{
|
|
try
|
|
{
|
|
if (cond.bad() &&
|
|
cond == DUL_ASSOCIATIONREJECTED)
|
|
{
|
|
T_ASC_RejectParameters rej;
|
|
ASC_getRejectParameters(params_, &rej);
|
|
|
|
OFString str;
|
|
CLOG(TRACE, DICOM) << "Association Rejected:" << std::endl
|
|
<< ASC_printRejectParameters(str, &rej);
|
|
}
|
|
|
|
CheckCondition(cond, parameters, "connecting");
|
|
}
|
|
catch (OrthancException&)
|
|
{
|
|
CloseInternal();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
|
|
void DicomAssociation::CloseInternal()
|
|
{
|
|
CLOG(INFO, DICOM) << "Closing DICOM association";
|
|
|
|
#if ORTHANC_ENABLE_SSL == 1
|
|
tls_.reset(NULL); // Transport layer must be destroyed before the association itself
|
|
#endif
|
|
|
|
if (assoc_ != NULL)
|
|
{
|
|
ASC_releaseAssociation(assoc_);
|
|
ASC_destroyAssociation(&assoc_);
|
|
assoc_ = NULL;
|
|
params_ = NULL;
|
|
}
|
|
else
|
|
{
|
|
if (params_ != NULL)
|
|
{
|
|
ASC_destroyAssociationParameters(¶ms_);
|
|
params_ = NULL;
|
|
}
|
|
}
|
|
|
|
if (net_ != NULL)
|
|
{
|
|
ASC_dropNetwork(&net_);
|
|
net_ = NULL;
|
|
}
|
|
|
|
accepted_.clear();
|
|
isOpen_ = false;
|
|
}
|
|
|
|
|
|
void DicomAssociation::AddAccepted(const std::string& abstractSyntax,
|
|
DicomTransferSyntax syntax,
|
|
uint8_t presentationContextId)
|
|
{
|
|
AcceptedPresentationContexts::iterator found = accepted_.find(abstractSyntax);
|
|
|
|
if (found == accepted_.end())
|
|
{
|
|
std::map<DicomTransferSyntax, uint8_t> syntaxes;
|
|
syntaxes[syntax] = presentationContextId;
|
|
accepted_[abstractSyntax] = syntaxes;
|
|
}
|
|
else
|
|
{
|
|
if (found->second.find(syntax) != found->second.end())
|
|
{
|
|
CLOG(WARNING, DICOM) << "The same transfer syntax ("
|
|
<< GetTransferSyntaxUid(syntax)
|
|
<< ") was accepted twice for the same abstract syntax UID ("
|
|
<< abstractSyntax << ")";
|
|
}
|
|
else
|
|
{
|
|
found->second[syntax] = presentationContextId;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
DicomAssociation::DicomAssociation()
|
|
{
|
|
isOpen_ = false;
|
|
net_ = NULL;
|
|
params_ = NULL;
|
|
assoc_ = NULL;
|
|
|
|
// Must be after "isOpen_ = false"
|
|
ClearPresentationContexts();
|
|
}
|
|
|
|
|
|
DicomAssociation::~DicomAssociation()
|
|
{
|
|
try
|
|
{
|
|
Close();
|
|
}
|
|
catch (OrthancException& e)
|
|
{
|
|
// Don't throw exception in destructors
|
|
CLOG(ERROR, DICOM) << "Error while destroying a DICOM association: " << e.What();
|
|
}
|
|
}
|
|
|
|
|
|
void DicomAssociation::ClearPresentationContexts()
|
|
{
|
|
Close();
|
|
proposed_.clear();
|
|
proposed_.reserve(MAX_PROPOSED_PRESENTATIONS);
|
|
}
|
|
|
|
|
|
static T_ASC_SC_ROLE GetDcmtkRole(DicomAssociationRole role)
|
|
{
|
|
switch (role)
|
|
{
|
|
case DicomAssociationRole_Default:
|
|
return ASC_SC_ROLE_DEFAULT;
|
|
|
|
case DicomAssociationRole_Scu:
|
|
return ASC_SC_ROLE_SCU;
|
|
|
|
case DicomAssociationRole_Scp:
|
|
return ASC_SC_ROLE_SCP;
|
|
|
|
default:
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
}
|
|
}
|
|
|
|
|
|
void DicomAssociation::Open(const DicomAssociationParameters& parameters)
|
|
{
|
|
if (isOpen_)
|
|
{
|
|
return; // Already open
|
|
}
|
|
|
|
// Timeout used during association negociation and ASC_releaseAssociation()
|
|
uint32_t acseTimeout = parameters.GetTimeout();
|
|
if (acseTimeout == 0)
|
|
{
|
|
/**
|
|
* Timeout is disabled. Global timeout (seconds) for
|
|
* connecting to remote hosts. Default value is -1 which
|
|
* selects infinite timeout, i.e. blocking connect().
|
|
**/
|
|
dcmConnectionTimeout.set(-1);
|
|
acseTimeout = 10;
|
|
}
|
|
else
|
|
{
|
|
dcmConnectionTimeout.set(acseTimeout);
|
|
}
|
|
|
|
|
|
assert(net_ == NULL &&
|
|
params_ == NULL &&
|
|
assoc_ == NULL);
|
|
|
|
#if ORTHANC_ENABLE_SSL == 1
|
|
assert(tls_.get() == NULL);
|
|
#endif
|
|
|
|
if (proposed_.empty())
|
|
{
|
|
throw OrthancException(ErrorCode_BadSequenceOfCalls,
|
|
"No presentation context was proposed");
|
|
}
|
|
|
|
std::string localAet = parameters.GetLocalApplicationEntityTitle();
|
|
if (parameters.GetRemoteModality().HasLocalAet())
|
|
{
|
|
localAet = parameters.GetRemoteModality().GetLocalAet();
|
|
}
|
|
|
|
CLOG(INFO, DICOM) << "Opening a DICOM SCU connection "
|
|
<< (parameters.GetRemoteModality().IsDicomTlsEnabled() ? "using DICOM TLS" : "without DICOM TLS")
|
|
<< " from AET \"" << localAet
|
|
<< "\" to AET \"" << parameters.GetRemoteModality().GetApplicationEntityTitle()
|
|
<< "\" on host " << parameters.GetRemoteModality().GetHost()
|
|
<< ":" << parameters.GetRemoteModality().GetPortNumber()
|
|
<< " (manufacturer: " << EnumerationToString(parameters.GetRemoteModality().GetManufacturer())
|
|
<< ", " << (parameters.HasTimeout() ?
|
|
"timeout: " + boost::lexical_cast<std::string>(parameters.GetTimeout()) + "s" :
|
|
"no timeout") << ")";
|
|
|
|
CheckConnecting(parameters, ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ acseTimeout, &net_));
|
|
#if DCMTK_VERSION_NUMBER >= 368
|
|
CheckConnecting(parameters, ASC_createAssociationParameters(¶ms_, parameters.GetMaximumPduLength(), acseTimeout));
|
|
#else
|
|
// from 3.6.8, this version is obsolete
|
|
CheckConnecting(parameters, ASC_createAssociationParameters(¶ms_, parameters.GetMaximumPduLength()));
|
|
#endif
|
|
|
|
#if ORTHANC_ENABLE_SSL == 1
|
|
if (parameters.GetRemoteModality().IsDicomTlsEnabled())
|
|
{
|
|
try
|
|
{
|
|
assert(net_ != NULL &&
|
|
params_ != NULL);
|
|
tls_.reset(Internals::InitializeDicomTls(net_, NET_REQUESTOR, parameters.GetOwnPrivateKeyPath(),
|
|
parameters.GetOwnCertificatePath(),
|
|
parameters.GetTrustedCertificatesPath(),
|
|
parameters.IsRemoteCertificateRequired(),
|
|
parameters.GetMinimumTlsVersion(),
|
|
parameters.GetAcceptedCiphers()));
|
|
}
|
|
catch (OrthancException&)
|
|
{
|
|
CloseInternal();
|
|
throw;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Set this application's title and the called application's title in the params
|
|
CheckConnecting(parameters, ASC_setAPTitles(
|
|
params_, localAet.c_str(),
|
|
parameters.GetRemoteModality().GetApplicationEntityTitle().c_str(), NULL));
|
|
|
|
// Set the network addresses of the local and remote entities
|
|
char localHost[HOST_NAME_MAX];
|
|
gethostname(localHost, HOST_NAME_MAX - 1);
|
|
|
|
char remoteHostAndPort[HOST_NAME_MAX];
|
|
|
|
#ifdef _MSC_VER
|
|
_snprintf
|
|
#else
|
|
snprintf
|
|
#endif
|
|
(remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d",
|
|
parameters.GetRemoteModality().GetHost().c_str(),
|
|
parameters.GetRemoteModality().GetPortNumber());
|
|
|
|
CheckConnecting(parameters, ASC_setPresentationAddresses(params_, localHost, remoteHostAndPort));
|
|
|
|
// Set various options
|
|
#if ORTHANC_ENABLE_SSL == 1
|
|
CheckConnecting(parameters, ASC_setTransportLayerType(params_, (tls_.get() != NULL) /*opt_secureConnection*/));
|
|
#else
|
|
CheckConnecting(parameters, ASC_setTransportLayerType(params_, false /*opt_secureConnection*/));
|
|
#endif
|
|
|
|
// Setup the list of proposed presentation contexts
|
|
unsigned int presentationContextId = 1;
|
|
for (size_t i = 0; i < proposed_.size(); i++)
|
|
{
|
|
assert(presentationContextId <= 255);
|
|
const char* abstractSyntax = proposed_[i].abstractSyntax_.c_str();
|
|
|
|
const std::list<DicomTransferSyntax>& source = proposed_[i].transferSyntaxes_;
|
|
|
|
std::vector<const char*> transferSyntaxes;
|
|
transferSyntaxes.reserve(source.size());
|
|
|
|
for (std::list<DicomTransferSyntax>::const_iterator
|
|
it = source.begin(); it != source.end(); ++it)
|
|
{
|
|
transferSyntaxes.push_back(GetTransferSyntaxUid(*it));
|
|
}
|
|
|
|
assert(!transferSyntaxes.empty());
|
|
CheckConnecting(parameters, ASC_addPresentationContext(
|
|
params_, presentationContextId, abstractSyntax,
|
|
&transferSyntaxes[0], transferSyntaxes.size(), GetDcmtkRole(proposed_[i].role_)));
|
|
|
|
presentationContextId += 2;
|
|
}
|
|
|
|
{
|
|
OFString str;
|
|
CLOG(TRACE, DICOM) << "Request Parameters:" << std::endl
|
|
<< ASC_dumpParameters(str, params_, ASC_ASSOC_RQ);
|
|
}
|
|
|
|
// Do the association
|
|
CheckConnecting(parameters, ASC_requestAssociation(net_, params_, &assoc_));
|
|
isOpen_ = true;
|
|
|
|
{
|
|
OFString str;
|
|
CLOG(TRACE, DICOM) << "Connection Parameters: "
|
|
<< ASC_dumpConnectionParameters(str, assoc_);
|
|
CLOG(TRACE, DICOM) << "Association Parameters Negotiated:" << std::endl
|
|
<< ASC_dumpParameters(str, params_, ASC_ASSOC_AC);
|
|
}
|
|
|
|
|
|
// Inspect the accepted transfer syntaxes
|
|
LST_HEAD **l = ¶ms_->DULparams.acceptedPresentationContext;
|
|
if (*l != NULL)
|
|
{
|
|
DUL_PRESENTATIONCONTEXT* pc = (DUL_PRESENTATIONCONTEXT*) LST_Head(l);
|
|
LST_Position(l, (LST_NODE*)pc);
|
|
while (pc)
|
|
{
|
|
if (pc->result == ASC_P_ACCEPTANCE && strlen(pc->abstractSyntax) > 0)
|
|
{
|
|
CLOG(TRACE, DICOM) << "DicomAssociation::Open, adding SOPClassUID " << pc->abstractSyntax << " - TS " << pc->acceptedTransferSyntax << " - PC ID " << boost::lexical_cast<std::string>(static_cast<int>(pc->presentationContextID));
|
|
|
|
DicomTransferSyntax transferSyntax;
|
|
if (LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax))
|
|
{
|
|
AddAccepted(pc->abstractSyntax, transferSyntax, pc->presentationContextID);
|
|
}
|
|
else
|
|
{
|
|
CLOG(WARNING, DICOM) << "Unknown transfer syntax received from AET \""
|
|
<< parameters.GetRemoteModality().GetApplicationEntityTitle()
|
|
<< "\": " << pc->acceptedTransferSyntax;
|
|
}
|
|
}
|
|
|
|
pc = (DUL_PRESENTATIONCONTEXT*) LST_Next(l);
|
|
}
|
|
}
|
|
|
|
if (accepted_.empty())
|
|
{
|
|
throw OrthancException(ErrorCode_NoPresentationContext,
|
|
"Unable to negotiate a presentation context with AET \"" +
|
|
parameters.GetRemoteModality().GetApplicationEntityTitle() + "\"");
|
|
}
|
|
}
|
|
|
|
void DicomAssociation::Close()
|
|
{
|
|
if (isOpen_)
|
|
{
|
|
CloseInternal();
|
|
}
|
|
}
|
|
|
|
|
|
bool DicomAssociation::LookupAcceptedPresentationContext(std::map<DicomTransferSyntax, uint8_t>& target,
|
|
const std::string& abstractSyntax) const
|
|
{
|
|
if (!IsOpen())
|
|
{
|
|
throw OrthancException(ErrorCode_BadSequenceOfCalls, "Connection not opened");
|
|
}
|
|
|
|
AcceptedPresentationContexts::const_iterator found = accepted_.find(abstractSyntax);
|
|
|
|
if (found == accepted_.end())
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
target = found->second;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void DicomAssociation::ProposeGenericPresentationContext(const std::string& abstractSyntax,
|
|
DicomAssociationRole role)
|
|
{
|
|
std::list<DicomTransferSyntax> ts;
|
|
ts.push_back(DicomTransferSyntax_LittleEndianExplicit); // the most standard one first !
|
|
ts.push_back(DicomTransferSyntax_LittleEndianImplicit);
|
|
ts.push_back(DicomTransferSyntax_BigEndianExplicit); // Retired but was historicaly proposed by Orthanc
|
|
ProposePresentationContext(abstractSyntax, ts, role);
|
|
}
|
|
|
|
void DicomAssociation::ProposeGenericPresentationContext(const std::string& abstractSyntax)
|
|
{
|
|
ProposeGenericPresentationContext(abstractSyntax, DicomAssociationRole_Default);
|
|
}
|
|
|
|
|
|
void DicomAssociation::ProposePresentationContext(const std::string& abstractSyntax,
|
|
DicomTransferSyntax transferSyntax)
|
|
{
|
|
ProposePresentationContext(abstractSyntax, transferSyntax, DicomAssociationRole_Default);
|
|
}
|
|
|
|
|
|
void DicomAssociation::ProposePresentationContext(const std::string& abstractSyntax,
|
|
DicomTransferSyntax transferSyntax,
|
|
DicomAssociationRole role)
|
|
{
|
|
std::list<DicomTransferSyntax> ts;
|
|
ts.push_back(transferSyntax);
|
|
ProposePresentationContext(abstractSyntax, ts, role);
|
|
}
|
|
|
|
size_t DicomAssociation::GetRemainingPropositions() const
|
|
{
|
|
assert(proposed_.size() <= MAX_PROPOSED_PRESENTATIONS);
|
|
return MAX_PROPOSED_PRESENTATIONS - proposed_.size();
|
|
}
|
|
|
|
void DicomAssociation::ProposePresentationContext(
|
|
const std::string& abstractSyntax,
|
|
const std::list<DicomTransferSyntax>& transferSyntaxes)
|
|
{
|
|
ProposePresentationContext(abstractSyntax, transferSyntaxes, DicomAssociationRole_Default);
|
|
}
|
|
|
|
|
|
void DicomAssociation::ProposePresentationContext(
|
|
const std::string& abstractSyntax,
|
|
const std::list<DicomTransferSyntax>& transferSyntaxes,
|
|
DicomAssociationRole role)
|
|
{
|
|
if (transferSyntaxes.empty())
|
|
{
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange,
|
|
"No transfer syntax provided");
|
|
}
|
|
|
|
if (proposed_.size() >= MAX_PROPOSED_PRESENTATIONS)
|
|
{
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange,
|
|
"Too many proposed presentation contexts");
|
|
}
|
|
|
|
if (IsOpen())
|
|
{
|
|
Close();
|
|
}
|
|
|
|
ProposedPresentationContext context;
|
|
context.abstractSyntax_ = abstractSyntax;
|
|
context.transferSyntaxes_ = transferSyntaxes;
|
|
context.role_ = role;
|
|
|
|
proposed_.push_back(context);
|
|
}
|
|
|
|
|
|
T_ASC_Association& DicomAssociation::GetDcmtkAssociation() const
|
|
{
|
|
if (isOpen_)
|
|
{
|
|
assert(assoc_ != NULL);
|
|
return *assoc_;
|
|
}
|
|
else
|
|
{
|
|
throw OrthancException(ErrorCode_BadSequenceOfCalls,
|
|
"The connection is not open");
|
|
}
|
|
}
|
|
|
|
bool DicomAssociation::GetAssociationParameters(std::string& remoteAet,
|
|
std::string& remoteIp,
|
|
std::string& calledAet) const
|
|
{
|
|
T_ASC_Association& dcmtkAssoc = GetDcmtkAssociation();
|
|
|
|
DIC_AE remoteAet_C;
|
|
DIC_AE calledAet_C;
|
|
DIC_AE remoteIp_C;
|
|
DIC_AE calledIP_C;
|
|
|
|
if (
|
|
#if DCMTK_VERSION_NUMBER >= 364
|
|
ASC_getAPTitles(dcmtkAssoc.params, remoteAet_C, sizeof(remoteAet_C), calledAet_C, sizeof(calledAet_C), NULL, 0).good() &&
|
|
ASC_getPresentationAddresses(dcmtkAssoc.params, remoteIp_C, sizeof(remoteIp_C), calledIP_C, sizeof(calledIP_C)).good()
|
|
#else
|
|
ASC_getAPTitles(dcmtkAssoc.params, remoteAet_C, calledAet_C, NULL).good() &&
|
|
ASC_getPresentationAddresses(dcmtkAssoc.params, remoteIp_C, calledIP_C).good()
|
|
#endif
|
|
)
|
|
{
|
|
remoteIp = std::string(/*OFSTRING_GUARD*/(remoteIp_C));
|
|
remoteAet = std::string(/*OFSTRING_GUARD*/(remoteAet_C));
|
|
calledAet = (/*OFSTRING_GUARD*/(calledAet_C));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
T_ASC_Network& DicomAssociation::GetDcmtkNetwork() const
|
|
{
|
|
if (isOpen_)
|
|
{
|
|
assert(net_ != NULL);
|
|
return *net_;
|
|
}
|
|
else
|
|
{
|
|
throw OrthancException(ErrorCode_BadSequenceOfCalls,
|
|
"The connection is not open");
|
|
}
|
|
}
|
|
|
|
|
|
void DicomAssociation::CheckCondition(const OFCondition& cond,
|
|
const DicomAssociationParameters& parameters,
|
|
const std::string& command)
|
|
{
|
|
if (cond.bad())
|
|
{
|
|
// Reformat the error message from DCMTK by turning multiline
|
|
// errors into a single line
|
|
|
|
std::string s(cond.text());
|
|
std::string info;
|
|
info.reserve(s.size());
|
|
|
|
bool isMultiline = false;
|
|
for (size_t i = 0; i < s.size(); i++)
|
|
{
|
|
if (s[i] == '\r')
|
|
{
|
|
// Ignore
|
|
}
|
|
else if (s[i] == '\n')
|
|
{
|
|
if (isMultiline)
|
|
{
|
|
info += "; ";
|
|
}
|
|
else
|
|
{
|
|
info += " (";
|
|
isMultiline = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
info.push_back(s[i]);
|
|
}
|
|
}
|
|
|
|
if (isMultiline)
|
|
{
|
|
info += ")";
|
|
}
|
|
|
|
throw OrthancException(ErrorCode_NetworkProtocol,
|
|
"DicomAssociation - " + command + " to AET \"" +
|
|
parameters.GetRemoteModality().GetApplicationEntityTitle() +
|
|
"\": " + info);
|
|
}
|
|
}
|
|
|
|
|
|
void DicomAssociation::ReportStorageCommitment(
|
|
const DicomAssociationParameters& parameters,
|
|
const std::string& transactionUid,
|
|
const std::vector<std::string>& sopClassUids,
|
|
const std::vector<std::string>& sopInstanceUids,
|
|
const std::vector<StorageCommitmentFailureReason>& failureReasons)
|
|
{
|
|
if (sopClassUids.size() != sopInstanceUids.size() ||
|
|
sopClassUids.size() != failureReasons.size())
|
|
{
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
}
|
|
|
|
|
|
std::vector<std::string> successSopClassUids, successSopInstanceUids, failedSopClassUids, failedSopInstanceUids;
|
|
std::vector<StorageCommitmentFailureReason> failedReasons;
|
|
|
|
successSopClassUids.reserve(sopClassUids.size());
|
|
successSopInstanceUids.reserve(sopClassUids.size());
|
|
failedSopClassUids.reserve(sopClassUids.size());
|
|
failedSopInstanceUids.reserve(sopClassUids.size());
|
|
failedReasons.reserve(sopClassUids.size());
|
|
|
|
for (size_t i = 0; i < sopClassUids.size(); i++)
|
|
{
|
|
switch (failureReasons[i])
|
|
{
|
|
case StorageCommitmentFailureReason_Success:
|
|
successSopClassUids.push_back(sopClassUids[i]);
|
|
successSopInstanceUids.push_back(sopInstanceUids[i]);
|
|
break;
|
|
|
|
case StorageCommitmentFailureReason_ProcessingFailure:
|
|
case StorageCommitmentFailureReason_NoSuchObjectInstance:
|
|
case StorageCommitmentFailureReason_ResourceLimitation:
|
|
case StorageCommitmentFailureReason_ReferencedSOPClassNotSupported:
|
|
case StorageCommitmentFailureReason_ClassInstanceConflict:
|
|
case StorageCommitmentFailureReason_DuplicateTransactionUID:
|
|
failedSopClassUids.push_back(sopClassUids[i]);
|
|
failedSopInstanceUids.push_back(sopInstanceUids[i]);
|
|
failedReasons.push_back(failureReasons[i]);
|
|
break;
|
|
|
|
default:
|
|
{
|
|
char buf[16];
|
|
sprintf(buf, "%04xH", failureReasons[i]);
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange,
|
|
"Unsupported failure reason for storage commitment: " + std::string(buf));
|
|
}
|
|
}
|
|
}
|
|
|
|
DicomAssociation association;
|
|
|
|
{
|
|
std::list<DicomTransferSyntax> transferSyntaxes;
|
|
transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianExplicit);
|
|
transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianImplicit);
|
|
|
|
association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass,
|
|
transferSyntaxes, DicomAssociationRole_Scp);
|
|
}
|
|
|
|
association.Open(parameters);
|
|
|
|
/**
|
|
* N-EVENT-REPORT
|
|
* http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
|
|
* http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-1
|
|
*
|
|
* Status code:
|
|
* http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8
|
|
**/
|
|
|
|
/**
|
|
* Send the "EVENT_REPORT_RQ" request
|
|
**/
|
|
|
|
CLOG(INFO, DICOM) << "Reporting modality \""
|
|
<< parameters.GetRemoteModality().GetApplicationEntityTitle()
|
|
<< "\" about storage commitment transaction: " << transactionUid
|
|
<< " (" << successSopClassUids.size() << " successes, "
|
|
<< failedSopClassUids.size() << " failures)";
|
|
const DIC_US messageId = association.GetDcmtkAssociation().nextMsgID++;
|
|
|
|
{
|
|
T_DIMSE_Message message;
|
|
memset(&message, 0, sizeof(message));
|
|
message.CommandField = DIMSE_N_EVENT_REPORT_RQ;
|
|
|
|
T_DIMSE_N_EventReportRQ& content = message.msg.NEventReportRQ;
|
|
content.MessageID = messageId;
|
|
strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
|
|
strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
|
|
content.DataSetType = DIMSE_DATASET_PRESENT;
|
|
|
|
DcmDataset dataset;
|
|
if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good())
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
|
|
{
|
|
std::vector<StorageCommitmentFailureReason> empty;
|
|
FillSopSequence(dataset, DCM_ReferencedSOPSequence, successSopClassUids,
|
|
successSopInstanceUids, empty, false);
|
|
}
|
|
|
|
// http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
|
|
if (failedSopClassUids.empty())
|
|
{
|
|
content.EventTypeID = 1; // "Storage Commitment Request Successful"
|
|
}
|
|
else
|
|
{
|
|
content.EventTypeID = 2; // "Storage Commitment Request Complete - Failures Exist"
|
|
|
|
// Failure reason
|
|
// http://dicom.nema.org/medical/dicom/2019a/output/chtml/part03/sect_C.14.html#sect_C.14.1.1
|
|
FillSopSequence(dataset, DCM_FailedSOPSequence, failedSopClassUids,
|
|
failedSopInstanceUids, failedReasons, true);
|
|
}
|
|
|
|
int presID = ASC_findAcceptedPresentationContextID(
|
|
&association.GetDcmtkAssociation(), UID_StorageCommitmentPushModelSOPClass);
|
|
if (presID == 0)
|
|
{
|
|
throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
|
|
"Unable to send N-EVENT-REPORT request to AET: " +
|
|
parameters.GetRemoteModality().GetApplicationEntityTitle());
|
|
}
|
|
|
|
{
|
|
std::stringstream s; // DcmObject::PrintHelper cannot be used with VS2008
|
|
dataset.print(s);
|
|
|
|
OFString str;
|
|
CLOG(TRACE, DICOM) << "Sending Storage Commitment Report:" << std::endl
|
|
<< DIMSE_dumpMessage(str, message, DIMSE_OUTGOING) << std::endl
|
|
<< s.str();
|
|
}
|
|
|
|
if (!DIMSE_sendMessageUsingMemoryData(
|
|
&association.GetDcmtkAssociation(), presID, &message, NULL /* status detail */,
|
|
&dataset, NULL /* callback */, NULL /* callback context */,
|
|
NULL /* commandSet */).good())
|
|
{
|
|
throw OrthancException(ErrorCode_NetworkProtocol);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read the "EVENT_REPORT_RSP" response
|
|
**/
|
|
|
|
{
|
|
T_ASC_PresentationContextID presID = 0;
|
|
T_DIMSE_Message message;
|
|
|
|
if (!DIMSE_receiveCommand(&association.GetDcmtkAssociation(),
|
|
(parameters.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
|
|
parameters.GetTimeout(), &presID, &message,
|
|
NULL /* no statusDetail */).good() ||
|
|
message.CommandField != DIMSE_N_EVENT_REPORT_RSP)
|
|
{
|
|
throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
|
|
"Unable to read N-EVENT-REPORT response from AET: " +
|
|
parameters.GetRemoteModality().GetApplicationEntityTitle());
|
|
}
|
|
|
|
{
|
|
OFString str;
|
|
CLOG(TRACE, DICOM) << "Received Storage Commitment Report Response:" << std::endl
|
|
<< DIMSE_dumpMessage(str, message, DIMSE_INCOMING, NULL, presID);
|
|
}
|
|
|
|
const T_DIMSE_N_EventReportRSP& content = message.msg.NEventReportRSP;
|
|
if (content.MessageIDBeingRespondedTo != messageId ||
|
|
!(content.opts & O_NEVENTREPORT_AFFECTEDSOPCLASSUID) ||
|
|
!(content.opts & O_NEVENTREPORT_AFFECTEDSOPINSTANCEUID) ||
|
|
//(content.opts & O_NEVENTREPORT_EVENTTYPEID) || // Pedantic test - The "content.EventTypeID" is not used by Orthanc
|
|
std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
|
|
std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance ||
|
|
content.DataSetType != DIMSE_DATASET_NULL)
|
|
{
|
|
throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
|
|
"Badly formatted N-EVENT-REPORT response from AET: " +
|
|
parameters.GetRemoteModality().GetApplicationEntityTitle());
|
|
}
|
|
|
|
if (content.DimseStatus != 0 /* success */)
|
|
{
|
|
throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
|
|
"The request cannot be handled by remote AET: " +
|
|
parameters.GetRemoteModality().GetApplicationEntityTitle());
|
|
}
|
|
}
|
|
|
|
association.Close();
|
|
}
|
|
|
|
|
|
void DicomAssociation::RequestStorageCommitment(
|
|
const DicomAssociationParameters& parameters,
|
|
const std::string& transactionUid,
|
|
const std::vector<std::string>& sopClassUids,
|
|
const std::vector<std::string>& sopInstanceUids)
|
|
{
|
|
if (sopClassUids.size() != sopInstanceUids.size())
|
|
{
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
}
|
|
|
|
for (size_t i = 0; i < sopClassUids.size(); i++)
|
|
{
|
|
if (sopClassUids[i].empty() ||
|
|
sopInstanceUids[i].empty())
|
|
{
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange,
|
|
"The SOP class/instance UIDs cannot be empty, found: \"" +
|
|
sopClassUids[i] + "\" / \"" + sopInstanceUids[i] + "\"");
|
|
}
|
|
}
|
|
|
|
if (transactionUid.size() < 5 ||
|
|
transactionUid.substr(0, 5) != "2.25.")
|
|
{
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
}
|
|
|
|
DicomAssociation association;
|
|
|
|
{
|
|
std::list<DicomTransferSyntax> transferSyntaxes;
|
|
transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianExplicit);
|
|
transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianImplicit);
|
|
|
|
// association.SetRole(DicomAssociationRole_Default);
|
|
association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass,
|
|
transferSyntaxes, DicomAssociationRole_Default);
|
|
}
|
|
|
|
association.Open(parameters);
|
|
|
|
/**
|
|
* N-ACTION
|
|
* http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html
|
|
* http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-4
|
|
*
|
|
* Status code:
|
|
* http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8
|
|
**/
|
|
|
|
/**
|
|
* Send the "N_ACTION_RQ" request
|
|
**/
|
|
|
|
CLOG(INFO, DICOM) << "Request to modality \""
|
|
<< parameters.GetRemoteModality().GetApplicationEntityTitle()
|
|
<< "\" about storage commitment for " << sopClassUids.size()
|
|
<< " instances, with transaction UID: " << transactionUid;
|
|
const DIC_US messageId = association.GetDcmtkAssociation().nextMsgID++;
|
|
|
|
{
|
|
T_DIMSE_Message message;
|
|
memset(&message, 0, sizeof(message));
|
|
message.CommandField = DIMSE_N_ACTION_RQ;
|
|
|
|
T_DIMSE_N_ActionRQ& content = message.msg.NActionRQ;
|
|
content.MessageID = messageId;
|
|
strncpy(content.RequestedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
|
|
strncpy(content.RequestedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
|
|
content.ActionTypeID = 1; // "Request Storage Commitment"
|
|
content.DataSetType = DIMSE_DATASET_PRESENT;
|
|
|
|
DcmDataset dataset;
|
|
if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good())
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
|
|
{
|
|
std::vector<StorageCommitmentFailureReason> empty;
|
|
FillSopSequence(dataset, DCM_ReferencedSOPSequence, sopClassUids, sopInstanceUids, empty, false);
|
|
}
|
|
|
|
int presID = ASC_findAcceptedPresentationContextID(
|
|
&association.GetDcmtkAssociation(), UID_StorageCommitmentPushModelSOPClass);
|
|
if (presID == 0)
|
|
{
|
|
throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
|
|
"Unable to send N-ACTION request to AET: " +
|
|
parameters.GetRemoteModality().GetApplicationEntityTitle());
|
|
}
|
|
|
|
{
|
|
std::stringstream s; // DcmObject::PrintHelper cannot be used with VS2008
|
|
dataset.print(s);
|
|
|
|
OFString str;
|
|
CLOG(TRACE, DICOM) << "Sending Storage Commitment Request:" << std::endl
|
|
<< DIMSE_dumpMessage(str, message, DIMSE_OUTGOING) << std::endl
|
|
<< s.str();
|
|
}
|
|
|
|
if (!DIMSE_sendMessageUsingMemoryData(
|
|
&association.GetDcmtkAssociation(), presID, &message, NULL /* status detail */,
|
|
&dataset, NULL /* callback */, NULL /* callback context */,
|
|
NULL /* commandSet */).good())
|
|
{
|
|
throw OrthancException(ErrorCode_NetworkProtocol);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read the "N_ACTION_RSP" response
|
|
**/
|
|
|
|
{
|
|
T_ASC_PresentationContextID presID = 0;
|
|
T_DIMSE_Message message;
|
|
|
|
if (!DIMSE_receiveCommand(&association.GetDcmtkAssociation(),
|
|
(parameters.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
|
|
parameters.GetTimeout(), &presID, &message,
|
|
NULL /* no statusDetail */).good() ||
|
|
message.CommandField != DIMSE_N_ACTION_RSP)
|
|
{
|
|
throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
|
|
"Unable to read N-ACTION response from AET: " +
|
|
parameters.GetRemoteModality().GetApplicationEntityTitle());
|
|
}
|
|
|
|
const T_DIMSE_N_ActionRSP& content = message.msg.NActionRSP;
|
|
if (content.MessageIDBeingRespondedTo != messageId ||
|
|
!(content.opts & O_NACTION_AFFECTEDSOPCLASSUID) ||
|
|
!(content.opts & O_NACTION_AFFECTEDSOPINSTANCEUID) ||
|
|
//(content.opts & O_NACTION_ACTIONTYPEID) || // Pedantic test - The "content.ActionTypeID" is not used by Orthanc
|
|
std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
|
|
std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance ||
|
|
content.DataSetType != DIMSE_DATASET_NULL)
|
|
{
|
|
throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
|
|
"Badly formatted N-ACTION response from AET: " +
|
|
parameters.GetRemoteModality().GetApplicationEntityTitle());
|
|
}
|
|
|
|
{
|
|
OFString str;
|
|
CLOG(TRACE, DICOM) << "Received Storage Commitment Request Response:" << std::endl
|
|
<< DIMSE_dumpMessage(str, message, DIMSE_INCOMING, NULL, presID);
|
|
}
|
|
|
|
if (content.DimseStatus != 0 /* success */)
|
|
{
|
|
throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
|
|
"The request cannot be handled by remote AET: " +
|
|
parameters.GetRemoteModality().GetApplicationEntityTitle());
|
|
}
|
|
}
|
|
|
|
association.Close();
|
|
}
|
|
}
|