PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
kcenon::pacs::security::atna_syslog_transport Class Reference

Sends ATNA audit messages via Syslog protocol. More...

#include <atna_syslog_transport.h>

Collaboration diagram for kcenon::pacs::security::atna_syslog_transport:
Collaboration graph

Classes

struct  tls_context
 

Public Member Functions

 atna_syslog_transport (const syslog_transport_config &config)
 Construct transport with configuration.
 
 ~atna_syslog_transport ()
 
 atna_syslog_transport (const atna_syslog_transport &)=delete
 
atna_syslog_transportoperator= (const atna_syslog_transport &)=delete
 
 atna_syslog_transport (atna_syslog_transport &&other) noexcept
 
atna_syslog_transportoperator= (atna_syslog_transport &&other) noexcept
 
kcenon::pacs::VoidResult send (const std::string &xml_message)
 Send an RFC 3881 XML audit message via Syslog.
 
std::string format_syslog_message (const std::string &xml_message) const
 Format an XML audit message as an RFC 5424 Syslog message.
 
bool is_connected () const noexcept
 Check if the transport is connected (TLS only)
 
void close ()
 Close the transport connection.
 
size_t messages_sent () const noexcept
 
size_t send_errors () const noexcept
 
void reset_statistics () noexcept
 
const syslog_transport_configconfig () const noexcept
 

Private Types

using socket_type = int
 

Private Member Functions

kcenon::pacs::VoidResult send_udp (const std::string &syslog_message)
 
kcenon::pacs::VoidResult send_tls (const std::string &syslog_message)
 
kcenon::pacs::VoidResult ensure_tls_connected ()
 

Static Private Member Functions

static std::string get_local_hostname ()
 
static std::string get_timestamp ()
 
static uint8_t compute_priority (syslog_facility facility, syslog_severity severity)
 

Private Attributes

syslog_transport_config config_
 
socket_type socket_ {invalid_socket}
 
tls_contexttls_ {nullptr}
 
std::atomic< size_t > messages_sent_ {0}
 
std::atomic< size_t > send_errors_ {0}
 

Static Private Attributes

static constexpr socket_type invalid_socket = -1
 

Detailed Description

Sends ATNA audit messages via Syslog protocol.

Formats RFC 3881 XML audit messages into RFC 5424 Syslog messages and sends them to an Audit Record Repository via UDP or TLS.

Usage

config.host = "audit-server.hospital.local";
config.port = 6514;
config.ca_cert_path = "/etc/pacs/certs/ca.pem";
auto xml = atna_audit_logger::to_xml(msg);
auto result = transport.send(xml);
static std::string to_xml(const atna_audit_message &message)
Serialize an audit message to RFC 3881 XML.
static atna_audit_message build_user_authentication(const std::string &source_id, const std::string &user_id, const std::string &user_ip, bool is_login, atna_event_outcome outcome=atna_event_outcome::success)
Build User Authentication audit message (login/logout)
Sends ATNA audit messages via Syslog protocol.
const syslog_transport_config & config() const noexcept
@ tls
TLS over TCP (RFC 5425) — Secure.
Configuration for the Syslog transport.
std::string ca_cert_path
Path to CA certificate file for server verification.
syslog_transport_protocol protocol
Transport protocol (UDP or TLS)
uint16_t port
Port number (514 for UDP, 6514 for TLS per IANA)
std::string host
Audit Record Repository hostname or IP.

Definition at line 164 of file atna_syslog_transport.h.

Member Typedef Documentation

◆ socket_type

Constructor & Destructor Documentation

◆ atna_syslog_transport() [1/3]

kcenon::pacs::security::atna_syslog_transport::atna_syslog_transport ( const syslog_transport_config & config)
explicit

Construct transport with configuration.

Parameters
configTransport configuration

Definition at line 91 of file atna_syslog_transport.cpp.

93 : config_(config) {
94 if (config_.hostname.empty()) {
96 }
97}
std::string hostname
Hostname to report in Syslog header (auto-detected if empty)

