comparison OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp @ 4044:d25f4c0fa160 framework

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