PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
atna_syslog_transport.cpp
Go to the documentation of this file.
1
7
8#include <chrono>
9#include <cstring>
10#include <ctime>
11#include <iomanip>
12#include <sstream>
13
14// Platform-specific network headers
15#ifdef _WIN32
16 #ifndef WIN32_LEAN_AND_MEAN
17 #define WIN32_LEAN_AND_MEAN
18 #endif
19 #include <winsock2.h>
20 #include <ws2tcpip.h>
21 #pragma comment(lib, "ws2_32.lib")
22#else
23 #include <arpa/inet.h>
24 #include <netdb.h>
25 #include <netinet/in.h>
26 #include <sys/socket.h>
27 #include <unistd.h>
28#endif
29
30// OpenSSL for TLS transport (conditionally included)
31#ifdef PACS_WITH_DIGITAL_SIGNATURES
32 #include <openssl/err.h>
33 #include <openssl/ssl.h>
34#endif
35
36namespace kcenon::pacs::security {
37
38// =============================================================================
39// TLS Context (opaque, avoids OpenSSL in header)
40// =============================================================================
41
43#ifdef PACS_WITH_DIGITAL_SIGNATURES
44 SSL_CTX* ctx{nullptr};
45 SSL* ssl{nullptr};
46
47 ~tls_context() {
48 if (ssl) {
49 SSL_shutdown(ssl);
50 SSL_free(ssl);
51 }
52 if (ctx) {
53 SSL_CTX_free(ctx);
54 }
55 }
56#endif
57};
58
59// =============================================================================
60// Platform Helpers
61// =============================================================================
62
63namespace {
64
65void close_socket([[maybe_unused]] int sock) {
66#ifdef _WIN32
67 ::closesocket(static_cast<SOCKET>(sock));
68#else
69 ::close(sock);
70#endif
71}
72
73#ifdef PACS_WITH_DIGITAL_SIGNATURES
74std::string get_openssl_error() {
75 unsigned long err = ERR_get_error();
76 if (err == 0) {
77 return "Unknown OpenSSL error";
78 }
79 char buf[256];
80 ERR_error_string_n(err, buf, sizeof(buf));
81 return std::string(buf);
82}
83#endif
84
85} // anonymous namespace
86
87// =============================================================================
88// Construction / Destruction
89// =============================================================================
90
92 const syslog_transport_config& config)
93 : config_(config) {
94 if (config_.hostname.empty()) {
95 config_.hostname = get_local_hostname();
96 }
97}
98
102
104 atna_syslog_transport&& other) noexcept
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}
113
115 atna_syslog_transport&& other) noexcept {
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}
132
133// =============================================================================
134// Send Operations
135// =============================================================================
136
137kcenon::pacs::VoidResult atna_syslog_transport::send(
138 const std::string& xml_message) {
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}
154
155// =============================================================================
156// RFC 5424 Message Formatting
157// =============================================================================
158
160 const std::string& xml_message) const {
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}
183
184// =============================================================================
185// Connection Management
186// =============================================================================
187
190 return true; // UDP is connectionless
191 }
192 return socket_ != invalid_socket && tls_ != nullptr;
193}
194
196 delete tls_;
197 tls_ = nullptr;
198
199 if (socket_ != invalid_socket) {
200 close_socket(socket_);
202 }
203}
204
205// =============================================================================
206// Statistics
207// =============================================================================
208
210 return messages_sent_.load(std::memory_order_relaxed);
211}
212
213size_t atna_syslog_transport::send_errors() const noexcept {
214 return send_errors_.load(std::memory_order_relaxed);
215}
216
218 messages_sent_.store(0, std::memory_order_relaxed);
219 send_errors_.store(0, std::memory_order_relaxed);
220}
221
223 return config_;
224}
225
226// =============================================================================
227// Private — UDP Transport (RFC 5426)
228// =============================================================================
229
230kcenon::pacs::VoidResult atna_syslog_transport::send_udp(
231 const std::string& syslog_message) {
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}
280
281// =============================================================================
282// Private — TLS Transport (RFC 5425)
283// =============================================================================
284
285kcenon::pacs::VoidResult atna_syslog_transport::send_tls(
286 const std::string& syslog_message) {
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}
319
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}
451
452// =============================================================================
453// Private — Utility Functions
454// =============================================================================
455
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}
464
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}
486
488 syslog_facility facility, syslog_severity severity) {
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}
494
495} // namespace kcenon::pacs::security
Syslog transport for ATNA audit messages (RFC 5424/5425/5426)
Sends ATNA audit messages via Syslog protocol.
atna_syslog_transport(const syslog_transport_config &config)
Construct transport with configuration.
bool is_connected() const noexcept
Check if the transport is connected (TLS only)
std::string format_syslog_message(const std::string &xml_message) const
Format an XML audit message as an RFC 5424 Syslog message.
const syslog_transport_config & config() const noexcept
kcenon::pacs::VoidResult send_tls(const std::string &syslog_message)
static uint8_t compute_priority(syslog_facility facility, syslog_severity severity)
void close()
Close the transport connection.
kcenon::pacs::VoidResult send_udp(const std::string &syslog_message)
kcenon::pacs::VoidResult send(const std::string &xml_message)
Send an RFC 3881 XML audit message via Syslog.
atna_syslog_transport & operator=(const atna_syslog_transport &)=delete
constexpr int send_failed
Definition result.h:96
constexpr int connection_failed
Definition result.h:94
@ udp
UDP (RFC 5426) — Fire-and-forget.
@ tls
TLS over TCP (RFC 5425) — Secure.
syslog_severity
Syslog severity levels.
syslog_facility
Syslog facility values.
VoidResult pacs_void_error(int code, const std::string &message, const std::string &details="")
Create a PACS void error result.
Definition result.h:249
Configuration for the Syslog transport.
std::string client_key_path
Path to client private key file (mutual TLS)
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.
std::string app_name
Application name in Syslog header.
std::string client_cert_path
Path to client certificate file (mutual TLS)
std::string hostname
Hostname to report in Syslog header (auto-detected if empty)
syslog_severity severity
Syslog severity for audit events.
bool verify_server
Whether to verify server certificate (disable only for testing)