• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

randombit / botan / 6659076905

26 Oct 2023 07:52PM UTC coverage: 91.692% (-0.002%) from 91.694%
6659076905

push

github

web-flow
Merge pull request #3763 from Rohde-Schwarz/feature/tls_http_server_on_beast

Rewrite ./botan tls_http_server using Boost Beast

80077 of 87333 relevant lines covered (91.69%)

8603314.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.55
/src/cli/tls_http_server.cpp
1
/*
2
* (C) 2014,2015,2017,2019,2023 Jack Lloyd
3
* (C) 2016 Matthias Gierlings
4
* (C) 2023 Fabian Albert, René Meusel - Rohde & Schwarz Cybersecurity
5
*
6
* This implementation is roughly based on this BSL-licensed example
7
* by Klemens D. Morgenstern:
8
*   www.boost.org/doc/libs/1_83_0/libs/beast/example/http/server/awaitable/http_server_awaitable.cpp
9
*
10
* Botan is released under the Simplified BSD License (see license.txt)
11
*/
12

13
#include "cli.h"
14

15
#if defined(BOTAN_HAS_BOOST_ASIO)
16
   #include <botan/boost_compat.h>
17
#endif
18

19
#if defined(BOTAN_FOUND_COMPATIBLE_BOOST_VERSION)
20
   #include <boost/asio/awaitable.hpp>
21
#endif
22

23
// If your boost version is too old, this might not be defined despite
24
// your toolchain supporting C++20 co_await.
25
#if defined(BOOST_ASIO_HAS_CO_AWAIT) && defined(BOTAN_HAS_TLS) && defined(BOTAN_TARGET_OS_HAS_FILESYSTEM)
26

27
   #include <ctime>
28
   #include <iomanip>
29
   #include <memory>
30
   #include <string>
31
   #include <thread>
32
   #include <vector>
33

34
   #include <boost/asio/co_spawn.hpp>
35
   #include <boost/asio/ip/tcp.hpp>
36
   #include <boost/asio/use_awaitable.hpp>
37
   #include <boost/beast/http.hpp>
38

39
   #include <botan/asio_stream.h>
40
   #include <botan/tls_messages.h>
41
   #include <botan/tls_session_manager_memory.h>
42
   #include <botan/version.h>
43
   #include <botan/internal/fmt.h>
44
   #include <botan/internal/os_utils.h>
45

46
   #if defined(BOTAN_HAS_TLS_SQLITE3_SESSION_MANAGER)
47
      #include <botan/tls_session_manager_sqlite.h>
48
   #endif
49

50
   #include "tls_helpers.h"
51

