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

randombit / botan / 9061561573

13 May 2024 10:46AM UTC coverage: 92.001% (+0.003%) from 91.998%
9061561573

push

github

web-flow
Merge pull request #4011 from randombit/jack/localtime-util

Add a os_utils helper for locatime->put_time

82944 of 90156 relevant lines covered (92.0%)

9255180.62 hits per line

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

88.19
/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_TLS_ASIO_STREAM)
16
   #include <botan/asio_compat.h>
17
#endif
18

19
#if defined(BOTAN_FOUND_COMPATIBLE_BOOST_ASIO_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
      std::string timestamp() const { return Botan::OS::format_time(std::time(nullptr), "%c"); }
8✔
67

68
   public:
69
      Logger(std::ostream& out, std::ostream& err) : m_out(out), m_err(err) {}
1✔
70

71
      void log(std::string_view out) {
4✔
72
         std::scoped_lock lk(m_mutex);
4✔
73
         m_out << Botan::fmt("[{}] {}", timestamp(), out) << "\n";
16✔
74
      }
4✔
75

76
      void error(std::string_view err) {
×
77
         std::scoped_lock lk(m_mutex);
×
78
         m_err << Botan::fmt("[{}] {}", timestamp(), err) << "\n";
×
79
      }
×
80

81
   private:
82
      std::mutex m_mutex;
83
      std::ostream& m_out;
84
      std::ostream& m_err;
85
};
86

87
class TlsHttpCallbacks final : public Botan::TLS::StreamCallbacks {
4✔
88
   public:
89
      void tls_session_activated() override {
4✔
90
         std::ostringstream strm;
4✔
91

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

94
         m_connection_summary = strm.str();
4✔
95
      }
4✔
96

97
      void tls_session_established(const Botan::TLS::Session_Summary& session) override {
4✔
98
         std::ostringstream strm;
4✔
99

100
         strm << "Version: " << session.version().to_string() << "\n";
8✔
101
         strm << "Ciphersuite: " << session.ciphersuite().to_string() << "\n";
8✔
102
         if(const auto& session_id = session.session_id(); !session_id.empty()) {
4✔
103
            strm << "SessionID: " << Botan::hex_encode(session_id.get()) << "\n";
8✔
104
         }
105
         if(!session.server_info().hostname().empty()) {
8✔
106
            strm << "SNI: " << session.server_info().hostname() << "\n";
12✔
107
         }
108

109
         m_session_summary = strm.str();
4✔
110
      }
4✔
111

112
      void tls_inspect_handshake_msg(const Botan::TLS::Handshake_Message& message) override {
34✔
113
         if(message.type() == Botan::TLS::Handshake_Type::ClientHello) {
34✔
114
            const Botan::TLS::Client_Hello& client_hello = dynamic_cast<const Botan::TLS::Client_Hello&>(message);
6✔
115

116
            std::ostringstream strm;
6✔
117

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

120
            strm << "Client offered following ciphersuites:\n";
6✔
121
            for(uint16_t suite_id : client_hello.ciphersuites()) {
74✔
122
               const auto ciphersuite = Botan::TLS::Ciphersuite::by_id(suite_id);
68✔
123

124
               strm << " - 0x" << std::hex << std::setfill('0') << std::setw(4) << suite_id << std::dec
68✔
125
                    << std::setfill(' ') << std::setw(0) << " ";
68✔
126

127
               if(ciphersuite && ciphersuite->valid()) {
68✔
128
                  strm << ciphersuite->to_string() << "\n";
186✔
129
               } else if(suite_id == 0x00FF) {
6✔
130
                  strm << "Renegotiation SCSV\n";
6✔
131
               } else {
132
                  strm << "Unknown ciphersuite\n";
×
133
               }
134
            }
135

136
            m_chello_summary = strm.str();
6✔
137
         }
6✔
138
      }
34✔
139

140
      std::string summary() const {
2✔
141
         BOTAN_STATE_CHECK(!m_connection_summary.empty() && !m_session_summary.empty() && !m_chello_summary.empty());
2✔
142
         return m_connection_summary + m_session_summary + m_chello_summary;
4✔
143
      }
144

145
   private:
146
      std::string m_chello_summary;
147
      std::string m_connection_summary;
148
      std::string m_session_summary;
149
};
150