References config_, and kcenon::pacs::security::syslog_transport_config::hostname.

◆ ~atna_syslog_transport()

kcenon::pacs::security::atna_syslog_transport::~atna_syslog_transport ( )

Definition at line 99 of file atna_syslog_transport.cpp.

99 {
100 close();
101}
void close()
Close the transport connection.

References close().

Here is the call graph for this function:

◆ atna_syslog_transport() [2/3]

kcenon::pacs::security::atna_syslog_transport::atna_syslog_transport ( const atna_syslog_transport & )
delete

◆ atna_syslog_transport() [3/3]

kcenon::pacs::security::atna_syslog_transport::atna_syslog_transport ( atna_syslog_transport && other)
noexcept

Definition at line 103 of file atna_syslog_transport.cpp.

105 : config_(std::move(other.config_)),
106 socket_(other.socket_),
107 tls_(other.tls_),
108 messages_sent_(other.messages_sent_.load(std::memory_order_relaxed)),
109 send_errors_(other.send_errors_.load(std::memory_order_relaxed)) {
110 other.socket_ = invalid_socket;
111 other.tls_ = nullptr;
112}

References kcenon::pacs::security::other.

Member Function Documentation

◆ close()

void kcenon::pacs::security::atna_syslog_transport::close ( )

Close the transport connection.

For TLS, performs graceful shutdown. For UDP, closes the socket.

Definition at line 195 of file atna_syslog_transport.cpp.

195 {
196 delete tls_;
197 tls_ = nullptr;
198
199 if (socket_ != invalid_socket) {
200 close_socket(socket_);
202 }
203}

References invalid_socket, socket_, and tls_.

Referenced by ensure_tls_connected(), send_tls(), and ~atna_syslog_transport().

Here is the caller graph for this function:

◆ compute_priority()

uint8_t kcenon::pacs::security::atna_syslog_transport::compute_priority ( syslog_facility facility,
syslog_severity severity )
staticnodiscardprivate

Definition at line 487 of file atna_syslog_transport.cpp.

488 {
489 // PRI = Facility * 8 + Severity
490 return static_cast<uint8_t>(
491 static_cast<uint8_t>(facility) * 8 +
492 static_cast<uint8_t>(severity));
493}

Referenced by format_syslog_message().

Here is the caller graph for this function:

◆ config()

const syslog_transport_config & kcenon::pacs::security::atna_syslog_transport::config ( ) const
nodiscardnoexcept

Definition at line 222 of file atna_syslog_transport.cpp.

222 {
223 return config_;
224}

References config_.

◆ ensure_tls_connected()

kcenon::pacs::VoidResult kcenon::pacs::security::atna_syslog_transport::ensure_tls_connected ( )
nodiscardprivate

Definition at line 320 of file atna_syslog_transport.cpp.

