/* * * Copyright (C) 2013-2020, OFFIS e.V. * All rights reserved. See COPYRIGHT file for details. * * This software and supporting documentation were developed by * * OFFIS e.V. * R&D Division Health * Escherweg 2 * D-26121 Oldenburg, Germany * * * Module: dcmnet * * Author: Joerg Riesmeier * * Purpose: Simple Storage Service Class Provider * */ #include "dcmtk/config/osconfig.h" /* make sure OS specific configuration is included first */ #include "dcmtk/ofstd/ofstd.h" /* for OFStandard functions */ #include "dcmtk/ofstd/ofconapp.h" /* for OFConsoleApplication */ #include "dcmtk/ofstd/ofstream.h" /* for OFStringStream et al. */ #include "dcmtk/dcmdata/dcdict.h" /* for global data dictionary */ #include "dcmtk/dcmdata/dcuid.h" /* for dcmtk version name */ #include "dcmtk/dcmdata/cmdlnarg.h" /* for prepareCmdLineArgs */ #include "dcmtk/dcmnet/dstorscp.h" /* for DcmStorageSCP */ #include "dcmtk/dcmtls/tlsopt.h" /* for DcmTLSOptions */ /* general definitions */ #define OFFIS_CONSOLE_APPLICATION "dcmrecv" static OFLogger dcmrecvLogger = OFLog::getLogger("dcmtk.apps." OFFIS_CONSOLE_APPLICATION); static char rcsid[] = "$dcmtk: " OFFIS_CONSOLE_APPLICATION " v" OFFIS_DCMTK_VERSION " " OFFIS_DCMTK_RELEASEDATE " $"; /* default application entity title */ #define APPLICATIONTITLE "DCMRECV" /* exit codes for this command line tool */ /* (common codes are defined in "ofexit.h" included from "ofconapp.h") */ // network errors #define EXITCODE_CANNOT_INITIALIZE_NETWORK 60 // placeholder, currently not used #define EXITCODE_CANNOT_START_SCP_AND_LISTEN 64 #define EXITCODE_INVALID_ASSOCIATION_CONFIG 66 #define EXITCODE_CANNOT_CREATE_TRANSPORT_LAYER 71 /* helper macro for converting stream output to a string */ #define CONVERT_TO_STRING(output, string) \ optStream.str(""); \ optStream.clear(); \ optStream << output << OFStringStream_ends; \ OFSTRINGSTREAM_GETOFSTRING(optStream, string) /* main program */ #define SHORTCOL 4 #define LONGCOL 21 int main(int argc, char *argv[]) { #ifdef WITH_OPENSSL DcmTLSTransportLayer::initializeOpenSSL(); #endif OFOStringStream optStream; DcmTLSOptions tlsOptions(NET_ACCEPTOR); const char *opt_configFile = NULL; const char *opt_profileName = NULL; const char *opt_aeTitle = APPLICATIONTITLE; const char *opt_outputDirectory = "."; const char *opt_filenameExtension = ""; OFCmdUnsignedInt opt_port = 0; OFCmdUnsignedInt opt_dimseTimeout = 0; OFCmdUnsignedInt opt_acseTimeout = 30; OFCmdUnsignedInt opt_maxPDULength = ASC_DEFAULTMAXPDU; T_DIMSE_BlockingMode opt_blockingMode = DIMSE_BLOCKING; OFBool opt_showPresentationContexts = OFFalse; // default: do not show presentation contexts in verbose mode OFBool opt_useCalledAETitle = OFFalse; // default: respond with specified application entity title OFBool opt_HostnameLookup = OFTrue; // default: perform hostname lookup (for log output) DcmStorageSCP::E_DirectoryGenerationMode opt_directoryGeneration = DcmStorageSCP::DGM_NoSubdirectory; DcmStorageSCP::E_FilenameGenerationMode opt_filenameGeneration = DcmStorageSCP::FGM_SOPInstanceUID; DcmStorageSCP::E_DatasetStorageMode opt_datasetStorage = DcmStorageSCP::DGM_StoreToFile; OFConsoleApplication app(OFFIS_CONSOLE_APPLICATION , "Simple DICOM storage SCP (receiver)", rcsid); OFCommandLine cmd; cmd.setParamColumn(LONGCOL + SHORTCOL + 4); cmd.addParam("port", "tcp/ip port number to listen on"); cmd.setOptionColumns(LONGCOL, SHORTCOL); cmd.addGroup("general options:", LONGCOL, SHORTCOL + 2); cmd.addOption("--help", "-h", "print this help text and exit", OFCommandLine::AF_Exclusive); cmd.addOption("--version", "print version information and exit", OFCommandLine::AF_Exclusive); OFLog::addOptions(cmd); cmd.addOption("--verbose-pc", "+v", "show presentation contexts in verbose mode"); cmd.addGroup("network options:"); cmd.addSubGroup("association negotiation profile from configuration file:"); cmd.addOption("--config-file", "-xf", 2, "[f]ilename, [p]rofile: string", "use profile p from configuration file f"); cmd.addSubGroup("application entity title:"); CONVERT_TO_STRING("set my AE title (default: " << opt_aeTitle << ")", optString1); cmd.addOption("--aetitle", "-aet", 1, "[a]etitle: string", optString1.c_str()); cmd.addOption("--use-called-aetitle", "-uca", "always respond with called AE title"); cmd.addSubGroup("other network options:"); CONVERT_TO_STRING("[s]econds: integer (default: " << opt_acseTimeout << ")", optString2); cmd.addOption("--acse-timeout", "-ta", 1, optString2.c_str(), "timeout for ACSE messages"); cmd.addOption("--dimse-timeout", "-td", 1, "[s]econds: integer (default: unlimited)", "timeout for DIMSE messages"); CONVERT_TO_STRING("[n]umber of bytes: integer (" << ASC_MINIMUMPDUSIZE << ".." << ASC_MAXIMUMPDUSIZE << ")", optString3); CONVERT_TO_STRING("set max receive pdu to n bytes (default: " << opt_maxPDULength << ")", optString4); cmd.addOption("--max-pdu", "-pdu", 1, optString3.c_str(), optString4.c_str()); cmd.addOption("--disable-host-lookup", "-dhl", "disable hostname lookup"); /* add TLS specific command line options if (and only if) we are compiling with OpenSSL */ tlsOptions.addTLSCommandlineOptions(cmd); cmd.addGroup("output options:"); cmd.addSubGroup("general:"); CONVERT_TO_STRING("[d]irectory: string (default: \"" << opt_outputDirectory << "\")", optString5); cmd.addOption("--output-directory", "-od", 1, optString5.c_str(), "write received objects to existing directory d"); cmd.addSubGroup("subdirectory generation:"); cmd.addOption("--no-subdir", "-s", "do not generate any subdirectories (default)"); cmd.addOption("--series-date-subdir", "+ssd", "generate subdirectories from series date"); cmd.addSubGroup("filename generation:"); cmd.addOption("--default-filenames", "+fd", "generate filename from instance UID (default)"); cmd.addOption("--unique-filenames", "+fu", "generate unique filename based on new UID"); cmd.addOption("--short-unique-names", "+fsu", "generate short pseudo-random unique filename"); cmd.addOption("--system-time-names", "+fst", "generate filename from current system time"); cmd.addOption("--filename-extension", "-fe", 1, "[e]xtension: string (default: none)", "append e to all generated filenames"); cmd.addSubGroup("storage mode:"); cmd.addOption("--normal", "-B", "allow implicit format conversions (default)"); cmd.addOption("--bit-preserving", "+B", "write dataset exactly as received"); cmd.addOption("--ignore", "ignore dataset, receive but do not store it"); /* evaluate command line */ prepareCmdLineArgs(argc, argv, OFFIS_CONSOLE_APPLICATION); if (app.parseCommandLine(cmd, argc, argv)) { /* check exclusive options first */ if (cmd.hasExclusiveOption()) { if (cmd.findOption("--version")) { app.printHeader(OFTrue /*print host identifier*/); COUT << OFendl << "External libraries used:"; #ifdef WITH_OPENSSL COUT << OFendl; tlsOptions.printLibraryVersion(); #else COUT << " none" << OFendl; #endif return EXITCODE_NO_ERROR; } /* check if the command line contains the --list-ciphers option */ if (tlsOptions.listOfCiphersRequested(cmd)) { tlsOptions.printSupportedCiphersuites(app, COUT); return EXITCODE_NO_ERROR; } } /* general options */ OFLog::configureFromCommandLine(cmd, app); if (cmd.findOption("--verbose-pc")) { app.checkDependence("--verbose-pc", "verbose mode", dcmrecvLogger.isEnabledFor(OFLogger::INFO_LOG_LEVEL)); opt_showPresentationContexts = OFTrue; } /* network options */ if (cmd.findOption("--config-file")) { app.checkValue(cmd.getValue(opt_configFile)); app.checkValue(cmd.getValue(opt_profileName)); } cmd.beginOptionBlock(); if (cmd.findOption("--aetitle")) { app.checkValue(cmd.getValue(opt_aeTitle)); opt_useCalledAETitle = OFFalse; } if (cmd.findOption("--use-called-aetitle")) opt_useCalledAETitle = OFTrue; cmd.endOptionBlock(); if (cmd.findOption("--acse-timeout")) app.checkValue(cmd.getValueAndCheckMin(opt_acseTimeout, 1)); if (cmd.findOption("--dimse-timeout")) { app.checkValue(cmd.getValueAndCheckMin(opt_dimseTimeout, 1)); opt_blockingMode = DIMSE_NONBLOCKING; } if (cmd.findOption("--max-pdu")) app.checkValue(cmd.getValueAndCheckMinMax(opt_maxPDULength, ASC_MINIMUMPDUSIZE, ASC_MAXIMUMPDUSIZE)); if (cmd.findOption("--disable-host-lookup")) opt_HostnameLookup = OFFalse; /* output options */ if (cmd.findOption("--output-directory")) app.checkValue(cmd.getValue(opt_outputDirectory)); cmd.beginOptionBlock(); if (cmd.findOption("--no-subdir")) opt_directoryGeneration = DcmStorageSCP::DGM_NoSubdirectory; if (cmd.findOption("--series-date-subdir")) opt_directoryGeneration = DcmStorageSCP::DGM_SeriesDate; cmd.endOptionBlock(); cmd.beginOptionBlock(); if (cmd.findOption("--default-filenames")) opt_filenameGeneration = DcmStorageSCP::FGM_SOPInstanceUID; if (cmd.findOption("--unique-filenames")) opt_filenameGeneration = DcmStorageSCP::FGM_UniqueFromNewUID; if (cmd.findOption("--short-unique-names")) opt_filenameGeneration = DcmStorageSCP::FGM_ShortUniquePseudoRandom; if (cmd.findOption("--system-time-names")) opt_filenameGeneration = DcmStorageSCP::FGM_CurrentSystemTime; cmd.endOptionBlock(); if (cmd.findOption("--filename-extension")) app.checkValue(cmd.getValue(opt_filenameExtension)); cmd.beginOptionBlock(); if (cmd.findOption("--normal")) opt_datasetStorage = DcmStorageSCP::DGM_StoreToFile; if (cmd.findOption("--bit-preserving")) { app.checkConflict("--bit-preserving", "--series-date-subdir", opt_directoryGeneration == DcmStorageSCP::DGM_SeriesDate); opt_datasetStorage = DcmStorageSCP::DGM_StoreBitPreserving; } if (cmd.findOption("--ignore")) opt_datasetStorage = DcmStorageSCP::DSM_Ignore; cmd.endOptionBlock(); /* command line parameters */ app.checkParam(cmd.getParamAndCheckMinMax(1, opt_port, 1, 65535)); /* evaluate (most of) the TLS command line options (if we are compiling with OpenSSL) */ tlsOptions.parseArguments(app, cmd); } /* print resource identifier */ OFLOG_DEBUG(dcmrecvLogger, rcsid << OFendl); /* make sure data dictionary is loaded */ if (!dcmDataDict.isDictionaryLoaded()) { OFLOG_WARN(dcmrecvLogger, "no data dictionary loaded, check environment variable: " << DCM_DICT_ENVIRONMENT_VARIABLE); } /* start with the real work */ DcmStorageSCP storageSCP; OFCondition status; OFLOG_INFO(dcmrecvLogger, "configuring service class provider ..."); /* set general network parameters */ storageSCP.setPort(OFstatic_cast(Uint16, opt_port)); storageSCP.setAETitle(opt_aeTitle); storageSCP.setMaxReceivePDULength(OFstatic_cast(Uint32, opt_maxPDULength)); storageSCP.setACSETimeout(OFstatic_cast(Uint32, opt_acseTimeout)); storageSCP.setDIMSETimeout(OFstatic_cast(Uint32, opt_dimseTimeout)); storageSCP.setDIMSEBlockingMode(opt_blockingMode); storageSCP.setVerbosePCMode(opt_showPresentationContexts); storageSCP.setRespondWithCalledAETitle(opt_useCalledAETitle); storageSCP.setHostLookupEnabled(opt_HostnameLookup); storageSCP.setDirectoryGenerationMode(opt_directoryGeneration); storageSCP.setFilenameGenerationMode(opt_filenameGeneration); storageSCP.setFilenameExtension(opt_filenameExtension); storageSCP.setDatasetStorageMode(opt_datasetStorage); /* load association negotiation profile from configuration file (if specified) */ if ((opt_configFile != NULL) && (opt_profileName != NULL)) { status = storageSCP.loadAssociationConfiguration(opt_configFile, opt_profileName); if (status.bad()) { OFLOG_FATAL(dcmrecvLogger, "cannot load association configuration: " << status.text()); return EXITCODE_INVALID_ASSOCIATION_CONFIG; } } else { /* report a warning message that the SCP will not accept any Storage SOP Classes */ OFLOG_WARN(dcmrecvLogger, "no configuration file specified, SCP will only support the Verification SOP Class"); } /* specify the output directory (also checks whether directory exists and is writable) */ status = storageSCP.setOutputDirectory(opt_outputDirectory); if (status.bad()) { OFLOG_FATAL(dcmrecvLogger, "cannot specify output directory: " << status.text()); return EXITCODE_INVALID_OUTPUT_DIRECTORY; } OFLOG_INFO(dcmrecvLogger, "starting service class provider and listening ..."); /* create a secure transport layer if requested and OpenSSL is available */ status = tlsOptions.createTransportLayer(NULL, NULL, app, cmd); if (status.bad()) { OFString tempStr; OFLOG_FATAL(dcmrecvLogger, DimseCondition::dump(tempStr, status)); return EXITCODE_CANNOT_CREATE_TRANSPORT_LAYER; } if (tlsOptions.secureConnectionRequested()) storageSCP.getConfig().setTransportLayer(tlsOptions.getTransportLayer()); /* start SCP and listen on the specified port */ status = storageSCP.listen(); if (status.bad()) { OFLOG_FATAL(dcmrecvLogger, "cannot start SCP and listen on port " << opt_port << ": " << status.text()); return EXITCODE_CANNOT_START_SCP_AND_LISTEN; } /* make sure that everything is cleaned up properly */ #ifdef DEBUG /* useful for debugging with dmalloc */ dcmDataDict.clear(); #endif return EXITCODE_NO_ERROR; }