151
std::string summarize_request(const Botan::TLS::Stream<tcp_stream>& tls_stream,
2✔
152
                              const http::request<http::string_body>& req) {
153
   std::ostringstream strm;
2✔
154

155
   const auto& remote = tls_stream.next_layer().socket().remote_endpoint();
2✔
156

157
   strm << "Client " << remote.address().to_string() << " requested " << req.method_string() << " " << req.target()
10✔
158
        << "\n";
8✔
159

160
   if(std::distance(req.begin(), req.end()) > 0) {
4✔
161
      strm << "Client HTTP headers:\n";
2✔
162
      for(const auto& header : req) {
8✔
163
         strm << " " << header.name_string() << ": " << header.value() << "\n";
6✔
164
      }
165
   }
166

167
   return strm.str();
4✔
168
}
2✔
169

170
auto make_final_completion_handler(const std::shared_ptr<Logger>& logger, const std::string& context) {
5✔
171
   return [=](std::exception_ptr e) {
10✔
172
      if(e) {
5✔
173
         try {
×
174
            std::rethrow_exception(std::move(e));
×
175
         } catch(const std::exception& ex) {
×
176
            logger->error(Botan::fmt("{}: {}", context, ex.what()));
×
177
         }
×
178
      }
179
   };
10✔
180
}
181

182
std::shared_ptr<http::response<http::string_body>> handle_response(const http::request<http::string_body>& req,
4✔
183
                                                                   const std::shared_ptr<TlsHttpCallbacks>& callbacks,
184
                                                                   const Botan::TLS::Stream<tcp_stream>& tls_stream,
185
                                                                   const std::shared_ptr<Logger>& logger) {
186
   logger->log(Botan::fmt("{} {}", req.method_string(), req.target()));
12✔
187

188
   auto [status_code, msg] = [&]() -> std::tuple<http::status, std::string> {
4✔
189
      if(req.method() != http::verb::get) {
4✔
190
         return {http::status::method_not_allowed, "Unsupported HTTP verb\n"};
2✔
191
      } else if(req.target() == "/" || req.target() == "/status") {
4✔
192
         return {http::status::ok, callbacks->summary() + summarize_request(tls_stream, req)};
6✔
193
      } else {
194
         return {http::status::not_found, "Not found\n"};
×
195
      }
196
   }();
4✔
197

198
   auto response = std::make_shared<http::response<http::string_body>>(status_code, req.version());
4✔
199
   response->body() = msg;
4✔
200
   response->set(http::field::content_type, "text/plain");
4✔
201
   response->keep_alive(req.keep_alive());
4✔
202
   response->prepare_payload();
4✔
203

204
   return response;
4✔
205
}
4✔
206

207
net::awaitable<void> do_session(tcp_stream stream,
12✔
208
                                std::shared_ptr<Botan::TLS::Context> tls_ctx,
209
                                std::shared_ptr<Logger> logger) {
210
   // This buffer is required to persist across reads
211
   beast::flat_buffer buffer;
212

213
   // Set up Botan's TLS stack
214
   auto callbacks = std::make_shared<TlsHttpCallbacks>();
215
   Botan::TLS::Stream<tcp_stream> tls_stream(std::move(stream), std::move(tls_ctx), callbacks);
216

217
   std::exception_ptr protocol_exception;
218

219
   try {
220
      // Perform a TLS handshake with the peer
221
      co_await tls_stream.async_handshake(Botan::TLS::Connection_Side::Server);
222

223
      while(true) {
224
         // Set the timeout.
225
         tls_stream.next_layer().expires_after(std::chrono::seconds(30));
226

227
         // Read a request
228
         http::request<http::string_body> req;
229
         co_await http::async_read(tls_stream, buffer, req);
230

231
         // Handle the request
232
         auto response = handle_response(req, callbacks, tls_stream, logger);
233

234
         // Send the response
235
         co_await http::async_write(tls_stream, *response);
236

237
         // Determine if we should close the connection
238
         if(!response->keep_alive()) {
239
            // This means we should close the connection, usually because
240
            // the response indicated the "Connection: close" semantic.
241
            break;
242
         }
243
      }
244
   } catch(boost::system::system_error& se) {
245
      if(se.code() != http::error::end_of_stream) {
246
         // Something went wrong during the communication, as good citizens we
247
         // try to shutdown the connection gracefully, anyway. The protocol
248
         // exception is kept for later re-throw.
249
         protocol_exception = std::current_exception();
250
      }
251
   }
252

253
   try {
254
      // Shut down the connection gracefully. It gives the stream a chance to
255
      // flush remaining send buffers and/or close the connection gracefully. If
256
      // the communication above failed this may or may not be successful.
257
      co_await tls_stream.async_shutdown();
258
      tls_stream.next_layer().close();
259
   } catch(const std::exception&) {
260
      // if the protocol interaction above produced an exception the shutdown
261
      // was "best effort" anyway and we swallow the secondary exception that
262
      // happened during shutdown.
263
      if(!protocol_exception) {
264
         throw;
265
      }
266
   }
267

268
   if(protocol_exception) {
269
      std::rethrow_exception(protocol_exception);
270
   }
271

272
   // At this point the connection is closed gracefully
273
   // we ignore the error because the client might have
274
   // dropped the connection already.
275
}
8✔
276

