16 #ifndef WIN32_LEAN_AND_MEAN
17 #define WIN32_LEAN_AND_MEAN
21 #pragma comment(lib, "ws2_32.lib")
23 #include <arpa/inet.h>
25 #include <netinet/in.h>
26 #include <sys/socket.h>
31#ifdef PACS_WITH_DIGITAL_SIGNATURES
32 #include <openssl/err.h>
33 #include <openssl/ssl.h>
43#ifdef PACS_WITH_DIGITAL_SIGNATURES
44 SSL_CTX* ctx{
nullptr};
65void close_socket([[maybe_unused]]
int sock) {
67 ::closesocket(
static_cast<SOCKET
>(sock));
73#ifdef PACS_WITH_DIGITAL_SIGNATURES
74std::string get_openssl_error() {
75 unsigned long err = ERR_get_error();
77 return "Unknown OpenSSL error";
80 ERR_error_string_n(err, buf,
sizeof(buf));
81 return std::string(buf);
95 config_.hostname = get_local_hostname();
105 : config_(std::move(
other.config_)),
106 socket_(
other.socket_),
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;
116 if (
this != &
other) {
118 config_ = std::move(
other.config_);
119 socket_ =
other.socket_;
121 messages_sent_.store(
122 other.messages_sent_.load(std::memory_order_relaxed),
123 std::memory_order_relaxed);
125 other.send_errors_.load(std::memory_order_relaxed),
126 std::memory_order_relaxed);
127 other.socket_ = invalid_socket;
128 other.tls_ =
nullptr;
138 const std::string& xml_message) {
146 if (result.is_ok()) {
160 const std::string& xml_message)
const {
169 std::ostringstream oss;
170 oss <<
"<" <<
static_cast<int>(pri) <<
">"
176 <<
" " <<
"IHE+RFC-3881"
178 <<
" " <<
"\xEF\xBB\xBF"
231 const std::string& syslog_message) {
234 struct addrinfo hints{};
235 hints.ai_family = AF_UNSPEC;
236 hints.ai_socktype = SOCK_DGRAM;
237 hints.ai_protocol = IPPROTO_UDP;
239 struct addrinfo* result =
nullptr;
240 std::string port_str = std::to_string(
config_.
port);
242 int ret = ::getaddrinfo(
243 config_.
host.c_str(), port_str.c_str(), &hints, &result);
244 if (ret != 0 || result ==
nullptr) {
252 result->ai_family, result->ai_socktype, result->ai_protocol);
254 ::freeaddrinfo(result);
257 "Failed to create UDP socket");
261 auto bytes_sent = ::sendto(
263 syslog_message.data(),
264 static_cast<int>(syslog_message.size()),
267 static_cast<int>(result->ai_addrlen));
269 ::freeaddrinfo(result);
272 if (bytes_sent < 0) {
275 "Failed to send UDP syslog message");
278 return kcenon::common::ok();
286 const std::string& syslog_message) {
288#ifndef PACS_WITH_DIGITAL_SIGNATURES
289 (void)syslog_message;
292 "TLS syslog transport requires OpenSSL (PACS_WITH_DIGITAL_SIGNATURES)");
295 if (!connect_result.is_ok()) {
296 return connect_result;
302 std::to_string(syslog_message.size()) +
" " + syslog_message;
304 int bytes_written = SSL_write(
305 tls_->ssl, framed.data(),
static_cast<int>(framed.size()));
307 if (bytes_written <= 0) {
308 int ssl_err = SSL_get_error(
tls_->ssl, bytes_written);
312 "TLS write failed (SSL error: " +
313 std::to_string(ssl_err) +
")");
316 return kcenon::common::ok();
321#ifndef PACS_WITH_DIGITAL_SIGNATURES
324 "TLS not available — OpenSSL not linked");
327 return kcenon::common::ok();
335 tls_->ctx = SSL_CTX_new(TLS_client_method());
340 "Failed to create TLS context: " + get_openssl_error());
344 SSL_CTX_set_min_proto_version(
tls_->ctx, TLS1_2_VERSION);
348 if (SSL_CTX_load_verify_locations(
353 "Failed to load CA certificate: " + get_openssl_error());
359 if (SSL_CTX_use_certificate_file(
361 SSL_FILETYPE_PEM) != 1) {
365 "Failed to load client certificate: " + get_openssl_error());
370 if (SSL_CTX_use_PrivateKey_file(
372 SSL_FILETYPE_PEM) != 1) {
376 "Failed to load client key: " + get_openssl_error());
382 SSL_CTX_set_verify(
tls_->ctx, SSL_VERIFY_PEER,
nullptr);
384 SSL_CTX_set_verify(
tls_->ctx, SSL_VERIFY_NONE,
nullptr);
388 struct addrinfo hints{};
389 hints.ai_family = AF_UNSPEC;
390 hints.ai_socktype = SOCK_STREAM;
391 hints.ai_protocol = IPPROTO_TCP;
393 struct addrinfo* result =
nullptr;
394 std::string port_str = std::to_string(
config_.
port);
396 int ret = ::getaddrinfo(
397 config_.
host.c_str(), port_str.c_str(), &hints, &result);
398 if (ret != 0 || result ==
nullptr) {
402 "Failed to resolve TLS syslog host: " +
config_.
host);
406 result->ai_family, result->ai_socktype, result->ai_protocol);
408 ::freeaddrinfo(result);
412 "Failed to create TCP socket");
415 if (::connect(
socket_, result->ai_addr,
416 static_cast<int>(result->ai_addrlen)) != 0) {
417 ::freeaddrinfo(result);
421 "Failed to connect to TLS syslog server: " +
424 ::freeaddrinfo(result);
432 "Failed to create SSL object: " + get_openssl_error());
440 if (SSL_connect(
tls_->ssl) != 1) {
441 std::string err = get_openssl_error();
445 "TLS handshake failed: " + err);
448 return kcenon::common::ok();
458 if (::gethostname(hostname,
sizeof(hostname)) == 0) {
459 hostname[
sizeof(hostname) - 1] =
'\0';
460 return std::string(hostname);
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;
474 gmtime_s(&tm_val, &time_t_val);
476 gmtime_r(&time_t_val, &tm_val);
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()
490 return static_cast<uint8_t
>(
491 static_cast<uint8_t
>(facility) * 8 +
492 static_cast<uint8_t
>(severity));
Syslog transport for ATNA audit messages (RFC 5424/5425/5426)
Sends ATNA audit messages via Syslog protocol.
syslog_transport_config config_
void reset_statistics() noexcept
static constexpr socket_type invalid_socket
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::atomic< size_t > messages_sent_
std::atomic< size_t > send_errors_
size_t send_errors() const noexcept
static std::string get_local_hostname()
size_t messages_sent() const noexcept
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)
static std::string get_timestamp()
kcenon::pacs::VoidResult ensure_tls_connected()
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
constexpr int connection_failed
@ 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.
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)
syslog_facility facility
Syslog facility.