52
namespace Botan_CLI {
53

54
namespace {
55

56
namespace beast = boost::beast;    // from <boost/beast.hpp>
57
namespace http = beast::http;      // from <boost/beast/http.hpp>
58
namespace net = boost::asio;       // from <boost/asio.hpp>
59
using tcp = boost::asio::ip::tcp;  // from <boost/asio/ip/tcp.hpp>
60

61
using tcp_stream = typename beast::tcp_stream::rebind_executor<
62
   net::use_awaitable_t<>::executor_with_default<net::any_io_executor>>::other;
63

64
class Logger final {
65
   private:
66
      auto timestamp() const {
4✔
67
         const auto t = std::time(nullptr);
4✔
68
         struct tm tm;
4✔
69

70
   #if defined(BOTAN_BUILD_COMPILER_IS_MSVC) || defined(BOTAN_TARGET_OS_IS_MINGW) || \
71
      defined(BOTAN_TARGET_OS_IS_CYGWIN) || defined(BOTAN_TARGET_OS_IS_WINDOWS)
72
         localtime_s(&tm, &t);
73
   #else
74
         localtime_r(&t, &tm);
4✔
75
   #endif
76

77
         return std::put_time(&tm, "%c");
4✔
78
      }
79

80
   public:
81
      Logger(std::ostream& out, std::ostream& err) : m_out(out), m_err(err) {}
1✔
82

83
      void log(std::string_view out) {
4✔
84
         std::scoped_lock lk(m_mutex);
4✔
85
         m_out << Botan::fmt("[{}] {}", timestamp(), out) << "\n";
12✔
86
      }
4✔
87

88
      void error(std::string_view err) {
×
89
         std::scoped_lock lk(m_mutex);
×
90
         m_err << Botan::fmt("[{}] {}", timestamp(), err) << "\n";
×
91
      }
×
92

93
   private:
94
      std::mutex m_mutex;
95
      std::ostream& m_out;
96
      std::ostream& m_err;
97
};
98

99
class TlsHttpCallbacks final : public Botan::TLS::StreamCallbacks {
4✔
100
   public:
101
      void tls_session_activated() override {
4✔
102
         std::ostringstream strm;
4✔
103

104
         strm << "TLS negotiation with " << Botan::version_string() << " test server\n\n";
8✔
105

106
         m_connection_summary = strm.str();
4✔
107
      }
4✔
108

109
      void tls_session_established(const Botan::TLS::Session_Summary& session) override {
4✔
110
         std::ostringstream strm;
4✔
111

112
         strm << "Version: " << session.version().to_string() << "\n";
8✔
113
         strm << "Ciphersuite: " << session.ciphersuite().to_string() << "\n";
8✔
114
         if(const auto& session_id = session.session_id(); !session_id.empty()) {
4✔
115
            strm << "SessionID: " << Botan::hex_encode(session_id.get()) << "\n";
8✔
116
         }
117
         if(!session.server_info().hostname().empty()) {
8✔
118
            strm << "SNI: " << session.server_info().hostname() << "\n";
12✔
119
         }
120

121
         m_session_summary = strm.str();
4✔
122
      }
4✔
123

124
      void tls_inspect_handshake_msg(const Botan::TLS::Handshake_Message& message) override {
34✔
125
         if(message.type() == Botan::TLS::Handshake_Type::ClientHello) {
34✔
126
            const Botan::TLS::Client_Hello& client_hello = dynamic_cast<const Botan::TLS::Client_Hello&>(message);
6✔
127

128
            std::ostringstream strm;
6✔
129

130
            strm << "Client random: " << Botan::hex_encode(client_hello.random()) << "\n";
6✔
131

132
            strm << "Client offered following ciphersuites:\n";
6✔
133
            for(uint16_t suite_id : client_hello.ciphersuites()) {
74✔
134
               const auto ciphersuite = Botan::TLS::Ciphersuite::by_id(suite_id);
68✔
135

136
               strm << " - 0x" << std::hex << std::setfill('0') << std::setw(4) << suite_id << std::dec
68✔
137
                    << std::setfill(' ') << std::setw(0) << " ";
68✔
138

139
               if(ciphersuite && ciphersuite->valid()) {
68✔
140
                  strm << ciphersuite->to_string() << "\n";
186✔
141
               } else if(suite_id == 0x00FF) {
6✔
142
                  strm << "Renegotiation SCSV\n";
6✔
143
               } else {
144
                  strm << "Unknown ciphersuite\n";
×
145
               }
146
            }
147

148
            m_chello_summary = strm.str();
6✔
149
         }
6✔
150
      }
34✔
151

152
      std::string summary() const {
2✔
153
         BOTAN_STATE_CHECK(!m_connection_summary.empty() && !m_session_summary.empty() && !m_chello_summary.empty());
2✔
154
         return m_connection_summary + m_session_summary + m_chello_summary;
4✔
155
      }
156

157
   private:
158
      std::string m_chello_summary;
159
      std::string m_connection_summary;
160
      std::string m_session_summary;
161
};
162

163
std::string summarize_request(const Botan::TLS::Stream<tcp_stream>& tls_stream,
2✔
164
                              const http::request<http::string_body>& req) {
165
   std::ostringstream strm;
2✔
166

167
   const auto& remote = tls_stream.next_layer().socket().remote_endpoint();
2✔
168

169
   strm << "Client " << remote.address().to_string() << " requested " << req.method_string() << " " << req.target()
10✔
170
        << "\n";
8✔
171

172
   if(std::distance(req.begin(), req.end()) > 0) {
4✔
173
      strm << "Client HTTP headers:\n";
2✔
174
      for(const auto& header : req) {
8✔
175
         strm << " " << header.name_string() << ": " << header.value() << "\n";
6✔
176
      }
177
   }
178

179
   return strm.str();
4✔
180
}
2✔
181

182
auto make_final_completion_handler(const std::shared_ptr<Logger>& logger, const std::string& context) {
5✔
183
   return [=](std::exception_ptr e) {
10✔
184
      if(e) {
5✔
185
         try {
×
186
            std::rethrow_exception(std::move(e));
×
187
         } catch(const std::exception& ex) {
×
188
            logger->error(Botan::fmt("{}: {}", context, ex.what()));
×
189
         }
×
190
      }
191
   };
10✔
192
}
193

194
std::shared_ptr<http::response<http::string_body>> handle_response(const http::request<http::string_body>& req,
4✔
195
                                                                   const std::shared_ptr<TlsHttpCallbacks>& callbacks,
196
                                                                   const Botan::TLS::Stream<tcp_stream>& tls_stream,
197
                                                                   const std::shared_ptr<Logger>& logger) {
198
   logger->log(Botan::fmt("{} {}", req.method_string(), req.target()));
12✔
199

200
   auto [status_code, msg] = [&]() -> std::tuple<http::status, std::string> {
4✔
201
      if(req.method() != http::verb::get) {
4✔
202
         return {http::status::method_not_allowed, "Unsupported HTTP verb\n"};
2✔
203
      } else if(req.target() == "/" || req.target() == "/status") {
4✔
204
         return {http::status::ok, callbacks->summary() + summarize_request(tls_stream, req)};
6✔
205
      } else {
206
         return {http::status::not_found, "Not found\n"};
×
207
      }
208
   }();
4✔
209

210
   auto response = std::make_shared<http::response<http::string_body>>(status_code, req.version());
4✔
211
   response->body() = msg;
4✔
212
   response->set(http::field::content_type, "text/plain");
4✔
213
   response->keep_alive(req.keep_alive());
4✔
214
   response->prepare_payload();
4✔
215

216
   return response;
4✔
217
}
4✔
218

219
net::awaitable<void> do_session(tcp_stream stream,
12✔
220
                                std::shared_ptr<Botan::TLS::Context> tls_ctx,
221
                                std::shared_ptr<Logger> logger) {
222
   // This buffer is required to persist across reads
223
   beast::flat_buffer buffer;
224

225
   // Set up Botan's TLS stack
226
   auto callbacks = std::make_shared<TlsHttpCallbacks>();
227
   Botan::TLS::Stream<tcp_stream> tls_stream(std::move(stream), std::move(tls_ctx), callbacks);
228

229
   try {
230
      // Perform a TLS handshake with the peer
231
      co_await tls_stream.async_handshake(Botan::TLS::Connection_Side::Server);
232

233
      while(true) {
234
         // Set the timeout.
235
         tls_stream.next_layer().expires_after(std::chrono::seconds(30));
236

237
         // Read a request
238
         http::request<http::string_body> req;
239
         co_await http::async_read(tls_stream, buffer, req);
240

241
         // Handle the request
242
         auto response = handle_response(req, callbacks, tls_stream, logger);
243

244
         // Send the response
245
         co_await http::async_write(tls_stream, *response);
246

247
         // Determine if we should close the connection
248
         if(!response->keep_alive()) {
249
            // This means we should close the connection, usually because
250
            // the response indicated the "Connection: close" semantic.
251
            break;
252
         }
253
      }
254
   } catch(boost::system::system_error& se) {
255
      if(se.code() != http::error::end_of_stream) {
256
         throw;
257
      }
258
   }
259

260
   // Shut down the connection gracefully
261
   co_await tls_stream.async_shutdown();
262
   beast::error_code ec;
263
   tls_stream.next_layer().socket().shutdown(tcp::socket::shutdown_send, ec);
264

265
   // At this point the connection is closed gracefully
266
   // we ignore the error because the client might have
267
   // dropped the connection already.
268
}
8✔
269

270
net::awaitable<void> do_listen(tcp::endpoint endpoint,
2✔
271
                               std::shared_ptr<Botan::TLS::Context> tls_ctx,
272
                               size_t max_clients,
273
                               std::shared_ptr<Logger> logger) {
274
   auto acceptor = net::use_awaitable.as_default_on(tcp::acceptor(co_await net::this_coro::executor));
275
   acceptor.open(endpoint.protocol());
276
   acceptor.set_option(net::socket_base::reuse_address(true));
277
   acceptor.bind(endpoint);
278
   acceptor.listen(net::socket_base::max_listen_connections);
279

280
   // If max_clients is zero in the beginning, we'll serve forever
281
   // otherwise we'll count down and stop eventually.
282
   do {
283
      boost::asio::co_spawn(acceptor.get_executor(),
284
                            do_session(tcp_stream(co_await acceptor.async_accept()), tls_ctx, logger),
285
                            make_final_completion_handler(logger, "Session"));
286
   } while(max_clients == 0 || --max_clients > 0);
287
}
2✔
288

289
}  // namespace
290

291
class TLS_HTTP_Server final : public Command {
292
   public:
293
      TLS_HTTP_Server() :
2✔
294
            Command(
295
               "tls_http_server server_cert server_key "
296
               "--port=443 --policy=default --threads=0 --max-clients=0 "
297
               "--session-db= --session-db-pass=") {}
4✔
298

299
      std::string group() const override { return "tls"; }
1✔
300

301
      std::string description() const override { return "Provides a simple HTTP server"; }
1✔
302

303
      size_t thread_count() const {
1✔
304
         if(size_t t = get_arg_sz("threads")) {
1✔
305
            return t;
306
         }
307
         if(size_t t = Botan::OS::get_cpu_available()) {
1✔
308
            return t;
1✔
309
         }
310
         return 2;
311
      }
312

313
      void go() override {
1✔
314
         const uint16_t listen_port = get_arg_u16("port");
1✔
315

316
         const std::string server_crt = get_arg("server_cert");
1✔
317
         const std::string server_key = get_arg("server_key");
1✔
318

319
         const size_t num_threads = thread_count();
1✔
320
         const size_t max_clients = get_arg_sz("max-clients");
1✔
321

322
         auto creds = std::make_shared<Basic_Credentials_Manager>(server_crt, server_key);
1✔
323

324
         auto policy = load_tls_policy(get_arg("policy"));
2✔
325

326
         std::shared_ptr<Botan::TLS::Session_Manager> session_mgr;
1✔
327

328
         const std::string sessions_db = get_arg("session-db");
1✔
329

330
         if(!sessions_db.empty()) {
1✔
331
   #if defined(BOTAN_HAS_TLS_SQLITE3_SESSION_MANAGER)
332
            const std::string sessions_passphrase = get_passphrase_arg("Session DB passphrase", "session-db-pass");
×
333
            session_mgr.reset(
×
334
               new Botan::TLS::Session_Manager_SQLite(sessions_passphrase, rng_as_shared(), sessions_db));
×
335
   #else
336
            throw CLI_Error_Unsupported("Sqlite3 support not available");
337
   #endif
338
         }
×
339

340
         if(!session_mgr) {
1✔
341
            session_mgr.reset(new Botan::TLS::Session_Manager_In_Memory(rng_as_shared()));
2✔
342
         }
343

344
         auto logger = std::make_shared<Logger>(output(), error_output());
1✔
345

346
         net::io_context io{static_cast<int>(num_threads)};
1✔
347
         auto address = net::ip::make_address("0.0.0.0");
1✔
348
         boost::asio::co_spawn(
2✔
349
            io,
350
            do_listen(tcp::endpoint{address, listen_port},
2✔
351
                      std::make_shared<Botan::TLS::Context>(creds, rng_as_shared(), session_mgr, policy),
3✔
352
                      max_clients,
353
                      logger),
354
            make_final_completion_handler(logger, "Acceptor"));
2✔
355

356
         std::vector<std::shared_ptr<std::thread>> threads;
1✔
357

358
         // run forever... first thread is main calling io.run below
359
         for(size_t i = 2; i <= num_threads; ++i) {
2✔
360
            threads.push_back(std::make_shared<std::thread>([&io]() { io.run(); }));
2✔
361
         }
362

363
         io.run();
1✔
364

365
         for(size_t i = 0; i < threads.size(); ++i) {
2✔
366
            threads[i]->join();
1✔
367
         }
368
      }
7✔
369
};
370

371
BOTAN_REGISTER_COMMAND("tls_http_server", TLS_HTTP_Server);
2✔
372

373
}  // namespace Botan_CLI
374

375
#endif
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc