Skip to main content

deboa_compio/client/http/conn/
mod.rs

1//! Connection management for the Deboa HTTP client.
2//!
3//! This module provides the building blocks for managing HTTP connections,
4//! including connection pooling and protocol-specific implementations.
5//!
6//! # Architecture
7//!
8//! - [`http`]: Core HTTP protocol implementations (HTTP/1.1, HTTP/2 and HTTP/3)
9//! - [`pool`]: Connection pooling for efficient request handling
10//!
11//! # Features
12//!
13//! - Automatic connection pooling
14//! - Protocol negotiation (HTTP/1.1, HTTP/2 and HTTP/3)
15//! - Connection lifecycle management
16//! - Thread-safe connection handling
17//! ```
18use crate::cert::{DeboaCertificate, DeboaIdentity};
19#[cfg(feature = "http1")]
20use deboa::request::Http1Request;
21#[cfg(feature = "http2")]
22use deboa::request::Http2Request;
23use deboa::{
24    conn::{ConnectionConfig, HttpConnectionDispatcher, ProtoConnection},
25    dns::DnsResolver,
26    errors::{DeboaError, RequestError},
27    response::DeboaResponse,
28    Result,
29};
30#[cfg(feature = "http3")]
31use deboa_h3::compio::Http3Request;
32use http::{Request, Version};
33use hyper_body_utils::HttpBody;
34use std::{marker::PhantomData, time::Duration};
35
36/// Connection pooling for efficient HTTP connections.
37///
38/// This module provides connection pooling functionality to reuse connections
39/// across multiple requests, reducing latency and resource usage.
40///
41/// # Features
42///
43/// - Automatic connection reuse
44/// - Connection lifecycle management
45/// - Thread-safe operation
46/// - Configurable pool size (coming soon)
47pub mod pool;
48
49#[cfg(feature = "http1")]
50pub(crate) type Http1Connection = BaseHttpConnection<Http1Request, HttpBody, HttpBody>;
51#[cfg(feature = "http2")]
52pub(crate) type Http2Connection = BaseHttpConnection<Http2Request, HttpBody, HttpBody>;
53#[cfg(feature = "http3")]
54pub(crate) type Http3Connection = BaseHttpConnection<Http3Request, HttpBody, HttpBody>;
55
56/// Enum that represents the connection type.
57///
58/// # Variants
59///
60/// * `Http1` - The HTTP/1.1 connection.
61/// * `Http2` - The HTTP/2 connection.
62/// * `Http3` - The HTTP/3 connection.
63pub enum DeboaConnection {
64    #[cfg(feature = "http1")]
65    Http1(Box<Http1Connection>),
66    #[cfg(feature = "http2")]
67    Http2(Box<Http2Connection>),
68    #[cfg(feature = "http3")]
69    Http3(Box<Http3Connection>),
70}
71
72impl DeboaConnection {
73    #[cfg(feature = "http1")]
74    pub fn http1(conn: Http1Connection) -> Self {
75        DeboaConnection::Http1(Box::new(conn))
76    }
77
78    #[cfg(feature = "http2")]
79    pub fn http2(conn: Http2Connection) -> Self {
80        DeboaConnection::Http2(Box::new(conn))
81    }
82
83    #[cfg(feature = "http3")]
84    pub fn http3(conn: Http3Connection) -> Self {
85        DeboaConnection::Http3(Box::new(conn))
86    }
87
88    async fn send(&mut self, request: Request<HttpBody>) -> Result<DeboaResponse> {
89        match self {
90            #[cfg(feature = "http1")]
91            DeboaConnection::Http1(ref mut conn) => {
92                let (parts, body) = conn
93                    .sender
94                    .send_request(request)
95                    .await
96                    .map_err(|e| {
97                        DeboaError::Request(RequestError::Send { message: e.to_string() })
98                    })?
99                    .into_parts();
100
101                Ok(DeboaResponse::new(http::Response::from_parts(
102                    parts,
103                    HttpBody::from_incoming(body),
104                )))
105            }
106            #[cfg(feature = "http2")]
107            DeboaConnection::Http2(ref mut conn) => {
108                let (parts, body) = conn
109                    .sender
110                    .send_request(request)
111                    .await
112                    .map_err(|e| {
113                        DeboaError::Request(RequestError::Send { message: e.to_string() })
114                    })?
115                    .into_parts();
116
117                Ok(DeboaResponse::new(http::Response::from_parts(
118                    parts,
119                    HttpBody::from_incoming(body),
120                )))
121            }
122            #[cfg(feature = "http3")]
123            DeboaConnection::Http3(ref mut conn) => {
124                let response = conn
125                    .sender
126                    .send_request(request)
127                    .await
128                    .map_err(|e| {
129                        DeboaError::Request(RequestError::Send { message: e.to_string() })
130                    })?;
131
132                Ok(DeboaResponse::new(response))
133            }
134            #[allow(unreachable_patterns, clippy::needless_return)]
135            _ => {
136                return Err(DeboaError::UnsupportedProtocol);
137            }
138        }
139    }
140}
141
142impl HttpConnectionDispatcher for DeboaConnection {
143    /// Send a request over the connection.
144    ///
145    /// # Arguments
146    ///
147    /// * `url` - The URL to send the request to.
148    /// * `request` - The request to send.
149    ///
150    /// # Returns
151    ///
152    /// * `Result<DeboaResponse>` - The response or error.
153    async fn send_request(
154        &mut self,
155        request: Request<HttpBody>,
156        timeout: Duration,
157    ) -> Result<DeboaResponse> {
158        compio::time::timeout(timeout, self.send(request))
159            .await
160            .map_err(|_| {
161                DeboaError::Request(RequestError::Send { message: "Request timed out".to_string() })
162            })?
163    }
164}
165
166/// Struct that represents the connection.
167///
168/// # Fields
169///
170/// * `sender` - The sender to use.
171pub struct BaseHttpConnection<Sender, ReqBody, ResBody> {
172    pub(crate) sender: Sender,
173    pub(crate) req_body: PhantomData<ReqBody>,
174    pub(crate) res_body: PhantomData<ResBody>,
175}
176
177impl<Sender, ReqBody, ResBody> BaseHttpConnection<Sender, ReqBody, ResBody> {
178    pub(crate) fn new(sender: Sender) -> Self {
179        Self { sender, req_body: PhantomData, res_body: PhantomData }
180    }
181}
182
183pub struct ConnectionFactory {}
184
185impl ConnectionFactory {
186    /// Create a new connection.
187    pub async fn create_connection<'a, D>(
188        config: &'a ConnectionConfig<'a, DeboaIdentity, DeboaCertificate>,
189        dns_resolver: &D,
190    ) -> Result<DeboaConnection>
191    where
192        D: DnsResolver,
193    {
194        let ips = dns_resolver
195            .resolve(
196                config
197                    .host()
198                    .to_string(),
199                config.port(),
200            )
201            .await?;
202        let ips = if config
203            .client_bind_addr()
204            .is_ipv4()
205        {
206            ips.into_iter()
207                .filter(|ip| ip.is_ipv4())
208                .collect::<Vec<_>>()
209        } else {
210            ips.into_iter()
211                .filter(|ip| ip.is_ipv6())
212                .collect::<Vec<_>>()
213        };
214
215        let Some(ip) = ips.first() else {
216            return Err(DeboaError::Request(RequestError::Send {
217                message: format!("No IP addresses found for hostname: {}", config.host()),
218            }));
219        };
220
221        #[cfg(any(feature = "http1", feature = "http2"))]
222        let stream = {
223            use compio::net::TcpStream;
224            use cyper_core::HyperStream;
225            use deboa::errors::ConnectionError;
226
227            let tcp_stream = TcpStream::connect(format!("{}:{}", ip, config.port()))
228                .await
229                .map_err(|e| {
230                    DeboaError::Connection(ConnectionError::Tcp { message: e.to_string() })
231                })?;
232            let use_tls = config.scheme() == "https" || config.scheme() == "wss";
233            if !use_tls {
234                HyperStream::new_plain(tcp_stream)
235            } else {
236                #[cfg(feature = "rust-tls")]
237                {
238                    use crate::client::tls::rustls::tcp::connect;
239                    use crate::client::tls::rustls::TlsConnectionBuilder;
240                    let tls_config = TlsConnectionBuilder::default()
241                        .certificate(config.certificate())
242                        .identity(config.identity())
243                        .build_config()?;
244
245                    HyperStream::new_tls(connect(tls_config, tcp_stream, config.host()).await?)
246                }
247
248                #[cfg(feature = "native-tls")]
249                {
250                    use crate::client::tls::native::TlsConnectionBuilder;
251                    let stream = TlsConnectionBuilder::new(tcp_stream, config.host())
252                        .certificate(config.certificate())
253                        .identity(config.identity())
254                        .connect()
255                        .await?;
256                    HyperStream::new_tls(stream)
257                }
258            }
259        };
260
261        let conn = match config.protocol_version() {
262            #[cfg(feature = "http1")]
263            &Version::HTTP_11 => {
264                let conn = Http1Connection::connect(stream).await?;
265                DeboaConnection::http1(conn)
266            }
267            #[cfg(feature = "http2")]
268            &Version::HTTP_2 => {
269                let conn = Http2Connection::connect(stream).await?;
270                DeboaConnection::http2(conn)
271            }
272            #[cfg(feature = "http3")]
273            &Version::HTTP_3 => {
274                let stream = {
275                    use crate::client::tls::rustls::udp::connect;
276                    #[cfg(feature = "rust-tls")]
277                    use crate::client::tls::rustls::TlsConnectionBuilder;
278                    use compio_quic::Endpoint;
279                    use deboa::errors::ConnectionError;
280                    use std::net::SocketAddr;
281
282                    let mut client_endpoint =
283                        Endpoint::client(SocketAddr::new(*config.client_bind_addr(), 0))
284                            .await
285                            .map_err(|e| {
286                                DeboaError::Connection(ConnectionError::Udp {
287                                    message: e.to_string(),
288                                })
289                            })?;
290
291                    let tls_config = TlsConnectionBuilder::default()
292                        .certificate(config.certificate())
293                        .identity(config.identity())
294                        .build_config()?;
295
296                    connect(
297                        tls_config,
298                        &mut client_endpoint,
299                        SocketAddr::new(*ip, config.port()),
300                        config.host(),
301                    )
302                    .await?
303                };
304
305                let conn = Http3Connection::connect(stream).await?;
306                DeboaConnection::http3(conn)
307            }
308            _ => {
309                return Err(DeboaError::UnsupportedProtocol);
310            }
311        };
312
313        Ok(conn)
314    }
315}