320 {
321#ifndef PACS_WITH_DIGITAL_SIGNATURES
324 "TLS not available — OpenSSL not linked");
325#else
326 if (tls_ && tls_->ssl && socket_ != invalid_socket) {
327 return kcenon::common::ok(); // Already connected
328 }
329
330 // Clean up previous state
331 close();
332
333 // Create SSL context
334 tls_ = new tls_context();
335 tls_->ctx = SSL_CTX_new(TLS_client_method());
336 if (!tls_->ctx) {
337 close();
340 "Failed to create TLS context: " + get_openssl_error());
341 }
342
343 // Set minimum TLS version to 1.2 (IHE ATNA requirement)
344 SSL_CTX_set_min_proto_version(tls_->ctx, TLS1_2_VERSION);
345
346 // Load CA certificate for server verification
347 if (!config_.ca_cert_path.empty()) {
348 if (SSL_CTX_load_verify_locations(
349 tls_->ctx, config_.ca_cert_path.c_str(), nullptr) != 1) {
350 close();
353 "Failed to load CA certificate: " + get_openssl_error());
354 }
355 }
356
357 // Load client certificate (mutual TLS)
358 if (!config_.client_cert_path.empty()) {
359 if (SSL_CTX_use_certificate_file(
360 tls_->ctx, config_.client_cert_path.c_str(),
361 SSL_FILETYPE_PEM) != 1) {
362 close();
365 "Failed to load client certificate: " + get_openssl_error());
366 }
367 }
368
369 if (!config_.client_key_path.empty()) {
370 if (SSL_CTX_use_PrivateKey_file(
371 tls_->ctx, config_.client_key_path.c_str(),
372 SSL_FILETYPE_PEM) != 1) {
373 close();
376 "Failed to load client key: " + get_openssl_error());
377 }
378 }
379
380 // Set verification mode
382 SSL_CTX_set_verify(tls_->ctx, SSL_VERIFY_PEER, nullptr);
383 } else {
384 SSL_CTX_set_verify(tls_->ctx, SSL_VERIFY_NONE, nullptr);
385 }
386
387 // Resolve and connect TCP socket
388 struct addrinfo hints{};
389 hints.ai_family = AF_UNSPEC;
390 hints.ai_socktype = SOCK_STREAM;
391 hints.ai_protocol = IPPROTO_TCP;
392
393 struct addrinfo* result = nullptr;
394 std::string port_str = std::to_string(config_.port);
395
396 int ret = ::getaddrinfo(
397 config_.host.c_str(), port_str.c_str(), &hints, &result);
398 if (ret != 0 || result == nullptr) {
399 close();
402 "Failed to resolve TLS syslog host: " + config_.host);
403 }
404
405 socket_ = ::socket(
406 result->ai_family, result->ai_socktype, result->ai_protocol);
407 if (socket_ == invalid_socket) {
408 ::freeaddrinfo(result);
409 close();
412 "Failed to create TCP socket");
413 }
414
415 if (::connect(socket_, result->ai_addr,
416 static_cast<int>(result->ai_addrlen)) != 0) {
417 ::freeaddrinfo(result);
418 close();
421 "Failed to connect to TLS syslog server: " +
422 config_.host + ":" + port_str);
423 }
424 ::freeaddrinfo(result);
425
426 // Create SSL object and perform handshake
427 tls_->ssl = SSL_new(tls_->ctx);
428 if (!tls_->ssl) {
429 close();
432 "Failed to create SSL object: " + get_openssl_error());
433 }
434
435 SSL_set_fd(tls_->ssl, static_cast<int>(socket_));
436
437 // Set SNI hostname
438 SSL_set_tlsext_host_name(tls_->ssl, config_.host.c_str());
439
440 if (SSL_connect(tls_->ssl) != 1) {
441 std::string err = get_openssl_error();
442 close();
445 "TLS handshake failed: " + err);
446 }
447
448 return kcenon::common::ok();
449#endif
450}
constexpr int connection_failed
Definition result.h:94
VoidResult pacs_void_error(int code, const std::string &message, const std::string &details="")
Create a PACS void error result.
Definition result.h:249
std::string client_key_path
Path to client private key file (mutual TLS)
std::string client_cert_path
Path to client certificate file (mutual TLS)
bool verify_server
Whether to verify server certificate (disable only for testing)

References kcenon::pacs::security::syslog_transport_config::ca_cert_path, kcenon::pacs::security::syslog_transport_config::client_cert_path, kcenon::pacs::security::syslog_transport_config::client_key_path, close(), config_, kcenon::pacs::error_codes::connection_failed, kcenon::pacs::security::syslog_transport_config::host, invalid_socket, kcenon::pacs::pacs_void_error(), kcenon::pacs::security::syslog_transport_config::port, socket_, tls_, and kcenon::pacs::security::syslog_transport_config::verify_server.

Referenced by send_tls().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ format_syslog_message()

std::string kcenon::pacs::security::atna_syslog_transport::format_syslog_message ( const std::string & xml_message) const
nodiscard

Format an XML audit message as an RFC 5424 Syslog message.

Produces the complete Syslog message without sending it. Useful for testing and logging.

Parameters
xml_messageThe audit XML payload
Returns
Formatted RFC 5424 message string

