reqwless/
client.rs

1use crate::Error;
2/// Client using embedded-nal-async traits to establish connections and perform HTTP requests.
3///
4use crate::body_writer::{BufferingChunkedBodyWriter, ChunkedBodyWriter, FixedBodyWriter};
5use crate::headers::ContentType;
6use crate::request::*;
7use crate::response::*;
8use buffered_io::asynch::BufferedWrite;
9use core::net::SocketAddr;
10use embedded_io::Error as _;
11use embedded_io::ErrorType;
12use embedded_io_async::{Read, Write};
13use embedded_nal_async::{Dns, TcpConnect};
14#[cfg(feature = "embedded-tls")]
15use embedded_tls::{
16    Aes128GcmSha256, CryptoProvider, NoClock, SignatureScheme, TlsError, TlsVerifier, pki::CertVerifier,
17};
18use nourl::{Url, UrlScheme};
19#[cfg(feature = "embedded-tls")]
20use p256::ecdsa::{DerSignature, signature::SignerMut};
21#[cfg(feature = "embedded-tls")]
22use rand_core::CryptoRngCore;
23
24/// An async HTTP client that can establish a TCP connection and perform
25/// HTTP requests.
26pub struct HttpClient<'a, T, D>
27where
28    T: TcpConnect + 'a,
29    D: Dns + 'a,
30{
31    client: &'a T,
32    dns: &'a D,
33    #[cfg(any(feature = "embedded-tls", feature = "esp-mbedtls"))]
34    tls: Option<TlsConfig<'a>>,
35}
36
37/// Type for TLS configuration of HTTP client.
38#[cfg(feature = "esp-mbedtls")]
39pub struct TlsConfig<'a, const RX_SIZE: usize = 4096, const TX_SIZE: usize = 4096> {
40    /// Minimum TLS version for the connection
41    version: crate::TlsVersion,
42
43    /// Client certificates. See [esp_mbedtls::Certificates]
44    certificates: crate::Certificates<'a>,
45
46    /// A reference to instance of the MbedTLS library.
47    tls_reference: esp_mbedtls::TlsReference<'a>,
48}
49
50/// Type for TLS configuration of HTTP client.
51#[cfg(feature = "embedded-tls")]
52pub struct TlsConfig<'a> {
53    seed: u64,
54    read_buffer: &'a mut [u8],
55    write_buffer: &'a mut [u8],
56    verify: TlsVerify<'a>,
57}
58
59#[cfg(feature = "embedded-tls")]
60struct Provider {
61    rng: rand_chacha::ChaCha8Rng,
62    verifier: CertVerifier<Aes128GcmSha256, NoClock, 4096>,
63}
64
65#[cfg(feature = "embedded-tls")]
66impl CryptoProvider for Provider {
67    type CipherSuite = Aes128GcmSha256;
68    type Signature = DerSignature;
69
70    fn rng(&mut self) -> impl CryptoRngCore {
71        &mut self.rng
72    }
73
74    fn verifier(&mut self) -> Result<&mut impl TlsVerifier<Self::CipherSuite>, TlsError> {
75        Ok(&mut self.verifier)
76    }
77
78    fn signer(&mut self, key_der: &[u8]) -> Result<(impl SignerMut<Self::Signature>, SignatureScheme), TlsError> {
79        use p256::{SecretKey, ecdsa::SigningKey};
80
81        let secret_key = SecretKey::from_sec1_der(key_der).map_err(|_| TlsError::InvalidPrivateKey)?;
82
83        Ok((SigningKey::from(&secret_key), SignatureScheme::EcdsaSecp256r1Sha256))
84    }
85}
86
87/// Supported verification modes.
88#[cfg(feature = "embedded-tls")]
89pub enum TlsVerify<'a> {
90    /// No verification of the remote host
91    None,
92    /// Use pre-shared keys for verifying
93    Psk { identity: &'a [u8], psk: &'a [u8] },
94    /// Use certificates for verifying
95    /// ca: CA cert in DER format
96    /// cert: Optional client cert in DER format (needed only for client verification)
97    /// key: Optional client privkey in DER format (needed only for client verification)
98    Certificate {
99        ca: &'a [u8],
100        cert: Option<&'a [u8]>,
101        key: Option<&'a [u8]>,
102    },
103}
104
105#[cfg(feature = "embedded-tls")]
106impl<'a> TlsConfig<'a> {
107    pub fn new(seed: u64, read_buffer: &'a mut [u8], write_buffer: &'a mut [u8], verify: TlsVerify<'a>) -> Self {
108        Self {
109            seed,
110            write_buffer,
111            read_buffer,
112            verify,
113        }
114    }
115}
116
117#[cfg(feature = "esp-mbedtls")]
118impl<'a, const RX_SIZE: usize, const TX_SIZE: usize> TlsConfig<'a, RX_SIZE, TX_SIZE> {
119    pub fn new(
120        version: crate::TlsVersion,
121        certificates: crate::Certificates<'a>,
122        tls_reference: crate::TlsReference<'a>,
123    ) -> Self {
124        Self {
125            version,
126            certificates,
127            tls_reference,
128        }
129    }
130}
131
132impl<'a, T, D> HttpClient<'a, T, D>
133where
134    T: TcpConnect + 'a,
135    D: Dns + 'a,
136{
137    /// Create a new HTTP client for a given connection handle and a target host.
138    pub fn new(client: &'a T, dns: &'a D) -> Self {
139        Self {
140            client,
141            dns,
142            #[cfg(any(feature = "embedded-tls", feature = "esp-mbedtls"))]
143            tls: None,
144        }
145    }
146
147    /// Create a new HTTP client for a given connection handle and a target host.
148    #[cfg(any(feature = "embedded-tls", feature = "esp-mbedtls"))]
149    pub fn new_with_tls(client: &'a T, dns: &'a D, tls: TlsConfig<'a>) -> Self {
150        Self {
151            client,
152            dns,
153            tls: Some(tls),
154        }
155    }
156
157    async fn connect<'conn>(
158        &'conn mut self,
159        url: &Url<'_>,
160    ) -> Result<HttpConnection<'conn, T::Connection<'conn>>, Error> {
161        let host = url.host();
162        let port = url.port_or_default();
163
164        let remote = self
165            .dns
166            .get_host_by_name(host, embedded_nal_async::AddrType::Either)
167            .await
168            .map_err(|_| Error::Dns)?;
169
170        let conn = self
171            .client
172            .connect(SocketAddr::new(remote, port))
173            .await
174            .map_err(|e| e.kind())?;
175
176        if url.scheme() == UrlScheme::HTTPS {
177            #[cfg(feature = "esp-mbedtls")]
178            if let Some(tls) = self.tls.as_mut() {
179                let mut servername = host.as_bytes().to_vec();
180                servername.push(0);
181                let mut session = esp_mbedtls::asynch::Session::new(
182                    conn,
183                    esp_mbedtls::Mode::Client {
184                        servername: unsafe { core::ffi::CStr::from_bytes_with_nul_unchecked(&servername) },
185                    },
186                    tls.version,
187                    tls.certificates,
188                    tls.tls_reference,
189                )?;
190
191                session.connect().await?;
192                Ok(HttpConnection::Tls(session))
193            } else {
194                Ok(HttpConnection::Plain(conn))
195            }
196
197            #[cfg(feature = "embedded-tls")]
198            if let Some(tls) = self.tls.as_mut() {
199                use embedded_tls::{TlsConfig, TlsContext, UnsecureProvider};
200                use rand_chacha::ChaCha8Rng;
201                use rand_core::SeedableRng;
202                let rng = ChaCha8Rng::seed_from_u64(tls.seed);
203                let mut config = TlsConfig::new().with_server_name(url.host());
204
205                let mut conn: embedded_tls::TlsConnection<'conn, T::Connection<'conn>, embedded_tls::Aes128GcmSha256> =
206                    embedded_tls::TlsConnection::new(conn, tls.read_buffer, tls.write_buffer);
207
208                match tls.verify {
209                    TlsVerify::None => {
210                        use embedded_tls::UnsecureProvider;
211                        conn.open(TlsContext::new(&config, UnsecureProvider::new(rng))).await?;
212                    }
213                    TlsVerify::Psk { identity, psk } => {
214                        use embedded_tls::UnsecureProvider;
215                        config = config.with_psk(psk, &[identity]);
216                        conn.open(TlsContext::new(&config, UnsecureProvider::new(rng))).await?;
217                    }
218                    TlsVerify::Certificate { ca, cert, key } => {
219                        use embedded_tls::Certificate;
220
221                        config = config.with_ca(Certificate::X509(ca));
222
223                        if let Some(cert) = cert {
224                            config = config.with_cert(Certificate::X509(cert));
225                        }
226
227                        if let Some(key) = key {
228                            let k = pkcs8::PrivateKeyInfo::try_from(key).map_err(|_| TlsError::InvalidPrivateKey)?;
229                            config = config.with_priv_key(k.private_key);
230                        }
231
232                        conn.open(TlsContext::new(
233                            &config,
234                            Provider {
235                                rng: rng,
236                                verifier: embedded_tls::pki::CertVerifier::new(),
237                            },
238                        ))
239                        .await?;
240                    }
241                }
242
243                Ok(HttpConnection::Tls(conn))
244            } else {
245                Ok(HttpConnection::Plain(conn))
246            }
247            #[cfg(all(not(feature = "embedded-tls"), not(feature = "esp-mbedtls")))]
248            Err(Error::InvalidUrl(nourl::Error::UnsupportedScheme))
249        } else {
250            #[cfg(feature = "embedded-tls")]
251            match self.tls.as_mut() {
252                Some(tls) => Ok(HttpConnection::PlainBuffered(BufferedWrite::new(
253                    conn,
254                    tls.write_buffer,
255                ))),
256                None => Ok(HttpConnection::Plain(conn)),
257            }
258            #[cfg(not(feature = "embedded-tls"))]
259            Ok(HttpConnection::Plain(conn))
260        }
261    }
262
263    /// Create a single http request.
264    pub async fn request<'conn>(
265        &'conn mut self,
266        method: Method,
267        url: &'conn str,
268    ) -> Result<HttpRequestHandle<'conn, T::Connection<'conn>, ()>, Error> {
269        let url = Url::parse(url)?;
270        let conn = self.connect(&url).await?;
271        Ok(HttpRequestHandle {
272            conn,
273            request: Some(Request::new(method, url.path()).host(url.host())),
274        })
275    }
276
277    /// Create a connection to a server with the provided `resource_url`.
278    /// The path in the url is considered the base path for subsequent requests.
279    pub async fn resource<'res>(
280        &'res mut self,
281        resource_url: &'res str,
282    ) -> Result<HttpResource<'res, T::Connection<'res>>, Error> {
283        let resource_url = Url::parse(resource_url)?;
284        let conn = self.connect(&resource_url).await?;
285        Ok(HttpResource {
286            conn,
287            host: resource_url.host(),
288            base_path: resource_url.path(),
289        })
290    }
291}
292
293/// Represents a HTTP connection that may be encrypted or unencrypted.
294#[allow(clippy::large_enum_variant)]
295pub enum HttpConnection<'conn, C>
296where
297    C: Read + Write,
298{
299    Plain(C),
300    PlainBuffered(BufferedWrite<'conn, C>),
301    #[cfg(feature = "esp-mbedtls")]
302    Tls(esp_mbedtls::asynch::Session<'conn, C>),
303    #[cfg(feature = "embedded-tls")]
304    Tls(embedded_tls::TlsConnection<'conn, C, embedded_tls::Aes128GcmSha256>),
305    #[cfg(all(not(feature = "embedded-tls"), not(feature = "esp-mbedtls")))]
306    Tls((&'conn mut (), core::convert::Infallible)), // Variant is impossible to create, but we need it to avoid "unused lifetime" warning
307}
308
309#[cfg(feature = "defmt")]
310impl<C> defmt::Format for HttpConnection<'_, C>
311where
312    C: Read + Write,
313{
314    fn format(&self, fmt: defmt::Formatter) {
315        match self {
316            HttpConnection::Plain(_) => defmt::write!(fmt, "Plain"),
317            HttpConnection::PlainBuffered(_) => defmt::write!(fmt, "PlainBuffered"),
318            HttpConnection::Tls(_) => defmt::write!(fmt, "Tls"),
319        }
320    }
321}
322
323impl<C> core::fmt::Debug for HttpConnection<'_, C>
324where
325    C: Read + Write,
326{
327    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
328        match self {
329            HttpConnection::Plain(_) => f.debug_tuple("Plain").finish(),
330            HttpConnection::PlainBuffered(_) => f.debug_tuple("PlainBuffered").finish(),
331            HttpConnection::Tls(_) => f.debug_tuple("Tls").finish(),
332        }
333    }
334}
335
336impl<'conn, T> HttpConnection<'conn, T>
337where
338    T: Read + Write,
339{
340    /// Turn the request into a buffered request.
341    ///
342    /// This is only relevant if no TLS is used, as `embedded-tls` buffers internally and we reuse
343    /// its buffer for non-TLS connections.
344    pub fn into_buffered<'buf>(self, tx_buf: &'buf mut [u8]) -> HttpConnection<'buf, T>
345    where
346        'conn: 'buf,
347    {
348        match self {
349            HttpConnection::Plain(conn) => HttpConnection::PlainBuffered(BufferedWrite::new(conn, tx_buf)),
350            HttpConnection::PlainBuffered(conn) => HttpConnection::PlainBuffered(conn),
351            HttpConnection::Tls(tls) => HttpConnection::Tls(tls),
352        }
353    }
354
355    /// Send a request on an established connection.
356    ///
357    /// The request is sent in its raw form without any base path from the resource.
358    /// The response headers are stored in the provided rx_buf, which should be sized to contain at least the response headers.
359    ///
360    /// The response is returned.
361    pub async fn send<'req, 'buf, B: RequestBody>(
362        &'conn mut self,
363        request: Request<'req, B>,
364        rx_buf: &'buf mut [u8],
365    ) -> Result<Response<'conn, 'buf, HttpConnection<'conn, T>>, Error> {
366        self.write_request(&request).await?;
367        self.flush().await?;
368        Response::read(self, request.method, rx_buf).await
369    }
370
371    async fn write_request<'req, B: RequestBody>(&mut self, request: &Request<'req, B>) -> Result<(), Error> {
372        request.write_header(self).await?;
373
374        if let Some(body) = request.body.as_ref() {
375            match body.len() {
376                Some(0) => {
377                    // Empty body
378                }
379                Some(len) => {
380                    trace!("Writing not-chunked body");
381                    let mut writer = FixedBodyWriter::new(self);
382                    body.write(&mut writer).await.map_err(|e| e.kind())?;
383
384                    if writer.written() != len {
385                        return Err(Error::IncorrectBodyWritten);
386                    }
387                }
388                None => {
389                    trace!("Writing chunked body");
390                    match self {
391                        HttpConnection::Plain(c) => {
392                            let mut writer = ChunkedBodyWriter::new(c);
393                            body.write(&mut writer).await?;
394                            writer.terminate().await.map_err(|e| e.kind())?;
395                        }
396                        HttpConnection::PlainBuffered(buffered) => {
397                            let (conn, buf, unwritten) = buffered.split();
398                            let mut writer = BufferingChunkedBodyWriter::new_with_data(conn, buf, unwritten);
399                            body.write(&mut writer).await?;
400                            writer.terminate().await.map_err(|e| e.kind())?;
401                            buffered.clear();
402                        }
403                        #[cfg(any(feature = "embedded-tls", feature = "esp-mbedtls"))]
404                        HttpConnection::Tls(c) => {
405                            let mut writer = ChunkedBodyWriter::new(c);
406                            body.write(&mut writer).await?;
407                            writer.terminate().await.map_err(|e| e.kind())?;
408                        }
409                        #[cfg(all(not(feature = "embedded-tls"), not(feature = "esp-mbedtls")))]
410                        HttpConnection::Tls(_) => unreachable!(),
411                    };
412                }
413            }
414        }
415        Ok(())
416    }
417}
418
419impl<T> ErrorType for HttpConnection<'_, T>
420where
421    T: Read + Write,
422{
423    type Error = embedded_io::ErrorKind;
424}
425
426impl<T> Read for HttpConnection<'_, T>
427where
428    T: Read + Write,
429{
430    async fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
431        match self {
432            Self::Plain(conn) => conn.read(buf).await.map_err(|e| e.kind()),
433            Self::PlainBuffered(conn) => conn.read(buf).await.map_err(|e| e.kind()),
434            #[cfg(any(feature = "embedded-tls", feature = "esp-mbedtls"))]
435            Self::Tls(conn) => conn.read(buf).await.map_err(|e| e.kind()),
436            #[cfg(not(any(feature = "embedded-tls", feature = "esp-mbedtls")))]
437            _ => unreachable!(),
438        }
439    }
440}
441
442impl<T> Write for HttpConnection<'_, T>
443where
444    T: Read + Write,
445{
446    async fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
447        match self {
448            Self::Plain(conn) => conn.write(buf).await.map_err(|e| e.kind()),
449            Self::PlainBuffered(conn) => conn.write(buf).await.map_err(|e| e.kind()),
450            #[cfg(any(feature = "embedded-tls", feature = "esp-mbedtls"))]
451            Self::Tls(conn) => conn.write(buf).await.map_err(|e| e.kind()),
452            #[cfg(not(any(feature = "embedded-tls", feature = "esp-mbedtls")))]
453            _ => unreachable!(),
454        }
455    }
456
457    async fn flush(&mut self) -> Result<(), Self::Error> {
458        match self {
459            Self::Plain(conn) => conn.flush().await.map_err(|e| e.kind()),
460            Self::PlainBuffered(conn) => conn.flush().await.map_err(|e| e.kind()),
461            #[cfg(any(feature = "embedded-tls", feature = "esp-mbedtls"))]
462            Self::Tls(conn) => conn.flush().await.map_err(|e| e.kind()),
463            #[cfg(not(any(feature = "embedded-tls", feature = "esp-mbedtls")))]
464            _ => unreachable!(),
465        }
466    }
467}
468
469/// A HTTP request handle
470///
471/// The underlying connection is closed when drop'ed.
472pub struct HttpRequestHandle<'conn, C, B>
473where
474    C: Read + Write,
475    B: RequestBody,
476{
477    pub conn: HttpConnection<'conn, C>,
478    request: Option<DefaultRequestBuilder<'conn, B>>,
479}
480
481impl<'conn, C, B> HttpRequestHandle<'conn, C, B>
482where
483    C: Read + Write,
484    B: RequestBody,
485{
486    /// Turn the request into a buffered request.
487    ///
488    /// This is only relevant if no TLS is used, as `embedded-tls` buffers internally and we reuse
489    /// its buffer for non-TLS connections.
490    pub fn into_buffered<'buf>(self, tx_buf: &'buf mut [u8]) -> HttpRequestHandle<'buf, C, B>
491    where
492        'conn: 'buf,
493    {
494        HttpRequestHandle {
495            conn: self.conn.into_buffered(tx_buf),
496            request: self.request,
497        }
498    }
499
500    /// Send the request.
501    ///
502    /// The response headers are stored in the provided rx_buf, which should be sized to contain at least the response headers.
503    ///
504    /// The response is returned.
505    pub async fn send<'req, 'buf>(
506        &'req mut self,
507        rx_buf: &'buf mut [u8],
508    ) -> Result<Response<'req, 'buf, HttpConnection<'conn, C>>, Error> {
509        let request = self.request.take().ok_or(Error::AlreadySent)?.build();
510        self.conn.write_request(&request).await?;
511        self.conn.flush().await?;
512        Response::read(&mut self.conn, request.method, rx_buf).await
513    }
514}
515
516impl<'m, C, B> RequestBuilder<'m, B> for HttpRequestHandle<'m, C, B>
517where
518    C: Read + Write,
519    B: RequestBody,
520{
521    type WithBody<T: RequestBody> = HttpRequestHandle<'m, C, T>;
522
523    fn headers(mut self, headers: &'m [(&'m str, &'m str)]) -> Self {
524        self.request = Some(self.request.unwrap().headers(headers));
525        self
526    }
527
528    fn path(mut self, path: &'m str) -> Self {
529        self.request = Some(self.request.unwrap().path(path));
530        self
531    }
532
533    fn body<T: RequestBody>(self, body: T) -> Self::WithBody<T> {
534        HttpRequestHandle {
535            conn: self.conn,
536            request: Some(self.request.unwrap().body(body)),
537        }
538    }
539
540    fn host(mut self, host: &'m str) -> Self {
541        self.request = Some(self.request.unwrap().host(host));
542        self
543    }
544
545    fn content_type(mut self, content_type: ContentType) -> Self {
546        self.request = Some(self.request.unwrap().content_type(content_type));
547        self
548    }
549
550    fn accept(mut self, content_type: ContentType) -> Self {
551        self.request = Some(self.request.unwrap().accept(content_type));
552        self
553    }
554
555    fn basic_auth(mut self, username: &'m str, password: &'m str) -> Self {
556        self.request = Some(self.request.unwrap().basic_auth(username, password));
557        self
558    }
559
560    fn build(self) -> Request<'m, B> {
561        self.request.unwrap().build()
562    }
563}
564
565/// A HTTP resource describing a scoped endpoint
566///
567/// The underlying connection is closed when drop'ed.
568pub struct HttpResource<'res, C>
569where
570    C: Read + Write,
571{
572    pub conn: HttpConnection<'res, C>,
573    pub host: &'res str,
574    pub base_path: &'res str,
575}
576
577impl<'res, C> HttpResource<'res, C>
578where
579    C: Read + Write,
580{
581    /// Turn the resource into a buffered resource
582    ///
583    /// This is only relevant if no TLS is used, as `embedded-tls` buffers internally and we reuse
584    /// its buffer for non-TLS connections.
585    pub fn into_buffered<'buf>(self, tx_buf: &'buf mut [u8]) -> HttpResource<'buf, C>
586    where
587        'res: 'buf,
588    {
589        HttpResource {
590            conn: self.conn.into_buffered(tx_buf),
591            host: self.host,
592            base_path: self.base_path,
593        }
594    }
595
596    pub fn request<'req>(
597        &'req mut self,
598        method: Method,
599        path: &'req str,
600    ) -> HttpResourceRequestBuilder<'req, 'res, C, ()> {
601        HttpResourceRequestBuilder {
602            conn: &mut self.conn,
603            request: Request::new(method, path).host(self.host),
604            base_path: self.base_path,
605        }
606    }
607
608    /// Create a new scoped GET http request.
609    pub fn get<'req>(&'req mut self, path: &'req str) -> HttpResourceRequestBuilder<'req, 'res, C, ()> {
610        self.request(Method::GET, path)
611    }
612
613    /// Create a new scoped POST http request.
614    pub fn post<'req>(&'req mut self, path: &'req str) -> HttpResourceRequestBuilder<'req, 'res, C, ()> {
615        self.request(Method::POST, path)
616    }
617
618    /// Create a new scoped PUT http request.
619    pub fn put<'req>(&'req mut self, path: &'req str) -> HttpResourceRequestBuilder<'req, 'res, C, ()> {
620        self.request(Method::PUT, path)
621    }
622
623    /// Create a new scoped DELETE http request.
624    pub fn delete<'req>(&'req mut self, path: &'req str) -> HttpResourceRequestBuilder<'req, 'res, C, ()> {
625        self.request(Method::DELETE, path)
626    }
627
628    /// Create a new scoped HEAD http request.
629    pub fn head<'req>(&'req mut self, path: &'req str) -> HttpResourceRequestBuilder<'req, 'res, C, ()> {
630        self.request(Method::HEAD, path)
631    }
632
633    /// Send a request to a resource.
634    ///
635    /// The base path of the resource is prepended to the request path.
636    /// The response headers are stored in the provided rx_buf, which should be sized to contain at least the response headers.
637    ///
638    /// The response is returned.
639    pub async fn send<'req, 'buf, B: RequestBody>(
640        &'req mut self,
641        mut request: Request<'req, B>,
642        rx_buf: &'buf mut [u8],
643    ) -> Result<Response<'req, 'buf, HttpConnection<'res, C>>, Error> {
644        request.base_path = Some(self.base_path);
645        self.conn.write_request(&request).await?;
646        self.conn.flush().await?;
647        Response::read(&mut self.conn, request.method, rx_buf).await
648    }
649}
650
651pub struct HttpResourceRequestBuilder<'req, 'conn, C, B>
652where
653    C: Read + Write,
654    B: RequestBody,
655{
656    conn: &'req mut HttpConnection<'conn, C>,
657    base_path: &'req str,
658    request: DefaultRequestBuilder<'req, B>,
659}
660
661impl<'req, 'conn, C, B> HttpResourceRequestBuilder<'req, 'conn, C, B>
662where
663    C: Read + Write,
664    B: RequestBody,
665{
666    /// Send the request.
667    ///
668    /// The base path of the resource is prepended to the request path.
669    /// The response headers are stored in the provided rx_buf, which should be sized to contain at least the response headers.
670    ///
671    /// The response is returned.
672    pub async fn send<'buf>(
673        self,
674        rx_buf: &'buf mut [u8],
675    ) -> Result<Response<'req, 'buf, HttpConnection<'conn, C>>, Error> {
676        let conn = self.conn;
677        let mut request = self.request.build();
678        request.base_path = Some(self.base_path);
679        conn.write_request(&request).await?;
680        conn.flush().await?;
681        Response::read(conn, request.method, rx_buf).await
682    }
683}
684
685impl<'req, 'conn, C, B> RequestBuilder<'req, B> for HttpResourceRequestBuilder<'req, 'conn, C, B>
686where
687    C: Read + Write,
688    B: RequestBody,
689{
690    type WithBody<T: RequestBody> = HttpResourceRequestBuilder<'req, 'conn, C, T>;
691
692    fn headers(mut self, headers: &'req [(&'req str, &'req str)]) -> Self {
693        self.request = self.request.headers(headers);
694        self
695    }
696
697    fn path(mut self, path: &'req str) -> Self {
698        self.request = self.request.path(path);
699        self
700    }
701
702    fn body<T: RequestBody>(self, body: T) -> Self::WithBody<T> {
703        HttpResourceRequestBuilder {
704            conn: self.conn,
705            base_path: self.base_path,
706            request: self.request.body(body),
707        }
708    }
709
710    fn host(mut self, host: &'req str) -> Self {
711        self.request = self.request.host(host);
712        self
713    }
714
715    fn content_type(mut self, content_type: ContentType) -> Self {
716        self.request = self.request.content_type(content_type);
717        self
718    }
719
720    fn accept(mut self, content_type: ContentType) -> Self {
721        self.request = self.request.accept(content_type);
722        self
723    }
724
725    fn basic_auth(mut self, username: &'req str, password: &'req str) -> Self {
726        self.request = self.request.basic_auth(username, password);
727        self
728    }
729
730    fn build(self) -> Request<'req, B> {
731        self.request.build()
732    }
733}
734
735#[cfg(test)]
736mod tests {
737    use core::convert::Infallible;
738
739    use super::*;
740
741    #[derive(Default)]
742    struct VecBuffer(Vec<u8>);
743
744    impl ErrorType for VecBuffer {
745        type Error = Infallible;
746    }
747
748    impl Read for VecBuffer {
749        async fn read(&mut self, _buf: &mut [u8]) -> Result<usize, Self::Error> {
750            unreachable!()
751        }
752    }
753
754    impl Write for VecBuffer {
755        async fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
756            self.0.extend_from_slice(buf);
757            Ok(buf.len())
758        }
759
760        async fn flush(&mut self) -> Result<(), Self::Error> {
761            self.0.flush().await
762        }
763    }
764
765    #[tokio::test]
766    async fn with_empty_body() {
767        let mut buffer = VecBuffer::default();
768        let mut conn = HttpConnection::Plain(&mut buffer);
769
770        let request = Request::new(Method::POST, "/").body([].as_slice()).build();
771        conn.write_request(&request).await.unwrap();
772
773        assert_eq!(b"POST / HTTP/1.1\r\nContent-Length: 0\r\n\r\n", buffer.0.as_slice());
774    }
775
776    #[tokio::test]
777    async fn with_known_body() {
778        let mut buffer = VecBuffer::default();
779        let mut conn = HttpConnection::Plain(&mut buffer);
780
781        let request = Request::new(Method::POST, "/").body(b"BODY".as_slice()).build();
782        conn.write_request(&request).await.unwrap();
783
784        assert_eq!(b"POST / HTTP/1.1\r\nContent-Length: 4\r\n\r\nBODY", buffer.0.as_slice());
785    }
786
787    struct ChunkedBody(&'static [&'static [u8]]);
788
789    impl RequestBody for ChunkedBody {
790        fn len(&self) -> Option<usize> {
791            None // Unknown length: triggers chunked body
792        }
793
794        async fn write<W: Write>(&self, writer: &mut W) -> Result<(), W::Error> {
795            for chunk in self.0 {
796                writer.write_all(chunk).await?;
797            }
798            Ok(())
799        }
800    }
801
802    #[tokio::test]
803    async fn with_unknown_body_unbuffered() {
804        let mut buffer = VecBuffer::default();
805        let mut conn = HttpConnection::Plain(&mut buffer);
806
807        static CHUNKS: [&'static [u8]; 2] = [b"PART1", b"PART2"];
808        let request = Request::new(Method::POST, "/").body(ChunkedBody(&CHUNKS)).build();
809        conn.write_request(&request).await.unwrap();
810
811        assert_eq!(
812            b"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nPART1\r\n5\r\nPART2\r\n0\r\n\r\n",
813            buffer.0.as_slice()
814        );
815    }
816
817    #[tokio::test]
818    async fn with_unknown_body_buffered() {
819        let mut buffer = VecBuffer::default();
820        let mut tx_buf = [0; 1024];
821        let mut conn = HttpConnection::Plain(&mut buffer).into_buffered(&mut tx_buf);
822
823        static CHUNKS: [&'static [u8]; 2] = [b"PART1", b"PART2"];
824        let request = Request::new(Method::POST, "/").body(ChunkedBody(&CHUNKS)).build();
825        conn.write_request(&request).await.unwrap();
826
827        assert_eq!(
828            b"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\na\r\nPART1PART2\r\n0\r\n\r\n",
829            buffer.0.as_slice()
830        );
831    }
832}