Mercurial > hg > orthanc
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 } |