Botan 3.9.0
Crypto and TLS for C&
asio_async_ops.h
Go to the documentation of this file.
1/*
2* Helpers for TLS ASIO Stream
3* (C) 2018-2020 Jack Lloyd
4* 2018-2020 Hannes Rantzsch, Tim Oesterreich, Rene Meusel
5* 2023 Fabian Albert, René Meusel - Rohde & Schwarz Cybersecurity
6*
7* Botan is released under the Simplified BSD License (see license.txt)
8*/
9
10#ifndef BOTAN_ASIO_ASYNC_OPS_H_
11#define BOTAN_ASIO_ASYNC_OPS_H_
12
13#include <botan/asio_compat.h>
14#if defined(BOTAN_FOUND_COMPATIBLE_BOOST_ASIO_VERSION)
15
16 #include <botan/asio_error.h>
17
18 // We need to define BOOST_ASIO_DISABLE_SERIAL_PORT before any asio imports. Otherwise asio will include <termios.h>,
19 // which interferes with Botan's amalgamation by defining macros like 'B0' and 'FF1'.
20 #define BOOST_ASIO_DISABLE_SERIAL_PORT
21 #include <boost/asio.hpp>
22 #include <boost/asio/yield.hpp>
23
24namespace Botan::TLS::detail {
25
26/**
27 * Base class for asynchronous stream operations.
28 *
29 * Asynchronous operations, used for example to implement an interface for boost::asio::async_read_some and
30 * boost::asio::async_write_some, are based on boost::asio::coroutines.
31 * Derived operations should implement a call operator and invoke it with the correct parameters upon construction. The
32 * call operator needs to make sure that the user-provided handler is not called directly. Typically, yield / reenter is
33 * used for this in the following fashion:
34 *
35 * ```
36 * void operator()(boost::system::error_code ec, std::size_t bytes_transferred, bool isContinuation = true)
37 * {
38 * reenter(this)
39 * {
40 * // operation specific logic, repeatedly interacting with the stream_core and the next_layer (socket)
41 *
42 * // make sure intermediate initiating function is called
43 * if(!isContinuation)
44 * {
45 * yield next_layer.async_operation(empty_buffer, this);
46 * }
47 *
48 * // call the completion handler
49 * complete_now(error_code, bytes_transferred);
50 * }
51 * }
52 * ```
53 *
54 * Once the operation is completed and ready to call the completion handler it checks if an intermediate initiating
55 * function has been called using the `isContinuation` parameter. If not, it will call an asynchronous operation, such
56 * as `async_read_some`, with and empty buffer, set the object itself as the handler, and `yield`. As a result, the call
57 * operator will be invoked again, this time as a continuation, and will jump to the location where it yielded before
58 * using `reenter`. It is now safe to call the handler function via `complete_now`.
59 *
60 * \tparam Handler Type of the completion handler
61 * \tparam Executor1 Type of the asio executor (usually derived from the lower layer)
62 * \tparam Allocator Type of the allocator to be used
63 */
64template <class Handler, class Executor1, class Allocator>
65class AsyncBase : public boost::asio::coroutine {
66 public:
67 using allocator_type = boost::asio::associated_allocator_t<Handler, Allocator>;
68 using executor_type = boost::asio::associated_executor_t<Handler, Executor1>;
69
70 allocator_type get_allocator() const noexcept { return boost::asio::get_associated_allocator(m_handler); }
71
72 executor_type get_executor() const noexcept {
73 return boost::asio::get_associated_executor(m_handler, m_work_guard_1.get_executor());
74 }
75
76 protected:
77 template <class HandlerT>
78 AsyncBase(HandlerT&& handler, const Executor1& executor) :
79 m_handler(std::forward<HandlerT>(handler)), m_work_guard_1(executor) {}
80
81 /**
82 * Call the completion handler.
83 *
84 * This function should only be called after an intermediate initiating function has been called.
85 *
86 * @param args Arguments forwarded to the completion handler function.
87 */
88 template <class... Args>
89 void complete_now(Args&&... args) {
90 m_work_guard_1.reset();
91 m_handler(std::forward<Args>(args)...);
92 }
93
94 Handler m_handler; // NOLINT(*-non-private-member-variable*)
95 boost::asio::executor_work_guard<Executor1> m_work_guard_1; // NOLINT(*-non-private-member-variable*)
96};
97
98template <class Handler, class Stream, class MutableBufferSequence, class Allocator = std::allocator<void>>
99class AsyncReadOperation : public AsyncBase<Handler, typename Stream::executor_type, Allocator> {
100 public:
101 /**
102 * Construct and invoke an AsyncReadOperation.
103 *
104 * @param handler Handler function to be called upon completion.
105 * @param stream The stream from which the data will be read
106 * @param buffers The buffers into which the data will be read.
107 * @param ec Optional error code; used to report an error to the handler function.
108 */
109 template <class HandlerT>
110 AsyncReadOperation(HandlerT&& handler,
111 Stream& stream,
112 const MutableBufferSequence& buffers,
113 const boost::system::error_code& ec = {}) :
114 AsyncBase<Handler, typename Stream::executor_type, Allocator>(std::forward<HandlerT>(handler),
115 stream.get_executor()),
116 m_stream(stream),
117 m_buffers(buffers),
118 m_decodedBytes(0) {
119 this->operator()(ec, std::size_t(0), false);
120 }
121
122 AsyncReadOperation(const AsyncReadOperation& other) = delete;
123 AsyncReadOperation(AsyncReadOperation&& other) = default;
124 AsyncReadOperation& operator=(const AsyncReadOperation& other) = delete;
125 AsyncReadOperation& operator=(AsyncReadOperation&& other) = delete;
126 ~AsyncReadOperation() = default;
127
128 /**
129 * Read and decrypt application data from the peer. Note, that this is
130 * unsuitable to receive initial handshake records! Post-handshake
131 * messages like KeyUpdate will transparently work, though.
132 *
133 * Depending on the situation, this handler will:
134 *
135 * 1. Process TLS data received from the peer, expecting to find:
136 * * a complete and decryptable application data record,
137 * * a TLS alert (e.g. close_notify)
138 * 2. Handle any pending TLS protocol errors and (if non were found) wait
139 * for more data from the peer, or
140 * 3. Copy any received application data out to the downstream application
141 * and finalize this operation.
142 */
143 void operator()(boost::system::error_code ec, std::size_t bytes_transferred, bool isContinuation = true) {
144 reenter(this) {
145 // Check whether we received a premature EOF from the next layer.
146 if(ec == boost::asio::error::eof && !m_stream.shutdown_received()) {
147 ec = StreamError::StreamTruncated;
148 }
149
150 // If we received data from the peer, we hand it to the native
151 // handle for processing. When enough bytes were received to decrypt
152 // an application data record, the "input buffer" will become
153 // non-empty.
154 if(!ec && bytes_transferred > 0) {
155 boost::asio::const_buffer read_buffer{m_stream.input_buffer().data(), bytes_transferred};
156 m_stream.process_encrypted_data(read_buffer);
157 }
158
159 // If the "input buffer" is still empty, indicating that we didn't
160 // successfully decrypt any application data from the peer, yet, ...
161 if(!ec && !m_stream.has_received_data()) {
162 // ... we first ensure that no TLS protocol error was detected until now.
163 // Otherwise the read operation is aborted with an error code.
164 m_stream.handle_tls_protocol_errors(ec);
165
166 if(!ec && boost::asio::buffer_size(m_buffers) > 0) {
167 // ... and wait for more incoming data.
168 m_stream.next_layer().async_read_some(m_stream.input_buffer(), std::move(*this));
169 return;
170 }
171 }
172
173 // If the "input buffer" holds successfully decrypted application
174 // data, we copy it to the "output buffers" and finalize the
175 // operation with the number of produced bytes.
176 if(!ec && m_stream.has_received_data()) {
177 m_decodedBytes = m_stream.copy_received_data(m_buffers);
178 }
179
180 if(!isContinuation) {
181 // Make sure the handler is not called without an intermediate initiating function.
182 // "Reading" into a zero-byte buffer will complete immediately.
183 m_ec = ec;
184 yield m_stream.next_layer().async_read_some(boost::asio::mutable_buffer(), std::move(*this));
185 ec = m_ec;
186 }
187
188 this->complete_now(ec, m_decodedBytes);
189 }
190 }
191
192 private:
193 Stream& m_stream;
194 MutableBufferSequence m_buffers;
195 std::size_t m_decodedBytes;
196 boost::system::error_code m_ec;
197};
198
199template <typename Handler, class Stream, class Allocator = std::allocator<void>>
200class AsyncWriteOperation : public AsyncBase<Handler, typename Stream::executor_type, Allocator> {
201 public:
202 /**
203 * Construct and invoke an AsyncWriteOperation.
204 *
205 * @param handler Handler function to be called upon completion.
206 * @param stream The stream from which the data will be read
207 * @param plainBytesTransferred Number of bytes to be reported to the user-provided handler function as
208 * bytes_transferred. This needs to be provided since the amount of plaintext data
209 * consumed from the input buffer can differ from the amount of encrypted data written
210 * to the next layer.
211 * @param ec Optional error code; used to report an error to the handler function.
212 */
213 template <class HandlerT>
214 AsyncWriteOperation(HandlerT&& handler,
215 Stream& stream,
216 std::size_t plainBytesTransferred,
217 const boost::system::error_code& ec = {}) :
218 AsyncBase<Handler, typename Stream::executor_type, Allocator>(std::forward<HandlerT>(handler),
219 stream.get_executor()),
220 m_stream(stream),
221 m_plainBytesTransferred(plainBytesTransferred) {
222 this->operator()(ec, std::size_t(0), false);
223 }
224
225 AsyncWriteOperation(const AsyncWriteOperation& other) = delete;
226 AsyncWriteOperation(AsyncWriteOperation&& other) = default;
227 AsyncWriteOperation& operator=(const AsyncWriteOperation& other) = delete;
228 AsyncWriteOperation& operator=(AsyncWriteOperation&& other) = delete;
229 ~AsyncWriteOperation() = default;
230
231 /**
232 * Write (encrypted) TLS record data generated by us and bound to the peer
233 * from the "send buffer" to the wire.
234 *
235 * Depending on the situation, this handler will:
236 *
237 * 1. Mark the successfully sent bytes as "consumed", or
238 * 2. Send remaining bytes to the peer,
239 *
240 * until the "send buffer" does not contain any more bytes to be sent.
241 */
242 void operator()(boost::system::error_code ec, std::size_t bytes_transferred, bool isContinuation = true) {
243 reenter(this) {
244 // Check whether we received a premature EOF from the next layer.
245 if(ec == boost::asio::error::eof && !m_stream.shutdown_received()) {
246 ec = StreamError::StreamTruncated;
247 }
248
249 // If we have successfully transferred some bytes to the peer, mark
250 // the number of sent bytes as "consumed".
251 if(!ec && bytes_transferred > 0) {
252 m_stream.consume_send_buffer(bytes_transferred);
253 }
254
255 // If we have more data to send, go for it...
256 if(!ec && m_stream.has_data_to_send()) {
257 m_stream.next_layer().async_write_some(m_stream.send_buffer(), std::move(*this));
258 return;
259 }
260
261 if(!isContinuation) {
262 // Make sure the handler is not called without an intermediate initiating function.
263 // "Writing" to a zero-byte buffer will complete immediately.
264 m_ec = ec;
265 yield m_stream.next_layer().async_write_some(boost::asio::const_buffer(), std::move(*this));
266 ec = m_ec;
267 }
268
269 // The size of the sent TLS record can differ from the size of the payload due to TLS encryption. We need to
270 // tell the handler how many bytes of the original data we already processed.
271 this->complete_now(ec, m_plainBytesTransferred);
272 }
273 }
274
275 private:
276 Stream& m_stream;
277 std::size_t m_plainBytesTransferred;
278 boost::system::error_code m_ec;
279};
280
281template <class Handler, class Stream, class Allocator = std::allocator<void>>
282class AsyncHandshakeOperation : public AsyncBase<Handler, typename Stream::executor_type, Allocator> {
283 public:
284 /**
285 * Construct and invoke an AsyncHandshakeOperation.
286 *
287 * @param handler Handler function to be called upon completion.
288 * @param stream The stream from which the data will be read
289 * @param ec Optional error code; used to report an error to the handler function.
290 */
291 template <class HandlerT>
292 AsyncHandshakeOperation(HandlerT&& handler, Stream& stream, const boost::system::error_code& ec = {}) :
293 AsyncBase<Handler, typename Stream::executor_type, Allocator>(std::forward<HandlerT>(handler),
294 stream.get_executor()),
295 m_stream(stream) {
296 this->operator()(ec, std::size_t(0), false);
297 }
298
299 AsyncHandshakeOperation(const AsyncHandshakeOperation& other) = delete;
300 AsyncHandshakeOperation(AsyncHandshakeOperation&& other) = default;
301 AsyncHandshakeOperation& operator=(const AsyncHandshakeOperation& other) = delete;
302 AsyncHandshakeOperation& operator=(AsyncHandshakeOperation&& other) = delete;
303 ~AsyncHandshakeOperation() = default;
304
305 /**
306 * Perform a TLS handshake with the peer.
307 *
308 * Depending on the situation, this handler will:
309 *
310 * 1. Process TLS data received from the peer, and potentially:
311 * * generate a response (another handshake flight, or an alert) or
312 * * finalize the handshake,
313 * 2. Send pending data to the peer, or
314 * 3. Handle any pending TLS protocol errors and (if none were found) wait
315 * for more data from the peer.
316 */
317 void operator()(boost::system::error_code ec, std::size_t bytesTransferred, bool isContinuation = true) {
318 reenter(this) {
319 // Check whether we received a premature EOF from the next layer.
320 // Note that the AsyncWriteOperation handles this internally; here
321 // we only have to handle reading.
322 if(ec == boost::asio::error::eof && !m_stream.shutdown_received()) {
323 ec = StreamError::StreamTruncated;
324 }
325
326 // If we received data from the peer, we hand it to the native
327 // handle for processing. When enough bytes were received this will
328 // result in the advancement of the handshake state and produce data
329 // in the output buffer.
330 if(!ec && bytesTransferred > 0) {
331 boost::asio::const_buffer read_buffer{m_stream.input_buffer().data(), bytesTransferred};
332 m_stream.process_encrypted_data(read_buffer);
333 }
334
335 // If we have data to be sent to the peer, we do that now. Note that
336 // this might either be a flight in our handshake, or a TLS alert
337 // record if we decided to abort due to some failure.
338 if(!ec && m_stream.has_data_to_send()) {
339 // Note: we construct `AsyncWriteOperation` with 0 as its last parameter (`plainBytesTransferred`). This
340 // operation will eventually call `*this` as its own handler, passing the 0 back to this call operator.
341 // This is necessary because the check of `bytesTransferred > 0` assumes that `bytesTransferred` bytes
342 // were just read and are available in input_buffer for further processing.
343 AsyncWriteOperation<AsyncHandshakeOperation<std::decay_t<Handler>, Stream, Allocator>, Stream, Allocator>
344 op{std::move(*this), m_stream, 0};
345 return;
346 }
347
348 // If we have no more data from the peer to process and no more data
349 // to be sent to the peer...
350 if(!ec && !m_stream.native_handle()->is_handshake_complete()) {
351 // ... we first ensure that no TLS protocol error was detected until now.
352 // Otherwise the handshake is aborted with an error code.
353 m_stream.handle_tls_protocol_errors(ec);
354
355 if(!ec) {
356 // The handshake is neither finished nor aborted. Wait for
357 // more data from the peer.
358 m_stream.next_layer().async_read_some(m_stream.input_buffer(), std::move(*this));
359 return;
360 }
361 }
362
363 if(!isContinuation) {
364 // Make sure the handler is not called without an intermediate initiating function.
365 // "Reading" into a zero-byte buffer will complete immediately.
366 m_ec = ec;
367 yield m_stream.next_layer().async_read_some(boost::asio::mutable_buffer(), std::move(*this));
368 ec = m_ec;
369 }
370
371 this->complete_now(ec);
372 }
373 }
374
375 private:
376 Stream& m_stream;
377 boost::system::error_code m_ec;
378 boost::system::error_code m_stashed_ec;
379};
380
381} // namespace Botan::TLS::detail
382
383 #include <boost/asio/unyield.hpp>
384
385#endif
386#endif // BOTAN_ASIO_ASYNC_OPS_H_