1 /**
2  * Orthanc - A Lightweight, RESTful DICOM Store
3  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
4  * Department, University Hospital of Liege, Belgium
5  * Copyright (C) 2017-2021 Osimis S.A., Belgium
6  *
7  * This program is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU Lesser General Public License
9  * as published by the Free Software Foundation, either version 3 of
10  * the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful, but
13  * WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15  * Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public
18  * License along with this program. If not, see
19  * <http://www.gnu.org/licenses/>.
20  **/
21 
22 
23 
24 
25 /*=========================================================================
26 
27   This file is based on portions of the following project:
28 
29   Program: DCMTK 3.6.0
30   Module:  http://dicom.offis.de/dcmtk.php.en
31 
32   Copyright (C) 1994-2011, OFFIS e.V.
33   All rights reserved.
34 
35   This software and supporting documentation were developed by
36 
37   OFFIS e.V.
38   R&D Division Health
39   Escherweg 2
40   26121 Oldenburg, Germany
41 
42   Redistribution and use in source and binary forms, with or without
43   modification, are permitted provided that the following conditions
44   are met:
45 
46   - Redistributions of source code must retain the above copyright
47   notice, this list of conditions and the following disclaimer.
48 
49   - Redistributions in binary form must reproduce the above copyright
50   notice, this list of conditions and the following disclaimer in the
51   documentation and/or other materials provided with the distribution.
52 
53   - Neither the name of OFFIS nor the names of its contributors may be
54   used to endorse or promote products derived from this software
55   without specific prior written permission.
56 
57   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
58   "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
59   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
60   A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
61   HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
62   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
63   LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
64   DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
65   THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
66   (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
67   OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
68 
69   =========================================================================*/
70 
71 
72 #include "../../PrecompiledHeaders.h"
73 #include "CommandDispatcher.h"
74 
75 #if !defined(DCMTK_VERSION_NUMBER)
76 #  error The macro DCMTK_VERSION_NUMBER must be defined
77 #endif
78 
79 #include "../../Compatibility.h"
80 #include "../../DicomParsing/FromDcmtkBridge.h"
81 #include "../../Logging.h"
82 #include "../../OrthancException.h"
83 #include "../../Toolbox.h"
84 #include "FindScp.h"
85 #include "GetScp.h"
86 #include "MoveScp.h"
87 #include "StoreScp.h"
88 
89 #include <dcmtk/dcmdata/dcdeftag.h>     /* for storage commitment */
90 #include <dcmtk/dcmdata/dcsequen.h>     /* for class DcmSequenceOfItems */
91 #include <dcmtk/dcmdata/dcuid.h>        /* for variable dcmAllStorageSOPClassUIDs */
92 #include <dcmtk/dcmnet/dcasccfg.h>      /* for class DcmAssociationConfiguration */
93 
94 #include <boost/lexical_cast.hpp>
95 
96 static OFBool    opt_rejectWithoutImplementationUID = OFFalse;
97 
98 
99 
100 static DUL_PRESENTATIONCONTEXT *
findPresentationContextID(LST_HEAD * head,T_ASC_PresentationContextID presentationContextID)101 findPresentationContextID(LST_HEAD * head,
102                           T_ASC_PresentationContextID presentationContextID)
103 {
104   DUL_PRESENTATIONCONTEXT *pc;
105   LST_HEAD **l;
106   OFBool found = OFFalse;
107 
108   if (head == NULL)
109     return NULL;
110 
111   l = &head;
112   if (*l == NULL)
113     return NULL;
114 
115   pc = OFstatic_cast(DUL_PRESENTATIONCONTEXT *, LST_Head(l));
116   (void)LST_Position(l, OFstatic_cast(LST_NODE *, pc));
117 
118   while (pc && !found) {
119     if (pc->presentationContextID == presentationContextID) {
120       found = OFTrue;
121     } else {
122       pc = OFstatic_cast(DUL_PRESENTATIONCONTEXT *, LST_Next(l));
123     }
124   }
125   return pc;
126 }
127 
128 
129 /** accept all presenstation contexts for unknown SOP classes,
130  *  i.e. UIDs appearing in the list of abstract syntaxes
131  *  where no corresponding name is defined in the UID dictionary.
132  *  @param params pointer to association parameters structure
133  *  @param transferSyntax transfer syntax to accept
134  *  @param acceptedRole SCU/SCP role to accept
135  */
acceptUnknownContextsWithTransferSyntax(T_ASC_Parameters * params,const char * transferSyntax,T_ASC_SC_ROLE acceptedRole)136 static OFCondition acceptUnknownContextsWithTransferSyntax(
137   T_ASC_Parameters * params,
138   const char* transferSyntax,
139   T_ASC_SC_ROLE acceptedRole)
140 {
141   int n, i, k;
142   DUL_PRESENTATIONCONTEXT *dpc;
143   T_ASC_PresentationContext pc;
144 
145   n = ASC_countPresentationContexts(params);
146   for (i = 0; i < n; i++)
147   {
148     OFCondition cond = ASC_getPresentationContext(params, i, &pc);
149     if (cond.bad()) return cond;
150     OFBool abstractOK = OFFalse;
151     OFBool accepted = OFFalse;
152 
153     if (dcmFindNameOfUID(pc.abstractSyntax) == NULL)
154     {
155       abstractOK = OFTrue;
156 
157       /* check the transfer syntax */
158       for (k = 0; (k < OFstatic_cast(int, pc.transferSyntaxCount)) && !accepted; k++)
159       {
160         if (strcmp(pc.proposedTransferSyntaxes[k], transferSyntax) == 0)
161         {
162           accepted = OFTrue;
163         }
164       }
165     }
166 
167     if (accepted)
168     {
169       cond = ASC_acceptPresentationContext(
170         params, pc.presentationContextID,
171         transferSyntax, acceptedRole);
172       if (cond.bad()) return cond;
173     } else {
174       T_ASC_P_ResultReason reason;
175 
176       /* do not refuse if already accepted */
177       dpc = findPresentationContextID(params->DULparams.acceptedPresentationContext,
178                                       pc.presentationContextID);
179       if (dpc == NULL || dpc->result != ASC_P_ACCEPTANCE)
180       {
181 
182         if (abstractOK) {
183           reason = ASC_P_TRANSFERSYNTAXESNOTSUPPORTED;
184         } else {
185           reason = ASC_P_ABSTRACTSYNTAXNOTSUPPORTED;
186         }
187         /*
188          * If previously this presentation context was refused
189          * because of bad transfer syntax let it stay that way.
190          */
191         if ((dpc != NULL) && (dpc->result == ASC_P_TRANSFERSYNTAXESNOTSUPPORTED))
192           reason = ASC_P_TRANSFERSYNTAXESNOTSUPPORTED;
193 
194         cond = ASC_refusePresentationContext(params, pc.presentationContextID, reason);
195         if (cond.bad()) return cond;
196       }
197     }
198   }
199   return EC_Normal;
200 }
201 
202 
203 /** accept all presenstation contexts for unknown SOP classes,
204  *  i.e. UIDs appearing in the list of abstract syntaxes
205  *  where no corresponding name is defined in the UID dictionary.
206  *  This method is passed a list of "preferred" transfer syntaxes.
207  *  @param params pointer to association parameters structure
208  *  @param transferSyntax transfer syntax to accept
209  *  @param acceptedRole SCU/SCP role to accept
210  */
acceptUnknownContextsWithPreferredTransferSyntaxes(T_ASC_Parameters * params,const char * transferSyntaxes[],int transferSyntaxCount,T_ASC_SC_ROLE acceptedRole)211 static OFCondition acceptUnknownContextsWithPreferredTransferSyntaxes(
212   T_ASC_Parameters * params,
213   const char* transferSyntaxes[], int transferSyntaxCount,
214   T_ASC_SC_ROLE acceptedRole)
215 {
216   OFCondition cond = EC_Normal;
217   /*
218   ** Accept in the order "least wanted" to "most wanted" transfer
219   ** syntax.  Accepting a transfer syntax will override previously
220   ** accepted transfer syntaxes.
221   */
222   for (int i = transferSyntaxCount - 1; i >= 0; i--)
223   {
224     cond = acceptUnknownContextsWithTransferSyntax(params, transferSyntaxes[i], acceptedRole);
225     if (cond.bad()) return cond;
226   }
227   return cond;
228 }
229 
230 
231 
232 namespace Orthanc
233 {
234   namespace Internals
235   {
AssociationCleanup(T_ASC_Association * assoc)236     OFCondition AssociationCleanup(T_ASC_Association *assoc)
237     {
238       OFCondition cond = ASC_dropSCPAssociation(assoc);
239       if (cond.bad())
240       {
241         CLOG(ERROR, DICOM) << cond.text();
242         return cond;
243       }
244 
245       cond = ASC_destroyAssociation(&assoc);
246       if (cond.bad())
247       {
248         CLOG(ERROR, DICOM) << cond.text();
249         return cond;
250       }
251 
252       return cond;
253     }
254 
255 
256 
AcceptAssociation(const DicomServer & server,T_ASC_Network * net,unsigned int maximumPduLength,bool useDicomTls)257     CommandDispatcher* AcceptAssociation(const DicomServer& server,
258                                          T_ASC_Network *net,
259                                          unsigned int maximumPduLength,
260                                          bool useDicomTls)
261     {
262       DcmAssociationConfiguration asccfg;
263       char buf[BUFSIZ];
264       T_ASC_Association *assoc;
265       OFCondition cond;
266       OFString sprofile;
267       OFString temp_str;
268 
269       cond = ASC_receiveAssociation(net, &assoc, maximumPduLength, NULL, NULL,
270                                     useDicomTls /*opt_secureConnection*/,
271                                     DUL_NOBLOCK, 1);
272 
273       if (cond == DUL_NOASSOCIATIONREQUEST)
274       {
275         // Timeout
276         AssociationCleanup(assoc);
277         return NULL;
278       }
279 
280       // if some kind of error occured, take care of it
281       if (cond.bad())
282       {
283         CLOG(ERROR, DICOM) << "Receiving Association failed: " << cond.text();
284         // no matter what kind of error occurred, we need to do a cleanup
285         AssociationCleanup(assoc);
286         return NULL;
287       }
288 
289       {
290         OFString str;
291         CLOG(TRACE, DICOM) << "Received Association Parameters:" << std::endl
292                            << ASC_dumpParameters(str, assoc->params, ASC_ASSOC_RQ);
293       }
294 
295       // Retrieve the AET and the IP address of the remote modality
296       std::string remoteAet;
297       std::string remoteIp;
298       std::string calledAet;
299 
300       {
301         DIC_AE remoteAet_C;
302         DIC_AE calledAet_C;
303         DIC_AE remoteIp_C;
304         DIC_AE calledIP_C;
305 
306         if (
307 #if DCMTK_VERSION_NUMBER >= 364
308           ASC_getAPTitles(assoc->params, remoteAet_C, sizeof(remoteAet_C), calledAet_C, sizeof(calledAet_C), NULL, 0).bad() ||
309           ASC_getPresentationAddresses(assoc->params, remoteIp_C, sizeof(remoteIp_C), calledIP_C, sizeof(calledIP_C)).bad()
310 #else
311           ASC_getAPTitles(assoc->params, remoteAet_C, calledAet_C, NULL).bad() ||
312           ASC_getPresentationAddresses(assoc->params, remoteIp_C, calledIP_C).bad()
313 #endif
314           )
315         {
316           T_ASC_RejectParameters rej =
317             {
318               ASC_RESULT_REJECTEDPERMANENT,
319               ASC_SOURCE_SERVICEUSER,
320               ASC_REASON_SU_NOREASON
321             };
322           ASC_rejectAssociation(assoc, &rej);
323           AssociationCleanup(assoc);
324           return NULL;
325         }
326 
327         remoteIp = std::string(/*OFSTRING_GUARD*/(remoteIp_C));
328         remoteAet = std::string(/*OFSTRING_GUARD*/(remoteAet_C));
329         calledAet = (/*OFSTRING_GUARD*/(calledAet_C));
330       }
331 
332       CLOG(INFO, DICOM) << "Association Received from AET " << remoteAet
333                         << " on IP " << remoteIp;
334 
335 
336       {
337         /* accept the abstract syntaxes for C-ECHO, C-FIND, C-MOVE,
338            and storage commitment, if presented */
339 
340         std::vector<const char*> genericTransferSyntaxes;
341         genericTransferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax);
342         genericTransferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax);
343         genericTransferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax);
344 
345         std::vector<const char*> knownAbstractSyntaxes;
346 
347         // For C-ECHO (always enabled since Orthanc 1.6.0; in earlier
348         // versions, only enabled if C-STORE was also enabled)
349         knownAbstractSyntaxes.push_back(UID_VerificationSOPClass);
350 
351         // For C-FIND
352         if (server.HasFindRequestHandlerFactory())
353         {
354           knownAbstractSyntaxes.push_back(UID_FINDPatientRootQueryRetrieveInformationModel);
355           knownAbstractSyntaxes.push_back(UID_FINDStudyRootQueryRetrieveInformationModel);
356         }
357 
358         if (server.HasWorklistRequestHandlerFactory())
359         {
360           knownAbstractSyntaxes.push_back(UID_FINDModalityWorklistInformationModel);
361         }
362 
363         // For C-MOVE
364         if (server.HasMoveRequestHandlerFactory())
365         {
366           knownAbstractSyntaxes.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel);
367           knownAbstractSyntaxes.push_back(UID_MOVEPatientRootQueryRetrieveInformationModel);
368         }
369 
370         // For C-GET
371         if (server.HasGetRequestHandlerFactory())
372         {
373           knownAbstractSyntaxes.push_back(UID_GETStudyRootQueryRetrieveInformationModel);
374           knownAbstractSyntaxes.push_back(UID_GETPatientRootQueryRetrieveInformationModel);
375         }
376 
377         cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
378           assoc->params,
379           &knownAbstractSyntaxes[0], knownAbstractSyntaxes.size(),
380           &genericTransferSyntaxes[0], genericTransferSyntaxes.size());
381         if (cond.bad())
382         {
383           CLOG(INFO, DICOM) << cond.text();
384           AssociationCleanup(assoc);
385           return NULL;
386         }
387 
388 
389         /* storage commitment support, new in Orthanc 1.6.0 */
390         if (server.HasStorageCommitmentRequestHandlerFactory())
391         {
392           /**
393            * "ASC_SC_ROLE_SCUSCP": The "SCU" role is needed to accept
394            * remote storage commitment requests, and the "SCP" role is
395            * needed to receive storage commitments answers.
396            **/
397           const char* as[1] = { UID_StorageCommitmentPushModelSOPClass };
398           cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
399             assoc->params, as, 1,
400             &genericTransferSyntaxes[0], genericTransferSyntaxes.size(), ASC_SC_ROLE_SCUSCP);
401           if (cond.bad())
402           {
403             CLOG(INFO, DICOM) << cond.text();
404             AssociationCleanup(assoc);
405             return NULL;
406           }
407         }
408       }
409 
410 
411       {
412         /* accept the abstract syntaxes for C-STORE, if presented */
413 
414         std::set<DicomTransferSyntax> storageTransferSyntaxes;
415 
416         if (server.HasApplicationEntityFilter())
417         {
418           server.GetApplicationEntityFilter().GetAcceptedTransferSyntaxes(
419             storageTransferSyntaxes, remoteIp, remoteAet, calledAet);
420         }
421         else
422         {
423           /**
424            * In the absence of filter, accept all the known transfer
425            * syntaxes. Note that this is different from Orthanc
426            * framework <= 1.8.2, where only the uncompressed transfer
427            * syntaxes are accepted by default.
428            **/
429           GetAllDicomTransferSyntaxes(storageTransferSyntaxes);
430         }
431 
432         if (storageTransferSyntaxes.empty())
433         {
434           LOG(WARNING) << "The DICOM server accepts no transfer syntax, thus C-STORE SCP is disabled";
435         }
436         else
437         {
438           /**
439            * If accepted, put "Little Endian Explicit" at the first
440            * place in the accepted transfer syntaxes. This first place
441            * has an impact on the result of "getscu" (cf. integration
442            * test "test_getscu"). We choose "Little Endian Explicit",
443            * as this preserves the VR of the private tags, even if the
444            * remote modality doesn't have the dictionary of private tags.
445            *
446            * TODO - Should "PREFERRED_TRANSFER_SYNTAX" be an option of
447            * class "DicomServer"?
448            **/
449           const DicomTransferSyntax PREFERRED_TRANSFER_SYNTAX = DicomTransferSyntax_LittleEndianExplicit;
450 
451           E_TransferSyntax dummy;
452           assert(FromDcmtkBridge::LookupDcmtkTransferSyntax(dummy, PREFERRED_TRANSFER_SYNTAX));
453 
454           std::vector<const char*> storageTransferSyntaxesC;
455           storageTransferSyntaxesC.reserve(storageTransferSyntaxes.size());
456 
457           if (storageTransferSyntaxes.find(PREFERRED_TRANSFER_SYNTAX) != storageTransferSyntaxes.end())
458           {
459             storageTransferSyntaxesC.push_back(GetTransferSyntaxUid(PREFERRED_TRANSFER_SYNTAX));
460           }
461 
462           for (std::set<DicomTransferSyntax>::const_iterator
463                  syntax = storageTransferSyntaxes.begin(); syntax != storageTransferSyntaxes.end(); ++syntax)
464           {
465             if (*syntax != PREFERRED_TRANSFER_SYNTAX &&  // Don't add the preferred transfer syntax twice
466                 // Make sure that the current version of DCMTK knows this transfer syntax
467                 FromDcmtkBridge::LookupDcmtkTransferSyntax(dummy, *syntax))
468             {
469               storageTransferSyntaxesC.push_back(GetTransferSyntaxUid(*syntax));
470             }
471           }
472 
473           /* the array of Storage SOP Class UIDs that is defined within "dcmdata/libsrc/dcuid.cc" */
474           size_t count = 0;
475           while (dcmAllStorageSOPClassUIDs[count] != NULL)
476           {
477             count++;
478           }
479 
480 #if DCMTK_VERSION_NUMBER >= 362
481           // The global variable "numberOfDcmAllStorageSOPClassUIDs" is
482           // only published if DCMTK >= 3.6.2:
483           // https://bugs.orthanc-server.com/show_bug.cgi?id=137
484           assert(static_cast<int>(count) == numberOfDcmAllStorageSOPClassUIDs);
485 #endif
486 
487           if (!server.HasGetRequestHandlerFactory())    // dcmqrsrv.cc line 828
488           {
489             // This branch exactly corresponds to Orthanc <= 1.6.1 (in
490             // which C-GET SCP was not supported)
491             cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
492               assoc->params, dcmAllStorageSOPClassUIDs, count,
493               &storageTransferSyntaxesC[0], storageTransferSyntaxesC.size());
494             if (cond.bad())
495             {
496               CLOG(INFO, DICOM) << cond.text();
497               AssociationCleanup(assoc);
498               return NULL;
499             }
500           }
501           else                                         // see dcmqrsrv.cc lines 839 - 876
502           {
503             /* accept storage syntaxes with proposed role */
504             int npc = ASC_countPresentationContexts(assoc->params);
505             for (int i = 0; i < npc; i++)
506             {
507               T_ASC_PresentationContext pc;
508               ASC_getPresentationContext(assoc->params, i, &pc);
509               if (dcmIsaStorageSOPClassUID(pc.abstractSyntax))
510               {
511                 /**
512                  * We are prepared to accept whatever role the caller
513                  * proposes.  Normally we can be the SCP of the Storage
514                  * Service Class.  When processing the C-GET operation
515                  * we can be the SCU of the Storage Service Class.
516                  **/
517                 const T_ASC_SC_ROLE role = pc.proposedRole;
518 
519                 /**
520                  * Accept in the order "least wanted" to "most wanted"
521                  * transfer syntax.  Accepting a transfer syntax will
522                  * override previously accepted transfer syntaxes.
523                  **/
524                 for (int k = static_cast<int>(storageTransferSyntaxesC.size()) - 1; k >= 0; k--)
525                 {
526                   for (int j = 0; j < static_cast<int>(pc.transferSyntaxCount); j++)
527                   {
528                     /**
529                      * If the transfer syntax was proposed then we can accept it
530                      * appears in our supported list of transfer syntaxes
531                      **/
532                     if (strcmp(pc.proposedTransferSyntaxes[j], storageTransferSyntaxesC[k]) == 0)
533                     {
534                       cond = ASC_acceptPresentationContext(
535                         assoc->params, pc.presentationContextID, storageTransferSyntaxesC[k], role);
536                       if (cond.bad())
537                       {
538                         CLOG(INFO, DICOM) << cond.text();
539                         AssociationCleanup(assoc);
540                         return NULL;
541                       }
542                     }
543                   }
544                 }
545               }
546             } /* for */
547           }
548 
549           if (!server.HasApplicationEntityFilter() ||
550               server.GetApplicationEntityFilter().IsUnknownSopClassAccepted(remoteIp, remoteAet, calledAet))
551           {
552             /*
553              * Promiscous mode is enabled: Accept everything not known not
554              * to be a storage SOP class.
555              **/
556             cond = acceptUnknownContextsWithPreferredTransferSyntaxes(
557               assoc->params, &storageTransferSyntaxesC[0], storageTransferSyntaxesC.size(), ASC_SC_ROLE_DEFAULT);
558             if (cond.bad())
559             {
560               CLOG(INFO, DICOM) << cond.text();
561               AssociationCleanup(assoc);
562               return NULL;
563             }
564           }
565         }
566       }
567 
568       /* set our app title */
569       ASC_setAPTitles(assoc->params, NULL, NULL, server.GetApplicationEntityTitle().c_str());
570 
571       /* acknowledge or reject this association */
572 #if DCMTK_VERSION_NUMBER >= 364
573       cond = ASC_getApplicationContextName(assoc->params, buf, sizeof(buf));
574 #else
575       cond = ASC_getApplicationContextName(assoc->params, buf);
576 #endif
577 
578       if ((cond.bad()) || strcmp(buf, UID_StandardApplicationContext) != 0)
579       {
580         /* reject: the application context name is not supported */
581         T_ASC_RejectParameters rej =
582           {
583             ASC_RESULT_REJECTEDPERMANENT,
584             ASC_SOURCE_SERVICEUSER,
585             ASC_REASON_SU_APPCONTEXTNAMENOTSUPPORTED
586           };
587 
588         CLOG(INFO, DICOM) << "Association Rejected: Bad Application Context Name: " << buf;
589         cond = ASC_rejectAssociation(assoc, &rej);
590         if (cond.bad())
591         {
592           CLOG(INFO, DICOM) << cond.text();
593         }
594         AssociationCleanup(assoc);
595         return NULL;
596       }
597 
598       /* check the AETs */
599       if (!server.IsMyAETitle(calledAet))
600       {
601         CLOG(WARNING, DICOM) << "Rejected association, because of a bad called AET in the request (" << calledAet << ")";
602         T_ASC_RejectParameters rej =
603           {
604             ASC_RESULT_REJECTEDPERMANENT,
605             ASC_SOURCE_SERVICEUSER,
606             ASC_REASON_SU_CALLEDAETITLENOTRECOGNIZED
607           };
608         ASC_rejectAssociation(assoc, &rej);
609         AssociationCleanup(assoc);
610         return NULL;
611       }
612 
613       if (server.HasApplicationEntityFilter() &&
614           !server.GetApplicationEntityFilter().IsAllowedConnection(remoteIp, remoteAet, calledAet))
615       {
616         CLOG(WARNING, DICOM) << "Rejected association for remote AET " << remoteAet << " on IP " << remoteIp;
617         T_ASC_RejectParameters rej =
618           {
619             ASC_RESULT_REJECTEDPERMANENT,
620             ASC_SOURCE_SERVICEUSER,
621             ASC_REASON_SU_CALLINGAETITLENOTRECOGNIZED
622           };
623         ASC_rejectAssociation(assoc, &rej);
624         AssociationCleanup(assoc);
625         return NULL;
626       }
627 
628       if (opt_rejectWithoutImplementationUID &&
629           strlen(assoc->params->theirImplementationClassUID) == 0)
630       {
631         /* reject: the no implementation Class UID provided */
632         T_ASC_RejectParameters rej =
633           {
634             ASC_RESULT_REJECTEDPERMANENT,
635             ASC_SOURCE_SERVICEUSER,
636             ASC_REASON_SU_NOREASON
637           };
638 
639         CLOG(INFO, DICOM) << "Association Rejected: No Implementation Class UID provided";
640         cond = ASC_rejectAssociation(assoc, &rej);
641         if (cond.bad())
642         {
643           CLOG(INFO, DICOM) << cond.text();
644         }
645         AssociationCleanup(assoc);
646         return NULL;
647       }
648 
649       {
650         cond = ASC_acknowledgeAssociation(assoc);
651         if (cond.bad())
652         {
653           CLOG(ERROR, DICOM) << cond.text();
654           AssociationCleanup(assoc);
655           return NULL;
656         }
657 
658         {
659           std::string suffix;
660           if (ASC_countAcceptedPresentationContexts(assoc->params) == 0)
661             suffix = " (but no valid presentation contexts)";
662 
663           CLOG(INFO, DICOM) << "Association Acknowledged (Max Send PDV: " << assoc->sendPDVLength
664                             << ") to AET " << remoteAet << " on IP " << remoteIp << suffix;
665         }
666 
667         {
668           OFString str;
669           CLOG(TRACE, DICOM) << "Association Acknowledged Details:" << std::endl
670                              << ASC_dumpParameters(str, assoc->params, ASC_ASSOC_AC);
671         }
672       }
673 
674       IApplicationEntityFilter* filter = server.HasApplicationEntityFilter() ? &server.GetApplicationEntityFilter() : NULL;
675       return new CommandDispatcher(server, assoc, remoteIp, remoteAet, calledAet, maximumPduLength, filter);
676     }
677 
678 
CommandDispatcher(const DicomServer & server,T_ASC_Association * assoc,const std::string & remoteIp,const std::string & remoteAet,const std::string & calledAet,unsigned int maximumPduLength,IApplicationEntityFilter * filter)679     CommandDispatcher::CommandDispatcher(const DicomServer& server,
680                                          T_ASC_Association* assoc,
681                                          const std::string& remoteIp,
682                                          const std::string& remoteAet,
683                                          const std::string& calledAet,
684                                          unsigned int maximumPduLength,
685                                          IApplicationEntityFilter* filter) :
686       server_(server),
687       assoc_(assoc),
688       remoteIp_(remoteIp),
689       remoteAet_(remoteAet),
690       calledAet_(calledAet),
691       filter_(filter)
692     {
693       associationTimeout_ = server.GetAssociationTimeout();
694       elapsedTimeSinceLastCommand_ = 0;
695     }
696 
697 
~CommandDispatcher()698     CommandDispatcher::~CommandDispatcher()
699     {
700       try
701       {
702         AssociationCleanup(assoc_);
703       }
704       catch (...)
705       {
706         CLOG(ERROR, DICOM) << "Some association was not cleanly aborted";
707       }
708     }
709 
710 
Step()711     bool CommandDispatcher::Step()
712     /*
713      * This function receives DIMSE commmands over the network connection
714      * and handles these commands correspondingly. Note that in case of
715      * storscp only C-ECHO-RQ and C-STORE-RQ commands can be processed.
716      */
717     {
718       bool finished = false;
719 
720       // receive a DIMSE command over the network, with a timeout of 1 second
721       DcmDataset *statusDetail = NULL;
722       T_ASC_PresentationContextID presID = 0;
723       T_DIMSE_Message msg;
724 
725       OFCondition cond = DIMSE_receiveCommand(assoc_, DIMSE_NONBLOCKING, 1, &presID, &msg, &statusDetail);
726       elapsedTimeSinceLastCommand_++;
727 
728       // if the command which was received has extra status
729       // detail information, dump this information
730       if (statusDetail != NULL)
731       {
732         std::stringstream s;  // DcmObject::PrintHelper cannot be used with VS2008
733         statusDetail->print(s);
734         CLOG(TRACE, DICOM) << "Status Detail:" << std::endl << s.str();
735 
736         delete statusDetail;
737       }
738 
739       if (cond == DIMSE_OUTOFRESOURCES)
740       {
741         finished = true;
742       }
743       else if (cond == DIMSE_NODATAAVAILABLE)
744       {
745         // Timeout due to DIMSE_NONBLOCKING
746         if (associationTimeout_ != 0 &&
747             elapsedTimeSinceLastCommand_ >= associationTimeout_)
748         {
749           // This timeout is actually a association timeout
750           finished = true;
751         }
752       }
753       else if (cond == EC_Normal)
754       {
755         {
756           OFString str;
757           CLOG(TRACE, DICOM) << "Received Command:" << std::endl
758                              << DIMSE_dumpMessage(str, msg, DIMSE_INCOMING, NULL, presID);
759         }
760 
761         // Reset the association timeout counter
762         elapsedTimeSinceLastCommand_ = 0;
763 
764         // Convert the type of request to Orthanc's internal type
765         bool supported = false;
766         DicomRequestType request;
767         switch (msg.CommandField)
768         {
769           case DIMSE_C_ECHO_RQ:
770             request = DicomRequestType_Echo;
771             supported = true;
772             break;
773 
774           case DIMSE_C_STORE_RQ:
775             request = DicomRequestType_Store;
776             supported = true;
777             break;
778 
779           case DIMSE_C_MOVE_RQ:
780             request = DicomRequestType_Move;
781             supported = true;
782             break;
783 
784           case DIMSE_C_GET_RQ:
785             request = DicomRequestType_Get;
786             supported = true;
787             break;
788 
789           case DIMSE_C_FIND_RQ:
790             request = DicomRequestType_Find;
791             supported = true;
792             break;
793 
794           case DIMSE_N_ACTION_RQ:
795             request = DicomRequestType_NAction;
796             supported = true;
797             break;
798 
799           case DIMSE_N_EVENT_REPORT_RQ:
800             request = DicomRequestType_NEventReport;
801             supported = true;
802             break;
803 
804           default:
805             // we cannot handle this kind of message
806             cond = DIMSE_BADCOMMANDTYPE;
807             CLOG(ERROR, DICOM) << "cannot handle command: 0x" << std::hex << msg.CommandField;
808             break;
809         }
810 
811 
812         // Check whether this request is allowed by the security filter
813         if (supported &&
814             filter_ != NULL &&
815             !filter_->IsAllowedRequest(remoteIp_, remoteAet_, calledAet_, request))
816         {
817           CLOG(WARNING, DICOM) << "Rejected " << EnumerationToString(request)
818                                << " request from remote DICOM modality with AET \""
819                                << remoteAet_ << "\" and hostname \"" << remoteIp_ << "\"";
820           cond = DIMSE_ILLEGALASSOCIATION;
821           supported = false;
822           finished = true;
823         }
824 
825         // in case we received a supported message, process this command
826         if (supported)
827         {
828           // If anything goes wrong, there will be a "BADCOMMANDTYPE" answer
829           cond = DIMSE_BADCOMMANDTYPE;
830 
831           switch (request)
832           {
833             case DicomRequestType_Echo:
834               cond = EchoScp(assoc_, &msg, presID);
835               break;
836 
837             case DicomRequestType_Store:
838               if (server_.HasStoreRequestHandlerFactory()) // Should always be true
839               {
840                 std::unique_ptr<IStoreRequestHandler> handler
841                   (server_.GetStoreRequestHandlerFactory().ConstructStoreRequestHandler());
842 
843                 if (handler.get() != NULL)
844                 {
845                   cond = Internals::storeScp(assoc_, &msg, presID, *handler, remoteIp_, associationTimeout_);
846                 }
847               }
848               break;
849 
850             case DicomRequestType_Move:
851               if (server_.HasMoveRequestHandlerFactory()) // Should always be true
852               {
853                 std::unique_ptr<IMoveRequestHandler> handler
854                   (server_.GetMoveRequestHandlerFactory().ConstructMoveRequestHandler());
855 
856                 if (handler.get() != NULL)
857                 {
858                   cond = Internals::moveScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_, calledAet_, associationTimeout_);
859                 }
860               }
861               break;
862 
863             case DicomRequestType_Get:
864               if (server_.HasGetRequestHandlerFactory()) // Should always be true
865               {
866                 std::unique_ptr<IGetRequestHandler> handler
867                   (server_.GetGetRequestHandlerFactory().ConstructGetRequestHandler());
868 
869                 if (handler.get() != NULL)
870                 {
871                   cond = Internals::getScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_, calledAet_, associationTimeout_);
872                 }
873               }
874               break;
875 
876             case DicomRequestType_Find:
877               if (server_.HasFindRequestHandlerFactory() || // Should always be true
878                   server_.HasWorklistRequestHandlerFactory())
879               {
880                 std::unique_ptr<IFindRequestHandler> findHandler;
881                 if (server_.HasFindRequestHandlerFactory())
882                 {
883                   findHandler.reset(server_.GetFindRequestHandlerFactory().ConstructFindRequestHandler());
884                 }
885 
886                 std::unique_ptr<IWorklistRequestHandler> worklistHandler;
887                 if (server_.HasWorklistRequestHandlerFactory())
888                 {
889                   worklistHandler.reset(server_.GetWorklistRequestHandlerFactory().ConstructWorklistRequestHandler());
890                 }
891 
892                 cond = Internals::findScp(assoc_, &msg, presID, findHandler.get(), worklistHandler.get(),
893                                           remoteIp_, remoteAet_, calledAet_, associationTimeout_);
894               }
895               break;
896 
897             case DicomRequestType_NAction:
898               cond = NActionScp(&msg, presID);
899               break;
900 
901             case DicomRequestType_NEventReport:
902               cond = NEventReportScp(&msg, presID);
903               break;
904 
905             default:
906               // Should never happen
907               break;
908           }
909         }
910       }
911       else
912       {
913         // Bad status, which indicates the closing of the connection by
914         // the peer or a network error
915         finished = true;
916 
917         CLOG(INFO, DICOM) << "Finishing association with AET " << remoteAet_
918                           << " on IP " << remoteIp_ << ": " << cond.text();
919       }
920 
921       if (finished)
922       {
923         if (cond == DUL_PEERREQUESTEDRELEASE)
924         {
925           CLOG(INFO, DICOM) << "Association Release with AET " << remoteAet_ << " on IP " << remoteIp_;
926           ASC_acknowledgeRelease(assoc_);
927         }
928         else if (cond == DUL_PEERABORTEDASSOCIATION)
929         {
930           CLOG(INFO, DICOM) << "Association Aborted with AET " << remoteAet_ << " on IP " << remoteIp_;
931         }
932         else
933         {
934           OFString temp_str;
935           CLOG(INFO, DICOM) << "DIMSE failure (aborting association with AET " << remoteAet_
936                             << " on IP " << remoteIp_ << "): " << cond.text();
937           /* some kind of error so abort the association */
938           ASC_abortAssociation(assoc_);
939         }
940       }
941 
942       return !finished;
943     }
944 
945 
EchoScp(T_ASC_Association * assoc,T_DIMSE_Message * msg,T_ASC_PresentationContextID presID)946     OFCondition EchoScp(T_ASC_Association * assoc, T_DIMSE_Message * msg, T_ASC_PresentationContextID presID)
947     {
948       OFString temp_str;
949       CLOG(INFO, DICOM) << "Received Echo Request";
950 
951       /* the echo succeeded !! */
952       OFCondition cond = DIMSE_sendEchoResponse(assoc, presID, &msg->msg.CEchoRQ, STATUS_Success, NULL);
953       if (cond.bad())
954       {
955         CLOG(ERROR, DICOM) << "Echo SCP Failed: " << cond.text();
956       }
957       return cond;
958     }
959 
960 
ReadDataset(T_ASC_Association * assoc,const char * errorMessage,int timeout)961     static DcmDataset* ReadDataset(T_ASC_Association* assoc,
962                                    const char* errorMessage,
963                                    int timeout)
964     {
965       DcmDataset *tmp = NULL;
966       T_ASC_PresentationContextID presIdData;
967 
968       OFCondition cond = DIMSE_receiveDataSetInMemory(
969         assoc, (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout,
970         &presIdData, &tmp, NULL, NULL);
971       if (!cond.good() ||
972           tmp == NULL)
973       {
974         throw OrthancException(ErrorCode_NetworkProtocol, errorMessage);
975       }
976 
977       return tmp;
978     }
979 
980 
ReadString(DcmDataset & dataset,const DcmTagKey & tag)981     static std::string ReadString(DcmDataset& dataset,
982                                   const DcmTagKey& tag)
983     {
984       const char* s = NULL;
985       if (!dataset.findAndGetString(tag, s).good() ||
986           s == NULL)
987       {
988         char buf[64];
989         sprintf(buf, "Missing mandatory tag in dataset: (%04X,%04X)",
990                 tag.getGroup(), tag.getElement());
991         throw OrthancException(ErrorCode_NetworkProtocol, buf);
992       }
993 
994       return std::string(s);
995     }
996 
997 
ReadSopSequence(std::vector<std::string> & sopClassUids,std::vector<std::string> & sopInstanceUids,std::vector<StorageCommitmentFailureReason> * failureReasons,DcmDataset & dataset,const DcmTagKey & tag,bool mandatory)998     static void ReadSopSequence(
999       std::vector<std::string>& sopClassUids,
1000       std::vector<std::string>& sopInstanceUids,
1001       std::vector<StorageCommitmentFailureReason>* failureReasons, // Can be NULL
1002       DcmDataset& dataset,
1003       const DcmTagKey& tag,
1004       bool mandatory)
1005     {
1006       sopClassUids.clear();
1007       sopInstanceUids.clear();
1008 
1009       if (failureReasons)
1010       {
1011         failureReasons->clear();
1012       }
1013 
1014       DcmSequenceOfItems* sequence = NULL;
1015       if (!dataset.findAndGetSequence(tag, sequence).good() ||
1016           sequence == NULL)
1017       {
1018         if (mandatory)
1019         {
1020           char buf[64];
1021           sprintf(buf, "Missing mandatory sequence in dataset: (%04X,%04X)",
1022                   tag.getGroup(), tag.getElement());
1023           throw OrthancException(ErrorCode_NetworkProtocol, buf);
1024         }
1025         else
1026         {
1027           return;
1028         }
1029       }
1030 
1031       sopClassUids.reserve(sequence->card());
1032       sopInstanceUids.reserve(sequence->card());
1033 
1034       if (failureReasons)
1035       {
1036         failureReasons->reserve(sequence->card());
1037       }
1038 
1039       for (unsigned long i = 0; i < sequence->card(); i++)
1040       {
1041         const char* a = NULL;
1042         const char* b = NULL;
1043         if (!sequence->getItem(i)->findAndGetString(DCM_ReferencedSOPClassUID, a).good() ||
1044             !sequence->getItem(i)->findAndGetString(DCM_ReferencedSOPInstanceUID, b).good() ||
1045             a == NULL ||
1046             b == NULL)
1047         {
1048           throw OrthancException(ErrorCode_NetworkProtocol,
1049                                  "Missing Referenced SOP Class/Instance UID "
1050                                  "in storage commitment dataset");
1051         }
1052 
1053         sopClassUids.push_back(a);
1054         sopInstanceUids.push_back(b);
1055 
1056         if (failureReasons != NULL)
1057         {
1058           Uint16 reason;
1059           if (!sequence->getItem(i)->findAndGetUint16(DCM_FailureReason, reason).good())
1060           {
1061             throw OrthancException(ErrorCode_NetworkProtocol,
1062                                    "Missing Failure Reason (0008,1197) "
1063                                    "in storage commitment dataset");
1064           }
1065 
1066           failureReasons->push_back(static_cast<StorageCommitmentFailureReason>(reason));
1067         }
1068       }
1069     }
1070 
1071 
NActionScp(T_DIMSE_Message * msg,T_ASC_PresentationContextID presID)1072     OFCondition CommandDispatcher::NActionScp(T_DIMSE_Message* msg,
1073                                               T_ASC_PresentationContextID presID)
1074     {
1075       /**
1076        * Starting with Orthanc 1.6.0, only storage commitment is
1077        * supported with DICOM N-ACTION. This corresponds to the case
1078        * where "Action Type ID" equals "1".
1079        * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html
1080        * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-4
1081        **/
1082 
1083       if (msg->CommandField != DIMSE_N_ACTION_RQ /* value == 304 == 0x0130 */ ||
1084           !server_.HasStorageCommitmentRequestHandlerFactory())
1085       {
1086         throw OrthancException(ErrorCode_InternalError);
1087       }
1088 
1089 
1090       /**
1091        * Check that the storage commitment request is correctly formatted.
1092        **/
1093 
1094       const T_DIMSE_N_ActionRQ& request = msg->msg.NActionRQ;
1095 
1096       if (request.ActionTypeID != 1)
1097       {
1098         throw OrthancException(ErrorCode_NotImplemented,
1099                                "Only storage commitment is implemented for DICOM N-ACTION SCP");
1100       }
1101 
1102       if (std::string(request.RequestedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
1103           std::string(request.RequestedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance)
1104       {
1105         throw OrthancException(ErrorCode_NetworkProtocol,
1106                                "Unexpected incoming SOP class or instance UID for storage commitment");
1107       }
1108 
1109       if (request.DataSetType != DIMSE_DATASET_PRESENT)
1110       {
1111         throw OrthancException(ErrorCode_NetworkProtocol,
1112                                "Incoming storage commitment request without a dataset");
1113       }
1114 
1115 
1116       /**
1117        * Extract the DICOM dataset that is associated with the DIMSE
1118        * message. The content of this dataset is documented in "Table
1119        * J.3-1. Storage Commitment Request - Action Information":
1120        * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html#table_J.3-1
1121        **/
1122 
1123       std::unique_ptr<DcmDataset> dataset(
1124         ReadDataset(assoc_, "Cannot read the dataset in N-ACTION SCP", associationTimeout_));
1125       assert(dataset.get() != NULL);
1126 
1127       {
1128         std::stringstream s;  // DcmObject::PrintHelper cannot be used with VS2008
1129         dataset->print(s);
1130         CLOG(TRACE, DICOM) << "Received Storage Commitment Request:" << std::endl << s.str();
1131       }
1132 
1133       std::string transactionUid = ReadString(*dataset, DCM_TransactionUID);
1134 
1135       std::vector<std::string> sopClassUid, sopInstanceUid;
1136       ReadSopSequence(sopClassUid, sopInstanceUid, NULL,
1137                       *dataset, DCM_ReferencedSOPSequence, true /* mandatory */);
1138 
1139       CLOG(INFO, DICOM) << "Incoming storage commitment request, with transaction UID: " << transactionUid;
1140 
1141       for (size_t i = 0; i < sopClassUid.size(); i++)
1142       {
1143         CLOG(INFO, DICOM) << "  (" << (i + 1) << "/" << sopClassUid.size()
1144                           << ") queried SOP Class/Instance UID: "
1145                           << sopClassUid[i] << " / " << sopInstanceUid[i];
1146       }
1147 
1148 
1149       /**
1150        * Call the Orthanc handler. The list of available DIMSE status
1151        * codes can be found at:
1152        * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.4.1.10
1153        **/
1154 
1155       DIC_US dimseStatus;
1156 
1157       try
1158       {
1159         std::unique_ptr<IStorageCommitmentRequestHandler> handler
1160           (server_.GetStorageCommitmentRequestHandlerFactory().
1161            ConstructStorageCommitmentRequestHandler());
1162 
1163         handler->HandleRequest(transactionUid, sopClassUid, sopInstanceUid,
1164                                remoteIp_, remoteAet_, calledAet_);
1165 
1166         dimseStatus = 0;  // Success
1167       }
1168       catch (OrthancException& e)
1169       {
1170         CLOG(ERROR, DICOM) << "Error while processing an incoming storage commitment request: " << e.What();
1171 
1172         // Code 0x0110 - "General failure in processing the operation was encountered"
1173         dimseStatus = STATUS_N_ProcessingFailure;
1174       }
1175 
1176 
1177       /**
1178        * Send the DIMSE status back to the SCU.
1179        **/
1180 
1181       {
1182         T_DIMSE_Message response;
1183         memset(&response, 0, sizeof(response));
1184         response.CommandField = DIMSE_N_ACTION_RSP;
1185 
1186         T_DIMSE_N_ActionRSP& content = response.msg.NActionRSP;
1187         content.MessageIDBeingRespondedTo = request.MessageID;
1188         strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
1189         content.DimseStatus = dimseStatus;
1190         strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
1191         content.ActionTypeID = 0; // Not present, as "O_NACTION_ACTIONTYPEID" not set in "opts"
1192         content.DataSetType = DIMSE_DATASET_NULL;  // Dataset is absent in storage commitment response
1193         content.opts = O_NACTION_AFFECTEDSOPCLASSUID | O_NACTION_AFFECTEDSOPINSTANCEUID;
1194 
1195         {
1196           OFString str;
1197           CLOG(TRACE, DICOM) << "Sending Storage Commitment Request Response:" << std::endl
1198                              << DIMSE_dumpMessage(str, response, DIMSE_OUTGOING);
1199         }
1200 
1201         return DIMSE_sendMessageUsingMemoryData(
1202           assoc_, presID, &response, NULL /* no dataset */, NULL /* dataObject */,
1203           NULL /* callback */, NULL /* callback context */, NULL /* commandSet */);
1204       }
1205     }
1206 
1207 
NEventReportScp(T_DIMSE_Message * msg,T_ASC_PresentationContextID presID)1208     OFCondition CommandDispatcher::NEventReportScp(T_DIMSE_Message* msg,
1209                                                    T_ASC_PresentationContextID presID)
1210     {
1211       /**
1212        * Starting with Orthanc 1.6.0, handling N-EVENT-REPORT for
1213        * storage commitment.
1214        * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
1215        * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-1
1216        **/
1217 
1218       if (msg->CommandField != DIMSE_N_EVENT_REPORT_RQ /* value == 256 == 0x0100 */ ||
1219           !server_.HasStorageCommitmentRequestHandlerFactory())
1220       {
1221         throw OrthancException(ErrorCode_InternalError);
1222       }
1223 
1224 
1225       /**
1226        * Check that the storage commitment report is correctly formatted.
1227        **/
1228 
1229       const T_DIMSE_N_EventReportRQ& report = msg->msg.NEventReportRQ;
1230 
1231       if (report.EventTypeID != 1 /* successful */ &&
1232           report.EventTypeID != 2 /* failures exist */)
1233       {
1234         throw OrthancException(ErrorCode_NotImplemented,
1235                                "Unknown event for DICOM N-EVENT-REPORT SCP");
1236       }
1237 
1238       if (std::string(report.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
1239           std::string(report.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance)
1240       {
1241         throw OrthancException(ErrorCode_NetworkProtocol,
1242                                "Unexpected incoming SOP class or instance UID for storage commitment");
1243       }
1244 
1245       if (report.DataSetType != DIMSE_DATASET_PRESENT)
1246       {
1247         throw OrthancException(ErrorCode_NetworkProtocol,
1248                                "Incoming storage commitment report without a dataset");
1249       }
1250 
1251 
1252       /**
1253        * Extract the DICOM dataset that is associated with the DIMSE
1254        * message. The content of this dataset is documented in "Table
1255        * J.3-2. Storage Commitment Result - Event Information":
1256        * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html#table_J.3-2
1257        **/
1258 
1259       std::unique_ptr<DcmDataset> dataset(
1260         ReadDataset(assoc_, "Cannot read the dataset in N-EVENT-REPORT SCP", associationTimeout_));
1261       assert(dataset.get() != NULL);
1262 
1263       {
1264         std::stringstream s;  // DcmObject::PrintHelper cannot be used with VS2008
1265         dataset->print(s);
1266         CLOG(TRACE, DICOM) << "Received Storage Commitment Report:" << std::endl << s.str();
1267       }
1268 
1269       std::string transactionUid = ReadString(*dataset, DCM_TransactionUID);
1270 
1271       std::vector<std::string> successSopClassUid, successSopInstanceUid;
1272       ReadSopSequence(successSopClassUid, successSopInstanceUid, NULL,
1273                       *dataset, DCM_ReferencedSOPSequence,
1274                       (report.EventTypeID == 1) /* mandatory in the case of success */);
1275 
1276       std::vector<std::string> failedSopClassUid, failedSopInstanceUid;
1277       std::vector<StorageCommitmentFailureReason> failureReasons;
1278 
1279       if (report.EventTypeID == 2 /* failures exist */)
1280       {
1281         ReadSopSequence(failedSopClassUid, failedSopInstanceUid, &failureReasons,
1282                         *dataset, DCM_FailedSOPSequence, true);
1283       }
1284 
1285       CLOG(INFO, DICOM) << "Incoming storage commitment report, with transaction UID: " << transactionUid;
1286 
1287       for (size_t i = 0; i < successSopClassUid.size(); i++)
1288       {
1289         CLOG(INFO, DICOM) << "  (success " << (i + 1) << "/" << successSopClassUid.size()
1290                           << ") SOP Class/Instance UID: "
1291                           << successSopClassUid[i] << " / " << successSopInstanceUid[i];
1292       }
1293 
1294       for (size_t i = 0; i < failedSopClassUid.size(); i++)
1295       {
1296         CLOG(INFO, DICOM) << "  (failure " << (i + 1) << "/" << failedSopClassUid.size()
1297                           << ") SOP Class/Instance UID: "
1298                           << failedSopClassUid[i] << " / " << failedSopInstanceUid[i];
1299       }
1300 
1301       /**
1302        * Call the Orthanc handler. The list of available DIMSE status
1303        * codes can be found at:
1304        * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.4.1.10
1305        **/
1306 
1307       DIC_US dimseStatus;
1308 
1309       try
1310       {
1311         std::unique_ptr<IStorageCommitmentRequestHandler> handler
1312           (server_.GetStorageCommitmentRequestHandlerFactory().
1313            ConstructStorageCommitmentRequestHandler());
1314 
1315         handler->HandleReport(transactionUid, successSopClassUid, successSopInstanceUid,
1316                               failedSopClassUid, failedSopInstanceUid, failureReasons,
1317                               remoteIp_, remoteAet_, calledAet_);
1318 
1319         dimseStatus = 0;  // Success
1320       }
1321       catch (OrthancException& e)
1322       {
1323         CLOG(ERROR, DICOM) << "Error while processing an incoming storage commitment report: " << e.What();
1324 
1325         // Code 0x0110 - "General failure in processing the operation was encountered"
1326         dimseStatus = STATUS_N_ProcessingFailure;
1327       }
1328 
1329 
1330       /**
1331        * Send the DIMSE status back to the SCU.
1332        **/
1333 
1334       {
1335         T_DIMSE_Message response;
1336         memset(&response, 0, sizeof(response));
1337         response.CommandField = DIMSE_N_EVENT_REPORT_RSP;
1338 
1339         T_DIMSE_N_EventReportRSP& content = response.msg.NEventReportRSP;
1340         content.MessageIDBeingRespondedTo = report.MessageID;
1341         strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
1342         content.DimseStatus = dimseStatus;
1343         strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
1344         content.EventTypeID = 0; // Not present, as "O_NEVENTREPORT_EVENTTYPEID" not set in "opts"
1345         content.DataSetType = DIMSE_DATASET_NULL;  // Dataset is absent in storage commitment response
1346         content.opts = O_NEVENTREPORT_AFFECTEDSOPCLASSUID | O_NEVENTREPORT_AFFECTEDSOPINSTANCEUID;
1347 
1348         {
1349           OFString str;
1350           CLOG(TRACE, DICOM) << "Sending Storage Commitment Report Response:" << std::endl
1351                              << DIMSE_dumpMessage(str, response, DIMSE_OUTGOING);
1352         }
1353 
1354         return DIMSE_sendMessageUsingMemoryData(
1355           assoc_, presID, &response, NULL /* no dataset */, NULL /* dataObject */,
1356           NULL /* callback */, NULL /* callback context */, NULL /* commandSet */);
1357       }
1358     }
1359   }
1360 }
1361