TLA Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3 : // Copyright (c) 2026 Michael Vandeberg
4 : // Copyright (c) 2026 Steve Gerbino
5 : //
6 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
7 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
8 : //
9 : // Official repository: https://github.com/cppalliance/corosio
10 : //
11 :
12 : #ifndef BOOST_COROSIO_TLS_STREAM_HPP
13 : #define BOOST_COROSIO_TLS_STREAM_HPP
14 :
15 : #include <boost/corosio/detail/config.hpp>
16 : #include <boost/capy/buffers.hpp>
17 : #include <boost/capy/detail/buffer_array.hpp>
18 : #include <boost/capy/io/any_stream.hpp>
19 : #include <boost/capy/results.hpp>
20 : #include <boost/capy/task.hpp>
21 :
22 : #include <cstddef>
23 : #include <string_view>
24 :
25 : namespace boost::corosio {
26 :
27 : namespace detail {
28 :
29 : /** A transport a TLS stream owns, held apart from its `any_stream`.
30 :
31 : Destroying the stream cancels the transport's pending operations
32 : while the TLS state they complete into stays alive; the transport
33 : itself outlives every operation that could be parked on it.
34 : */
35 : struct tls_owned_transport
36 : {
37 : /// The owned transport, or null in reference mode.
38 : void* p = nullptr;
39 :
40 : /// Destroy the transport `p` points to.
41 : void (*destroy)(void*) noexcept = nullptr;
42 :
43 : /// Cancel the transport's pending operations, or null if it can't.
44 : void (*cancel)(void*) noexcept = nullptr;
45 : };
46 :
47 : /// Destroy an owned transport of type `S`.
48 : template<class S>
49 : void
50 : destroy_owned_transport(void* p) noexcept
51 : {
52 : delete static_cast<S*>(p);
53 : }
54 :
55 : /// Cancel an owned transport of type `S`'s pending operations.
56 : template<class S>
57 : void
58 : cancel_owned_transport(void* p) noexcept
59 : {
60 : static_cast<S*>(p)->cancel();
61 : }
62 :
63 : /// Make the owned-transport record for a transport of type `S`.
64 : template<class S>
65 : tls_owned_transport
66 : make_owned_transport(S* p) noexcept
67 : {
68 : tls_owned_transport t{p, &destroy_owned_transport<S>, nullptr};
69 : if constexpr (requires(S& s) {
70 : { s.cancel() } noexcept;
71 : })
72 : t.cancel = &cancel_owned_transport<S>;
73 : return t;
74 : }
75 :
76 : } // namespace detail
77 :
78 : /** TLS handshake role.
79 :
80 : Specifies whether to perform the TLS handshake as a client or server.
81 :
82 : @see tls_stream::handshake
83 : */
84 : enum class tls_role
85 : {
86 : /// Perform handshake as the connecting client.
87 : client,
88 :
89 : /// Perform handshake as the accepting server.
90 : server
91 : };
92 :
93 : /** Reads, writes, and manages the handshake lifecycle of a TLS
94 : session over an underlying stream.
95 :
96 : This class provides a runtime-polymorphic interface for TLS
97 : implementations. Derived classes (`openssl_stream`, `wolfssl_stream`)
98 : implement the virtual functions to provide backend-specific
99 : TLS functionality.
100 :
101 : An @ref io_stream represents OS-level I/O completed by the kernel.
102 : TLS streams are coroutine-based instead: their operations are
103 : coroutines that orchestrate sub-operations on the underlying stream.
104 :
105 : The non-virtual template wrappers (`read_some`, `write_some`)
106 : satisfy the `capy::Stream` concept, enabling TLS streams to
107 : be used anywhere a Stream is expected.
108 :
109 : @par Thread Safety
110 : Distinct objects: Safe.@n
111 : Shared objects: Unsafe, with one exception: one read operation and
112 : one write operation may be in flight simultaneously. `shutdown()`
113 : may overlap a pending read. On a multi-threaded execution context,
114 : all operations on one stream must run within the same
115 : `capy::strand`, or must otherwise never run concurrently. A
116 : single-threaded context needs no strand.
117 :
118 : @see openssl_stream, wolfssl_stream
119 : */
120 : class BOOST_COROSIO_DECL tls_stream
121 : {
122 : public:
123 : /// Destroy the TLS stream.
124 : virtual ~tls_stream() = default;
125 :
126 : /// Copy construction is disabled; copying a stream would slice the derived session.
127 : tls_stream(tls_stream const&) = delete;
128 : /// Copy assignment is disabled; copying a stream would slice the derived session.
129 : tls_stream& operator=(tls_stream const&) = delete;
130 :
131 : /** Initiate an asynchronous read operation.
132 :
133 : Reads decrypted data into the provided buffer sequence. The
134 : operation completes when it reads at least one byte,
135 : or an error occurs.
136 :
137 : This non-virtual template wrapper satisfies the `capy::Stream`
138 : concept by delegating to the virtual `do_read_some`.
139 :
140 : @par Thread Safety
141 : May run concurrently with one operation in the other
142 : direction, subject to the class-level threading contract.
143 : Two concurrent operations in the same direction are
144 : undefined.
145 :
146 : @param buffers The buffer sequence to read data into.
147 :
148 : @return An awaitable yielding `(error_code,std::size_t)`.
149 : */
150 : template<capy::MutableBufferSequence Buffers>
151 MIS 0 : [[nodiscard]] auto read_some(Buffers const& buffers)
152 : {
153 0 : return do_read_some(buffers);
154 : }
155 :
156 : /** Initiate an asynchronous write operation.
157 :
158 : Encrypts and writes data from the provided buffer sequence.
159 : The operation completes when it writes at least one byte,
160 : or an error occurs.
161 :
162 : This non-virtual template wrapper satisfies the `capy::Stream`
163 : concept by delegating to the virtual `do_write_some`.
164 :
165 : @par Thread Safety
166 : May run concurrently with one operation in the other
167 : direction, subject to the class-level threading contract.
168 : Two concurrent operations in the same direction are
169 : undefined.
170 :
171 : @param buffers The buffer sequence containing data to write.
172 :
173 : @return An awaitable yielding `(error_code,std::size_t)`.
174 : */
175 : template<capy::ConstBufferSequence Buffers>
176 0 : [[nodiscard]] auto write_some(Buffers const& buffers)
177 : {
178 0 : return do_write_some(buffers);
179 : }
180 :
181 : /** Asynchronously perform the TLS handshake.
182 :
183 : Initiates the TLS handshake process. For client connections,
184 : this sends the ClientHello and processes the server's response.
185 : For server connections, this waits for the ClientHello and
186 : sends the server's response.
187 :
188 : A handshake attempt consumes the stream state, whether it
189 : succeeds or not. A subsequent call behaves as if `reset()` ran
190 : first, and performs a fresh handshake using the current
191 : configuration.
192 :
193 : @pre The underlying stream must be connected. No other TLS
194 : operation may be in progress on this stream.
195 :
196 : @param role The handshake role, client or server.
197 :
198 : @return An awaitable yielding `(error_code)`.
199 : */
200 : [[nodiscard]] virtual capy::task<capy::results<>> handshake(tls_role role) = 0;
201 :
202 : /** Asynchronously perform a graceful TLS shutdown.
203 :
204 : Initiates the TLS shutdown sequence by sending a close_notify
205 : alert and waiting for the peer's close_notify response.
206 :
207 : @pre A handshake must have completed successfully. May overlap
208 : a pending read. No concurrent write may be in progress.
209 :
210 : @par Postconditions
211 : If the transport ends before the peer's close_notify arrives,
212 : the result is `capy::error::stream_truncated`, not success. An
213 : unannounced close is indistinguishable from a truncation attack,
214 : so it must not be reported as a clean shutdown. A shutdown
215 : stopped mid-flight reports canceled. Any other transport
216 : error propagates unchanged.
217 :
218 : @return An awaitable yielding `(error_code)`.
219 : */
220 : [[nodiscard]] virtual capy::task<capy::results<>> shutdown() = 0;
221 :
222 : /** Cancel operations pending on the owned underlying stream.
223 :
224 : Operations parked on the underlying stream complete with
225 : `capy::error::canceled`. A stream that owns another TLS stream
226 : reaches the innermost transport through this, so destroying
227 : the outer stream cancels a read parked through both.
228 :
229 : In reference mode, and when the owned stream has no `cancel()`
230 : member, this does nothing; cancel the underlying stream
231 : directly instead.
232 :
233 : @par Thread Safety
234 : Not thread safe with respect to destroying or moving this
235 : stream.
236 : */
237 : virtual void cancel() noexcept = 0;
238 :
239 : /** Reset TLS session state for reuse.
240 :
241 : Releases TLS session state including session keys and peer
242 : certificates, returning the stream to a state where
243 : `handshake()` can be called again. Internal memory
244 : allocations (I/O buffers) are preserved.
245 :
246 : Calling `handshake()` on a previously-used stream
247 : implicitly performs a reset first, so explicit calls
248 : are only needed to eagerly release session state.
249 :
250 : @pre No TLS operation (handshake, read, write, shutdown) is
251 : in progress.
252 :
253 : @par Thread Safety
254 : Not thread safe. The caller must ensure no concurrent
255 : operations are in progress on this stream.
256 :
257 : @note If called mid-session before `shutdown()`, pending
258 : TLS data is discarded and the peer observes a
259 : truncated stream.
260 : */
261 : virtual void reset() = 0;
262 :
263 : /** Set the peer hostname for SNI and certificate verification.
264 :
265 : Configures the hostname sent in the TLS Server Name
266 : Indication extension and matched against the peer
267 : certificate during verification. The value takes effect
268 : at the next `handshake()`; an established session is not
269 : affected. It persists across `reset()`, so a stream reused
270 : to reach a different host must set the new name before
271 : handshaking again.
272 :
273 : An empty hostname (the default) disables SNI and hostname
274 : verification.
275 :
276 : If `hostname` is an IP literal (IPv4 or IPv6), it is matched
277 : against the certificate's iPAddress entries instead of its DNS
278 : names. No SNI is sent, because RFC 6066 excludes literals.
279 : A backend build that cannot match iPAddress entries fails the
280 : handshake with `std::errc::function_not_supported` rather
281 : than skip verification.
282 :
283 : @par Postconditions
284 : The next `handshake()` uses `hostname` for SNI and
285 : certificate verification, or neither if it is empty.
286 :
287 : @note The hostname is used for client handshakes only;
288 : it is ignored when handshaking as a server.
289 :
290 : @param hostname The peer hostname, or empty to disable.
291 : */
292 : virtual void set_hostname(std::string_view hostname) = 0;
293 :
294 : /** Return a reference to the underlying stream.
295 :
296 : Provides access to the type-erased underlying stream for
297 : operations like cancellation or accessing native handles.
298 :
299 : @warning Do not reseat (assign to) the returned reference.
300 : The TLS implementation holds internal state bound to
301 : the original stream. Replacing it causes undefined
302 : behavior.
303 :
304 : @return Reference to the wrapped stream.
305 : */
306 : virtual capy::any_stream& next_layer() noexcept = 0;
307 :
308 : /** Return a const reference to the underlying stream.
309 :
310 : @return Const reference to the wrapped stream.
311 : */
312 : virtual capy::any_stream const& next_layer() const noexcept = 0;
313 :
314 : /** Return the name of the TLS backend.
315 :
316 : @return A string identifying the TLS implementation,
317 : such as "openssl" or "wolfssl".
318 : */
319 : virtual std::string_view name() const noexcept = 0;
320 :
321 : /** Return the ALPN protocol negotiated during the handshake.
322 :
323 : Application-Layer Protocol Negotiation selects a single
324 : application protocol (for example `"h2"` or `"http/1.1"`)
325 : during the TLS handshake, from the list supplied via
326 : @ref tls_context::set_alpn.
327 :
328 : @return The negotiated protocol, or an empty view. It is empty
329 : if no protocol was negotiated or ALPN was not offered. It is
330 : also empty if the handshake has not completed, or if the build
331 : lacks ALPN support.
332 :
333 : @par Thread Safety
334 : Safe to call after the handshake completes; not safe to call
335 : concurrently with a handshake or reset.
336 : */
337 : virtual std::string_view alpn_protocol() const noexcept
338 : {
339 : return {};
340 : } // LCOV_EXCL_LINE every concrete stream overrides this; the base default is never called
341 :
342 : protected:
343 : /// Default construct; a derived class supplies the session.
344 : tls_stream() = default;
345 :
346 : /** Perform the backend-specific decrypted read.
347 :
348 : Derived classes override this to perform TLS decryption
349 : and read operations.
350 :
351 : @param buffers Buffer sequence to read into.
352 :
353 : @return An awaitable yielding `(error_code,std::size_t)`.
354 : */
355 : virtual capy::task<capy::results<std::size_t>> do_read_some(
356 : capy::detail::mutable_buffer_array<capy::detail::max_iovec_>
357 : buffers) = 0;
358 :
359 : /** Perform the backend-specific encrypted write.
360 :
361 : Derived classes override this to perform TLS encryption
362 : and write operations.
363 :
364 : @param buffers Buffer sequence to write from.
365 :
366 : @return An awaitable yielding `(error_code,std::size_t)`.
367 : */
368 : virtual capy::task<capy::results<std::size_t>> do_write_some(
369 : capy::detail::const_buffer_array<capy::detail::max_iovec_> buffers) = 0;
370 : };
371 :
372 : } // namespace boost::corosio
373 :
374 : #endif
|