Definition at line 159 of file atna_syslog_transport.cpp.

160 {
161
162 // RFC 5424 format:
163 // <PRI>VERSION SP TIMESTAMP SP HOSTNAME SP APP-NAME SP PROCID SP MSGID
164 // SP STRUCTURED-DATA SP MSG
165
167 std::string timestamp = get_timestamp();
168
169 std::ostringstream oss;
170 oss << "<" << static_cast<int>(pri) << ">"
171 << "1" // Version
172 << " " << timestamp
173 << " " << (config_.hostname.empty() ? "-" : config_.hostname)
174 << " " << (config_.app_name.empty() ? "-" : config_.app_name)
175 << " " << "-" // PROCID (NIL)
176 << " " << "IHE+RFC-3881" // MSGID — IHE ATNA identifier
177 << " " << "-" // STRUCTURED-DATA (NIL)
178 << " " << "\xEF\xBB\xBF" // BOM (RFC 5424 Section 6.4)
179 << xml_message;
180
181 return oss.str();
182}
static uint8_t compute_priority(syslog_facility facility, syslog_severity severity)
std::string app_name
Application name in Syslog header.
syslog_severity severity
Syslog severity for audit events.

References kcenon::pacs::security::syslog_transport_config::app_name, compute_priority(), config_, kcenon::pacs::security::syslog_transport_config::facility, get_timestamp(), kcenon::pacs::security::syslog_transport_config::hostname, and kcenon::pacs::security::syslog_transport_config::severity.

Referenced by send().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ get_local_hostname()

std::string kcenon::pacs::security::atna_syslog_transport::get_local_hostname ( )
staticnodiscardprivate

Definition at line 456 of file atna_syslog_transport.cpp.

456 {
457 char hostname[256];
458 if (::gethostname(hostname, sizeof(hostname)) == 0) {
459 hostname[sizeof(hostname) - 1] = '\0';
460 return std::string(hostname);
461 }
462 return "localhost";
463}

◆ get_timestamp()

std::string kcenon::pacs::security::atna_syslog_transport::get_timestamp ( )
staticnodiscardprivate

Definition at line 465 of file atna_syslog_transport.cpp.

465 {
466 // RFC 5424 TIMESTAMP format: YYYY-MM-DDThh:mm:ss.sssZ
467 auto now = std::chrono::system_clock::now();
468 auto time_t_val = std::chrono::system_clock::to_time_t(now);
469 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
470 now.time_since_epoch()) % 1000;
471
472 std::tm tm_val{};
473#if defined(_WIN32)
474 gmtime_s(&tm_val, &time_t_val);
475#else
476 gmtime_r(&time_t_val, &tm_val);
477#endif
478
479 std::ostringstream oss;
480 oss << std::put_time(&tm_val, "%Y-%m-%dT%H:%M:%S")
481 << '.' << std::setfill('0') << std::setw(3) << ms.count()
482 << "Z";
483
484 return oss.str();
485}

Referenced by format_syslog_message().

Here is the caller graph for this function:

◆ is_connected()

bool kcenon::pacs::security::atna_syslog_transport::is_connected ( ) const
nodiscardnoexcept

Check if the transport is connected (TLS only)

For UDP, always returns true since UDP is connectionless.

Definition at line 188 of file atna_syslog_transport.cpp.

188 {
190 return true; // UDP is connectionless
191 }
192 return socket_ != invalid_socket && tls_ != nullptr;
193}
@ udp
UDP (RFC 5426) — Fire-and-forget.

References config_, invalid_socket, kcenon::pacs::security::syslog_transport_config::protocol, socket_, tls_, and kcenon::pacs::security::udp.

◆ messages_sent()

size_t kcenon::pacs::security::atna_syslog_transport::messages_sent ( ) const
nodiscardnoexcept

Definition at line 209 of file atna_syslog_transport.cpp.

209 {
210 return messages_sent_.load(std::memory_order_relaxed);
211}

References messages_sent_.

