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