277
net::awaitable<void> do_listen(tcp::endpoint endpoint,
2✔
278
                               std::shared_ptr<Botan::TLS::Context> tls_ctx,
279
                               size_t max_clients,
280
                               std::shared_ptr<Logger> logger) {
281
   auto acceptor = net::use_awaitable.as_default_on(tcp::acceptor(co_await net::this_coro::executor));
282
   acceptor.open(endpoint.protocol());
283
   acceptor.set_option(net::socket_base::reuse_address(true));
284
   acceptor.bind(endpoint);
285
   acceptor.listen(net::socket_base::max_listen_connections);
286

287
   // If max_clients is zero in the beginning, we'll serve forever
288
   // otherwise we'll count down and stop eventually.
289
   do {
290
      boost::asio::co_spawn(acceptor.get_executor(),
291
                            do_session(tcp_stream(co_await acceptor.async_accept()), tls_ctx, logger),
292
                            make_final_completion_handler(logger, "Session"));
293
   } while(max_clients == 0 || --max_clients > 0);
294
}
2✔
295

296
}  // namespace
297

298
class TLS_HTTP_Server final : public Command {
299
   public:
300
      TLS_HTTP_Server() :
2✔
301
            Command(
302
               "tls_http_server server_cert server_key "
303
               "--port=443 --policy=default --threads=0 --max-clients=0 "
304
               "--session-db= --session-db-pass=") {}
4✔
305

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

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

310
      size_t thread_count() const {
1✔
311
         if(size_t t = get_arg_sz("threads")) {
1✔
312
            return t;
313
         }
314
         if(size_t t = Botan::OS::get_cpu_available()) {
1✔
315
            return t;
1✔
316
         }
317
         return 2;
318
      }
319

320
      void go() override {
1✔
321
         const uint16_t listen_port = get_arg_u16("port");
1✔
322

323
         const std::string server_crt = get_arg("server_cert");
1✔
324
         const std::string server_key = get_arg("server_key");
1✔
325

326
         const size_t num_threads = thread_count();
1✔
327
         const size_t max_clients = get_arg_sz("max-clients");
1✔
328

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

331
         auto policy = load_tls_policy(get_arg("policy"));
2✔
332

333
         std::shared_ptr<Botan::TLS::Session_Manager> session_mgr;
1✔
334

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

337
         if(!sessions_db.empty()) {
1✔
338
   #if defined(BOTAN_HAS_TLS_SQLITE3_SESSION_MANAGER)
339
            const std::string sessions_passphrase = get_passphrase_arg("Session DB passphrase", "session-db-pass");
×
340
            session_mgr.reset(
×
341
               new Botan::TLS::Session_Manager_SQLite(sessions_passphrase, rng_as_shared(), sessions_db));
×
342
   #else
343
            throw CLI_Error_Unsupported("Sqlite3 support not available");
344
   #endif
345
         }
×
346

347
         if(!session_mgr) {
1✔
348
            session_mgr.reset(new Botan::TLS::Session_Manager_In_Memory(rng_as_shared()));
2✔
349
         }
350

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

353
         net::io_context io{static_cast<int>(num_threads)};
1✔
354
         auto address = net::ip::make_address("0.0.0.0");
1✔
355
         boost::asio::co_spawn(
2✔
356
            io,
357
            do_listen(tcp::endpoint{address, listen_port},
2✔
358
                      std::make_shared<Botan::TLS::Context>(creds, rng_as_shared(), session_mgr, policy),
3✔
359
                      max_clients,
360
                      logger),
361
            make_final_completion_handler(logger, "Acceptor"));
2✔
362

363
         std::vector<std::shared_ptr<std::thread>> threads;
1✔
364

365
         // run forever... first thread is main calling io.run below
366
         for(size_t i = 2; i <= num_threads; ++i) {
4✔
367
            threads.push_back(std::make_shared<std::thread>([&io]() { io.run(); }));
6✔
368
         }
369

370
         io.run();
1✔
371

372
         for(size_t i = 0; i < threads.size(); ++i) {
4✔
373
            threads[i]->join();
3✔
374
         }
375
      }
7✔
376
};
377

378
BOTAN_REGISTER_COMMAND("tls_http_server", TLS_HTTP_Server);
2✔
379

380
}  // namespace Botan_CLI
381

382
#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

© 2026 Coveralls, Inc