◆ operator=() [1/2]

atna_syslog_transport & kcenon::pacs::security::atna_syslog_transport::operator= ( atna_syslog_transport && other)
noexcept

Definition at line 114 of file atna_syslog_transport.cpp.

115 {
116 if (this != &other) {
117 close();
118 config_ = std::move(other.config_);
119 socket_ = other.socket_;
120 tls_ = other.tls_;
121 messages_sent_.store(
122 other.messages_sent_.load(std::memory_order_relaxed),
123 std::memory_order_relaxed);
124 send_errors_.store(
125 other.send_errors_.load(std::memory_order_relaxed),
126 std::memory_order_relaxed);
127 other.socket_ = invalid_socket;
128 other.tls_ = nullptr;
129 }
130 return *this;
131}

References kcenon::pacs::security::other.

◆ operator=() [2/2]

atna_syslog_transport & kcenon::pacs::security::atna_syslog_transport::operator= ( const atna_syslog_transport & )
delete

◆ reset_statistics()

void kcenon::pacs::security::atna_syslog_transport::reset_statistics ( )
noexcept

Definition at line 217 of file atna_syslog_transport.cpp.

217 {
218 messages_sent_.store(0, std::memory_order_relaxed);
219 send_errors_.store(0, std::memory_order_relaxed);
220}

References messages_sent_, and send_errors_.

◆ send()

kcenon::pacs::VoidResult kcenon::pacs::security::atna_syslog_transport::send ( const std::string & xml_message)
nodiscard

Send an RFC 3881 XML audit message via Syslog.

Wraps the XML payload in an RFC 5424 Syslog message and sends it to the configured Audit Record Repository.

Parameters
xml_messageRFC 3881 XML audit message string
Returns
VoidResult indicating success or transport error

Definition at line 137 of file atna_syslog_transport.cpp.

138 {
139
140 auto syslog_msg = format_syslog_message(xml_message);
141
143 ? send_tls(syslog_msg)
144 : send_udp(syslog_msg);
145
146 if (result.is_ok()) {
147 messages_sent_.fetch_add(1, std::memory_order_relaxed);
148 } else {
149 send_errors_.fetch_add(1, std::memory_order_relaxed);
150 }
151
152 return result;
153}
std::string format_syslog_message(const std::string &xml_message) const
Format an XML audit message as an RFC 5424 Syslog message.
kcenon::pacs::VoidResult send_tls(const std::string &syslog_message)
kcenon::pacs::VoidResult send_udp(const std::string &syslog_message)

References config_, format_syslog_message(), messages_sent_, kcenon::pacs::security::syslog_transport_config::protocol, send_errors_, send_tls(), send_udp(), and kcenon::pacs::security::tls.

Referenced by kcenon::pacs::security::atna_service_auditor::send_audit().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ send_errors()

size_t kcenon::pacs::security::atna_syslog_transport::send_errors ( ) const
nodiscardnoexcept

Definition at line 213 of file atna_syslog_transport.cpp.

213 {
214 return send_errors_.load(std::memory_order_relaxed);
215}

References send_errors_.

◆ send_tls()

kcenon::pacs::VoidResult kcenon::pacs::security::atna_syslog_transport::send_tls ( const std::string & syslog_message)
nodiscardprivate

Definition at line 285 of file atna_syslog_transport.cpp.

286 {
287
288#ifndef PACS_WITH_DIGITAL_SIGNATURES
289 (void)syslog_message;
292 "TLS syslog transport requires OpenSSL (PACS_WITH_DIGITAL_SIGNATURES)");
293#else
294 auto connect_result = ensure_tls_connected();
295 if (!connect_result.is_ok()) {
296 return connect_result;
297 }
298
299 // RFC 5425: Octet-counting framing
300 // MSG-LEN SP SYSLOG-MSG
301 std::string framed =
302 std::to_string(syslog_message.size()) + " " + syslog_message;
303
304 int bytes_written = SSL_write(
305 tls_->ssl, framed.data(), static_cast<int>(framed.size()));
306
307 if (bytes_written <= 0) {
308 int ssl_err = SSL_get_error(tls_->ssl, bytes_written);
309 close();
312 "TLS write failed (SSL error: " +
313 std::to_string(ssl_err) + ")");
314 }
315
316 return kcenon::common::ok();
317#endif
318}
constexpr int send_failed
Definition result.h:96

