# HG changeset patch # User Stacy Loesch # Date 1585318018 14400 # Node ID 4f78da5613a19ebf93c0258d6f591b8f64b34ddb # Parent c81ac6ff232b378f8fc4257b1bb418fe8a236464 Add C-GET SCP support diff -r c81ac6ff232b -r 4f78da5613a1 CMakeLists.txt --- a/CMakeLists.txt Wed Apr 08 14:01:37 2020 +0200 +++ b/CMakeLists.txt Fri Mar 27 10:06:58 2020 -0400 @@ -75,6 +75,7 @@ OrthancServer/OrthancHttpHandler.cpp OrthancServer/OrthancInitialization.cpp OrthancServer/OrthancMoveRequestHandler.cpp + OrthancServer/OrthancGetRequestHandler.cpp OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp OrthancServer/OrthancRestApi/OrthancRestApi.cpp OrthancServer/OrthancRestApi/OrthancRestArchive.cpp diff -r c81ac6ff232b -r 4f78da5613a1 Core/DicomNetworking/DicomServer.cpp --- a/Core/DicomNetworking/DicomServer.cpp Wed Apr 08 14:01:37 2020 +0200 +++ b/Core/DicomNetworking/DicomServer.cpp Fri Mar 27 10:06:58 2020 -0400 @@ -92,6 +92,7 @@ modalities_ = NULL; findRequestHandlerFactory_ = NULL; moveRequestHandlerFactory_ = NULL; + getRequestHandlerFactory_ = NULL; storeRequestHandlerFactory_ = NULL; worklistRequestHandlerFactory_ = NULL; storageCommitmentFactory_ = NULL; @@ -244,6 +245,29 @@ } } + 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(); diff -r c81ac6ff232b -r 4f78da5613a1 Core/DicomNetworking/DicomServer.h --- a/Core/DicomNetworking/DicomServer.h Wed Apr 08 14:01:37 2020 +0200 +++ b/Core/DicomNetworking/DicomServer.h Fri Mar 27 10:06:58 2020 -0400 @@ -39,6 +39,7 @@ #include "IFindRequestHandlerFactory.h" #include "IMoveRequestHandlerFactory.h" +#include "IGetRequestHandlerFactory.h" #include "IStoreRequestHandlerFactory.h" #include "IWorklistRequestHandlerFactory.h" #include "IStorageCommitmentRequestHandlerFactory.h" @@ -81,6 +82,7 @@ IRemoteModalities* modalities_; IFindRequestHandlerFactory* findRequestHandlerFactory_; IMoveRequestHandlerFactory* moveRequestHandlerFactory_; + IGetRequestHandlerFactory* getRequestHandlerFactory_; IStoreRequestHandlerFactory* storeRequestHandlerFactory_; IWorklistRequestHandlerFactory* worklistRequestHandlerFactory_; IStorageCommitmentRequestHandlerFactory* storageCommitmentFactory_; @@ -116,6 +118,10 @@ bool HasMoveRequestHandlerFactory() const; IMoveRequestHandlerFactory& GetMoveRequestHandlerFactory() const; + void SetGetRequestHandlerFactory(IGetRequestHandlerFactory& handler); + bool HasGetRequestHandlerFactory() const; + IGetRequestHandlerFactory& GetGetRequestHandlerFactory() const; + void SetStoreRequestHandlerFactory(IStoreRequestHandlerFactory& handler); bool HasStoreRequestHandlerFactory() const; IStoreRequestHandlerFactory& GetStoreRequestHandlerFactory() const; diff -r c81ac6ff232b -r 4f78da5613a1 Core/DicomNetworking/IGetRequestHandler.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IGetRequestHandler.h Fri Mar 27 10:06:58 2020 -0400 @@ -0,0 +1,80 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include + +#include "../../Core/DicomFormat/DicomMap.h" + +#include +#include + + +namespace Orthanc +{ + class IGetRequestHandler + { + public: + enum Status + { + Status_Success, + Status_Failure, + Status_Warning + }; + + virtual ~IGetRequestHandler() + { + } + + virtual bool Handle(const DicomMap& input, + const std::string& originatorIp, + const std::string& originatorAet, + const std::string& calledAet) = 0; + + virtual unsigned int GetSubOperationCount() const = 0; + + virtual Status DoNext(T_ASC_Association *) = 0; + + virtual unsigned int nRemaining() = 0; + + virtual unsigned int nCompleted() = 0; + + virtual unsigned int warningCount() = 0; + + virtual const char * failedUids() = 0; + + virtual unsigned int nFailed() = 0; + }; + +} diff -r c81ac6ff232b -r 4f78da5613a1 Core/DicomNetworking/IGetRequestHandlerFactory.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IGetRequestHandlerFactory.h Fri Mar 27 10:06:58 2020 -0400 @@ -0,0 +1,49 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IGetRequestHandler.h" + +namespace Orthanc +{ + class IGetRequestHandlerFactory : public boost::noncopyable + { + public: + virtual ~IGetRequestHandlerFactory() + { + } + + virtual IGetRequestHandler* ConstructGetRequestHandler() = 0; + }; +} diff -r c81ac6ff232b -r 4f78da5613a1 Core/DicomNetworking/Internals/CommandDispatcher.cpp --- a/Core/DicomNetworking/Internals/CommandDispatcher.cpp Wed Apr 08 14:01:37 2020 +0200 +++ b/Core/DicomNetworking/Internals/CommandDispatcher.cpp Fri Mar 27 10:06:58 2020 -0400 @@ -90,6 +90,7 @@ #include "FindScp.h" #include "StoreScp.h" #include "MoveScp.h" +#include "GetScp.h" #include "../../Compatibility.h" #include "../../Toolbox.h" #include "../../Logging.h" @@ -275,6 +276,8 @@ OFString sprofile; OFString temp_str; + + cond = ASC_receiveAssociation(net, &assoc, /*opt_maxPDU*/ ASC_DEFAULTMAXPDU, NULL, NULL, @@ -371,6 +374,14 @@ knownAbstractSyntaxes.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel); knownAbstractSyntaxes.push_back(UID_MOVEPatientRootQueryRetrieveInformationModel); } + + // For C-GET + if (server.HasGetRequestHandlerFactory()) + { + knownAbstractSyntaxes.push_back(UID_GETStudyRootQueryRetrieveInformationModel); + knownAbstractSyntaxes.push_back(UID_GETPatientRootQueryRetrieveInformationModel); + } + cond = ASC_acceptContextsWithPreferredTransferSyntaxes( assoc->params, @@ -511,6 +522,8 @@ assert(static_cast(count) == numberOfDcmAllStorageSOPClassUIDs); #endif + if (!server.HasGetRequestHandlerFactory()) // dcmqrsrv.cc line 828 + { cond = ASC_acceptContextsWithPreferredTransferSyntaxes( assoc->params, dcmAllStorageSOPClassUIDs, count, @@ -521,6 +534,55 @@ AssociationCleanup(assoc); return NULL; } + } + else // see dcmqrsrv.cc lines 839 - 876 + { + /* accept storage syntaxes with proposed role */ + T_ASC_PresentationContext pc; + T_ASC_SC_ROLE role; + int npc = ASC_countPresentationContexts(assoc->params); + for (int i = 0; i < npc; i++) + { + ASC_getPresentationContext(assoc->params, i, &pc); + if (dcmIsaStorageSOPClassUID(pc.abstractSyntax)) + { + /* + ** We are prepared to accept whatever role the caller proposes. + ** Normally we can be the SCP of the Storage Service Class. + ** When processing the C-GET operation we can be the SCU of the Storage Service Class. + */ + role = pc.proposedRole; + + /* + ** Accept in the order "least wanted" to "most wanted" transfer + ** syntax. Accepting a transfer syntax will override previously + ** accepted transfer syntaxes. + */ + for (int k = (int) storageTransferSyntaxes.size() - 1; k >= 0; k--) + { + for (int j = 0; j < (int)pc.transferSyntaxCount; j++) + { + /* if the transfer syntax was proposed then we can accept it + * appears in our supported list of transfer syntaxes + */ + if (strcmp(pc.proposedTransferSyntaxes[j], storageTransferSyntaxes[k]) == 0) + { + cond = ASC_acceptPresentationContext( + assoc->params, pc.presentationContextID, storageTransferSyntaxes[k], role); + if (cond.bad()) + { + LOG(INFO) << cond.text(); + AssociationCleanup(assoc); + return NULL; + } + } + } + } + } + } /* for */ + + } + if (!server.HasApplicationEntityFilter() || server.GetApplicationEntityFilter().IsUnknownSopClassAccepted(remoteIp, remoteAet, calledAet)) @@ -733,6 +795,11 @@ request = DicomRequestType_Move; supported = true; break; + + case DIMSE_C_GET_RQ: + request = DicomRequestType_Get; + supported = true; + break; case DIMSE_C_FIND_RQ: request = DicomRequestType_Find; @@ -807,6 +874,19 @@ } } break; + + case DicomRequestType_Get: + if (server_.HasGetRequestHandlerFactory()) // Should always be true + { + std::auto_ptr handler + (server_.GetGetRequestHandlerFactory().ConstructGetRequestHandler()); + + if (handler.get() != NULL) + { + cond = Internals::getScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_, calledAet_); + } + } + break; case DicomRequestType_Find: if (server_.HasFindRequestHandlerFactory() || // Should always be true diff -r c81ac6ff232b -r 4f78da5613a1 Core/DicomNetworking/Internals/GetScp.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/Internals/GetScp.cpp Fri Mar 27 10:06:58 2020 -0400 @@ -0,0 +1,284 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + + + +/*========================================================================= + + This file is based on portions of the following project: + + Program: DCMTK 3.6.0 + Module: http://dicom.offis.de/dcmtk.php.en + +Copyright (C) 1994-2011, OFFIS e.V. +All rights reserved. + +This software and supporting documentation were developed by + + OFFIS e.V. + R&D Division Health + Escherweg 2 + 26121 Oldenburg, Germany + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +- Neither the name of OFFIS nor the names of its contributors may be + used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=========================================================================*/ + + +#include "../../PrecompiledHeaders.h" +#include +#include +#include "GetScp.h" + +#include + +#include "../../DicomParsing/FromDcmtkBridge.h" +#include "../../DicomParsing/ToDcmtkBridge.h" +#include "../../Logging.h" +#include "../../OrthancException.h" + +#include + +/** + * Macro specifying whether to apply the patch suggested in issue 66: + * "Orthanc responses C-GET with zero Get Originator Message ID" + * https://bitbucket.org/sjodogne/orthanc/issues/66/ + **/ + +namespace Orthanc +{ + namespace + { + struct GetScpData + { + // Handle returns void. + IGetRequestHandler* handler_; + DcmDataset* lastRequest_; + T_ASC_Association * assoc_; + + std::string remoteIp_; + std::string remoteAet_; + std::string calledAet_; + + GetScpData() + { + handler_ = NULL; + lastRequest_ = NULL; + assoc_ = NULL; + }; + }; + + void BuildFailedInstanceList(const char* failedUIDs, DcmDataset ** rspIds) + { + OFBool ok; + + if (failedUIDs != NULL) + { + *rspIds = new DcmDataset(); + ok = DU_putStringDOElement(*rspIds, DCM_FailedSOPInstanceUIDList, failedUIDs); + if (!ok) + { + LOG (ERROR) << "getSCP: failed to build DCM_FailedSOPInstanceUIDList"; + } + + } + } + + void GetScpCallback( + /* in */ + void *callbackData, + OFBool cancelled, + T_DIMSE_C_GetRQ *request, + DcmDataset *requestIdentifiers, + int responseCount, + /* out */ + T_DIMSE_C_GetRSP *response, + DcmDataset **responseIdentifiers, + DcmDataset **statusDetail) + { + bzero(response, sizeof(T_DIMSE_C_GetRSP)); + *statusDetail = NULL; + *responseIdentifiers = NULL; + + GetScpData& data = *reinterpret_cast(callbackData); + if (data.lastRequest_ == NULL) + { + DicomMap input; + FromDcmtkBridge::ExtractDicomSummary(input, *requestIdentifiers); + + try + { + if(!data.handler_->Handle(input, data.remoteIp_, data.remoteAet_, + data.calledAet_)) + { + response->DimseStatus = STATUS_GET_Failed_UnableToProcess; + return; + } + } + catch (OrthancException& e) + { + // Internal error! + LOG(ERROR) << "IGetRequestHandler Failed: " << e.What(); + response->DimseStatus = STATUS_GET_Failed_UnableToProcess; + return; + } + + data.lastRequest_ = requestIdentifiers; + } + else if (data.lastRequest_ != requestIdentifiers) + { + // Internal error! + LOG(ERROR) << "IGetRequestHandler Failed: Internal error lastRequestIdentifier"; + response->DimseStatus = STATUS_GET_Failed_UnableToProcess; + return; + } + + if (data.handler_->nRemaining() == 0) + { + response->DimseStatus = STATUS_Success; + } + else + { + IGetRequestHandler::Status status; + + try + { + status = data.handler_->DoNext(data.assoc_); + } + catch (OrthancException& e) + { + // Internal error! + LOG(ERROR) << "IGetRequestHandler Failed: " << e.What(); + response->DimseStatus = STATUS_GET_Failed_UnableToProcess; + return; + } + + if (status == STATUS_Success) + { + if (responseCount < static_cast(data.handler_->nRemaining())) + { + response->DimseStatus = STATUS_Pending; + } + else + { + response->DimseStatus = STATUS_Success; + } + } + else + { + response->DimseStatus = STATUS_GET_Failed_UnableToProcess; + + if (data.handler_->nFailed() > 0 || data.handler_->warningCount() > 0) + { + response->DimseStatus = STATUS_GET_Warning_SubOperationsCompleteOneOrMoreFailures; + } + /* + * if all the sub-operations failed then we need to generate a failed or refused status. + * cf. DICOM part 4, C.4.3.3.1 + * we choose to generate a "Refused - Out of Resources - Unable to perform suboperations" status. + */ + if ((data.handler_->nFailed() > 0) && ((data.handler_->nCompleted() + data.handler_->warningCount()) == 0)) + { + response->DimseStatus = STATUS_GET_Refused_OutOfResourcesSubOperations; + } + + BuildFailedInstanceList(data.handler_->failedUids(), responseIdentifiers); + } + } + + response->NumberOfRemainingSubOperations = data.handler_->nRemaining(); + response->NumberOfCompletedSubOperations = data.handler_->nCompleted(); + response->NumberOfFailedSubOperations = data.handler_->nFailed(); + response->NumberOfWarningSubOperations = data.handler_->warningCount(); + } + + } + + OFCondition Internals::getScp(T_ASC_Association * assoc, + T_DIMSE_Message * msg, + T_ASC_PresentationContextID presID, + IGetRequestHandler& handler, + std::string remoteIp, + std::string remoteAet, + std::string calledAet) + { + + GetScpData data; + data.lastRequest_ = NULL; + data.handler_ = &handler; + data.assoc_ = assoc; + data.remoteIp_ = remoteIp; + data.remoteAet_ = remoteAet; + data.calledAet_ = calledAet; + + + OFCondition cond = DIMSE_getProvider(assoc, presID, &msg->msg.CGetRQ, + GetScpCallback, &data, + /*opt_blockMode*/ DIMSE_BLOCKING, + /*opt_dimse_timeout*/ 0); + + // if some error occured, dump corresponding information and remove the outfile if necessary + if (cond.bad()) + { + OFString temp_str; + LOG(ERROR) << "Get SCP Failed: " << cond.text(); + } + + return cond; + } +} diff -r c81ac6ff232b -r 4f78da5613a1 Core/DicomNetworking/Internals/GetScp.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/Internals/GetScp.h Fri Mar 27 10:06:58 2020 -0400 @@ -0,0 +1,51 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../IGetRequestHandler.h" + +namespace Orthanc +{ + + namespace Internals + { + OFCondition getScp(T_ASC_Association * assoc, + T_DIMSE_Message * msg, + T_ASC_PresentationContextID presID, + IGetRequestHandler& handler, + std::string remoteIp, + std::string remoteAet, + std::string calledAet); + } +} diff -r c81ac6ff232b -r 4f78da5613a1 Core/Enumerations.cpp --- a/Core/Enumerations.cpp Wed Apr 08 14:01:37 2020 +0200 +++ b/Core/Enumerations.cpp Fri Mar 27 10:06:58 2020 -0400 @@ -369,6 +369,9 @@ case ErrorCode_AlreadyExistingTag: return "Cannot override the value of a tag that already exists"; + case ErrorCode_NoCGetHandler: + return "No request handler factory for DICOM C-GET SCP"; + case ErrorCode_NoStorageCommitmentHandler: return "No request handler factory for DICOM N-ACTION SCP (storage commitment)"; diff -r c81ac6ff232b -r 4f78da5613a1 Core/Enumerations.h --- a/Core/Enumerations.h Wed Apr 08 14:01:37 2020 +0200 +++ b/Core/Enumerations.h Fri Mar 27 10:06:58 2020 -0400 @@ -241,6 +241,7 @@ ErrorCode_NoWorklistHandler = 2041 /*!< No request handler factory for DICOM C-Find Modality SCP */, ErrorCode_AlreadyExistingTag = 2042 /*!< Cannot override the value of a tag that already exists */, ErrorCode_NoStorageCommitmentHandler = 2043 /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */, + ErrorCode_NoCGetHandler = 2044 /*!< No request handler factory for DICOM C-GET SCP */, ErrorCode_UnsupportedMediaType = 3000 /*!< Unsupported media type */, ErrorCode_START_PLUGINS = 1000000 }; diff -r c81ac6ff232b -r 4f78da5613a1 OrthancServer/OrthancGetRequestHandler.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/OrthancGetRequestHandler.cpp Fri Mar 27 10:06:58 2020 -0400 @@ -0,0 +1,368 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + +#include "PrecompiledHeadersServer.h" +#include "OrthancGetRequestHandler.h" + +#include +#include +#include +#include +#include + +#include "../../Core/DicomParsing/FromDcmtkBridge.h" +#include "../Core/DicomFormat/DicomArray.h" +#include "../Core/Logging.h" +#include "../Core/MetricsRegistry.h" +#include "OrthancConfiguration.h" +#include "ServerContext.h" +#include "ServerJobs/DicomModalityStoreJob.h" + + + +namespace Orthanc +{ + namespace + { + // Anonymous namespace to avoid clashes between compilation modules + + static void getSubOpProgressCallback(void * /* callbackData */, + T_DIMSE_StoreProgress *progress, + T_DIMSE_C_StoreRQ * /*req*/) + { + // SBL - no logging to be done here. + } + } + + OrthancGetRequestHandler::Status OrthancGetRequestHandler::DoNext(T_ASC_Association* assoc) + { + if (position_ >= instances_.size()) + { + return Status_Failure; + } + + const std::string& id = instances_[position_++]; + + std::string dicom; + context_.ReadDicom(dicom, id); + + if(dicom.size() <= 0) + { + return Status_Failure; + } + + DcmInputBufferStream is; + is.setBuffer(&dicom[0], dicom.size()); + is.setEos(); + + DcmFileFormat dcmff; + OFCondition cond = dcmff.read(is, EXS_Unknown, EGL_noChange, DCM_MaxReadLength); + if(cond.bad()) + { + return Status_Failure; + } + + // Determine the storage SOP class UID for this instance + DIC_UI sopClass; + DIC_UI sopInstance; + +#if DCMTK_VERSION_NUMBER >= 364 + if (!DU_findSOPClassAndInstanceInDataSet(static_cast (dcmff.getDataset()), + sopClass, sizeof(sopClass), + sopInstance, sizeof(sopInstance))) +#else + if (!DU_findSOPClassAndInstanceInDataSet(dcmff.getDataset(), sopClassUid, sopInstance)) +#endif + { + throw OrthancException(ErrorCode_NoSopClassOrInstance, + "Unable to determine the SOP class/instance for C-STORE with AET " + + originatorAet_); + } + + + + + cond = performGetSubOp(assoc, sopClass, sopInstance, dcmff.getDataset()); + if (getCancelled_) { + LOG (INFO) << "Get SCP: Received C-Cancel RQ"; + } + + if(cond.bad() || getCancelled_) + { + return Status_Failure; + } + + return Status_Success; + } + + void OrthancGetRequestHandler::addFailedUIDInstance(const char *sopInstance) + { + size_t len; + + if (failedUIDs_ == NULL) { + if ((failedUIDs_ = (char*)malloc(DIC_UI_LEN+1)) == NULL) { + LOG (ERROR) << "malloc failure: addFailedUIDInstance"; + return; + } + strcpy(failedUIDs_, sopInstance); + } else { + len = strlen(failedUIDs_); + if ((failedUIDs_ = (char*)realloc(failedUIDs_, + (len+strlen(sopInstance)+2))) == NULL) { + LOG (ERROR) << "realloc failure: addFailedUIDInstance"; + return; + } + // tag sopInstance onto end of old with '\' between + strcat(failedUIDs_, "\\"); + strcat(failedUIDs_, sopInstance); + } + } + + + OFCondition OrthancGetRequestHandler::performGetSubOp(T_ASC_Association* assoc, + DIC_UI sopClass, + DIC_UI sopInstance, + DcmDataset *dataset) + { + OFCondition cond = EC_Normal; + T_DIMSE_C_StoreRQ req; + T_DIMSE_C_StoreRSP rsp; + DIC_US msgId; + T_ASC_PresentationContextID presId; + DcmDataset *stDetail = NULL; + + + msgId = assoc->nextMsgID++; + + // which presentation context should be used + presId = ASC_findAcceptedPresentationContextID(assoc, + sopClass); + if (presId == 0) { + nFailed_++; + addFailedUIDInstance(sopInstance); + LOG (ERROR) << "Get SCP: storeSCU: No presentation context for: (" + << dcmSOPClassUIDToModality(sopClass, "OT") << ") " << sopClass; + return DIMSE_NOVALIDPRESENTATIONCONTEXTID; + } else { + // make sure that we can send images in this presentation context + T_ASC_PresentationContext pc; + ASC_findAcceptedPresentationContext(assoc->params, presId, &pc); + // the acceptedRole is the association requestor role + if ((pc.acceptedRole != ASC_SC_ROLE_SCP) && (pc.acceptedRole != ASC_SC_ROLE_SCUSCP)) + { + // the role is not appropriate + nFailed_++; + addFailedUIDInstance(sopInstance); + LOG (ERROR) <<"Get SCP: storeSCU: [No presentation context with requestor SCP role for: (" + << dcmSOPClassUIDToModality(sopClass, "OT") << ") " << sopClass; + return DIMSE_NOVALIDPRESENTATIONCONTEXTID; + } + } + + req.MessageID = msgId; + strcpy(req.AffectedSOPClassUID, sopClass); + strcpy(req.AffectedSOPInstanceUID, sopInstance); + req.DataSetType = DIMSE_DATASET_PRESENT; + req.Priority = priority_; + req.opts = 0; + + LOG (INFO) << "Store SCU RQ: MsgID " << msgId << ", (" + << dcmSOPClassUIDToModality(sopClass, "OT") << ")"; + + T_DIMSE_DetectedCancelParameters cancelParameters; + + cond = DIMSE_storeUser(assoc, presId, &req, + NULL, dataset, getSubOpProgressCallback, this, DIMSE_BLOCKING, 0, + &rsp, &stDetail, &cancelParameters); + + if (cond.good()) { + if (cancelParameters.cancelEncountered) { + if (origPresId == cancelParameters.presId && + origMsgId == cancelParameters.req.MessageIDBeingRespondedTo) { + getCancelled_ = OFTrue; + } else { + LOG (ERROR) << "Get SCP: Unexpected C-Cancel-RQ encountered: pid=" << (int)cancelParameters.presId + << ", mid=" << (int)cancelParameters.req.MessageIDBeingRespondedTo; + } + } + + if (rsp.DimseStatus == STATUS_Success) { + // everything ok + nCompleted_++; + } else if ((rsp.DimseStatus & 0xf000) == 0xb000) { + // a warning status message + warningCount_++; + LOG (ERROR) << "Get SCP: Store Warning: Response Status: " << DU_cstoreStatusString(rsp.DimseStatus); + } else { + nFailed_++; + addFailedUIDInstance(sopInstance); + // print a status message + LOG (ERROR) << "Get SCP: Store Failed: Response Status: " + << DU_cstoreStatusString(rsp.DimseStatus); + } + } else { + nFailed_++; + addFailedUIDInstance(sopInstance); + OFString temp_str; + LOG (ERROR) << "Get SCP: storeSCU: Store Request Failed: " << DimseCondition::dump(temp_str, cond); + } + if (stDetail) { + LOG (INFO) << " Status Detail:" << OFendl << DcmObject::PrintHelper(*stDetail); + delete stDetail; + } + return cond; + } + + bool OrthancGetRequestHandler::LookupIdentifiers(std::vector& publicIds, + ResourceType level, + const DicomMap& input) + { + DicomTag tag(0, 0); // Dummy initialization + + switch (level) + { + case ResourceType_Patient: + tag = DICOM_TAG_PATIENT_ID; + break; + + case ResourceType_Study: + tag = (input.HasTag(DICOM_TAG_ACCESSION_NUMBER) ? + DICOM_TAG_ACCESSION_NUMBER : DICOM_TAG_STUDY_INSTANCE_UID); + break; + + case ResourceType_Series: + tag = DICOM_TAG_SERIES_INSTANCE_UID; + break; + + case ResourceType_Instance: + tag = DICOM_TAG_SOP_INSTANCE_UID; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (!input.HasTag(tag)) + { + return false; + } + + const DicomValue& value = input.GetValue(tag); + if (value.IsNull() || + value.IsBinary()) + { + return false; + } + else + { + const std::string& content = value.GetContent(); + context_.GetIndex().LookupIdentifierExact(publicIds, level, tag, content); + return true; + } + } + + + bool OrthancGetRequestHandler::Handle(const DicomMap& input, + const std::string& originatorIp, + const std::string& originatorAet, + const std::string& calledAet) + { + MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_get_scp_duration_ms"); + + LOG(WARNING) << "Get-SCU request received from AET \"" << originatorAet << "\""; + + { + DicomArray query(input); + for (size_t i = 0; i < query.GetSize(); i++) + { + if (!query.GetElement(i).GetValue().IsNull()) + { + LOG(INFO) << " " << query.GetElement(i).GetTag() + << " " << FromDcmtkBridge::GetTagName(query.GetElement(i)) + << " = " << query.GetElement(i).GetValue().GetContent(); + } + } + } + + /** + * Retrieve the query level. + **/ + + const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL); + + assert(levelTmp != NULL); + ResourceType level = StringToResourceType(levelTmp->GetContent().c_str()); + + + /** + * Lookup for the resource to be sent. + **/ + + std::vector publicIds; + + bool retVal = LookupIdentifiers(publicIds, level, input); + localAet_ = context_.GetDefaultLocalApplicationEntityTitle(); + position_ = 0; + originatorAet_ = originatorAet; + + { + OrthancConfiguration::ReaderLock lock; + remote_ = lock.GetConfiguration().GetModalityUsingAet(originatorAet); + } + + for (size_t i = 0; i < publicIds.size(); i++) + { + LOG(INFO) << "Sending resource " << publicIds[i] << " to modality \"" + << originatorAet << "\" in synchronous mode"; + + std::list tmp; + context_.GetIndex().GetChildInstances(tmp, publicIds[i]); + + instances_.reserve(tmp.size()); + for (std::list::iterator it = tmp.begin(); it != tmp.end(); ++it) + { + instances_.push_back(*it); + } + } + failedUIDs_ = NULL; + getCancelled_ = OFFalse; + + nRemaining_ = GetSubOperationCount(); + nCompleted_ = 0; + nFailed_ = 0; + warningCount_ = 0; + + return retVal; + + + } +}; diff -r c81ac6ff232b -r 4f78da5613a1 OrthancServer/OrthancGetRequestHandler.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/OrthancGetRequestHandler.h Fri Mar 27 10:06:58 2020 -0400 @@ -0,0 +1,131 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + +#pragma once + +#include "../Core/DicomNetworking/IGetRequestHandler.h" +#include "../Core/DicomNetworking/RemoteModalityParameters.h" + +#include + +namespace Orthanc +{ + class ServerContext; + + class OrthancGetRequestHandler : public IGetRequestHandler + { + private: + ServerContext& context_; + std::string localAet_; + std::vector instances_; + size_t position_; + RemoteModalityParameters remote_; + std::string originatorAet_; + + unsigned int nRemaining_; + unsigned int nCompleted_; + unsigned int warningCount_; + unsigned int nFailed_; + char *failedUIDs_; + + T_DIMSE_Priority priority_; + DIC_US origMsgId; + T_ASC_PresentationContextID origPresId; + + bool getCancelled_; + + bool LookupIdentifiers(std::vector& publicIds, + ResourceType level, + const DicomMap& input); + + OFCondition performGetSubOp(T_ASC_Association *assoc, + DIC_UI sopClass, + DIC_UI sopInstance, + DcmDataset *dataset); + + void addFailedUIDInstance(const char *sopInstance); + + public: + OrthancGetRequestHandler(ServerContext& context) : + context_(context) + { + position_ = 0; + + nRemaining_ = 0; + nCompleted_ = 0; + warningCount_ = 0; + nFailed_ = 0; + + failedUIDs_ = NULL; + } + + bool Handle(const DicomMap& input, + const std::string& originatorIp, + const std::string& originatorAet, + const std::string& calledAet); + + virtual Status DoNext(T_ASC_Association *); + + virtual unsigned int GetSubOperationCount() const + { + return (unsigned int) instances_.size(); + } + + virtual unsigned int nRemaining() + { + return nRemaining_; + } + + virtual unsigned int nCompleted() + { + return nCompleted_; + } + + virtual unsigned int warningCount() + { + return warningCount_; + } + + virtual unsigned int nFailed() + { + return nFailed_; + } + + virtual const char * failedUids() + { + return failedUIDs_; + } + + + + }; +} diff -r c81ac6ff232b -r 4f78da5613a1 OrthancServer/main.cpp --- a/OrthancServer/main.cpp Wed Apr 08 14:01:37 2020 +0200 +++ b/OrthancServer/main.cpp Fri Mar 27 10:06:58 2020 -0400 @@ -50,6 +50,7 @@ #include "OrthancFindRequestHandler.h" #include "OrthancInitialization.h" #include "OrthancMoveRequestHandler.h" +#include "OrthancGetRequestHandler.h" #include "ServerContext.h" #include "ServerJobs/StorageCommitmentScpJob.h" #include "ServerToolbox.h" @@ -190,7 +191,8 @@ class MyDicomServerFactory : public IStoreRequestHandlerFactory, public IFindRequestHandlerFactory, - public IMoveRequestHandlerFactory, + public IMoveRequestHandlerFactory, + public IGetRequestHandlerFactory, public IStorageCommitmentRequestHandlerFactory { private: @@ -243,11 +245,17 @@ { return new OrthancMoveRequestHandler(context_); } - + + virtual IGetRequestHandler* ConstructGetRequestHandler() + { + return new OrthancGetRequestHandler(context_); + } + virtual IStorageCommitmentRequestHandler* ConstructStorageCommitmentRequestHandler() { return new OrthancStorageCommitmentRequestHandler(context_); } + void Done() { @@ -760,6 +768,7 @@ PrintErrorCode(ErrorCode_CannotOrderSlices, "Unable to order the slices of the series"); PrintErrorCode(ErrorCode_NoWorklistHandler, "No request handler factory for DICOM C-Find Modality SCP"); PrintErrorCode(ErrorCode_AlreadyExistingTag, "Cannot override the value of a tag that already exists"); + PrintErrorCode(ErrorCode_NoCGetHandler, "No request handler factory for DICOM C-GET SCP"); PrintErrorCode(ErrorCode_NoStorageCommitmentHandler, "No request handler factory for DICOM N-ACTION SCP (storage commitment)"); PrintErrorCode(ErrorCode_UnsupportedMediaType, "Unsupported media type"); } @@ -1032,10 +1041,12 @@ OrthancPlugins* plugins) { bool dicomServerEnabled; + bool dicomCGetEnabled; { OrthancConfiguration::ReaderLock lock; dicomServerEnabled = lock.GetConfiguration().GetBooleanParameter("DicomServerEnabled", true); + dicomCGetEnabled = lock.GetConfiguration().GetBooleanParameter("DicomEnableCGet", false); } if (!dicomServerEnabled) @@ -1054,6 +1065,10 @@ dicomServer.SetRemoteModalities(modalities); dicomServer.SetStoreRequestHandlerFactory(serverFactory); dicomServer.SetMoveRequestHandlerFactory(serverFactory); + if (dicomCGetEnabled) + { + dicomServer.SetGetRequestHandlerFactory(serverFactory); + } dicomServer.SetFindRequestHandlerFactory(serverFactory); dicomServer.SetStorageCommitmentRequestHandlerFactory(serverFactory); @@ -1138,7 +1153,7 @@ context.GetHttpHandler().Register(*plugins, false); } #endif - + // Secondly, apply the "static resources" layer #if ORTHANC_STANDALONE == 1 EmbeddedResourceHttpHandler staticResources("/app", EmbeddedResources::ORTHANC_EXPLORER); diff -r c81ac6ff232b -r 4f78da5613a1 Plugins/Include/orthanc/OrthancCPlugin.h --- a/Plugins/Include/orthanc/OrthancCPlugin.h Wed Apr 08 14:01:37 2020 +0200 +++ b/Plugins/Include/orthanc/OrthancCPlugin.h Fri Mar 27 10:06:58 2020 -0400 @@ -304,6 +304,7 @@ OrthancPluginErrorCode_NoWorklistHandler = 2041 /*!< No request handler factory for DICOM C-Find Modality SCP */, OrthancPluginErrorCode_AlreadyExistingTag = 2042 /*!< Cannot override the value of a tag that already exists */, OrthancPluginErrorCode_NoStorageCommitmentHandler = 2043 /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */, + OrthancPluginErrorCode_NoCGetHandler = 2044 /*!< No request handler factory for DICOM C-GET SCP */, OrthancPluginErrorCode_UnsupportedMediaType = 3000 /*!< Unsupported media type */, _OrthancPluginErrorCode_INTERNAL = 0x7fffffff diff -r c81ac6ff232b -r 4f78da5613a1 Resources/CMake/OrthancFrameworkConfiguration.cmake --- a/Resources/CMake/OrthancFrameworkConfiguration.cmake Wed Apr 08 14:01:37 2020 +0200 +++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake Fri Mar 27 10:06:58 2020 -0400 @@ -487,6 +487,7 @@ ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/CommandDispatcher.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/FindScp.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/MoveScp.cpp + ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/GetScp.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/StoreScp.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/RemoteModalityParameters.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp diff -r c81ac6ff232b -r 4f78da5613a1 Resources/Configuration.json --- a/Resources/Configuration.json Wed Apr 08 14:01:37 2020 +0200 +++ b/Resources/Configuration.json Fri Mar 27 10:06:58 2020 -0400 @@ -96,6 +96,9 @@ // receive files or to do query/retrieve through the DICOM protocol. "DicomServerEnabled" : true, + // Enable DICOM server handling of incoming C-Get requests. + "DicomEnableCGet" : true, + // The DICOM Application Entity Title (cannot be longer than 16 // characters) "DicomAet" : "ORTHANC", @@ -205,13 +208,15 @@ /** * By default, the Orthanc SCP accepts all DICOM commands (C-ECHO, - * C-STORE, C-FIND, C-MOVE, and storage commitment) issued by the + * C-STORE, C-FIND, C-MOVE, C-GET and storage commitment) issued by the * registered remote SCU modalities. Starting with Orthanc 1.5.0, * it is possible to specify which DICOM commands are allowed, * separately for each remote modality, using the syntax * below. The "AllowEcho" (resp. "AllowStore") option only has an * effect respectively if global option "DicomAlwaysAllowEcho" * (resp. "DicomAlwaysAllowStore") is set to false. + * The "AllowGet" option only has an effect if the global option + * DicomEnableCGet is set to true. **/ //"untrusted" : { // "AET" : "ORTHANC", @@ -220,6 +225,7 @@ // "Manufacturer" : "Generic", // "AllowEcho" : false, // "AllowFind" : false, + // "AllowGet" : false, // "AllowMove" : false, // "AllowStore" : true, // "AllowStorageCommitment" : false // new in 1.6.0 diff -r c81ac6ff232b -r 4f78da5613a1 Resources/ErrorCodes.json --- a/Resources/ErrorCodes.json Wed Apr 08 14:01:37 2020 +0200 +++ b/Resources/ErrorCodes.json Fri Mar 27 10:06:58 2020 -0400 @@ -557,6 +557,11 @@ "Name": "NoStorageCommitmentHandler", "Description": "No request handler factory for DICOM N-ACTION SCP (storage commitment)" }, + { + "Code": 2044, + "Name": "NoCGetHandler", + "Description": "No request handler factory for DICOM C-GET SCP" + }