1use crate::Error;
2use 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
24pub 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#[cfg(feature = "esp-mbedtls")]
39pub struct TlsConfig<'a, const RX_SIZE: usize = 4096, const TX_SIZE: usize = 4096> {
40 version: crate::TlsVersion,
42
43 certificates: crate::Certificates<'a>,
45
46 tls_reference: esp_mbedtls::TlsReference<'a>,
48}
49
50#[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#[cfg(feature = "embedded-tls")]
89pub enum TlsVerify<'a> {
90 None,
92 Psk { identity: &'a [u8], psk: &'a [u8] },
94 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 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 #[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 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 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#[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)), }
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 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 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 }
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
469pub 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 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 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
565pub 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 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 pub fn get<'req>(&'req mut self, path: &'req str) -> HttpResourceRequestBuilder<'req, 'res, C, ()> {
610 self.request(Method::GET, path)
611 }
612
613 pub fn post<'req>(&'req mut self, path: &'req str) -> HttpResourceRequestBuilder<'req, 'res, C, ()> {
615 self.request(Method::POST, path)
616 }
617
618 pub fn put<'req>(&'req mut self, path: &'req str) -> HttpResourceRequestBuilder<'req, 'res, C, ()> {
620 self.request(Method::PUT, path)
621 }
622
623 pub fn delete<'req>(&'req mut self, path: &'req str) -> HttpResourceRequestBuilder<'req, 'res, C, ()> {
625 self.request(Method::DELETE, path)
626 }
627
628 pub fn head<'req>(&'req mut self, path: &'req str) -> HttpResourceRequestBuilder<'req, 'res, C, ()> {
630 self.request(Method::HEAD, path)
631 }
632
633 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 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 }
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}