References close(), kcenon::pacs::error_codes::connection_failed, ensure_tls_connected(), kcenon::pacs::pacs_void_error(), kcenon::pacs::error_codes::send_failed, and tls_.

Referenced by send().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ send_udp()

kcenon::pacs::VoidResult kcenon::pacs::security::atna_syslog_transport::send_udp ( const std::string & syslog_message)
nodiscardprivate

Definition at line 230 of file atna_syslog_transport.cpp.

231 {
232
233 // Resolve destination address
234 struct addrinfo hints{};
235 hints.ai_family = AF_UNSPEC;
236 hints.ai_socktype = SOCK_DGRAM;
237 hints.ai_protocol = IPPROTO_UDP;
238
239 struct addrinfo* result = nullptr;
240 std::string port_str = std::to_string(config_.port);
241
242 int ret = ::getaddrinfo(
243 config_.host.c_str(), port_str.c_str(), &hints, &result);
244 if (ret != 0 || result == nullptr) {
247 "Failed to resolve syslog host: " + config_.host);
248 }
249
250 // Create UDP socket
251 socket_type sock = ::socket(
252 result->ai_family, result->ai_socktype, result->ai_protocol);
253 if (sock == invalid_socket) {
254 ::freeaddrinfo(result);
257 "Failed to create UDP socket");
258 }
259
260 // Send the message
261 auto bytes_sent = ::sendto(
262 sock,
263 syslog_message.data(),
264 static_cast<int>(syslog_message.size()),
265 0,
266 result->ai_addr,
267 static_cast<int>(result->ai_addrlen));
268
269 ::freeaddrinfo(result);
270 close_socket(sock);
271
272 if (bytes_sent < 0) {
275 "Failed to send UDP syslog message");
276 }
277
278 return kcenon::common::ok();
279}

References config_, kcenon::pacs::error_codes::connection_failed, kcenon::pacs::security::syslog_transport_config::host, invalid_socket, kcenon::pacs::pacs_void_error(), kcenon::pacs::security::syslog_transport_config::port, and kcenon::pacs::error_codes::send_failed.

Referenced by send().

Here is the call graph for this function:
Here is the caller graph for this function:

Member Data Documentation

◆ config_

syslog_transport_config kcenon::pacs::security::atna_syslog_transport::config_
private

◆ invalid_socket

socket_type kcenon::pacs::security::atna_syslog_transport::invalid_socket = -1
staticconstexprprivate

Definition at line 275 of file atna_syslog_transport.h.

Referenced by close(), ensure_tls_connected(), is_connected(), and send_udp().

◆ messages_sent_

std::atomic<size_t> kcenon::pacs::security::atna_syslog_transport::messages_sent_ {0}
private

Definition at line 283 of file atna_syslog_transport.h.

283{0};

Referenced by messages_sent(), reset_statistics(), and send().

◆ send_errors_

std::atomic<size_t> kcenon::pacs::security::atna_syslog_transport::send_errors_ {0}
private

Definition at line 284 of file atna_syslog_transport.h.

284{0};

Referenced by reset_statistics(), send(), and send_errors().

◆ socket_

socket_type kcenon::pacs::security::atna_syslog_transport::socket_ {invalid_socket}
private

Definition at line 277 of file atna_syslog_transport.h.

Referenced by close(), ensure_tls_connected(), and is_connected().

◆ tls_

tls_context* kcenon::pacs::security::atna_syslog_transport::tls_ {nullptr}
private

Definition at line 281 of file atna_syslog_transport.h.

281{nullptr};

Referenced by close(), ensure_tls_connected(), is_connected(), and send_tls().


The documentation for this class was generated from the following files: