CCF
Loading...
Searching...
No Matches
rpc_sessions.h
Go to the documentation of this file.
1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the Apache 2.0 License.
3#pragma once
4
5#include "ccf/pal/locking.h"
8#include "ds/serialized.h"
9#include "enclave/session.h"
10#include "forwarder_types.h"
11#include "http/http2_session.h"
12#include "http/http_responder.h"
13#include "http/http_session.h"
16#include "rpc_handler.h"
17#include "tls/cert.h"
18#include "tls/client.h"
19#include "tls/context.h"
21#include "tls/server.h"
22#include "udp/msg_types.h"
23
24// NB: This should be HTTP3 including QUIC, but this is
25// ok for now, as we only have an echo service for now
26#include "quic/quic_session.h"
27
28#include <limits>
29#include <map>
30#include <stdexcept>
31#include <unordered_map>
32
33namespace ccf
34{
36
37 static constexpr size_t max_open_sessions_soft_default = 1000;
38 static constexpr size_t max_open_sessions_hard_default = 1010;
39 static const ccf::Endorsement endorsement_default = {ccf::Authority::SERVICE};
40
41 class RPCSessions : public std::enable_shared_from_this<RPCSessions>,
44 {
45 private:
46 struct ListenInterface
47 {
48 size_t open_sessions = 0;
49 size_t peak_sessions = 0;
50 size_t max_open_sessions_soft = 0;
51 size_t max_open_sessions_hard = 0;
52 ccf::Endorsement endorsement{};
53 http::ParserConfiguration http_configuration;
55 ccf::ApplicationProtocol app_protocol;
56 };
57 std::map<ListenInterfaceID, ListenInterface> listening_interfaces;
58
60 ringbuffer::WriterPtr to_host = nullptr;
61 std::shared_ptr<RPCMap> rpc_map;
62 std::unordered_map<ListenInterfaceID, std::shared_ptr<::tls::Cert>> certs;
63 std::shared_ptr<CustomProtocolSubsystem> custom_protocol_subsystem =
64 nullptr;
65 std::shared_ptr<CommitCallbackSubsystem> commit_callbacks_subsystem =
66 nullptr;
67
68 ccf::pal::Mutex lock;
69 std::unordered_map<
71 std::pair<ListenInterfaceID, std::shared_ptr<ccf::Session>>>
72 sessions;
73 size_t sessions_peak = 0;
74
75 // Negative sessions are reserved for those originating from
76 // the enclave via create_client().
77 std::atomic<ccf::tls::ConnID> next_client_session_id = -1;
78
79 template <typename Base>
80 class NoMoreSessionsImpl : public Base
81 {
82 public:
83 template <typename... Ts>
84 NoMoreSessionsImpl(Ts&&... ts) : Base(std::forward<Ts>(ts)...)
85 {}
86
87 void handle_incoming_data_thread(std::vector<uint8_t>&& data) override
88 {
89 Base::tls_io->recv_buffered(data.data(), data.size());
90
91 if (Base::tls_io->get_status() == ccf::SessionStatus::ready)
92 {
93 // Send response describing soft session limit
94 Base::send_odata_error_response(ccf::ErrorDetails{
95 HTTP_STATUS_SERVICE_UNAVAILABLE,
96 ccf::errors::SessionCapExhausted,
97 "Service is currently busy and unable to serve new connections"});
98
99 // Close connection
100 Base::tls_io->close();
101 }
102 }
103 };
104
105 ccf::tls::ConnID get_next_client_id()
106 {
107 auto id = next_client_session_id--;
108 const auto initial = id;
109
110 if (next_client_session_id > 0)
111 {
112 next_client_session_id = -1;
113 }
114
115 while (sessions.find(id) != sessions.end())
116 {
117 id--;
118
119 if (id > 0)
120 {
121 id = -1;
122 }
123
124 if (id == initial)
125 {
126 throw std::runtime_error(
127 "Exhausted all IDs for enclave client sessions");
128 }
129 }
130
131 return id;
132 }
133
134 ListenInterface& get_interface_from_interface_id(
135 const ccf::ListenInterfaceID& id)
136 {
137 auto it = listening_interfaces.find(id);
138 if (it != listening_interfaces.end())
139 {
140 return it->second;
141 }
142
143 throw std::logic_error(
144 fmt::format("No RPC interface for interface ID {}", id));
145 }
146
147 std::shared_ptr<ccf::Session> make_server_session(
148 const std::string& app_protocol,
150 const ListenInterfaceID& listen_interface_id,
151 std::unique_ptr<tls::Context>&& ctx,
152 const http::ParserConfiguration& parser_configuration)
153 {
154 if (app_protocol == "HTTP2")
155 {
156 return std::make_shared<::http::HTTP2ServerSession>(
157 rpc_map,
158 id,
159 listen_interface_id,
160 writer_factory,
161 std::move(ctx),
162 parser_configuration,
163 shared_from_this());
164 }
165 if (app_protocol == "HTTP1")
166 {
167 return std::make_shared<::http::HTTPServerSession>(
168 rpc_map,
169 id,
170 listen_interface_id,
171 writer_factory,
172 std::move(ctx),
173 parser_configuration,
174 shared_from_this(),
175 commit_callbacks_subsystem);
176 }
177 if (custom_protocol_subsystem)
178 {
179 return custom_protocol_subsystem->create_session(
180 app_protocol, id, std::move(ctx));
181 }
182
183 throw std::runtime_error(fmt::format(
184 "unknown protocol '{}' and custom protocol subsystem missing",
185 app_protocol));
186 }
187
188 public:
190 ringbuffer::AbstractWriterFactory& writer_factory,
191 std::shared_ptr<RPCMap> rpc_map_) :
192 writer_factory(writer_factory),
193 rpc_map(std::move(rpc_map_))
194 {
195 to_host = writer_factory.create_writer_to_outside();
196 }
197
199 std::shared_ptr<CustomProtocolSubsystem> cpss)
200 {
201 custom_protocol_subsystem = cpss;
202 }
203
205 std::shared_ptr<CommitCallbackSubsystem> fcss)
206 {
207 commit_callbacks_subsystem = fcss;
208 }
209
211 {
212 std::lock_guard<ccf::pal::Mutex> guard(lock);
213 get_interface_from_interface_id(id).errors.parsing++;
214 }
215
217 const ccf::ListenInterfaceID& id) override
218 {
219 std::lock_guard<ccf::pal::Mutex> guard(lock);
220 get_interface_from_interface_id(id).errors.request_payload_too_large++;
221 }
222
224 const ccf::ListenInterfaceID& id) override
225 {
226 std::lock_guard<ccf::pal::Mutex> guard(lock);
227 get_interface_from_interface_id(id).errors.request_header_too_large++;
228 }
229
231 const ccf::NodeInfoNetwork& node_info)
232 {
233 std::lock_guard<ccf::pal::Mutex> guard(lock);
234
235 for (const auto& [name, interface] : node_info.rpc_interfaces)
236 {
237 auto& li = listening_interfaces[name];
238
239 li.max_open_sessions_soft = interface.max_open_sessions_soft.value_or(
240 max_open_sessions_soft_default);
241
242 li.max_open_sessions_hard = interface.max_open_sessions_hard.value_or(
243 max_open_sessions_hard_default);
244
245 li.endorsement = interface.endorsement.value_or(endorsement_default);
246
247 li.http_configuration =
248 interface.http_configuration.value_or(http::ParserConfiguration{});
249
250 li.app_protocol = interface.app_protocol.value_or("HTTP1");
251
253 "Setting max open sessions on interface \"{}\" ({}) to [{}, "
254 "{}] and endorsement authority to {}",
255 name,
256 interface.bind_address,
257 li.max_open_sessions_soft,
258 li.max_open_sessions_hard,
259 li.endorsement.authority);
260 }
261 }
262
264 {
266 std::lock_guard<ccf::pal::Mutex> guard(lock);
267
268 sm.active = sessions.size();
269 sm.peak = sessions_peak;
270
271 for (const auto& [name, interface] : listening_interfaces)
272 {
273 sm.interfaces[name] = {
274 interface.open_sessions,
275 interface.peak_sessions,
276 interface.max_open_sessions_soft,
277 interface.max_open_sessions_hard,
278 interface.errors};
279 }
280
281 return sm;
282 }
283
285 {
286 // Note: this is a temporary function to conveniently find out which
287 // protocol to use when creating client endpoints (e.g. for join
288 // protocol). This can be removed once the HTTP and HTTP/2 endpoints have
289 // been merged.
290 if (listening_interfaces.empty())
291 {
292 throw std::logic_error("No listening interface for this node");
293 }
294
295 return listening_interfaces.begin()->second.app_protocol;
296 }
297
299 const ccf::crypto::Pem& cert_, const ccf::crypto::Pem& pk)
300 {
301 set_cert(ccf::Authority::NODE, cert_, pk);
302 }
303
305 const ccf::crypto::Pem& cert_, const ccf::crypto::Pem& pk)
306 {
308 }
309
311 ccf::Authority authority,
312 const ccf::crypto::Pem& cert_,
313 const ccf::crypto::Pem& pk)
314 {
315 // Caller authentication is done by each frontend by looking up
316 // the caller's certificate in the relevant store table. The caller
317 // certificate does not have to be signed by a known CA (nullptr) and
318 // verification is not required here.
319 auto cert = std::make_shared<::tls::Cert>(
320 nullptr, cert_, pk, std::nullopt, /*auth_required ==*/false);
321
322 std::lock_guard<ccf::pal::Mutex> guard(lock);
323
324 for (auto& [listen_interface_id, interface] : listening_interfaces)
325 {
326 if (interface.endorsement.authority == authority)
327 {
328 certs.insert_or_assign(listen_interface_id, cert);
329 }
330 }
331 }
332
333 void accept(
335 const ListenInterfaceID& listen_interface_id,
336 bool udp = false)
337 {
338 std::lock_guard<ccf::pal::Mutex> guard(lock);
339
340 if (sessions.find(id) != sessions.end())
341 {
342 throw std::logic_error(
343 fmt::format("Duplicate conn ID received inside enclave: {}", id));
344 }
345
346 auto it = listening_interfaces.find(listen_interface_id);
347 if (it == listening_interfaces.end())
348 {
349 throw std::logic_error(fmt::format(
350 "Can't accept new RPC session {} - comes from unknown listening "
351 "interface {}",
352 id,
353 listen_interface_id));
354 }
355
356 auto& per_listen_interface = it->second;
357
358 if (
359 per_listen_interface.endorsement.authority != Authority::UNSECURED &&
360 certs.find(listen_interface_id) == certs.end())
361 {
363 "Refusing TLS session {} inside the enclave - interface {} "
364 "has no TLS certificate yet",
365 id,
366 listen_interface_id);
367
369 ::tcp::tcp_stop, to_host, id, std::string("Session refused"));
370 }
371 else if (
372 per_listen_interface.open_sessions >=
373 per_listen_interface.max_open_sessions_hard)
374 {
376 "Refusing TLS session {} inside the enclave - already have {} "
377 "sessions from interface {} and limit is {}",
378 id,
379 per_listen_interface.open_sessions,
380 listen_interface_id,
381 per_listen_interface.max_open_sessions_hard);
382
384 ::tcp::tcp_stop, to_host, id, std::string("Session refused"));
385 }
386 else if (
387 per_listen_interface.open_sessions >=
388 per_listen_interface.max_open_sessions_soft)
389 {
391 "Soft refusing session {} (returning 503) inside the enclave - "
392 "already have {} sessions from interface {} and limit is {}",
393 id,
394 per_listen_interface.open_sessions,
395 listen_interface_id,
396 per_listen_interface.max_open_sessions_soft);
397
398 auto ctx = std::make_unique<::tls::Server>(certs[listen_interface_id]);
399 std::shared_ptr<Session> capped_session;
400 if (per_listen_interface.app_protocol == "HTTP2")
401 {
402 capped_session =
403 std::make_shared<NoMoreSessionsImpl<::http::HTTP2ServerSession>>(
404 rpc_map,
405 id,
406 listen_interface_id,
407 writer_factory,
408 std::move(ctx),
409 per_listen_interface.http_configuration,
410 shared_from_this());
411 }
412 else
413 {
414 capped_session =
415 std::make_shared<NoMoreSessionsImpl<::http::HTTPServerSession>>(
416 rpc_map,
417 id,
418 listen_interface_id,
419 writer_factory,
420 std::move(ctx),
421 per_listen_interface.http_configuration,
422 shared_from_this(),
423 commit_callbacks_subsystem);
424 }
425 sessions.insert(std::make_pair(
426 id, std::make_pair(listen_interface_id, std::move(capped_session))));
427 per_listen_interface.open_sessions++;
428 per_listen_interface.peak_sessions = std::max(
429 per_listen_interface.peak_sessions,
430 per_listen_interface.open_sessions);
431 }
432 else
433 {
435 "Accepting a session {} inside the enclave from interface \"{}\"",
436 id,
437 listen_interface_id);
438
439 if (udp)
440 {
441 LOG_DEBUG_FMT("New UDP endpoint at {}", id);
442 if (per_listen_interface.app_protocol == "QUIC")
443 {
444 auto session = std::make_shared<QUICSessionImpl>(
445 rpc_map, id, listen_interface_id, writer_factory);
446 sessions.insert(std::make_pair(
447 id, std::make_pair(listen_interface_id, std::move(session))));
448 }
449 else if (custom_protocol_subsystem)
450 {
451 // We know it's a custom protocol, but the session creation function
452 // hasn't been registered yet, so we keep a nullptr until the first
453 // udp::udp_inbound message.
454 sessions.insert(
455 std::make_pair(id, std::make_pair(listen_interface_id, nullptr)));
456 }
457 else
458 {
459 throw std::runtime_error(
460 "unknown UDP protocol and custom protocol subsystem missing");
461 }
462 per_listen_interface.open_sessions++;
463 per_listen_interface.peak_sessions = std::max(
464 per_listen_interface.peak_sessions,
465 per_listen_interface.open_sessions);
466 }
467 else
468 {
469 std::unique_ptr<tls::Context> ctx;
470 if (
471 per_listen_interface.endorsement.authority == Authority::UNSECURED)
472 {
473 ctx = std::make_unique<nontls::PlaintextServer>();
474 }
475 else
476 {
477 ctx = std::make_unique<::tls::Server>(
478 certs[listen_interface_id],
479 per_listen_interface.app_protocol == "HTTP2");
480 }
481
482 auto session = make_server_session(
483 per_listen_interface.app_protocol,
484 id,
485 listen_interface_id,
486 std::move(ctx),
487 per_listen_interface.http_configuration);
488
489 sessions.insert(std::make_pair(
490 id, std::make_pair(listen_interface_id, std::move(session))));
491 per_listen_interface.open_sessions++;
492 per_listen_interface.peak_sessions = std::max(
493 per_listen_interface.peak_sessions,
494 per_listen_interface.open_sessions);
495 }
496 }
497
498 sessions_peak = std::max(sessions_peak, sessions.size());
499 }
500
501 std::shared_ptr<Session> find_session(ccf::tls::ConnID id)
502 {
503 std::lock_guard<ccf::pal::Mutex> guard(lock);
504
505 auto search = sessions.find(id);
506 if (search == sessions.end())
507 {
508 return nullptr;
509 }
510
511 return search->second.second;
512 }
513
516 bool terminate_after_send,
517 std::vector<uint8_t>&& data) override
518 {
519 auto session = find_session(id);
520 if (session == nullptr)
521 {
522 LOG_DEBUG_FMT("Refusing to reply to unknown session {}", id);
523 return false;
524 }
525
526 LOG_DEBUG_FMT("Replying to session {}", id);
527
528 session->send_data(std::move(data));
529
530 if (terminate_after_send)
531 {
532 session->close_session();
533 }
534
535 return true;
536 }
537
539 {
540 std::lock_guard<ccf::pal::Mutex> guard(lock);
541 LOG_DEBUG_FMT("Closing a session inside the enclave: {}", id);
542 const auto search = sessions.find(id);
543 if (search != sessions.end())
544 {
545 auto it = listening_interfaces.find(search->second.first);
546 if (it != listening_interfaces.end())
547 {
548 it->second.open_sessions--;
549 }
550 sessions.erase(search);
551 }
552 else
553 {
554 // Enclave doesn't know this ID, but host is still talking about it.
555 // Continue with the normal closure flow
556 RINGBUFFER_WRITE_MESSAGE(::tcp::tcp_closed, to_host, id);
557 }
558 }
559
560 std::shared_ptr<ClientSession> create_client(
561 const std::shared_ptr<::tls::Cert>& cert,
562 const std::string& app_protocol = "HTTP1")
563 {
564 std::lock_guard<ccf::pal::Mutex> guard(lock);
565 auto ctx = std::make_unique<::tls::Client>(cert);
566 auto id = get_next_client_id();
567
568 LOG_DEBUG_FMT("Creating a new client session inside the enclave: {}", id);
569
570 // There are no limits on outbound client sessions (we do not check any
571 // session caps here). We expect this type of session to be rare and
572 // want it to succeed even when we are busy.
573 if (app_protocol == "HTTP2")
574 {
575 auto session = std::make_shared<::http::HTTP2ClientSession>(
576 id, writer_factory, std::move(ctx));
577 sessions.insert(std::make_pair(id, std::make_pair("", session)));
578 sessions_peak = std::max(sessions_peak, sessions.size());
579 return session;
580 }
581 if (app_protocol == "HTTP1")
582 {
583 auto session = std::make_shared<::http::HTTPClientSession>(
584 id, writer_factory, std::move(ctx));
585 sessions.insert(std::make_pair(id, std::make_pair("", session)));
586 sessions_peak = std::max(sessions_peak, sessions.size());
587 return session;
588 }
589
590 throw std::runtime_error("unsupported client application protocol");
591 }
592
593 std::shared_ptr<ClientSession> create_unencrypted_client()
594 {
595 std::lock_guard<ccf::pal::Mutex> guard(lock);
596 auto id = get_next_client_id();
597 auto session = std::make_shared<::http::UnencryptedHTTPClientSession>(
598 id, writer_factory);
599 sessions.insert(std::make_pair(id, std::make_pair("", session)));
600 sessions_peak = std::max(sessions_peak, sessions.size());
601 return session;
602 }
603
606 {
608 disp, ::tcp::tcp_start, [this](const uint8_t* data, size_t size) {
609 auto [new_tls_id, listen_interface_name] =
610 ringbuffer::read_message<::tcp::tcp_start>(data, size);
611 accept(new_tls_id, listen_interface_name);
612 });
613
615 disp, ::tcp::tcp_inbound, [this](const uint8_t* data, size_t size) {
616 auto id = serialized::peek<ccf::tls::ConnID>(data, size);
617
618 auto session = find_session(id);
619 if (session == nullptr)
620 {
622 "Ignoring tls_inbound for unknown or refused session: {}", id);
623 return;
624 }
625
626 session->handle_incoming_data({data, size});
627 });
628
630 disp, ::tcp::tcp_close, [this](const uint8_t* data, size_t size) {
631 auto [id] = ringbuffer::read_message<::tcp::tcp_close>(data, size);
632 remove_session(id);
633 });
634
636 disp, udp::udp_start, [this](const uint8_t* data, size_t size) {
637 auto [new_id, listen_interface_name] =
638 ringbuffer::read_message<udp::udp_start>(data, size);
639 accept(new_id, listen_interface_name, true);
640 });
641
643 disp, udp::udp_inbound, [this](const uint8_t* data, size_t size) {
644 auto id = serialized::peek<int64_t>(data, size);
645
646 std::shared_ptr<Session> session;
647 {
648 std::lock_guard<ccf::pal::Mutex> guard(lock);
649
650 auto search = sessions.find(id);
651 if (search == sessions.end())
652 {
654 "Ignoring udp::udp_inbound for unknown or refused session: {}",
655 id);
656 return;
657 }
658
659 if (!search->second.second && custom_protocol_subsystem)
660 {
661 LOG_DEBUG_FMT("Creating custom UDP session {}", id);
662
663 try
664 {
665 const auto& conn_id = search->first;
666 const auto& interface_id = search->second.first;
667
668 auto iit = listening_interfaces.find(interface_id);
669 if (iit == listening_interfaces.end())
670 {
672 "Failure to create custom protocol session because of "
673 "unknown interface '{}', ignoring udp::udp_inbound for "
674 "session: "
675 "{}",
676 interface_id,
677 id);
678 }
679
680 const auto& interface = iit->second;
681
682 search->second.second =
683 custom_protocol_subsystem->create_session(
684 interface.app_protocol, conn_id, nullptr);
685
686 if (!search->second.second)
687 {
689 "Failure to create custom protocol session, ignoring "
690 "udp::udp_inbound for session: {}",
691 id);
692 return;
693 }
694 }
695 catch (const std::exception& ex)
696 {
698 "Failure to create custom protocol session: {}", ex.what());
699 return;
700 }
701 }
702
703 session = search->second.second;
704 }
705
706 session->handle_incoming_data({data, size});
707 });
708 }
709 };
710}
Definition forwarder_types.h:14
Definition rpc_sessions.h:44
RPCSessions(ringbuffer::AbstractWriterFactory &writer_factory, std::shared_ptr< RPCMap > rpc_map_)
Definition rpc_sessions.h:189
ccf::SessionMetrics get_session_metrics()
Definition rpc_sessions.h:263
void report_request_payload_too_large_error(const ccf::ListenInterfaceID &id) override
Definition rpc_sessions.h:216
void report_request_header_too_large_error(const ccf::ListenInterfaceID &id) override
Definition rpc_sessions.h:223
std::shared_ptr< ClientSession > create_client(const std::shared_ptr<::tls::Cert > &cert, const std::string &app_protocol="HTTP1")
Definition rpc_sessions.h:560
void set_node_cert(const ccf::crypto::Pem &cert_, const ccf::crypto::Pem &pk)
Definition rpc_sessions.h:298
void register_message_handlers(messaging::Dispatcher< ringbuffer::Message > &disp)
Definition rpc_sessions.h:604
void set_network_cert(const ccf::crypto::Pem &cert_, const ccf::crypto::Pem &pk)
Definition rpc_sessions.h:304
void remove_session(ccf::tls::ConnID id)
Definition rpc_sessions.h:538
void set_commit_callbacks_subsystem(std::shared_ptr< CommitCallbackSubsystem > fcss)
Definition rpc_sessions.h:204
void update_listening_interface_options(const ccf::NodeInfoNetwork &node_info)
Definition rpc_sessions.h:230
void set_custom_protocol_subsystem(std::shared_ptr< CustomProtocolSubsystem > cpss)
Definition rpc_sessions.h:198
bool reply_async(ccf::tls::ConnID id, bool terminate_after_send, std::vector< uint8_t > &&data) override
Definition rpc_sessions.h:514
std::shared_ptr< ClientSession > create_unencrypted_client()
Definition rpc_sessions.h:593
std::shared_ptr< Session > find_session(ccf::tls::ConnID id)
Definition rpc_sessions.h:501
void report_parsing_error(const ccf::ListenInterfaceID &id) override
Definition rpc_sessions.h:210
void set_cert(ccf::Authority authority, const ccf::crypto::Pem &cert_, const ccf::crypto::Pem &pk)
Definition rpc_sessions.h:310
ccf::ApplicationProtocol get_app_protocol_main_interface() const
Definition rpc_sessions.h:284
void accept(ccf::tls::ConnID id, const ListenInterfaceID &listen_interface_id, bool udp=false)
Definition rpc_sessions.h:333
Definition pem.h:18
Definition error_reporter.h:10
Definition messaging.h:38
Definition quic_session.h:414
Definition ring_buffer_types.h:157
virtual WriterPtr create_writer_to_outside()=0
#define LOG_INFO_FMT
Definition internal_logger.h:15
#define LOG_DEBUG_FMT
Definition internal_logger.h:14
#define DISPATCHER_SET_MESSAGE_HANDLER(DISP, MSG,...)
Definition messaging.h:292
std::mutex Mutex
Definition locking.h:12
int64_t ConnID
Definition custom_protocol_subsystem_interface.h:20
Definition app_interface.h:13
std::string ApplicationProtocol
Definition node_info_network.h:29
std::string ListenInterfaceID
Definition rpc_context.h:21
Authority
Definition node_info_network.h:16
@ ready
Definition tls_session.h:19
std::shared_ptr< AbstractWriter > WriterPtr
Definition ring_buffer_types.h:154
STL namespace.
Definition msg_types.h:10
#define RINGBUFFER_WRITE_MESSAGE(MSG,...)
Definition ring_buffer_types.h:259
Definition node_info_network.h:32
Definition odata_error.h:58
RpcInterfaces rpc_interfaces
RPC interfaces.
Definition node_info_network.h:151
Definition node_info_network.h:179
Definition session_metrics.h:15
Definition session_metrics.h:13
size_t peak
Definition session_metrics.h:31
std::map< std::string, PerInterface > interfaces
Definition session_metrics.h:32
size_t active
Definition session_metrics.h:30
Definition http_configuration.h:24