Network System 0.1.1
High-performance modular networking library for scalable client-server applications
Loading...
Searching...
No Matches
secure_messaging_server.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2024, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
6
8
12
13#include <algorithm>
14#include <chrono>
15#include <string_view>
16
18{
19
20 using tcp = asio::ip::tcp;
21
23 const std::string& cert_file,
24 const std::string& key_file)
25 : server_id_(server_id)
26 {
27 // Initialize SSL context with TLS 1.3 support (TICKET-009: TLS 1.3 only by default)
28 ssl_context_ = std::make_unique<asio::ssl::context>(
29 asio::ssl::context::tls_server);
30
31 // Set SSL context options - disable all legacy protocols
32 ssl_context_->set_options(
33 asio::ssl::context::default_workarounds |
34 asio::ssl::context::no_sslv2 |
35 asio::ssl::context::no_sslv3 |
36 asio::ssl::context::no_tlsv1 |
37 asio::ssl::context::no_tlsv1_1 |
38 asio::ssl::context::single_dh_use);
39
40 // Enforce TLS 1.3 minimum version using native OpenSSL handle.
41 // This prevents protocol downgrade attacks (CVSS 7.5).
42 //
43 // NOTE: This TLS 1.3-only policy is intentionally stricter than the
44 // BCP 195 basic profile used in pacs_system (which allows TLS 1.2).
45 // Rationale:
46 // - network_system handles internal service-to-service messaging
47 // where all endpoints are under our control and support TLS 1.3.
48 // - pacs_system must interoperate with legacy DICOM equipment that
49 // may only support TLS 1.2, hence the broader BCP 195 profile.
50 // See: pacs_system/src/security/tls_policy.cpp bcp195_basic_profile()
51 SSL_CTX* native_ctx = ssl_context_->native_handle();
52 if (native_ctx)
53 {
54 // Set minimum protocol version to TLS 1.3 (security hardening)
55 SSL_CTX_set_min_proto_version(native_ctx, TLS1_3_VERSION);
56 SSL_CTX_set_max_proto_version(native_ctx, TLS1_3_VERSION);
57
58 // Set strong cipher suites for TLS 1.3
59 SSL_CTX_set_ciphersuites(native_ctx,
60 "TLS_AES_256_GCM_SHA384:"
61 "TLS_CHACHA20_POLY1305_SHA256:"
62 "TLS_AES_128_GCM_SHA256");
63 }
64
65 // Load certificate and private key
66 try
67 {
68 ssl_context_->use_certificate_chain_file(cert_file);
69 ssl_context_->use_private_key_file(key_file, asio::ssl::context::pem);
70
71 NETWORK_LOG_INFO("[secure_messaging_server] SSL context initialized with TLS 1.3 only, cert: " +
72 cert_file + ", key: " + key_file);
73 }
74 catch (const std::exception& e)
75 {
76 NETWORK_LOG_ERROR("[secure_messaging_server] Failed to load SSL certificates: " +
77 std::string(e.what()));
78 throw;
79 }
80 }
81
83 {
85 {
86 (void)stop_server();
87 }
88 }
89
90 // =====================================================================
91 // Lifecycle Management
92 // =====================================================================
93
95 {
96 if (lifecycle_.is_running())
97 {
98 return error_void(
100 "Server is already running",
101 "secure_messaging_server::start_server");
102 }
103
104 if (!lifecycle_.try_start())
105 {
106 return error_void(
108 "Server is already starting",
109 "secure_messaging_server::start_server");
110 }
111
112 // Call implementation
113 auto result = do_start_impl(port);
114 if (result.is_err())
115 {
116 lifecycle_.mark_stopped();
117 }
118
119 return result;
120 }
121
123 {
124 if (!lifecycle_.is_running())
125 {
126 return error_void(
128 "Server is not running",
129 "secure_messaging_server::stop_server");
130 }
131
132 if (!lifecycle_.prepare_stop())
133 {
134 return ok(); // Already stopping
135 }
136
137 // Call implementation
138 auto result = do_stop_impl();
139
140 // Signal stop completion
141 lifecycle_.mark_stopped();
142
143 return result;
144 }
145
147 {
148 lifecycle_.wait_for_stop();
149 }
150
151 auto secure_messaging_server::is_running() const noexcept -> bool
152 {
153 return lifecycle_.is_running();
154 }
155
156 auto secure_messaging_server::server_id() const -> const std::string&
157 {
158 return server_id_;
159 }
160
161 // =====================================================================
162 // Callback Setters
163 // =====================================================================
164
166 {
167 callbacks_.set<to_index(callback_index::connection)>(std::move(callback));
168 }
169
171 {
172 callbacks_.set<to_index(callback_index::disconnection)>(std::move(callback));
173 }
174
176 {
177 callbacks_.set<to_index(callback_index::receive)>(std::move(callback));
178 }
179
181 {
182 callbacks_.set<to_index(callback_index::error)>(std::move(callback));
183 }
184
185 // =====================================================================
186 // Internal Callback Helpers
187 // =====================================================================
188
190 {
191 return callbacks_.get<to_index(callback_index::connection)>();
192 }
193
195 {
196 return callbacks_.get<to_index(callback_index::disconnection)>();
197 }
198
200 {
201 return callbacks_.get<to_index(callback_index::receive)>();
202 }
203
205 {
206 return callbacks_.get<to_index(callback_index::error)>();
207 }
208
210 std::shared_ptr<session::secure_session> session) -> void
211 {
212 callbacks_.invoke<to_index(callback_index::connection)>(session);
213 }
214
215 // =====================================================================
216 // Internal Implementation Methods
217 // =====================================================================
218
220 {
221 try
222 {
223 // Create io_context and acceptor
224 io_context_ = std::make_unique<asio::io_context>();
225 // Create work guard to keep io_context running
226 work_guard_ = std::make_unique<asio::executor_work_guard<asio::io_context::executor_type>>(
227 asio::make_work_guard(*io_context_)
228 );
229 acceptor_ = std::make_unique<tcp::acceptor>(
230 *io_context_, tcp::endpoint(tcp::v4(), port));
231
232 // Create cleanup timer
233 cleanup_timer_ = std::make_unique<asio::steady_timer>(*io_context_);
234
235 // Begin accepting connections
236 do_accept();
237
238 // Start periodic cleanup timer
239 start_cleanup_timer();
240
241 // Get thread pool from network context
243 if (!thread_pool_) {
244 // Fallback: create a temporary thread pool if network_context is not initialized
245 NETWORK_LOG_WARN("[secure_messaging_server] network_context not initialized, creating temporary thread pool");
246 thread_pool_ = std::make_shared<integration::basic_thread_pool>(std::thread::hardware_concurrency());
247 }
248
249 // Start io_context on thread pool
250 io_context_future_ = thread_pool_->submit(
251 [this]()
252 {
253 try
254 {
255 NETWORK_LOG_DEBUG("[secure_messaging_server] io_context started");
256 io_context_->run();
257 NETWORK_LOG_DEBUG("[secure_messaging_server] io_context stopped");
258 }
259 catch (const std::exception& e)
260 {
261 NETWORK_LOG_ERROR("[secure_messaging_server] Exception in io_context: " +
262 std::string(e.what()));
263 }
264 });
265
266 NETWORK_LOG_INFO("[secure_messaging_server] Started listening on port " +
267 std::to_string(port) + " (TLS/SSL enabled)");
268 return ok();
269 }
270 catch (const std::system_error& e)
271 {
272 // Check for specific error codes
273 if (e.code() == asio::error::address_in_use ||
274 e.code() == std::errc::address_in_use)
275 {
276 return error_void(
278 "Failed to bind to port: address already in use",
279 "secure_messaging_server::do_start_impl",
280 "Port: " + std::to_string(port)
281 );
282 }
283 else if (e.code() == asio::error::access_denied ||
284 e.code() == std::errc::permission_denied)
285 {
286 return error_void(
288 "Failed to bind to port: permission denied",
289 "secure_messaging_server::do_start_impl",
290 "Port: " + std::to_string(port)
291 );
292 }
293
294 return error_void(
296 "Failed to start secure server: " + std::string(e.what()),
297 "secure_messaging_server::do_start_impl",
298 "Port: " + std::to_string(port)
299 );
300 }
301 catch (const std::exception& e)
302 {
303 return error_void(
305 "Failed to start secure server: " + std::string(e.what()),
306 "secure_messaging_server::do_start_impl",
307 "Port: " + std::to_string(port)
308 );
309 }
310 }
311
313 {
314 try
315 {
316 // Step 1: Cancel and close the acceptor to stop accepting new connections
317 if (acceptor_)
318 {
319 asio::error_code ec;
320 // Cancel pending async_accept operations to prevent memory leaks
321 acceptor_->cancel(ec);
322 if (acceptor_->is_open())
323 {
324 acceptor_->close(ec);
325 }
326 }
327
328 // Cancel cleanup timer
329 if (cleanup_timer_)
330 {
331 cleanup_timer_->cancel();
332 }
333
334 // Step 2: Stop all active sessions (close sockets, stop reading)
335 // NOTE: Do NOT clear sessions here - they may still be referenced by
336 // pending async callbacks. We must wait for io_context to finish first.
337 {
338 std::lock_guard<std::mutex> lock(sessions_mutex_);
339 for (auto& sess : sessions_)
340 {
341 if (sess)
342 {
343 sess->stop_session();
344 }
345 }
346 }
347
348 // Step 3: Release work guard to allow io_context to finish
349 if (work_guard_)
350 {
351 work_guard_.reset();
352 }
353
354 // Step 4: Stop io_context (this cancels all remaining pending operations)
355 if (io_context_)
356 {
357 io_context_->stop();
358 }
359
360 // Step 5: Wait for io_context task to complete
361 // This ensures all pending async callbacks have finished executing
362 if (io_context_future_.valid())
363 {
364 try {
365 io_context_future_.wait();
366 } catch (const std::exception& e) {
367 NETWORK_LOG_ERROR("[secure_messaging_server] Exception while waiting for io_context: " +
368 std::string(e.what()));
369 }
370 }
371
372 // Step 6: NOW it's safe to clear sessions - all async operations are done
373 {
374 std::lock_guard<std::mutex> lock(sessions_mutex_);
375 sessions_.clear();
376 }
377
378 // Step 7: Release resources explicitly to ensure cleanup
379 acceptor_.reset();
380 cleanup_timer_.reset();
381 thread_pool_.reset();
382 io_context_.reset();
383
384 NETWORK_LOG_INFO("[secure_messaging_server] Stopped.");
385 return ok();
386 }
387 catch (const std::exception& e)
388 {
389 return error_void(
391 "Failed to stop secure server: " + std::string(e.what()),
392 "secure_messaging_server::do_stop_impl",
393 "Server ID: " + server_id_
394 );
395 }
396 }
397
398 // =====================================================================
399 // Internal Connection Handlers
400 // =====================================================================
401
403 {
404 auto self = shared_from_this();
405 acceptor_->async_accept(
406 [this, self](std::error_code ec, tcp::socket sock)
407 { on_accept(ec, std::move(sock)); });
408 }
409
410 auto secure_messaging_server::on_accept(std::error_code ec, tcp::socket socket)
411 -> void
412 {
413 if (!is_running())
414 {
415 return;
416 }
417 if (ec)
418 {
419 NETWORK_LOG_ERROR("[secure_messaging_server] Accept error: " + ec.message());
420#if KCENON_WITH_COMMON_SYSTEM
421 // Record connection error
422 connection_errors_.fetch_add(1, std::memory_order_relaxed);
423 if (monitor_) {
424 monitor_->record_metric("connection_errors", static_cast<double>(connection_errors_.load()));
425 }
426#endif
427 return;
428 }
429
430 // Cleanup dead sessions before adding new one
431 cleanup_dead_sessions();
432
433 // Create a new secure_session
434 auto new_session = std::make_shared<kcenon::network::session::secure_session>(
435 std::move(socket), *ssl_context_, server_id_);
436
437 // Set up session callbacks
438 auto self = shared_from_this();
439 auto receive_cb = get_receive_callback();
440 auto disconnection_cb = get_disconnection_callback();
441 auto error_cb = get_error_callback();
442
443 if (receive_cb)
444 {
445 new_session->set_receive_callback(
446 [this, self, new_session, receive_cb](const std::vector<uint8_t>& data)
447 {
448 receive_cb(new_session, data);
449 });
450 }
451
452 if (disconnection_cb)
453 {
454 new_session->set_disconnection_callback(
455 [this, self, new_session, disconnection_cb](const std::string& session_id)
456 {
457 disconnection_cb(session_id);
458 });
459 }
460
461 if (error_cb)
462 {
463 new_session->set_error_callback(
464 [this, self, new_session, error_cb](std::error_code err)
465 {
466 error_cb(new_session, err);
467 });
468 }
469
470 // Track it in our sessions_ vector (protected by mutex)
471 {
472 std::lock_guard<std::mutex> lock(sessions_mutex_);
473 sessions_.push_back(new_session);
474 }
475
476 // Start the session (this will trigger SSL handshake)
477 new_session->start_session();
478
479 // Invoke connection callback
480 invoke_connection_callback(new_session);
481
482#if KCENON_WITH_COMMON_SYSTEM
483 // Record active connections metric
484 {
485 std::lock_guard<std::mutex> lock(sessions_mutex_);
486 if (monitor_) {
487 monitor_->record_metric("active_connections", static_cast<double>(sessions_.size()));
488 }
489 }
490#endif
491
492 // Accept next connection
493 do_accept();
494 }
495
497 {
498 std::lock_guard<std::mutex> lock(sessions_mutex_);
499
500 // Remove sessions that have been stopped
501 sessions_.erase(
502 std::remove_if(sessions_.begin(), sessions_.end(),
503 [](const auto& session) {
504 return session && session->is_stopped();
505 }),
506 sessions_.end()
507 );
508
509 NETWORK_LOG_DEBUG("[secure_messaging_server] Cleaned up dead sessions. Active: " +
510 std::to_string(sessions_.size()));
511
512#if KCENON_WITH_COMMON_SYSTEM
513 // Update active connections metric after cleanup
514 if (monitor_) {
515 monitor_->record_metric("active_connections", static_cast<double>(sessions_.size()));
516 }
517#endif
518 }
519
521 {
522 if (!cleanup_timer_ || !is_running())
523 {
524 return;
525 }
526
527 // Schedule cleanup every 30 seconds
528 cleanup_timer_->expires_after(std::chrono::seconds(30));
529
530 auto self = shared_from_this();
531 cleanup_timer_->async_wait(
532 [this, self](const std::error_code& ec)
533 {
534 if (!ec && is_running())
535 {
536 cleanup_dead_sessions();
537 start_cleanup_timer(); // Reschedule
538 }
539 }
540 );
541 }
542
543#if KCENON_WITH_COMMON_SYSTEM
544 auto secure_messaging_server::set_monitor(kcenon::common::interfaces::IMonitor* monitor) -> void
545 {
546 monitor_ = monitor;
547 }
548
549 auto secure_messaging_server::get_monitor() const -> kcenon::common::interfaces::IMonitor*
550 {
551 return monitor_;
552 }
553#endif
554
555} // namespace kcenon::network::core
std::shared_ptr< kcenon::network::integration::thread_pool_interface > get_thread_pool()
Get current thread pool.
static network_context & instance()
Get the singleton instance.
std::function< void(const std::string &)> disconnection_callback_t
Callback type for disconnection.
auto on_accept(std::error_code ec, asio::ip::tcp::socket socket) -> void
Handler called when an asynchronous accept finishes.
auto set_connection_callback(connection_callback_t callback) -> void
Sets the callback for new client connections.
auto set_error_callback(error_callback_t callback) -> void
Sets the callback for session errors.
auto server_id() const -> const std::string &
Returns the server identifier.
auto cleanup_dead_sessions() -> void
Removes stopped sessions from the sessions vector.
auto do_stop_impl() -> VoidResult
Secure TCP-specific implementation of server stop.
auto get_connection_callback() const -> connection_callback_t
Gets a copy of the connection callback.
auto get_disconnection_callback() const -> disconnection_callback_t
Gets a copy of the disconnection callback.
secure_messaging_server(std::string_view server_id, const std::string &cert_file, const std::string &key_file)
Constructs a secure_messaging_server with SSL/TLS support.
auto start_cleanup_timer() -> void
Starts a periodic timer that triggers session cleanup.
auto invoke_connection_callback(std::shared_ptr< session::secure_session > session) -> void
Invokes the connection callback with the given session.
~secure_messaging_server() noexcept
Destructor. If the server is still running, stop_server() is invoked.
auto start_server(unsigned short port) -> VoidResult
Starts the server on the specified port.
auto get_error_callback() const -> error_callback_t
Gets a copy of the error callback.
auto stop_server() -> VoidResult
Stops the server and closes all connections.
std::unique_ptr< asio::ssl::context > ssl_context_
std::function< void(std::shared_ptr< session::secure_session >, std::error_code)> error_callback_t
Callback type for errors.
auto get_receive_callback() const -> receive_callback_t
Gets a copy of the receive callback.
auto do_accept() -> void
Initiates an asynchronous accept operation (async_accept).
std::function< void(std::shared_ptr< session::secure_session >)> connection_callback_t
Callback type for new connection.
std::function< void(std::shared_ptr< session::secure_session >, const std::vector< uint8_t > &)> receive_callback_t
Callback type for received data.
auto do_start_impl(unsigned short port) -> VoidResult
Secure TCP-specific implementation of server start.
auto is_running() const noexcept -> bool
Check if the server is currently running.
auto set_disconnection_callback(disconnection_callback_t callback) -> void
Sets the callback for client disconnections.
auto wait_for_stop() -> void
Blocks until stop_server() is called.
auto set_receive_callback(receive_callback_t callback) -> void
Sets the callback for received messages.
virtual std::future< void > submit(std::function< void()> task)=0
Submit a task to the thread pool.
auto get() const -> std::tuple_element_t< Index, std::tuple< CallbackTypes... > >
Gets a callback at the specified index.
auto is_running() const -> bool
Checks if the component is currently running.
struct ssl_ctx_st SSL_CTX
Definition crypto.h:20
Feature flags for network_system.
Logger system integration interface for network_system.
#define NETWORK_LOG_WARN(msg)
#define NETWORK_LOG_INFO(msg)
#define NETWORK_LOG_ERROR(msg)
#define NETWORK_LOG_DEBUG(msg)
constexpr auto to_index(E e) noexcept -> std::size_t
Helper to convert enum to std::size_t for callback_manager access.
VoidResult error_void(int code, const std::string &message, const std::string &source="network_system", const std::string &details="")
VoidResult ok()
Global context for shared network system resources.
Secure TCP server class with TLS/SSL support.
TLS-secured session wrapper for encrypted communication.