1 /*
2 *
3 * Copyright (C) 1999-2019, OFFIS e.V.
4 * All rights reserved. See COPYRIGHT file for details.
5 *
6 * This software and supporting documentation were developed by
7 *
8 * OFFIS e.V.
9 * R&D Division Health
10 * Escherweg 2
11 * D-26121 Oldenburg, Germany
12 *
13 *
14 * Module: dcmpstat
15 *
16 * Authors: Marco Eichelberg
17 *
18 * Purpose: Presentation State Viewer - Network Send Component (Store SCU)
19 *
20 */
21
22 #include "dcmtk/config/osconfig.h" /* make sure OS specific configuration is included first */
23
24 BEGIN_EXTERN_C
25 #ifdef HAVE_FCNTL_H
26 #include <fcntl.h> /* for O_RDONLY */
27 #endif
28 #ifdef HAVE_SYS_TYPES_H
29 #include <sys/types.h> /* required for sys/stat.h */
30 #endif
31 #ifdef HAVE_SYS_STAT_H
32 #include <sys/stat.h> /* for stat, fstat */
33 #endif
34 END_EXTERN_C
35
36 #include "dcmtk/dcmpstat/dvpsdef.h" /* for constants */
37 #include "dcmtk/dcmpstat/dvpscf.h" /* for class DVConfiguration */
38 #include "dcmtk/ofstd/ofbmanip.h" /* for OFBitmanipTemplate */
39 #include "dcmtk/dcmdata/dcuid.h" /* for dcmtk version name */
40 #include "dcmtk/dcmdata/dcdeftag.h"
41 #include "dcmtk/dcmdata/dcdict.h"
42 #include "dcmtk/dcmnet/diutil.h"
43 #include "dcmtk/dcmdata/cmdlnarg.h"
44 #include "dcmtk/ofstd/ofconapp.h"
45 #include "dcmtk/ofstd/ofstd.h"
46 #include "dcmtk/dcmpstat/dvpshlp.h" /* for class DVPSHelper */
47 #include "dcmtk/dcmqrdb/dcmqrdbi.h" /* for LOCK_IMAGE_FILES */
48 #include "dcmtk/dcmqrdb/dcmqrdbs.h" /* for DcmQueryRetrieveDatabaseStatus */
49 #include "dcmtk/dcmpstat/dvpsmsg.h"
50
51 #ifdef WITH_OPENSSL
52 #include "dcmtk/dcmtls/tlstrans.h"
53 #include "dcmtk/dcmtls/tlslayer.h"
54 #endif
55
56 #ifdef WITH_ZLIB
57 #include <zlib.h> /* for zlibVersion() */
58 #endif
59
60 #include "dcmtk/ofstd/ofstream.h"
61
62 #define OFFIS_CONSOLE_APPLICATION "dcmpssnd"
63
64 static OFLogger dcmpssndLogger = OFLog::getLogger("dcmtk.apps." OFFIS_CONSOLE_APPLICATION);
65
66 static char rcsid[] = "$dcmtk: " OFFIS_CONSOLE_APPLICATION " v"
67 OFFIS_DCMTK_VERSION " " OFFIS_DCMTK_RELEASEDATE " $";
68
69 DVPSIPCClient *messageClient = NULL; // global pointer to IPC message client, if present
70
71 /** sends a single DICOM instance over an association which must be already established.
72 * @param assoc DICOM network association
73 * @param sopClass SOP Class UID of the image (used for the C-Store-RQ)
74 * @param sopInstance SOP Instance UID of the image (used for the C-Store-RQ)
75 * @param imgFile path to the image file to be transmitted
76 * @return EC_Normal if successful, a different DIMSE code otherwise.
77 */
sendImage(T_ASC_Association * assoc,const char * sopClass,const char * sopInstance,const char * imgFile)78 static OFCondition sendImage(T_ASC_Association *assoc, const char *sopClass, const char *sopInstance, const char *imgFile)
79 {
80 DcmDataset *statusDetail = NULL;
81 T_ASC_PresentationContextID presId=0;
82 T_DIMSE_C_StoreRQ req;
83 T_DIMSE_C_StoreRSP rsp;
84
85 if (assoc == NULL) return DIMSE_NULLKEY;
86 if ((sopClass == NULL)||(strlen(sopClass) == 0)) return DIMSE_NULLKEY;
87 if ((sopInstance == NULL)||(strlen(sopInstance) == 0)) return DIMSE_NULLKEY;
88 if ((imgFile == NULL)||(strlen(imgFile) == 0)) return DIMSE_NULLKEY;
89
90 #ifdef LOCK_IMAGE_FILES
91 /* shared lock image file */
92 #ifdef O_BINARY
93 int lockfd = open(imgFile, O_RDONLY | O_BINARY, 0666);
94 #else
95 int lockfd = open(imgFile, O_RDONLY, 0666);
96 #endif
97 if (lockfd < 0)
98 {
99 OFLOG_INFO(dcmpssndLogger, "unable to lock image file '" << imgFile << "'");
100 return DIMSE_BADDATA;
101 }
102 dcmtk_flock(lockfd, LOCK_SH);
103 #endif
104
105 /* which presentation context should be used */
106 presId = ASC_findAcceptedPresentationContextID(assoc, sopClass);
107 if (presId == 0)
108 {
109 OFLOG_INFO(dcmpssndLogger, "no presentation context for: ("
110 << dcmSOPClassUIDToModality(sopClass, "OT") << ") " << sopClass);
111 if (messageClient)
112 {
113 OFString buf("unable to send image: no presentation context for ");
114 const char *sopClassName = dcmFindNameOfUID(sopClass);
115 if (sopClassName == NULL) buf.append(sopClass); else buf.append(sopClassName);
116 buf.append("\n");
117 messageClient->notifySentDICOMObject(DVPSIPCMessage::statusWarning, buf.c_str());
118 }
119 return DIMSE_NOVALIDPRESENTATIONCONTEXTID;
120 }
121
122 /* start store */
123 OFBitmanipTemplate<char>::zeroMem((char *)&req, sizeof(req));
124 req.MessageID = assoc->nextMsgID++;
125
126 OFStandard::strlcpy(req.AffectedSOPClassUID, sopClass, sizeof(req.AffectedSOPClassUID));
127 OFStandard::strlcpy(req.AffectedSOPInstanceUID, sopInstance, sizeof(req.AffectedSOPInstanceUID));
128 req.DataSetType = DIMSE_DATASET_PRESENT;
129 req.Priority = DIMSE_PRIORITY_MEDIUM;
130
131 OFCondition cond = DIMSE_storeUser(assoc, presId, &req,
132 imgFile, NULL, NULL, NULL, DIMSE_BLOCKING, 0, &rsp, &statusDetail);
133
134 #ifdef LOCK_IMAGE_FILES
135 /* unlock image file */
136 dcmtk_flock(lockfd, LOCK_UN);
137 close(lockfd);
138 #endif
139
140 if (cond.good())
141 {
142 OFLOG_INFO(dcmpssndLogger, "[MsgID " << req.MessageID << "] Complete [Status: "
143 << DU_cstoreStatusString(rsp.DimseStatus) << "]");
144 } else {
145 OFString temp_str;
146 OFLOG_INFO(dcmpssndLogger, "[MsgID " << req.MessageID << "] Failed [Status: "
147 << DU_cstoreStatusString(rsp.DimseStatus) << "]\n" << DimseCondition::dump(temp_str, cond));
148 }
149 if (statusDetail) delete statusDetail;
150
151 if (messageClient)
152 {
153 OFOStringStream out;
154 Uint32 operationStatus = DVPSIPCMessage::statusError;
155 if (cond.good())
156 {
157 if (rsp.DimseStatus == STATUS_Success) operationStatus = DVPSIPCMessage::statusOK;
158 else operationStatus = DVPSIPCMessage::statusWarning;
159 }
160 const char *sopClassName = dcmFindNameOfUID(sopClass);
161 const char *successName = "failed";
162 if (operationStatus == DVPSIPCMessage::statusOK) successName = "successful";
163 if (sopClassName==NULL) sopClassName = sopClass;
164 unsigned long fileSize = 0;
165 struct stat fileStat;
166 if (0 == stat(imgFile, &fileStat)) fileSize = fileStat.st_size;
167 out << "DICOM C-STORE transmission " << successName << ": " << OFendl
168 << "\tSOP class UID : " << sopClassName << OFendl
169 << "\tSOP instance UID : " << sopInstance << OFendl
170 << "\tSource file path : " << imgFile << OFendl
171 << "\tSource file size (kB) : " << (fileSize+1023)/1024 << OFendl
172 << "\tDIMSE presentation ctx : " << (int)presId << OFendl
173 << "\tDIMSE message ID : " << req.MessageID << OFendl
174 << "\tDIMSE status : " << DU_cstoreStatusString(rsp.DimseStatus) << OFendl
175 << OFStringStream_ends;
176 OFSTRINGSTREAM_GETSTR(out, theString)
177 messageClient->notifySentDICOMObject(operationStatus, theString);
178 OFSTRINGSTREAM_FREESTR(theString)
179 }
180 return cond;
181 }
182
183 /** sends a complete DICOM study, series or a single instance
184 * over an association which must be already established.
185 * The instances (files) to be transmitted are derived from the database.
186 * @param handle open database handle
187 * @param assoc DICOM network association
188 * @param studyUID Study Instance UID of the study/series/image to be transmitted.
189 * @param seriesUID Series Instance UID of the series/image to be transmitted.
190 * If NULL, a complete study is transmitted.
191 * @param instanceUID SOP Instance UID of the image to be transmitted.
192 * If NULL, a complete series is transmitted.
193 * @return EC_Normal if successful, a different DIMSE code otherwise.
194 */
195
sendStudy(DcmQueryRetrieveIndexDatabaseHandle & handle,T_ASC_Association * assoc,const char * studyUID,const char * seriesUID,const char * instanceUID)196 static OFCondition sendStudy(
197 DcmQueryRetrieveIndexDatabaseHandle &handle,
198 T_ASC_Association *assoc,
199 const char *studyUID,
200 const char *seriesUID,
201 const char *instanceUID)
202 {
203 if ((assoc==NULL)||(studyUID==NULL)) return DIMSE_NULLKEY;
204
205 /* build query */
206 DcmDataset query;
207 OFCondition cond = DVPSHelper::putStringValue(&query, DCM_StudyInstanceUID, studyUID);
208 if (cond.bad()) return cond;
209 if (seriesUID && instanceUID)
210 {
211 cond = DVPSHelper::putStringValue(&query, DCM_QueryRetrieveLevel, "IMAGE");
212 if (cond.bad()) return cond;
213 cond = DVPSHelper::putStringValue(&query, DCM_SeriesInstanceUID, seriesUID);
214 if (cond.bad()) return cond;
215 cond = DVPSHelper::putStringValue(&query, DCM_SOPInstanceUID, instanceUID);
216 if (cond.bad()) return cond;
217
218 OFLOG_INFO(dcmpssndLogger, "Sending at IMAGE level:" << OFendl
219 << " Study Instance UID : " << studyUID << OFendl
220 << " Series Instance UID: " << seriesUID << OFendl
221 << " SOP Instance UID : " << instanceUID);
222 }
223 else if (seriesUID)
224 {
225 cond = DVPSHelper::putStringValue(&query, DCM_QueryRetrieveLevel, "SERIES");
226 if (cond.bad()) return cond;
227 cond = DVPSHelper::putStringValue(&query, DCM_SeriesInstanceUID, seriesUID);
228 if (cond.bad()) return cond;
229
230 OFLOG_INFO(dcmpssndLogger, "Sending at SERIES level:" << OFendl
231 << " Study Instance UID : " << studyUID << OFendl
232 << " Series Instance UID: " << seriesUID);
233 }
234 else
235 {
236 cond = DVPSHelper::putStringValue(&query, DCM_QueryRetrieveLevel, "STUDY");
237 if (cond.bad()) return cond;
238
239 OFLOG_INFO(dcmpssndLogger, "Sending at STUDY level:" << OFendl
240 << " Study Instance UID : " << studyUID);
241 }
242
243 DcmQueryRetrieveDatabaseStatus dbStatus(STATUS_Pending);
244 DIC_UI sopClass;
245 DIC_UI sopInstance;
246 char imgFile[MAXPATHLEN+1];
247 DIC_US nRemaining = 0;
248
249 cond = handle.startMoveRequest(UID_MOVEStudyRootQueryRetrieveInformationModel, &query, &dbStatus);
250 if (cond.bad()) return cond;
251
252 while (dbStatus.status() == STATUS_Pending)
253 {
254 cond = handle.nextMoveResponse(sopClass, sizeof(sopClass), sopInstance, sizeof(sopInstance), imgFile, sizeof(imgFile), &nRemaining, &dbStatus);
255 if (cond.bad()) return cond;
256
257 if (dbStatus.status() == STATUS_Pending)
258 {
259 cond = sendImage(assoc, sopClass, sopInstance, imgFile);
260 if (cond.bad())
261 {
262 handle.cancelMoveRequest(&dbStatus);
263 return cond;
264 }
265 }
266 }
267 return cond;
268 }
269
270 /** adds presentation contexts for all storage SOP classes
271 * to the association parameters.
272 * If the opt_implicitOnly flag is set, only Implicit VR Little Endian
273 * is offered as transfer syntax. Otherwise, three xfer syntaxes are offered:
274 * first the explicit VR with local byte ordering, followed by explicit VR
275 * with opposite byte ordering, followed by implicit VR little endian.
276 * @param params parameter set to which presentation contexts are added
277 * @param opt_implicitOnly flag defining whether only Implicit VR Little Endian
278 * should be offered as xfer syntax.
279 * @return EC_Normal upon success, an error code otherwise.
280 */
addAllStoragePresentationContexts(T_ASC_Parameters * params,int opt_implicitOnly)281 static OFCondition addAllStoragePresentationContexts(T_ASC_Parameters *params, int opt_implicitOnly)
282 {
283 OFCondition cond = EC_Normal;
284 int pid = 1;
285
286 const char* transferSyntaxes[3];
287 int transferSyntaxCount = 0;
288
289 if (opt_implicitOnly)
290 {
291 transferSyntaxes[0] = UID_LittleEndianImplicitTransferSyntax;
292 transferSyntaxCount = 1;
293 } else {
294 /* gLocalByteOrder is defined in dcxfer.h */
295 if (gLocalByteOrder == EBO_LittleEndian) {
296 /* we are on a little endian machine */
297 transferSyntaxes[0] = UID_LittleEndianExplicitTransferSyntax;
298 transferSyntaxes[1] = UID_BigEndianExplicitTransferSyntax;
299 } else {
300 /* we are on a big endian machine */
301 transferSyntaxes[0] = UID_BigEndianExplicitTransferSyntax;
302 transferSyntaxes[1] = UID_LittleEndianExplicitTransferSyntax;
303 }
304 transferSyntaxes[2] = UID_LittleEndianImplicitTransferSyntax;
305 transferSyntaxCount = 3;
306 }
307
308 for (int i=0; i<numberOfDcmLongSCUStorageSOPClassUIDs && cond.good(); i++) {
309 cond = ASC_addPresentationContext(
310 params, pid, dcmLongSCUStorageSOPClassUIDs[i],
311 transferSyntaxes, transferSyntaxCount);
312 pid += 2; /* only odd presentation context id's */
313 }
314
315 return cond;
316 }
317
318 // ********************************************
319
320 #define SHORTCOL 3
321 #define LONGCOL 12
322
main(int argc,char * argv[])323 int main(int argc, char *argv[])
324 {
325 OFStandard::initializeNetwork();
326 #ifdef WITH_OPENSSL
327 DcmTLSTransportLayer::initializeOpenSSL();
328 #endif
329
330 OFString temp_str;
331
332 const char *opt_cfgName = NULL; /* config file name */
333 const char *opt_target = NULL; /* send target name */
334 const char *opt_studyUID = NULL; /* study instance UID */
335 const char *opt_seriesUID = NULL; /* series instance UID */
336 const char *opt_instanceUID = NULL; /* instance instance UID */
337
338 OFConsoleApplication app(OFFIS_CONSOLE_APPLICATION , "Network send for presentation state viewer", rcsid);
339 OFCommandLine cmd;
340 cmd.setOptionColumns(LONGCOL, SHORTCOL);
341 cmd.setParamColumn(LONGCOL + SHORTCOL + 2);
342
343 cmd.addParam("config-file", "configuration file to be read");
344 cmd.addParam("target", "symbolic identifier of send target in config file");
345 cmd.addParam("study", "study instance UID of study in database to be sent");
346 cmd.addParam("series", "series instance UID (default: send complete study)", OFCmdParam::PM_Optional);
347 cmd.addParam("instance", "SOP instance UID (default: send complete series)", OFCmdParam::PM_Optional);
348
349 cmd.addGroup("general options:");
350 cmd.addOption("--help", "-h", "print this help text and exit", OFCommandLine::AF_Exclusive);
351 cmd.addOption("--version", "print version information and exit", OFCommandLine::AF_Exclusive);
352 OFLog::addOptions(cmd);
353
354 /* evaluate command line */
355 prepareCmdLineArgs(argc, argv, OFFIS_CONSOLE_APPLICATION);
356 if (app.parseCommandLine(cmd, argc, argv))
357 {
358 /* check exclusive options first */
359 if (cmd.hasExclusiveOption())
360 {
361 if (cmd.findOption("--version"))
362 {
363 app.printHeader(OFTrue /*print host identifier*/);
364 COUT << OFendl << "External libraries used:";
365 #if !defined(WITH_ZLIB) && !defined(WITH_OPENSSL)
366 COUT << " none" << OFendl;
367 #else
368 COUT << OFendl;
369 #endif
370 #ifdef WITH_ZLIB
371 COUT << "- ZLIB, Version " << zlibVersion() << OFendl;
372 #endif
373 #ifdef WITH_OPENSSL
374 COUT << "- " << DcmTLSTransportLayer::getOpenSSLVersionName() << OFendl;
375 #endif
376 return 0;
377 }
378 }
379
380 /* command line parameters */
381 cmd.getParam(1, opt_cfgName);
382 cmd.getParam(2, opt_target);
383 cmd.getParam(3, opt_studyUID);
384 if (cmd.getParamCount() >= 4) cmd.getParam(4, opt_seriesUID);
385 if (cmd.getParamCount() >= 5) cmd.getParam(5, opt_instanceUID);
386
387 OFLog::configureFromCommandLine(cmd, app);
388 }
389
390 /* print resource identifier */
391 OFLOG_DEBUG(dcmpssndLogger, rcsid << OFendl);
392
393 if (opt_cfgName)
394 {
395 FILE *cfgfile = fopen(opt_cfgName, "rb");
396 if (cfgfile) fclose(cfgfile); else
397 {
398 OFLOG_FATAL(dcmpssndLogger, "can't open configuration file '" << opt_cfgName << "'");
399 return 10;
400 }
401 } else {
402 OFLOG_FATAL(dcmpssndLogger, "missing configuration file name");
403 return 10;
404 }
405
406 /* make sure data dictionary is loaded */
407 if (!dcmDataDict.isDictionaryLoaded())
408 {
409 OFLOG_WARN(dcmpssndLogger, "no data dictionary loaded, check environment variable: "
410 << DCM_DICT_ENVIRONMENT_VARIABLE);
411 }
412
413 DVConfiguration dvi(opt_cfgName);
414
415 /* get send target from configuration file */
416 const char *targetHostname = dvi.getTargetHostname(opt_target);
417 const char *targetDescription = dvi.getTargetDescription(opt_target);
418 const char *targetAETitle = dvi.getTargetAETitle(opt_target);
419 unsigned short targetPort = dvi.getTargetPort(opt_target);
420 unsigned long targetMaxPDU = dvi.getTargetMaxPDU(opt_target);
421 OFBool targetImplicitOnly = dvi.getTargetImplicitOnly(opt_target);
422 OFBool targetDisableNewVRs = dvi.getTargetDisableNewVRs(opt_target);
423
424 unsigned short messagePort = dvi.getMessagePort(); /* port number for IPC */
425 OFBool keepMessagePortOpen = dvi.getMessagePortKeepOpen();
426 OFBool useTLS = dvi.getTargetUseTLS(opt_target);
427
428 Sint32 timeout = dvi.getTargetTimeout(opt_target);
429 if (timeout > 0) dcmConnectionTimeout.set(timeout);
430
431 #ifdef WITH_OPENSSL
432 /* TLS directory */
433 const char *current = NULL;
434 const char *tlsFolder = dvi.getTLSFolder();
435 if (tlsFolder==NULL) tlsFolder = ".";
436
437 /* certificate file */
438 OFString tlsCertificateFile(tlsFolder);
439 tlsCertificateFile += PATH_SEPARATOR;
440 current = dvi.getTargetCertificate(opt_target);
441 if (current) tlsCertificateFile += current; else tlsCertificateFile.clear();
442
443 /* private key file */
444 OFString tlsPrivateKeyFile(tlsFolder);
445 tlsPrivateKeyFile += PATH_SEPARATOR;
446 current = dvi.getTargetPrivateKey(opt_target);
447 if (current) tlsPrivateKeyFile += current; else tlsPrivateKeyFile.clear();
448
449 /* private key password */
450 const char *tlsPrivateKeyPassword = dvi.getTargetPrivateKeyPassword(opt_target);
451
452 /* certificate verification */
453 DcmCertificateVerification tlsCertVerification = DCV_requireCertificate;
454 switch (dvi.getTargetPeerAuthentication(opt_target))
455 {
456 case DVPSQ_require:
457 tlsCertVerification = DCV_requireCertificate;
458 break;
459 case DVPSQ_verify:
460 tlsCertVerification = DCV_checkCertificate;
461 break;
462 case DVPSQ_ignore:
463 tlsCertVerification = DCV_ignoreCertificate;
464 break;
465 }
466
467 /* DH parameter file */
468 OFString tlsDHParametersFile;
469 current = dvi.getTargetDiffieHellmanParameters(opt_target);
470 if (current)
471 {
472 tlsDHParametersFile = tlsFolder;
473 tlsDHParametersFile += PATH_SEPARATOR;
474 tlsDHParametersFile += current;
475 }
476
477 /* random seed file */
478 OFString tlsRandomSeedFile(tlsFolder);
479 tlsRandomSeedFile += PATH_SEPARATOR;
480 current = dvi.getTargetRandomSeed(opt_target);
481 if (current) tlsRandomSeedFile += current; else tlsRandomSeedFile += "siteseed.bin";
482
483 /* CA certificate directory */
484 const char *tlsCACertificateFolder = dvi.getTLSCACertificateFolder();
485 if (tlsCACertificateFolder==NULL) tlsCACertificateFolder = ".";
486
487 /* key file format */
488 DcmKeyFileFormat keyFileFormat = DCF_Filetype_PEM;
489 if (! dvi.getTLSPEMFormat()) keyFileFormat = DCF_Filetype_ASN1;
490
491 #else
492 if (useTLS)
493 {
494 OFLOG_FATAL(dcmpssndLogger, "not compiled with OpenSSL, cannot use TLS");
495 return 10;
496 }
497 #endif
498
499 if (targetHostname==NULL)
500 {
501 OFLOG_FATAL(dcmpssndLogger, "no hostname for send target '" << opt_target << "'");
502 return 10;
503 }
504
505 if (targetAETitle==NULL)
506 {
507 OFLOG_FATAL(dcmpssndLogger, "no aetitle for send target '" << opt_target << "'");
508 return 10;
509 }
510
511 if (targetPort==0)
512 {
513 OFLOG_FATAL(dcmpssndLogger, "no or invalid port number for send target '" << opt_target << "'");
514 return 10;
515 }
516
517 if (targetMaxPDU==0) targetMaxPDU = DEFAULT_MAXPDU;
518 else if (targetMaxPDU > ASC_MAXIMUMPDUSIZE)
519 {
520 OFLOG_WARN(dcmpssndLogger, "max PDU size " << targetMaxPDU << " too big for send target '"
521 << opt_target << "', using default: " << DEFAULT_MAXPDU);
522 targetMaxPDU = DEFAULT_MAXPDU;
523 }
524
525 if (targetDisableNewVRs)
526 {
527 dcmDisableGenerationOfNewVRs();
528 }
529
530 OFOStringStream verboseParameters;
531
532 verboseParameters << "Send target parameters:" << OFendl
533 << "\thostname : " << targetHostname << OFendl
534 << "\tport : " << targetPort << OFendl
535 << "\tdescription : ";
536 if (targetDescription) verboseParameters << targetDescription; else verboseParameters << "(none)";
537 verboseParameters << OFendl
538 << "\taetitle : " << targetAETitle << OFendl
539 << "\tmax pdu : " << targetMaxPDU << OFendl
540 << "\ttimeout : " << timeout << OFendl
541 << "\toptions : ";
542 if (targetImplicitOnly && targetDisableNewVRs) verboseParameters << "implicit xfer syntax only, disable post-1993 VRs";
543 else if (targetImplicitOnly) verboseParameters << "implicit xfer syntax only";
544 else if (targetDisableNewVRs) verboseParameters << "disable post-1993 VRs";
545 else verboseParameters << "none";
546 verboseParameters << OFendl;
547
548 verboseParameters << "\tTLS : ";
549 if (useTLS) verboseParameters << "enabled" << OFendl; else verboseParameters << "disabled" << OFendl;
550
551 /* open database */
552 const char *dbfolder = dvi.getDatabaseFolder();
553
554 OFLOG_INFO(dcmpssndLogger, "Opening database in directory '" << dbfolder << "'");
555
556 OFCondition result;
557 DcmQueryRetrieveIndexDatabaseHandle dbhandle(dbfolder, PSTAT_MAXSTUDYCOUNT, PSTAT_STUDYSIZE, result);
558 if (result.bad())
559 {
560 OFLOG_FATAL(dcmpssndLogger, "Unable to access database '" << dbfolder << "'");
561 return 1;
562 }
563
564 #ifdef WITH_OPENSSL
565
566 DcmTLSTransportLayer *tLayer = NULL;
567 if (useTLS)
568 {
569 tLayer = new DcmTLSTransportLayer(NET_REQUESTOR, tlsRandomSeedFile.c_str(), OFFalse);
570 if (tLayer == NULL)
571 {
572 OFLOG_FATAL(dcmpssndLogger, "unable to create TLS transport layer");
573 return 1;
574 }
575
576 // determine TLS profile
577 OFString profileName;
578 const char *profileNamePtr = dvi.getTargetTLSProfile(opt_target);
579 if (profileNamePtr) profileName = profileNamePtr;
580 DcmTLSSecurityProfile tlsProfile = TSP_Profile_BCP195; // default
581 if (profileName == "BCP195") tlsProfile = TSP_Profile_BCP195;
582 else if (profileName == "BCP195-ND") tlsProfile = TSP_Profile_BCP195_ND;
583 else if (profileName == "BCP195-EX") tlsProfile = TSP_Profile_BCP195_Extended;
584 else if (profileName == "AES") tlsProfile = TSP_Profile_AES;
585 else if (profileName == "BASIC") tlsProfile = TSP_Profile_Basic;
586 else if (profileName == "NULL") tlsProfile = TSP_Profile_IHE_ATNA_Unencrypted;
587 else
588 {
589 OFLOG_WARN(dcmpssndLogger, "unknown TLS profile '" << profileName << "', ignoring");
590 }
591
592 if (TCS_ok != tLayer->setTLSProfile(tlsProfile))
593 {
594 OFLOG_FATAL(dcmpssndLogger, "unable to select the TLS security profile");
595 return 1;
596 }
597
598 // activate cipher suites
599 if (TCS_ok != tLayer->activateCipherSuites())
600 {
601 OFLOG_FATAL(dcmpssndLogger, "unable to activate the selected list of TLS ciphersuites");
602 return 1;
603 }
604
605 if (tlsCACertificateFolder && (TCS_ok != tLayer->addTrustedCertificateDir(tlsCACertificateFolder, keyFileFormat)))
606 {
607 OFLOG_WARN(dcmpssndLogger, "unable to load certificates from directory '" << tlsCACertificateFolder << "', ignoring");
608 }
609 if ((tlsDHParametersFile.size() > 0) && ! (tLayer->setTempDHParameters(tlsDHParametersFile.c_str())))
610 {
611 OFLOG_WARN(dcmpssndLogger, "unable to load temporary DH parameter file '" << tlsDHParametersFile << "', ignoring");
612 }
613 tLayer->setPrivateKeyPasswd(tlsPrivateKeyPassword); // never prompt on console
614
615 if (!tlsPrivateKeyFile.empty() && !tlsCertificateFile.empty())
616 {
617 if (TCS_ok != tLayer->setPrivateKeyFile(tlsPrivateKeyFile.c_str(), keyFileFormat))
618 {
619 OFLOG_FATAL(dcmpssndLogger, "unable to load private TLS key from '" << tlsPrivateKeyFile<< "'");
620 return 1;
621 }
622 if (TCS_ok != tLayer->setCertificateFile(tlsCertificateFile.c_str(), keyFileFormat))
623 {
624 OFLOG_FATAL(dcmpssndLogger, "unable to load certificate from '" << tlsCertificateFile << "'");
625 return 1;
626 }
627 if (! tLayer->checkPrivateKeyMatchesCertificate())
628 {
629 OFLOG_FATAL(dcmpssndLogger, "private key '" << tlsPrivateKeyFile << "' and certificate '" << tlsCertificateFile << "' do not match");
630 return 1;
631 }
632 }
633
634 tLayer->setCertificateVerification(tlsCertVerification);
635
636 // a generated UID contains the process ID and current time.
637 // Adding it to the PRNG seed guarantees that we have different seeds for different processes.
638 char randomUID[65];
639 dcmGenerateUniqueIdentifier(randomUID);
640 tLayer->addPRNGseed(randomUID, strlen(randomUID));
641 }
642
643 if (useTLS)
644 {
645 OFString cslist;
646 if (tLayer) tLayer->getListOfCipherSuitesForOpenSSL(cslist);
647 verboseParameters << "\tTLS certificate : " << tlsCertificateFile << OFendl
648 << "\tTLS key file : " << tlsPrivateKeyFile << OFendl
649 << "\tTLS DH params : " << tlsDHParametersFile << OFendl
650 << "\tTLS PRNG seed : " << tlsRandomSeedFile << OFendl
651 << "\tTLS CA directory: " << tlsCACertificateFolder << OFendl
652 << "\tTLS ciphersuites: " << cslist << OFendl
653 << "\tTLS key format : ";
654 if (keyFileFormat == DCF_Filetype_PEM) verboseParameters << "PEM" << OFendl; else verboseParameters << "DER" << OFendl;
655 verboseParameters << "\tTLS cert verify : ";
656 switch (tlsCertVerification)
657 {
658 case DCV_checkCertificate:
659 verboseParameters << "verify" << OFendl;
660 break;
661 case DCV_ignoreCertificate:
662 verboseParameters << "ignore" << OFendl;
663 break;
664 default:
665 verboseParameters << "require" << OFendl;
666 break;
667 }
668 }
669 #endif
670
671 verboseParameters << OFStringStream_ends;
672 OFSTRINGSTREAM_GETOFSTRING(verboseParameters, verboseParametersString)
673 OFLOG_INFO(dcmpssndLogger, verboseParametersString);
674
675 /* open network connection */
676 T_ASC_Network *net=NULL;
677 T_ASC_Parameters *params=NULL;
678 DIC_NODENAME peerHost;
679 T_ASC_Association *assoc=NULL;
680
681 OFCondition cond = ASC_initializeNetwork(NET_REQUESTOR, 0, 30, &net);
682 if (cond.bad())
683 {
684 OFLOG_FATAL(dcmpssndLogger, DimseCondition::dump(temp_str, cond));
685 return 1;
686 }
687
688 #ifdef WITH_OPENSSL
689 if (tLayer)
690 {
691 cond = ASC_setTransportLayer(net, tLayer, 0);
692 if (cond.bad())
693 {
694 OFLOG_FATAL(dcmpssndLogger, DimseCondition::dump(temp_str, cond));
695 return 1;
696 }
697 }
698 #endif
699
700 cond = ASC_createAssociationParameters(¶ms, targetMaxPDU);
701 if (cond.bad())
702 {
703 OFLOG_FATAL(dcmpssndLogger, DimseCondition::dump(temp_str, cond));
704 return 1;
705 }
706
707 cond = ASC_setTransportLayerType(params, useTLS);
708 if (cond.bad())
709 {
710 OFLOG_FATAL(dcmpssndLogger, DimseCondition::dump(temp_str, cond));
711 return 1;
712 }
713
714 ASC_setAPTitles(params, dvi.getNetworkAETitle(), targetAETitle, NULL);
715
716 sprintf(peerHost, "%s:%d", targetHostname, (int)targetPort);
717 ASC_setPresentationAddresses(params, OFStandard::getHostName().c_str(), peerHost);
718
719 cond = addAllStoragePresentationContexts(params, targetImplicitOnly);
720 if (cond.bad())
721 {
722 OFLOG_FATAL(dcmpssndLogger, DimseCondition::dump(temp_str, cond));
723 return 1;
724 }
725
726 if (messagePort > 0)
727 {
728 messageClient = new DVPSIPCClient(DVPSIPCMessage::clientStoreSCU, verboseParametersString.c_str(), messagePort, keepMessagePortOpen);
729 if (! messageClient->isServerActive())
730 {
731 OFLOG_WARN(dcmpssndLogger, "no IPC message server found at port " << messagePort << ", disabling IPC");
732 }
733 }
734
735 /* create association */
736 OFLOG_INFO(dcmpssndLogger, "Requesting Association");
737
738 cond = ASC_requestAssociation(net, params, &assoc);
739 if (cond.bad())
740 {
741 if (cond == DUL_ASSOCIATIONREJECTED)
742 {
743 T_ASC_RejectParameters rej;
744
745 ASC_getRejectParameters(params, &rej);
746 OFLOG_ERROR(dcmpssndLogger, "Association Rejected" << OFendl << ASC_printRejectParameters(temp_str, &rej));
747 if (messageClient)
748 {
749 // notify about rejected association
750 OFOStringStream out;
751 out << "DIMSE Association Rejected:" << OFendl
752 << "\t" << ASC_printRejectParameters(temp_str, &rej) << OFendl;
753 out << "\tcalled presentation address: " << assoc->params->DULparams.calledPresentationAddress << OFendl
754 << "\tcalling AE title: " << assoc->params->DULparams.callingAPTitle << OFendl
755 << "\tcalled AE title: " << assoc->params->DULparams.calledAPTitle << OFendl;
756 out << ASC_dumpConnectionParameters(temp_str, assoc) << OFendl;
757 out << OFStringStream_ends;
758 OFSTRINGSTREAM_GETSTR(out, theString)
759 if (useTLS)
760 messageClient->notifyRequestedEncryptedDICOMConnection(DVPSIPCMessage::statusError, theString);
761 else messageClient->notifyRequestedUnencryptedDICOMConnection(DVPSIPCMessage::statusError, theString);
762 messageClient->notifyApplicationTerminates(DVPSIPCMessage::statusError);
763 OFSTRINGSTREAM_FREESTR(theString)
764 delete messageClient;
765 }
766 return 1;
767 } else {
768 OFLOG_ERROR(dcmpssndLogger, "Association Request Failed " << DimseCondition::dump(temp_str, cond));
769 if (messageClient)
770 {
771 // notify about rejected association
772 OFOStringStream out;
773 out << "DIMSE Association Request Failed:" << OFendl
774 << "\tcalled presentation address: " << assoc->params->DULparams.calledPresentationAddress << OFendl
775 << "\tcalling AE title: " << assoc->params->DULparams.callingAPTitle << OFendl
776 << "\tcalled AE title: " << assoc->params->DULparams.calledAPTitle << OFendl
777 << ASC_dumpConnectionParameters(temp_str, assoc) << OFendl
778 << cond.text() << OFendl << OFStringStream_ends;
779 OFSTRINGSTREAM_GETSTR(out, theString)
780 if (useTLS)
781 messageClient->notifyRequestedEncryptedDICOMConnection(DVPSIPCMessage::statusError, theString);
782 else messageClient->notifyRequestedUnencryptedDICOMConnection(DVPSIPCMessage::statusError, theString);
783 messageClient->notifyApplicationTerminates(DVPSIPCMessage::statusError);
784 OFSTRINGSTREAM_FREESTR(theString)
785 delete messageClient;
786 }
787 return 1;
788 }
789 }
790
791 if (ASC_countAcceptedPresentationContexts(params) == 0)
792 {
793 OFLOG_ERROR(dcmpssndLogger, "No Acceptable Presentation Contexts");
794 cond = ASC_abortAssociation(assoc);
795 if (cond.bad())
796 {
797 OFLOG_ERROR(dcmpssndLogger, "Association Abort Failed\n" << DimseCondition::dump(temp_str, cond));
798 }
799 if (messageClient)
800 {
801 // notify about rejected association
802 OFOStringStream out;
803 out << "DIMSE association accepted, but no acceptable presentation contexts - aborting" << OFendl
804 << "\tcalled presentation address: " << assoc->params->DULparams.calledPresentationAddress << OFendl
805 << "\tcalling AE title: " << assoc->params->DULparams.callingAPTitle << OFendl
806 << "\tcalled AE title: " << assoc->params->DULparams.calledAPTitle << OFendl
807 << ASC_dumpConnectionParameters(temp_str, assoc) << OFendl
808 << OFStringStream_ends;
809 OFSTRINGSTREAM_GETSTR(out, theString)
810 if (useTLS)
811 messageClient->notifyRequestedEncryptedDICOMConnection(DVPSIPCMessage::statusError, theString);
812 else messageClient->notifyRequestedUnencryptedDICOMConnection(DVPSIPCMessage::statusError, theString);
813 messageClient->notifyApplicationTerminates(DVPSIPCMessage::statusError);
814 OFSTRINGSTREAM_FREESTR(theString)
815 delete messageClient;
816 }
817 return 1;
818 }
819
820 OFLOG_INFO(dcmpssndLogger, "Association accepted (Max Send PDV: " << assoc->sendPDVLength << ")");
821
822 if (messageClient)
823 {
824 // notify about successfully negotiated association
825 OFOStringStream out;
826 out << "DIMSE Association Accepted:" << OFendl
827 << "\tcalled presentation address: " << assoc->params->DULparams.calledPresentationAddress << OFendl
828 << "\tcalling AE title: " << assoc->params->DULparams.callingAPTitle << OFendl
829 << "\tcalled AE title: " << assoc->params->DULparams.calledAPTitle << OFendl
830 << "\tmax send PDV: " << assoc->sendPDVLength << OFendl
831 << "\tpresentation contexts: " << ASC_countAcceptedPresentationContexts(assoc->params) << OFendl;
832 out << ASC_dumpConnectionParameters(temp_str, assoc) << OFendl;
833 out << OFStringStream_ends;
834 OFSTRINGSTREAM_GETSTR(out, theString)
835 if (useTLS)
836 messageClient->notifyRequestedEncryptedDICOMConnection(DVPSIPCMessage::statusOK, theString);
837 else messageClient->notifyRequestedUnencryptedDICOMConnection(DVPSIPCMessage::statusOK, theString);
838 OFSTRINGSTREAM_FREESTR(theString)
839 }
840
841
842 /* do the real work */
843 cond = sendStudy(dbhandle, assoc, opt_studyUID, opt_seriesUID, opt_instanceUID);
844
845 /* tear down association */
846 if (cond.good())
847 {
848 /* release association */
849 OFLOG_INFO(dcmpssndLogger, "Releasing Association");
850 cond = ASC_releaseAssociation(assoc);
851 if (cond.bad())
852 {
853 OFLOG_ERROR(dcmpssndLogger, "Association Release Failed\n" << DimseCondition::dump(temp_str, cond));
854 if (messageClient)
855 {
856 messageClient->notifyApplicationTerminates(DVPSIPCMessage::statusError);
857 delete messageClient;
858 }
859 return 1;
860 }
861 if (messageClient) messageClient->notifyConnectionClosed(DVPSIPCMessage::statusOK);
862 }
863 else if (cond == DUL_PEERREQUESTEDRELEASE)
864 {
865 OFLOG_ERROR(dcmpssndLogger, "Protocol Error: peer requested release (Aborting)");
866 OFLOG_INFO(dcmpssndLogger, "Aborting Association");
867 cond = ASC_abortAssociation(assoc);
868 if (cond.bad())
869 {
870 OFLOG_ERROR(dcmpssndLogger, "Association Abort Failed" << DimseCondition::dump(temp_str, cond));
871 if (messageClient)
872 {
873 messageClient->notifyApplicationTerminates(DVPSIPCMessage::statusError);
874 delete messageClient;
875 }
876 return 1;
877 }
878 if (messageClient) messageClient->notifyConnectionAborted(DVPSIPCMessage::statusError, "Protocol error: peer requested release, aborting association");
879 }
880 else if (cond == DUL_PEERABORTEDASSOCIATION)
881 {
882 OFLOG_INFO(dcmpssndLogger, "Peer Aborted Association");
883 if (messageClient) messageClient->notifyConnectionAborted(DVPSIPCMessage::statusError, "Peer aborted association");
884 }
885 else
886 {
887 OFLOG_ERROR(dcmpssndLogger, "SCU Failed" << DimseCondition::dump(temp_str, cond));
888 OFLOG_INFO(dcmpssndLogger, "Aborting Association");
889 cond = ASC_abortAssociation(assoc);
890 if (cond.bad())
891 {
892 OFLOG_ERROR(dcmpssndLogger, "Association Abort Failed" << DimseCondition::dump(temp_str, cond));
893 if (messageClient)
894 {
895 messageClient->notifyApplicationTerminates(DVPSIPCMessage::statusError);
896 delete messageClient;
897 }
898 return 1;
899 }
900 if (messageClient) messageClient->notifyConnectionAborted(DVPSIPCMessage::statusError, "Storage SCU failed, aborting association");
901 }
902
903 cond = ASC_destroyAssociation(&assoc);
904 if (cond.bad())
905 {
906 OFLOG_ERROR(dcmpssndLogger, DimseCondition::dump(temp_str, cond));
907 if (messageClient)
908 {
909 messageClient->notifyApplicationTerminates(DVPSIPCMessage::statusError);
910 delete messageClient;
911 }
912 return 1;
913 }
914 cond = ASC_dropNetwork(&net);
915 if (cond.bad())
916 {
917 OFLOG_ERROR(dcmpssndLogger, DimseCondition::dump(temp_str, cond));
918 if (messageClient)
919 {
920 messageClient->notifyApplicationTerminates(DVPSIPCMessage::statusError);
921 delete messageClient;
922 }
923 return 1;
924 }
925
926 // tell the IPC server that we're going to terminate.
927 // We need to do this before we shutdown WinSock.
928 if (messageClient)
929 {
930 messageClient->notifyApplicationTerminates(DVPSIPCMessage::statusOK);
931 delete messageClient;
932 }
933
934 OFStandard::shutdownNetwork();
935
936 #ifdef WITH_OPENSSL
937 if (tLayer)
938 {
939 if (tLayer->canWriteRandomSeed())
940 {
941 if (!tLayer->writeRandomSeed(tlsRandomSeedFile.c_str()))
942 {
943 OFLOG_WARN(dcmpssndLogger, "cannot write back random seed file '" << tlsRandomSeedFile << "', ignoring");
944 }
945 } else {
946 OFLOG_WARN(dcmpssndLogger, "cannot write back random seed, ignoring");
947 }
948 }
949 delete tLayer;
950 #endif
951
952 #ifdef DEBUG
953 dcmDataDict.clear(); /* useful for debugging with dmalloc */
954 #endif
955
956 return 0;
957 }
958