PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
secure_echo_scu.cpp
Go to the documentation of this file.
1
25
26#include <chrono>
27#include <cstdlib>
28#include <filesystem>
29#include <iostream>
30#include <string>
31
32namespace {
33
35constexpr const char* default_calling_ae = "SECURE_SCU";
36
38constexpr auto default_timeout = std::chrono::milliseconds{30000};
39
43struct tls_options {
44 std::filesystem::path cert_path;
45 std::filesystem::path key_path;
46 std::filesystem::path ca_path;
47 bool verify_server = true;
48 std::string tls_version = "1.2";
49};
50
55void print_usage(const char* program_name) {
56 std::cout << R"(
57Secure Echo SCU - TLS-secured DICOM Connectivity Test Client
58
59Usage: )" << program_name << R"( <host> <port> <called_ae> [--cert <file> --key <file>] [options]
60
61Arguments:
62 host Remote host address (IP or hostname)
63 port Remote port number (typically 2762 for DICOM TLS)
64 called_ae Called AE Title (remote SCP's AE title)
65
66TLS Options:
67 --cert <file> Client certificate file for mutual TLS (PEM format)
68 --key <file> Client private key file for mutual TLS (PEM format)
69 --ca <file> CA certificate for server verification (PEM format)
70 --no-verify Disable server certificate verification (not recommended)
71 --tls-version <ver> Minimum TLS version: 1.2 or 1.3 (default: 1.2)
72
73Other Options:
74 --calling-ae <ae> Calling AE Title (default: SECURE_SCU)
75 --timeout <ms> Operation timeout in milliseconds (default: 30000)
76 --help Show this help message
77
78Examples:
79 # Basic TLS connection (server cert verification only)
80 )" << program_name << R"( localhost 2762 PACS_SCP --ca ca.crt
81
82 # Mutual TLS (client and server certificates)
83 )" << program_name << R"( localhost 2762 PACS_SCP --cert client.crt --key client.key --ca ca.crt
84
85 # TLS 1.3 with custom AE title
86 )" << program_name << R"( 192.168.1.100 2762 REMOTE_PACS --ca ca.crt --tls-version 1.3 --calling-ae MY_SCANNER
87
88Notes:
89 - Standard DICOM TLS port is 2762
90 - For production, always verify server certificates (avoid --no-verify)
91 - Mutual TLS requires both --cert and --key
92
93Exit Codes:
94 0 Success - Echo response received
95 1 Error - Connection, TLS, or echo failed
96)";
97}
98
102bool parse_arguments(
103 int argc,
104 char* argv[],
105 std::string& host,
106 uint16_t& port,
107 std::string& called_ae,
108 std::string& calling_ae,
109 tls_options& tls,
110 std::chrono::milliseconds& timeout) {
111
112 if (argc < 4) {
113 return false;
114 }
115
116 // Check for help flag
117 for (int i = 1; i < argc; ++i) {
118 if (std::string(argv[i]) == "--help" || std::string(argv[i]) == "-h") {
119 return false;
120 }
121 }
122
123 // Parse required arguments
124 host = argv[1];
125
126 // Parse port
127 try {
128 int port_int = std::stoi(argv[2]);
129 if (port_int < 1 || port_int > 65535) {
130 std::cerr << "Error: Port must be between 1 and 65535\n";
131 return false;
132 }
133 port = static_cast<uint16_t>(port_int);
134 } catch (const std::exception&) {
135 std::cerr << "Error: Invalid port number '" << argv[2] << "'\n";
136 return false;
137 }
138
139 called_ae = argv[3];
140 if (called_ae.length() > 16) {
141 std::cerr << "Error: Called AE title exceeds 16 characters\n";
142 return false;
143 }
144
145 // Default values
146 calling_ae = default_calling_ae;
148 tls.verify_server = true;
149 tls.tls_version = "1.2";
150
151 // Parse options
152 for (int i = 4; i < argc; ++i) {
153 std::string arg = argv[i];
154
155 if (arg == "--cert" && i + 1 < argc) {
156 tls.cert_path = argv[++i];
157 } else if (arg == "--key" && i + 1 < argc) {
158 tls.key_path = argv[++i];
159 } else if (arg == "--ca" && i + 1 < argc) {
160 tls.ca_path = argv[++i];
161 } else if (arg == "--no-verify") {
162 tls.verify_server = false;
163 std::cerr << "Warning: Server certificate verification disabled. "
164 << "This is not recommended for production.\n";
165 } else if (arg == "--tls-version" && i + 1 < argc) {
166 tls.tls_version = argv[++i];
167 if (tls.tls_version != "1.2" && tls.tls_version != "1.3") {
168 std::cerr << "Error: Invalid TLS version (use 1.2 or 1.3)\n";
169 return false;
170 }
171 } else if (arg == "--calling-ae" && i + 1 < argc) {
172 calling_ae = argv[++i];
173 if (calling_ae.length() > 16) {
174 std::cerr << "Error: Calling AE title exceeds 16 characters\n";
175 return false;
176 }
177 } else if (arg == "--timeout" && i + 1 < argc) {
178 try {
179 int val = std::stoi(argv[++i]);
180 if (val < 0) {
181 std::cerr << "Error: timeout cannot be negative\n";
182 return false;
183 }
184 timeout = std::chrono::milliseconds{val};
185 } catch (const std::exception&) {
186 std::cerr << "Error: Invalid timeout value\n";
187 return false;
188 }
189 } else if (arg[0] == '-') {
190 std::cerr << "Error: Unknown option '" << arg << "'\n";
191 return false;
192 }
193 }
194
195 // Validate mutual TLS configuration
196 if (!tls.cert_path.empty() != !tls.key_path.empty()) {
197 std::cerr << "Error: Both --cert and --key are required for mutual TLS\n";
198 return false;
199 }
200
201 return true;
202}
203
207bool validate_tls_files(const tls_options& tls) {
208 // Check client certificate file if specified
209 if (!tls.cert_path.empty() && !std::filesystem::exists(tls.cert_path)) {
210 std::cerr << "Error: Client certificate file not found: " << tls.cert_path << "\n";
211 return false;
212 }
213
214 // Check client key file if specified
215 if (!tls.key_path.empty() && !std::filesystem::exists(tls.key_path)) {
216 std::cerr << "Error: Client key file not found: " << tls.key_path << "\n";
217 return false;
218 }
219
220 // Check CA file if specified
221 if (!tls.ca_path.empty() && !std::filesystem::exists(tls.ca_path)) {
222 std::cerr << "Error: CA certificate file not found: " << tls.ca_path << "\n";
223 return false;
224 }
225
226 return true;
227}
228
232bool perform_secure_echo(
233 const std::string& host,
234 uint16_t port,
235 const std::string& called_ae,
236 const std::string& calling_ae,
237 const tls_options& tls,
238 std::chrono::milliseconds timeout) {
239
240 using namespace kcenon::pacs::network;
241 using namespace kcenon::pacs::network::dimse;
242 using namespace kcenon::pacs::services;
243 using namespace kcenon::pacs::integration;
244
245 std::cout << "Connecting securely to " << host << ":" << port << "...\n";
246 std::cout << " Calling AE: " << calling_ae << "\n";
247 std::cout << " Called AE: " << called_ae << "\n";
248 std::cout << " TLS Version: " << tls.tls_version << "+\n";
249 std::cout << " Verify Server: " << (tls.verify_server ? "Yes" : "No") << "\n";
250 if (!tls.cert_path.empty()) {
251 std::cout << " Client Cert: " << tls.cert_path << "\n";
252 std::cout << " (Mutual TLS enabled)\n";
253 }
254 std::cout << "\n";
255
256 // Configure TLS
257 tls_config tls_cfg;
258 tls_cfg.enabled = true;
259 tls_cfg.cert_path = tls.cert_path;
260 tls_cfg.key_path = tls.key_path;
261 tls_cfg.ca_path = tls.ca_path;
262 tls_cfg.verify_peer = tls.verify_server;
263 tls_cfg.min_version = (tls.tls_version == "1.3")
264 ? tls_config::tls_version::v1_3
265 : tls_config::tls_version::v1_2;
266
267 // Validate TLS configuration
268 auto tls_result = network_adapter::configure_tls(tls_cfg);
269 if (tls_result.is_err()) {
270 std::cerr << "TLS configuration error: " << tls_result.error().message << "\n";
271 return false;
272 }
273
274 // Configure association
275 association_config config;
276 config.calling_ae_title = calling_ae;
277 config.called_ae_title = called_ae;
278 config.implementation_class_uid = "1.2.826.0.1.3680043.2.1545.1";
279 config.implementation_version_name = "SECURE_SCU_001";
280
281 // Propose Verification SOP Class with Explicit VR Little Endian
282 config.proposed_contexts.push_back({
283 1, // Context ID (must be odd: 1, 3, 5, ...)
284 std::string(verification_sop_class_uid),
285 {
286 "1.2.840.10008.1.2.1", // Explicit VR Little Endian
287 "1.2.840.10008.1.2" // Implicit VR Little Endian
288 }
289 });
290
291 // Establish secure association
292 auto start_time = std::chrono::steady_clock::now();
293
294 // Note: In a full implementation, the connect method would accept TLS config
295 // For this sample, we demonstrate the TLS configuration pattern
296 auto connect_result = association::connect(host, port, config, timeout);
297
298 if (connect_result.is_err()) {
299 std::cerr << "Failed to establish secure association: "
300 << connect_result.error().message << "\n";
301 return false;
302 }
303
304 auto& assoc = connect_result.value();
305 auto connect_time = std::chrono::steady_clock::now();
306 auto connect_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
307 connect_time - start_time);
308
309 std::cout << "Secure association established in " << connect_duration.count() << " ms\n";
310
311 // Verify we have an accepted context for Verification
312 if (!assoc.has_accepted_context(verification_sop_class_uid)) {
313 std::cerr << "Error: Verification SOP Class not accepted by remote SCP\n";
314 assoc.abort();
315 return false;
316 }
317
318 // Get the accepted context ID
319 auto context_id_opt = assoc.accepted_context_id(verification_sop_class_uid);
320 if (!context_id_opt) {
321 std::cerr << "Error: Could not get presentation context ID\n";
322 assoc.abort();
323 return false;
324 }
325 uint8_t context_id = *context_id_opt;
326
327 // Create C-ECHO request
328 auto echo_rq = make_c_echo_rq(1, verification_sop_class_uid);
329
330 std::cout << "Sending C-ECHO request (TLS encrypted)...\n";
331
332 // Send C-ECHO request
333 auto send_result = assoc.send_dimse(context_id, echo_rq);
334 if (send_result.is_err()) {
335 std::cerr << "Failed to send C-ECHO: " << send_result.error().message << "\n";
336 assoc.abort();
337 return false;
338 }
339
340 // Receive C-ECHO response
341 auto recv_result = assoc.receive_dimse(timeout);
342 if (recv_result.is_err()) {
343 std::cerr << "Failed to receive C-ECHO response: "
344 << recv_result.error().message << "\n";
345 assoc.abort();
346 return false;
347 }
348
349 auto& [recv_context_id, echo_rsp] = recv_result.value();
350
351 auto echo_time = std::chrono::steady_clock::now();
352 auto echo_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
353 echo_time - connect_time);
354
355 // Check response
356 if (echo_rsp.command() != command_field::c_echo_rsp) {
357 std::cerr << "Error: Unexpected response (expected C-ECHO-RSP)\n";
358 assoc.abort();
359 return false;
360 }
361
362 auto status = echo_rsp.status();
363 if (status != status_success) {
364 std::cerr << "C-ECHO failed with status: 0x"
365 << std::hex << static_cast<uint16_t>(status) << std::dec << "\n";
366 (void)assoc.release();
367 return false;
368 }
369
370 std::cout << "C-ECHO successful! Round-trip time: " << echo_duration.count() << " ms\n";
371
372 // Release association gracefully
373 std::cout << "Releasing secure association...\n";
374 auto release_result = assoc.release(timeout);
375 if (release_result.is_err()) {
376 std::cerr << "Warning: Release failed: " << release_result.error().message << "\n";
377 }
378
379 auto total_time = std::chrono::steady_clock::now();
380 auto total_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
381 total_time - start_time);
382
383 std::cout << "\nSummary:\n";
384 std::cout << " Remote AE: " << called_ae << "\n";
385 std::cout << " Security: TLS " << tls.tls_version << "+\n";
386 std::cout << " Connection time: " << connect_duration.count() << " ms\n";
387 std::cout << " Echo time: " << echo_duration.count() << " ms\n";
388 std::cout << " Total time: " << total_duration.count() << " ms\n";
389 std::cout << " Status: SUCCESS (SECURE)\n";
390
391 return true;
392}
393
394} // namespace
395
396int main(int argc, char* argv[]) {
397 std::cout << R"(
398 ____ _____ ____ _ _ ____ _____ _____ ____ ____
399 / ___|| ____/ ___| | | | _ \| ____| | ____/ ___/ ___|
400 \___ \| _|| | | | | | |_) | _| | _|| | \___ \
401 ___) | |__| |___| |_| | _ <| |___ | |__| |___ ___) |
402 |____/|_____\____|\___/|_| \_\_____| |_____\____|____/
403 ____ ____ _ _
404 / ___| / ___| | | |
405 \___ \| | | | | |
406 ___) | |___| |_| |
407 |____/ \____|\___/
408
409 TLS-Secured DICOM Connectivity Test Client
410)" << "\n";
411
412 std::string host;
413 uint16_t port = 0;
414 std::string called_ae;
415 std::string calling_ae;
416 tls_options tls;
417 std::chrono::milliseconds timeout;
418
419 if (!parse_arguments(argc, argv, host, port, called_ae, calling_ae, tls, timeout)) {
420 print_usage(argv[0]);
421 return 1;
422 }
423
424 // Validate TLS files if specified
425 if (!validate_tls_files(tls)) {
426 return 1;
427 }
428
429 bool success = perform_secure_echo(host, port, called_ae, calling_ae, tls, timeout);
430
431 return success ? 0 : 1;
432}
DICOM Association management per PS3.8.
DIMSE message encoding and decoding.
int main()
Definition main.cpp:84
constexpr dicom_tag status
Status.
std::chrono::milliseconds default_timeout()
Default timeout for test operations (5s normal, 30s CI)
@ tls
TLS over TCP (RFC 5425) — Secure.
constexpr core::dicom_tag echo_time
Echo Time (0018,0081) - Type 2 Time in ms between the middle of the excitation pulse and peak of echo...
constexpr int timeout
Lock timeout exceeded.
Adapter for integrating network_system for DICOM protocol.
Configuration for SCU association request.
std::string called_ae_title
Remote AE Title (16 chars max)
std::string calling_ae_title
Our AE Title (16 chars max)
std::vector< proposed_presentation_context > proposed_contexts
DICOM Verification SCP service (C-ECHO handler)