PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
dicom_association_handler.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
13
14#include <span>
15#include <sstream>
16#include <stdexcept>
17
18// KCENON_HAS_COMMON_SYSTEM is defined by CMake when common_system is available
19#ifndef KCENON_HAS_COMMON_SYSTEM
20#define KCENON_HAS_COMMON_SYSTEM 0
21#endif
22
23#ifdef PACS_WITH_NETWORK_SYSTEM
24#include <kcenon/network/interfaces/i_session.h>
25#endif
26
27#if KCENON_HAS_COMMON_SYSTEM
28using kcenon::common::error_info;
29#endif
30
32
33// =============================================================================
34// Construction / Destruction
35// =============================================================================
36
38 session_ptr session,
39 const server_config& config,
40 const service_map& services)
41 : session_(std::move(session))
42 , config_(config)
43 , services_(services)
44 , last_activity_(clock::now()) {
45}
46
48 try {
49 // Ensure we're stopped
50 if (!is_closed()) {
51 stop(false); // Force abort if still running
52 }
53 } catch (...) {
54 // Suppress exceptions in destructor to prevent std::terminate
55 }
56}
57
58// =============================================================================
59// Lifecycle Management
60// =============================================================================
61
63 std::lock_guard<std::mutex> lock(mutex_);
64
66 report_error("Cannot start: handler not in idle state");
67 return;
68 }
69
70 if (!session_) {
71 report_error("Cannot start: no session available");
72 return;
73 }
74
75#ifdef PACS_WITH_NETWORK_SYSTEM
76 // In the i_protocol_server architecture, the server forwards events
77 // to the handler via feed_data(), handle_disconnect(), and handle_error().
78 // No session-level callback setup needed.
79#endif
80
81 touch();
82}
83
85 handler_state expected = state_.load();
86 if (expected == handler_state::closed) {
87 return; // Already closed
88 }
89
90 if (graceful && expected == handler_state::established) {
91 // Try graceful release first
93
94 // Send A-RELEASE-RQ would go here if we were initiating release
95 // For now, just close
96 }
97
98 close_handler(graceful);
99}
100
101// =============================================================================
102// State Queries
103// =============================================================================
104
106 return state_.load(std::memory_order_acquire);
107}
108
110 return state_.load(std::memory_order_acquire) == handler_state::established;
111}
112
114 return state_.load(std::memory_order_acquire) == handler_state::closed;
115}
116
118#ifdef PACS_WITH_NETWORK_SYSTEM
119 std::lock_guard<std::mutex> lock(mutex_);
120 if (session_) {
121 return std::string(session_->id());
122 }
123#endif
124 return {};
125}
126
128 std::lock_guard<std::mutex> lock(mutex_);
129 return std::string(association_.calling_ae());
130}
131
133 std::lock_guard<std::mutex> lock(mutex_);
134 return std::string(association_.called_ae());
135}
136
138 std::lock_guard<std::mutex> lock(mutex_);
139 if (!is_established()) {
141 "Association not established", "network"};
142 }
143 return std::ref(association_);
144}
145
147 std::lock_guard<std::mutex> lock(mutex_);
148 if (!is_established()) {
150 "Association not established", "network"};
151 }
152 return std::cref(association_);
153}
154
156 std::lock_guard<std::mutex> lock(mutex_);
157 return last_activity_;
158}
159
160// =============================================================================
161// Callbacks
162// =============================================================================
163
166 std::lock_guard<std::mutex> lock(callback_mutex_);
167 established_callback_ = std::move(callback);
168}
169
172 std::lock_guard<std::mutex> lock(callback_mutex_);
173 closed_callback_ = std::move(callback);
174}
175
177 std::lock_guard<std::mutex> lock(callback_mutex_);
178 error_callback_ = std::move(callback);
179}
180
181// =============================================================================
182// Statistics
183// =============================================================================
184
186 return pdus_received_.load(std::memory_order_relaxed);
187}
188
189uint64_t dicom_association_handler::pdus_sent() const noexcept {
190 return pdus_sent_.load(std::memory_order_relaxed);
191}
192
194 return messages_processed_.load(std::memory_order_relaxed);
195}
196
197// =============================================================================
198// Security / Access Control
199// =============================================================================
200
202 std::shared_ptr<security::access_control_manager> acm) {
203 std::lock_guard<std::mutex> lock(mutex_);
204 access_control_ = std::move(acm);
205}
206
208 std::lock_guard<std::mutex> lock(mutex_);
209 access_control_enabled_ = enabled;
210}
211
212// =============================================================================
213// Server-Level Event Forwarding
214// =============================================================================
215
216void dicom_association_handler::feed_data(const std::vector<uint8_t>& data) {
217 on_data_received(data);
218}
219
223
225 on_error(ec);
226}
227
228// =============================================================================
229// Network Callbacks
230// =============================================================================
231
232void dicom_association_handler::on_data_received(const std::vector<uint8_t>& data) {
233 if (is_closed()) {
234 return;
235 }
236
237 {
238 std::lock_guard<std::mutex> lock(mutex_);
239 // Append to receive buffer
240 receive_buffer_.insert(receive_buffer_.end(), data.begin(), data.end());
241 touch();
242 }
243
244 // Process any complete PDUs
246}
247
248void dicom_association_handler::on_disconnected(const std::string& /*session_id*/) {
249 close_handler(true);
250}
251
252void dicom_association_handler::on_error(std::error_code ec) {
253 std::ostringstream oss;
254 oss << "Network error: " << ec.message() << " (" << ec.value() << ")";
255 report_error(oss.str());
256
257 close_handler(false);
258}
259
260// =============================================================================
261// PDU Processing
262// =============================================================================
263
265 std::lock_guard<std::mutex> lock(mutex_);
266
267 while (receive_buffer_.size() >= pdu_header_size) {
268 // Check if we have a complete PDU
269 auto length_opt = pdu_decoder::pdu_length(receive_buffer_);
270 if (!length_opt) {
271 // Not enough data yet
272 break;
273 }
274
275 size_t pdu_total_length = *length_opt;
276 if (receive_buffer_.size() < pdu_total_length) {
277 // Not enough data for complete PDU
278 break;
279 }
280
281 // Extract the complete PDU
282 std::vector<uint8_t> pdu_data(
283 receive_buffer_.begin(),
284 receive_buffer_.begin() + static_cast<std::ptrdiff_t>(pdu_total_length));
285
286 // Remove from buffer
287 receive_buffer_.erase(
288 receive_buffer_.begin(),
289 receive_buffer_.begin() + static_cast<std::ptrdiff_t>(pdu_total_length));
290
291 // Parse PDU type
292 auto type_opt = pdu_decoder::peek_pdu_type(pdu_data);
293 if (!type_opt) {
294 report_error("Invalid PDU type received");
296 close_handler(false);
297 return;
298 }
299
300 pdus_received_.fetch_add(1, std::memory_order_relaxed);
301
302 // Extract payload (skip 6-byte header)
303 std::vector<uint8_t> payload(
304 pdu_data.begin() + pdu_header_size,
305 pdu_data.end());
306
307 // Create pdu_data structure for processing
308 integration::pdu_data pdu(*type_opt, std::move(payload));
310 }
311}
312
314 // Note: mutex_ is already held by caller (process_buffer)
315
316 switch (pdu.type) {
319 handle_associate_rq(pdu.payload);
320 } else {
321 report_error("Unexpected A-ASSOCIATE-RQ in current state");
323 close_handler(false);
324 }
325 break;
326
329 handle_p_data_tf(pdu.payload);
330 } else {
331 report_error("Unexpected P-DATA-TF in current state");
333 close_handler(false);
334 }
335 break;
336
340 } else {
341 report_error("Unexpected A-RELEASE-RQ in current state");
343 close_handler(false);
344 }
345 break;
346
347 case pdu_type::abort:
348 handle_abort(pdu.payload);
349 break;
350
354 // These are responses, SCP should not receive them in normal flow
355 report_error("Unexpected PDU type for SCP: " + std::string(to_string(pdu.type)));
357 close_handler(false);
358 break;
359
360 default:
361 report_error("Unknown PDU type");
363 close_handler(false);
364 break;
365 }
366}
367
368void dicom_association_handler::handle_associate_rq(const std::vector<uint8_t>& payload) {
369 // Build full PDU for decoder (add header back)
370 std::vector<uint8_t> full_pdu;
371 full_pdu.reserve(pdu_header_size + payload.size());
372
373 // PDU type
374 full_pdu.push_back(static_cast<uint8_t>(pdu_type::associate_rq));
375 // Reserved
376 full_pdu.push_back(0x00);
377 // Length (big-endian)
378 uint32_t length = static_cast<uint32_t>(payload.size());
379 full_pdu.push_back(static_cast<uint8_t>((length >> 24) & 0xFF));
380 full_pdu.push_back(static_cast<uint8_t>((length >> 16) & 0xFF));
381 full_pdu.push_back(static_cast<uint8_t>((length >> 8) & 0xFF));
382 full_pdu.push_back(static_cast<uint8_t>(length & 0xFF));
383 // Payload
384 full_pdu.insert(full_pdu.end(), payload.begin(), payload.end());
385
386 // Decode the A-ASSOCIATE-RQ
387 auto result = pdu_decoder::decode_associate_rq(full_pdu);
388 if (!result.is_ok()) {
389 report_error("Failed to decode A-ASSOCIATE-RQ");
391 static_cast<uint8_t>(reject_source::service_provider_acse),
392 static_cast<uint8_t>(reject_reason_provider_acse::no_reason));
393 close_handler(false);
394 return;
395 }
396
397 const associate_rq& rq = result.value();
398
399 // Validate called AE title
400 if (rq.called_ae_title != config_.ae_title) {
401 report_error("Called AE title mismatch: expected '" + config_.ae_title +
402 "', got '" + rq.called_ae_title + "'");
404 static_cast<uint8_t>(reject_source::service_user),
406 close_handler(false);
407 return;
408 }
409
410 // Validate calling AE title against whitelist
412 bool found = false;
413 for (const auto& allowed : config_.ae_whitelist) {
414 if (allowed == rq.calling_ae_title) {
415 found = true;
416 break;
417 }
418 }
419 if (!found) {
420 report_error("Calling AE title not in whitelist: " + rq.calling_ae_title);
422 static_cast<uint8_t>(reject_source::service_user),
424 close_handler(false);
425 return;
426 }
427 }
428
429 // Build SCP config for association negotiation
430 scp_config scp_cfg;
431 scp_cfg.ae_title = config_.ae_title;
435
436 // Collect supported abstract syntaxes from services
437 for (const auto& [uid, _] : services_) {
438 scp_cfg.supported_abstract_syntaxes.push_back(uid);
439 }
440
441 // Accept compressed and uncompressed transfer syntaxes
443 "1.2.840.10008.1.2.4.201", // HTJ2K Lossless Only
444 "1.2.840.10008.1.2.4.202", // HTJ2K RPCL Lossless Only
445 "1.2.840.10008.1.2.4.203", // HTJ2K (Lossless or Lossy)
446 "1.2.840.10008.1.2.4.90", // JPEG 2000 Lossless
447 "1.2.840.10008.1.2.4.91", // JPEG 2000 Lossy
448 "1.2.840.10008.1.2.4.80", // JPEG-LS Lossless
449 "1.2.840.10008.1.2.4.81", // JPEG-LS Near-Lossless
450 "1.2.840.10008.1.2.4.70", // JPEG Lossless
451 "1.2.840.10008.1.2.4.50", // JPEG Baseline
452 "1.2.840.10008.1.2.5", // RLE Lossless
453 "1.2.840.10008.1.2.1", // Explicit VR Little Endian
454 "1.2.840.10008.1.2", // Implicit VR Little Endian
455 };
456
457 // Perform association negotiation
458 association_ = association::accept(rq, scp_cfg);
459
460 // Check if any presentation contexts were accepted
461 if (association_.accepted_contexts().empty()) {
462 report_error("No presentation contexts accepted");
464 static_cast<uint8_t>(reject_source::service_user),
465 static_cast<uint8_t>(reject_reason_user::no_reason));
466 close_handler(false);
467 return;
468 }
469
470 // Send A-ASSOCIATE-AC
472
473 // Transition to established state
476
477 // Set up user context for access control
479 user_context_ = access_control_->get_context_for_ae(
481 }
482
483 // Notify callback
484 {
485 std::lock_guard<std::mutex> cb_lock(callback_mutex_);
489 rq.called_ae_title);
490 }
491 }
492}
493
494void dicom_association_handler::handle_p_data_tf(const std::vector<uint8_t>& payload) {
495 // Build full PDU for decoder
496 std::vector<uint8_t> full_pdu;
497 full_pdu.reserve(pdu_header_size + payload.size());
498
499 full_pdu.push_back(static_cast<uint8_t>(pdu_type::p_data_tf));
500 full_pdu.push_back(0x00);
501 uint32_t length = static_cast<uint32_t>(payload.size());
502 full_pdu.push_back(static_cast<uint8_t>((length >> 24) & 0xFF));
503 full_pdu.push_back(static_cast<uint8_t>((length >> 16) & 0xFF));
504 full_pdu.push_back(static_cast<uint8_t>((length >> 8) & 0xFF));
505 full_pdu.push_back(static_cast<uint8_t>(length & 0xFF));
506 full_pdu.insert(full_pdu.end(), payload.begin(), payload.end());
507
508 // Decode P-DATA-TF
509 auto result = pdu_decoder::decode_p_data_tf(full_pdu);
510 if (!result.is_ok()) {
511 report_error("Failed to decode P-DATA-TF");
513 close_handler(false);
514 return;
515 }
516
517 const p_data_tf_pdu& p_data = result.value();
518
519 // Process each PDV
520 for (const auto& pdv : p_data.pdvs) {
521 // Get abstract syntax for this presentation context
522 std::string abstract_syntax;
523 for (const auto& ctx : association_.accepted_contexts()) {
524 if (ctx.id == pdv.context_id) {
525 abstract_syntax = ctx.abstract_syntax;
526 break;
527 }
528 }
529
530 if (abstract_syntax.empty()) {
531 report_error("Unknown presentation context ID: " +
532 std::to_string(pdv.context_id));
533 continue;
534 }
535
536 // If this is a complete command, decode DIMSE message
537 // Note: For simplicity, we're handling command-only messages here.
538 // Full DIMSE message handling with dataset fragmentation would require
539 // accumulating PDVs until is_last is true for both command and data.
540 if (pdv.is_last && pdv.is_command) {
541 // For command-only messages (like C-ECHO), dataset is empty
542 std::vector<uint8_t> empty_dataset;
543
544 // Get transfer syntax for the context
545 auto ts_result = association_.context_transfer_syntax(pdv.context_id);
546 if (ts_result.is_err()) {
547 report_error("Invalid presentation context: " +
548 ts_result.error().message);
549 continue;
550 }
551
552 // Decode DIMSE command
553 auto dimse_result = dimse::dimse_message::decode(
554 std::span<const uint8_t>(pdv.data),
555 std::span<const uint8_t>(empty_dataset),
556 ts_result.value());
557
558 if (dimse_result.is_err()) {
559 report_error("Failed to decode DIMSE message: " +
560 dimse_result.error().message);
561 continue;
562 }
563
564 messages_processed_.fetch_add(1, std::memory_order_relaxed);
565
566 // Dispatch to service
567 auto dispatch_result = dispatch_to_service(pdv.context_id, dimse_result.value());
568 if (dispatch_result.is_err()) {
569 report_error("Service dispatch failed: " + dispatch_result.error().message);
570 }
571 }
572 }
573}
574
577
578 // Process release on the association
580
581 // Send A-RELEASE-RP
583
584 // Close gracefully
585 close_handler(true);
586}
587
588void dicom_association_handler::handle_abort(const std::vector<uint8_t>& payload) {
589 // Parse abort source and reason if present
592
593 if (payload.size() >= 4) {
594 // Reserved, Reserved, Source, Reason
595 source = static_cast<abort_source>(payload[2]);
596 reason = static_cast<abort_reason>(payload[3]);
597 }
598
599 association_.process_abort(source, reason);
600
601 // Close immediately
602 close_handler(false);
603}
604
605// =============================================================================
606// Response Sending
607// =============================================================================
608
614
616 reject_result result, uint8_t source, uint8_t reason) {
617 associate_rj rj(result, source, reason);
618 auto encoded = pdu_encoder::encode_associate_rj(rj);
620}
621
622void dicom_association_handler::send_p_data_tf(const std::vector<uint8_t>& payload) {
623 // Payload should already be a complete P-DATA-TF PDU
625}
626
631
633 auto encoded = pdu_encoder::encode_abort(source, reason);
634 send_pdu(pdu_type::abort, encoded);
635}
636
637void dicom_association_handler::send_pdu(pdu_type /*type*/, const std::vector<uint8_t>& encoded_pdu) {
638#ifdef PACS_WITH_NETWORK_SYSTEM
639 if (session_ && session_->is_connected()) {
640 // encoded_pdu already includes header for most PDU types
641 // For encode_associate_ac, encode_associate_rj, etc., the encoder returns full PDU
642
643 // We need to make a copy for the async send
644 std::vector<uint8_t> data_copy = encoded_pdu;
645 (void)session_->send(std::move(data_copy));
646 pdus_sent_.fetch_add(1, std::memory_order_relaxed);
647 }
648#else
649 (void)encoded_pdu;
650#endif
651}
652
653// =============================================================================
654// Service Dispatching
655// =============================================================================
656
658 uint8_t context_id,
659 const dimse::dimse_message& msg) {
660
661 // Find the abstract syntax for this context
662 std::string abstract_syntax;
663 for (const auto& ctx : association_.accepted_contexts()) {
664 if (ctx.id == context_id) {
665 abstract_syntax = ctx.abstract_syntax;
666 break;
667 }
668 }
669
670 if (abstract_syntax.empty()) {
671 return error_info("Unknown presentation context ID");
672 }
673
674 // Find service for this SOP class
675 auto* service = find_service(abstract_syntax);
676 if (!service) {
677 return error_info("No service registered for SOP Class: " + abstract_syntax);
678 }
679
680 // Perform access control check if enabled
682 return error_info("Access denied: unregistered AE title");
683 }
685 // Map DIMSE command to DICOM operation
687 switch (msg.command()) {
690 break;
693 break;
696 break;
699 break;
702 break;
705 break;
708 break;
711 break;
714 break;
717 break;
720 break;
721 default:
722 // Response messages don't need access control
723 break;
724 }
725
726 // Check permission
727 auto check_result = access_control_->check_dicom_operation(*user_context_, op);
728 if (!check_result) {
729 return error_info("Access denied: " + check_result.reason);
730 }
731 }
732
733 // Dispatch to service
734 return service->handle_message(association_, context_id, msg);
735}
736
738 const std::string& sop_class_uid) const {
739 auto it = services_.find(sop_class_uid);
740 if (it != services_.end()) {
741 return it->second;
742 }
743 return nullptr;
744}
745
746// =============================================================================
747// State Management
748// =============================================================================
749
751 handler_state old_state = state_.exchange(new_state, std::memory_order_acq_rel);
752
753 // Log state transition if needed
754 (void)old_state;
755}
756
758 // Note: mutex_ should be held by caller
759 last_activity_ = clock::now();
760}
761
762void dicom_association_handler::report_error(const std::string& error) {
763 std::lock_guard<std::mutex> lock(callback_mutex_);
764 if (error_callback_) {
765 error_callback_(session_id(), error);
766 }
767}
768
770 handler_state expected = state_.load();
771 if (expected == handler_state::closed) {
772 return; // Already closed
773 }
774
776
777#ifdef PACS_WITH_NETWORK_SYSTEM
778 // Close the session
779 try {
780 std::lock_guard<std::mutex> lock(mutex_);
781 if (session_) {
782 session_->close();
783 }
784 } catch (...) {
785 // Suppress exceptions during session cleanup
786 }
787#endif
788
789 // Notify closed callback
790 try {
791 std::lock_guard<std::mutex> cb_lock(callback_mutex_);
792 if (closed_callback_) {
793 closed_callback_(session_id(), graceful);
794 }
795 } catch (...) {
796 // Suppress exceptions during callback invocation
797 }
798}
799
800} // namespace kcenon::pacs::network::v2
static association accept(const associate_rq &rq, const scp_config &config)
Accept an incoming SCP association.
void set_state(association_state new_state)
Force state transition (for testing).
const std::vector< accepted_presentation_context > & accepted_contexts() const noexcept
Get all accepted presentation contexts.
std::string_view calling_ae() const noexcept
Get calling AE title.
std::string_view called_ae() const noexcept
Get called AE title.
void process_release_rq()
Process received A-RELEASE-RQ.
associate_ac build_associate_ac() const
Build A-ASSOCIATE-AC PDU for sending.
Result< encoding::transfer_syntax > context_transfer_syntax(uint8_t pc_id) const
Get the transfer syntax for an accepted context.
void process_abort(const abort_source &source, const abort_reason &reason)
Process received A-ABORT PDU.
static auto decode(std::span< const uint8_t > command_data, std::span< const uint8_t > dataset_data, const encoding::transfer_syntax &dataset_ts) -> dimse_result< dimse_message >
Decode a DIMSE message from bytes.
auto command() const noexcept -> command_field
Get the command field.
static DecodeResult< associate_rq > decode_associate_rq(std::span< const uint8_t > data)
Decode an A-ASSOCIATE-RQ PDU.
static std::optional< size_t > pdu_length(std::span< const uint8_t > data)
Check if a complete PDU is available in the buffer.
static std::optional< pdu_type > peek_pdu_type(std::span< const uint8_t > data)
Get the PDU type from buffer without full decoding.
static DecodeResult< p_data_tf_pdu > decode_p_data_tf(std::span< const uint8_t > data)
Decode a P-DATA-TF PDU.
static std::vector< uint8_t > encode_release_rp()
Encodes an A-RELEASE-RP PDU.
static std::vector< uint8_t > encode_associate_ac(const associate_ac &ac)
Encodes an A-ASSOCIATE-AC PDU.
static std::vector< uint8_t > encode_associate_rj(const associate_rj &rj)
Encodes an A-ASSOCIATE-RJ PDU.
static std::vector< uint8_t > encode_abort(uint8_t source, uint8_t reason)
Encodes an A-ABORT PDU.
uint64_t pdus_received() const noexcept
Get number of PDUs received.
Result< std::monostate > dispatch_to_service(uint8_t context_id, const dimse::dimse_message &msg)
Dispatch DIMSE message to appropriate service.
void transition_to(handler_state new_state)
Transition to new state.
void send_abort(abort_source source, abort_reason reason)
Send A-ABORT PDU.
void process_buffer()
Process accumulated buffer for complete PDUs.
void feed_data(const std::vector< uint8_t > &data)
Feed received data to the handler.
void on_disconnected(const std::string &session_id)
Handle session disconnection.
void set_access_control_enabled(bool enabled)
Enable or disable access control enforcement.
handler_state state() const noexcept
Get current handler state.
void send_associate_rj(reject_result result, uint8_t source, uint8_t reason)
Send A-ASSOCIATE-RJ response.
uint64_t pdus_sent() const noexcept
Get number of PDUs sent.
services::scp_service * find_service(const std::string &sop_class_uid) const
Find service for SOP Class UID.
association_established_callback established_callback_
Callbacks.
bool is_established() const noexcept
Check if the association is established.
std::map< std::string, services::scp_service * > service_map
std::shared_ptr< security::access_control_manager > access_control_
Access control manager for RBAC.
void send_p_data_tf(const std::vector< uint8_t > &payload)
Send P-DATA-TF PDU.
service_map services_
Service registry (non-owning pointers)
bool access_control_enabled_
Whether access control is enabled.
std::string session_id() const
Get the session identifier.
void handle_associate_rq(const std::vector< uint8_t > &payload)
Handle A-ASSOCIATE-RQ PDU.
dicom_association_handler(session_ptr session, const server_config &config, const service_map &services)
Construct a handler for a network session.
Result< std::reference_wrapper< association > > get_association()
Get the underlying association object.
void on_data_received(const std::vector< uint8_t > &data)
Handle data received from session.
static constexpr size_t pdu_header_size
PDU header size (type + reserved + length)
std::atomic< handler_state > state_
Current handler state.
void set_established_callback(association_established_callback callback)
Set callback for association established event.
void set_closed_callback(association_closed_callback callback)
Set callback for association closed event.
time_point last_activity() const noexcept
Get time of last activity.
bool is_closed() const noexcept
Check if the handler is closed.
std::string calling_ae() const
Get the calling AE title.
~dicom_association_handler()
Destructor (stops handler if still running).
void handle_error(std::error_code ec)
Handle session error.
std::shared_ptr< kcenon::network::interfaces::i_session > session_ptr
void set_access_control(std::shared_ptr< security::access_control_manager > acm)
Set the access control manager for RBAC.
void stop(bool graceful=false)
Stop the handler and close the session.
void send_pdu(pdu_type type, const std::vector< uint8_t > &payload)
Send raw PDU data.
uint64_t messages_processed() const noexcept
Get number of DIMSE messages processed.
std::optional< security::user_context > user_context_
User context for this association (set after A-ASSOCIATE negotiation)
void report_error(const std::string &error)
Report error through callback.
void set_error_callback(handler_error_callback callback)
Set callback for error events.
void handle_p_data_tf(const std::vector< uint8_t > &payload)
Handle P-DATA-TF PDU.
void on_error(std::error_code ec)
Handle session error.
std::vector< uint8_t > receive_buffer_
PDU receive buffer.
std::string called_ae() const
Get the called AE title.
void handle_abort(const std::vector< uint8_t > &payload)
Handle A-ABORT PDU.
void process_pdu(const integration::pdu_data &pdu)
Process a complete PDU.
DICOM association handler for network_system integration.
constexpr int invalid_association_state
Definition result.h:101
@ n_get_rq
N-GET Request - Get attribute values.
@ c_store_rq
C-STORE Request - Store composite SOP instance.
@ n_create_rq
N-CREATE Request - Create SOP instance.
@ n_set_rq
N-SET Request - Set attribute values.
@ c_echo_rq
C-ECHO Request - Verify DICOM connection.
@ c_find_rq
C-FIND Request - Query for matching instances.
@ n_delete_rq
N-DELETE Request - Delete SOP instance.
@ n_event_report_rq
N-EVENT-REPORT Request - Report event notification.
@ c_get_rq
C-GET Request - Retrieve composite SOP instances.
@ c_move_rq
C-MOVE Request - Move composite SOP instances.
@ n_action_rq
N-ACTION Request - Request action.
constexpr const char * to_string(handler_state state) noexcept
Convert handler_state to string representation.
std::function< void(const std::string &session_id, bool graceful)> association_closed_callback
Callback type for association closed events.
handler_state
State machine states for the association handler.
@ closed
Association closed (released or aborted)
@ releasing
Graceful release in progress.
@ established
Association established, processing DIMSE.
@ idle
Initial state, waiting for A-ASSOCIATE-RQ.
std::function< void(const std::string &session_id, const std::string &error)> handler_error_callback
Callback type for error events.
std::function< void(const std::string &session_id, const std::string &calling_ae, const std::string &called_ae)> association_established_callback
Callback type for association established events.
reject_result
Reject result values.
Definition pdu_types.h:92
@ rejected_permanent
Rejected-permanent.
pdu_type
PDU (Protocol Data Unit) types as defined in DICOM PS3.8.
Definition pdu_types.h:25
@ associate_rj
A-ASSOCIATE-RJ (Association Reject)
@ associate_ac
A-ASSOCIATE-AC (Association Accept)
@ p_data_tf
P-DATA-TF (Data Transfer)
@ release_rq
A-RELEASE-RQ (Release Request)
@ release_rp
A-RELEASE-RP (Release Response)
@ associate_rq
A-ASSOCIATE-RQ (Association Request)
@ abstract_syntax
Abstract Syntax Sub-item.
abort_reason
Abort reason values when source is service-provider.
Definition pdu_types.h:79
@ not_specified
Reason not specified.
@ invalid_pdu_parameter
Invalid PDU parameter value.
std::variant< associate_rq, associate_ac, associate_rj, p_data_tf_pdu, release_rq_pdu, release_rp_pdu, abort_pdu > pdu
Variant type that can hold any PDU.
Definition pdu_decoder.h:62
@ service_provider_acse
DICOM UL service-provider (ACSE)
@ service_user
DICOM UL service-user.
abort_source
Abort source values.
Definition pdu_types.h:70
@ service_user
DICOM UL service-user.
@ service_provider
DICOM UL service-provider (ACSE)
@ established
Sta6: Association established, ready for DIMSE.
@ called_ae_not_recognized
Called-AE-title not recognized.
@ calling_ae_not_recognized
Calling-AE-title not recognized.
DicomOperation
DICOM operation types for permission checking.
kcenon::common::error_info error_info
Error information type.
Definition result.h:40
Container for received PDU data.
A-ASSOCIATE-AC PDU data.
Definition pdu_types.h:218
A-ASSOCIATE-RJ PDU data.
Definition pdu_types.h:231
A-ASSOCIATE-RQ PDU data.
Definition pdu_types.h:205
std::string called_ae_title
Called AE Title (16 chars max)
Definition pdu_types.h:206
std::string calling_ae_title
Calling AE Title (16 chars max)
Definition pdu_types.h:207
std::vector< presentation_data_value > pdvs
Presentation Data Values.
Definition pdu_decoder.h:52
Configuration for SCP to accept associations.
std::string ae_title
Our AE Title.
std::vector< std::string > supported_abstract_syntaxes
std::vector< std::string > supported_transfer_syntaxes
uint32_t max_pdu_size
Maximum PDU size for data transfer.
std::vector< std::string > ae_whitelist
AE Title whitelist (empty = accept all)
std::string implementation_version_name
Implementation Version Name.
std::string ae_title
Application Entity Title for this server (16 chars max)
std::string implementation_class_uid
Implementation Class UID.
bool accept_unknown_calling_ae
Accept unknown calling AE titles (when whitelist is non-empty)
std::string_view uid