# HG changeset patch # User Alain Mazy # Date 1790254616 -7200 # Node ID 8c8be0a58e462263040b51e41ea4816f31b83027 # Parent 12bcfedb323aac2b94597ef9c746a7f537034bf2 Now rejecting Storage Commitment Report (N-EVENT-REPORT) if they don't perfectly match the request initiated by Orthanc (N-ACTION) diff -r 12bcfedb323a -r 8c8be0a58e46 NEWS --- a/NEWS Wed Sep 23 15:47:45 2026 +0200 +++ b/NEWS Thu Sep 24 14:56:56 2026 +0200 @@ -18,6 +18,8 @@ ----------- * Fixed a TCP socket leak when a DICOM association is received with an invalid calling AET. +* Now rejecting Storage Commitment Report (N-EVENT-REPORT) if they don't perfectly match the + request initiated by Orthanc (N-ACTION) * Added more tolerance to invalid OW value representations * Fix use of the static runtime under Visual Studio (/MT), which broke in 1.12.10 * New CMake options: diff -r 12bcfedb323a -r 8c8be0a58e46 OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp --- a/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp Wed Sep 23 15:47:45 2026 +0200 +++ b/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp Thu Sep 24 14:56:56 2026 +0200 @@ -87,6 +87,7 @@ #include "GetScp.h" #include "MoveScp.h" #include "StoreScp.h" +#include "../DimseErrorPayload.h" #include /* for storage commitment */ #include /* for class DcmSequenceOfItems */ @@ -1363,8 +1364,16 @@ { CLOG(ERROR, DICOM) << "Error while processing an incoming storage commitment report: " << e.What(); - // Code 0x0110 - "General failure in processing the operation was encountered" - dimseStatus = STATUS_N_ProcessingFailure; + if (e.GetPayload().HasContent() && + e.GetPayload().GetType() == ErrorPayloadType_Dimse) + { + dimseStatus = GetDimseErrorStatusFromPayload(e.GetPayload()); + } + else + { + // Code 0x0110 - "General failure in processing the operation was encountered" + dimseStatus = STATUS_N_ProcessingFailure; + } } diff -r 12bcfedb323a -r 8c8be0a58e46 OrthancServer/Plugins/Samples/MultitenantDicom/OrthancFrameworkDependencies.cpp --- a/OrthancServer/Plugins/Samples/MultitenantDicom/OrthancFrameworkDependencies.cpp Wed Sep 23 15:47:45 2026 +0200 +++ b/OrthancServer/Plugins/Samples/MultitenantDicom/OrthancFrameworkDependencies.cpp Thu Sep 24 14:56:56 2026 +0200 @@ -52,6 +52,7 @@ #include "../../../../OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.cpp" #include "../../../../OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp" #include "../../../../OrthancFramework/Sources/DicomNetworking/DicomServer.cpp" +#include "../../../../OrthancFramework/Sources/DicomNetworking/DimseErrorPayload.cpp" #include "../../../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp" #include "../../../../OrthancFramework/Sources/DicomNetworking/Internals/FindScp.cpp" #include "../../../../OrthancFramework/Sources/DicomNetworking/Internals/GetScp.cpp" diff -r 12bcfedb323a -r 8c8be0a58e46 OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp Wed Sep 23 15:47:45 2026 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp Thu Sep 24 14:56:56 2026 +0200 @@ -2517,9 +2517,22 @@ // Create a "pending" storage commitment report BEFORE the // actual SCU call in order to avoid race conditions + std::unique_ptr request(new StorageCommitmentReports::Report(remoteAet)); + + std::list::const_iterator itInstanceUid = sopInstanceUids.begin(); + std::list::const_iterator itClassUid = sopClassUids.begin(); + + while (itInstanceUid != sopInstanceUids.end() && itClassUid != sopClassUids.end()) + { + request->AddRequestedInstance(*itClassUid, *itInstanceUid); + + ++itInstanceUid; + ++itClassUid; + } + context.GetStorageCommitmentReports().Store( - transactionUid, new StorageCommitmentReports::Report(remoteAet)); - + transactionUid, request.release()); + DicomAssociationParameters parameters(localAet, remote); InjectAssociationTimeout(parameters, json); diff -r 12bcfedb323a -r 8c8be0a58e46 OrthancServer/Sources/StorageCommitmentReports.cpp --- a/OrthancServer/Sources/StorageCommitmentReports.cpp Wed Sep 23 15:47:45 2026 +0200 +++ b/OrthancServer/Sources/StorageCommitmentReports.cpp Thu Sep 24 14:56:56 2026 +0200 @@ -41,6 +41,18 @@ } } + void StorageCommitmentReports::Report::AddRequestedInstance(const std::string& sopClassUid, + const std::string& sopInstanceUid) + { + if (isComplete_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + requetsedInstances_[sopInstanceUid] = sopClassUid; + } + + void StorageCommitmentReports::Report::AddSuccess(const std::string& sopClassUid, const std::string& sopInstanceUid) { @@ -180,6 +192,12 @@ } + const std::map& StorageCommitmentReports::Report::GetRequestedInstancesAndSopClasses() const + { + return requetsedInstances_; + } + + StorageCommitmentReports::~StorageCommitmentReports() { while (!content_.IsEmpty()) diff -r 12bcfedb323a -r 8c8be0a58e46 OrthancServer/Sources/StorageCommitmentReports.h --- a/OrthancServer/Sources/StorageCommitmentReports.h Wed Sep 23 15:47:45 2026 +0200 +++ b/OrthancServer/Sources/StorageCommitmentReports.h Thu Sep 24 14:56:56 2026 +0200 @@ -35,6 +35,8 @@ class Report : public boost::noncopyable { public: + typedef std::map RequestedInstancesAndSopClasses; + enum Status { Status_Success, @@ -60,6 +62,7 @@ std::list success_; std::list failures_; std::string remoteAet_; + RequestedInstancesAndSopClasses requetsedInstances_; public: explicit Report(const std::string& remoteAet) : @@ -75,6 +78,9 @@ void MarkAsComplete(); + void AddRequestedInstance(const std::string& sopClassUid, + const std::string& sopInstanceUid); + void AddSuccess(const std::string& sopClassUid, const std::string& sopInstanceUid); @@ -87,6 +93,8 @@ void Format(Json::Value& json) const; void GetSuccessSopInstanceUids(std::vector& target) const; + + const RequestedInstancesAndSopClasses& GetRequestedInstancesAndSopClasses() const; }; private: diff -r 12bcfedb323a -r 8c8be0a58e46 OrthancServer/Sources/main.cpp --- a/OrthancServer/Sources/main.cpp Wed Sep 23 15:47:45 2026 +0200 +++ b/OrthancServer/Sources/main.cpp Thu Sep 24 14:56:56 2026 +0200 @@ -31,6 +31,7 @@ #include "../../OrthancFramework/Sources/DicomFormat/DicomArray.h" #include "../../OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h" #include "../../OrthancFramework/Sources/DicomNetworking/DicomServer.h" +#include "../../OrthancFramework/Sources/DicomNetworking/DimseErrorPayload.h" #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h" #include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h" @@ -174,21 +175,91 @@ THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_InternalError); } - std::unique_ptr report( - new StorageCommitmentReports::Report(connection.GetRemoteAet())); - - for (size_t i = 0; i < successSopClassUids.size(); i++) + std::unique_ptr report; + { - report->AddSuccess(successSopClassUids[i], successSopInstanceUids[i]); + Orthanc::StorageCommitmentReports::Accessor requestAccessor(context_.GetStorageCommitmentReports(), transactionUid); + if (!requestAccessor.IsValid()) + { + throw OrthancException(ErrorCode_UnknownResource, + std::string("This Storage Commitment transaction UID has not been initiated by this Orthanc or is too old: ") + transactionUid + ).SetPayload(MakeDimseErrorStatusPayload(STATUS_N_UnrecognizedOperation)); + } + + if (requestAccessor.GetReport().GetRemoteAet() != connection.GetRemoteAet()) + { + throw OrthancException(ErrorCode_UnknownResource, + std::string("This Storage Commitment transaction is invalid: the remote AET the N-EVENT-REPORT orginates from (") + connection.GetRemoteAet() + ") does not match the AET the request was sent to (" + requestAccessor.GetReport().GetRemoteAet() + ")" + ); + } + + const StorageCommitmentReports::Report::RequestedInstancesAndSopClasses& requestedInstances = requestAccessor.GetReport().GetRequestedInstancesAndSopClasses(); + // make sure all requestedInstances have been answered: store all requested instances ids in a set and remove it from the set when they have been answered. + std::set remainingRequestedInstancesIds; + StorageCommitmentReports::Report::RequestedInstancesAndSopClasses::const_iterator it = requestedInstances.begin(); + for (; it != requestedInstances.end(); ++it) + { + remainingRequestedInstancesIds.insert(it->first); + } + + report.reset(new StorageCommitmentReports::Report(connection.GetRemoteAet())); + + for (size_t i = 0; i < successSopClassUids.size(); i++) + { + StorageCommitmentReports::Report::RequestedInstancesAndSopClasses::const_iterator it = requestedInstances.find(successSopInstanceUids[i]); + + if (it == requestedInstances.end()) + { + throw OrthancException(ErrorCode_UnknownResource, + std::string("This (success) SOPInstanceUID (") + successSopInstanceUids[i] + ") was not requested in the Storage Commitment transaction " + transactionUid + ).SetPayload(MakeDimseErrorStatusPayload(STATUS_N_InvalidSOPInstance)); + } + + if (it->second != successSopClassUids[i]) + { + throw OrthancException(ErrorCode_UnknownResource, + std::string("This (success) SOPInstanceUID (") + successSopInstanceUids[i] + ") was not requested with this SOPClassUID (" + successSopClassUids[i] + ") in the Storage Commitment transaction " + transactionUid + ).SetPayload(MakeDimseErrorStatusPayload(STATUS_N_ClassInstanceConflict)); + } + + report->AddSuccess(successSopClassUids[i], successSopInstanceUids[i]); + remainingRequestedInstancesIds.erase(successSopInstanceUids[i]); + } + + for (size_t i = 0; i < failedSopClassUids.size(); i++) + { + StorageCommitmentReports::Report::RequestedInstancesAndSopClasses::const_iterator it = requestedInstances.find(successSopInstanceUids[i]); + + if (it == requestedInstances.end()) + { + throw OrthancException(ErrorCode_UnknownResource, + std::string("This (failed) SOPInstanceUID (") + successSopInstanceUids[i] + ") was not requested in the Storage Commitment transaction " + transactionUid + ).SetPayload(MakeDimseErrorStatusPayload(STATUS_N_InvalidSOPInstance)); + } + + report->AddFailure(failedSopClassUids[i], failedSopInstanceUids[i], failureReasons[i]); + remainingRequestedInstancesIds.erase(failedSopInstanceUids[i]); + } + + if (remainingRequestedInstancesIds.size() > 0) // make sure all requestedInstances have been answered + { + throw OrthancException(ErrorCode_InexistentItem, + std::string("Not all requested SOPInstanceUID have been answered in the Storage Commitment transaction ") + transactionUid + ).SetPayload(MakeDimseErrorStatusPayload(STATUS_N_ProcessingFailure)); + } + + // now that we are sure that all requestedInstances have been answered, we can reject any size mismatch (if more instances have been answered) + if (failedSopClassUids.size() + successSopClassUids.size() > requestedInstances.size()) + { + throw OrthancException(ErrorCode_UnknownResource, + std::string("This Storage Commitment transaction UID contains more instances than requested: ") + transactionUid + ).SetPayload(MakeDimseErrorStatusPayload(STATUS_N_UnrecognizedOperation)); + } + + + report->MarkAsComplete(); } - for (size_t i = 0; i < failedSopClassUids.size(); i++) - { - report->AddFailure(failedSopClassUids[i], failedSopInstanceUids[i], failureReasons[i]); - } - - report->MarkAsComplete(); - context_.GetStorageCommitmentReports().Store(transactionUid, report.release()); } };