├── .gitignore ├── rustfmt.toml ├── src ├── body_writer │ ├── mod.rs │ ├── fixed.rs │ ├── chunked.rs │ └── buffering_chunked.rs ├── response │ ├── fixed_length.rs │ ├── chunked.rs │ └── mod.rs ├── headers.rs ├── reader.rs ├── lib.rs ├── fmt.rs ├── request.rs └── client.rs ├── tests ├── certs │ ├── key.pem │ └── cert.pem ├── orange.json ├── request.rs ├── connection.rs └── client.rs ├── .github └── workflows │ └── ci.yaml ├── Cargo.toml ├── CHANGELOG.md ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width=120 2 | edition = "2021" 3 | -------------------------------------------------------------------------------- /src/body_writer/mod.rs: -------------------------------------------------------------------------------- 1 | mod buffering_chunked; 2 | mod chunked; 3 | mod fixed; 4 | 5 | pub use buffering_chunked::BufferingChunkedBodyWriter; 6 | pub use chunked::ChunkedBodyWriter; 7 | pub use fixed::FixedBodyWriter; 8 | -------------------------------------------------------------------------------- /tests/certs/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+N1TvmZPYJn+Zr/H 3 | MnAA+Tj9E3d80dfBkMi0771MO5ChRANCAATK5bnTHYcZ2FgPoTBlQWQZHLbw0LST 4 | a+f3/p7vuUeIUjeeWkcoMy0CDnt3I65p8kuQ+9pvmN9YygvT/BT8t//M 5 | -----END PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /tests/certs/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICKDCCAc6gAwIBAgIUaptPaaO7FrO+ER4qLqOns4SiCoswCgYIKoZIzj0EAwIw 3 | QjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UECgwT 4 | RGVmYXVsdCBDb21wYW55IEx0ZDAgFw0yMjEwMTAxMzM5MjlaGA8yMDUyMTEyMTEz 5 | MzkyOVowcjELMAkGA1UEBhMCTk8xDjAMBgNVBAgMBUhhbWFyMQ4wDAYDVQQHDAVI 6 | YW1hcjEYMBYGA1UECgwPR2xvYmFsIFNlY3VyaXR5MRUwEwYDVQQLDAxIb2xzZXRi 7 | YWtrZW4xEjAQBgNVBAMMCTEyNy4wLjAuMTBZMBMGByqGSM49AgEGCCqGSM49AwEH 8 | A0IABMrludMdhxnYWA+hMGVBZBkctvDQtJNr5/f+nu+5R4hSN55aRygzLQIOe3cj 9 | rmnyS5D72m+Y31jKC9P8FPy3/8yjcDBuMB8GA1UdIwQYMBaAFLhJjHTOs/fIADvD 10 | Bd2I+hXp89lOMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMBQGA1UdEQQNMAuCCWxv 11 | Y2FsaG9zdDAdBgNVHQ4EFgQUhVW31o5frrZoYqV7xZqEnNiYKe4wCgYIKoZIzj0E 12 | AwIDSAAwRQIgCkr4VgZ9TWvxLzUuTnzcjZ14FKESp8e5lkgbMwAc1hoCIQCXt+kg 13 | 35L2/0F3h+kDKKT3drkR5huYHnx++ds9RKF2tg== 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /src/body_writer/fixed.rs: -------------------------------------------------------------------------------- 1 | use embedded_io::ErrorType; 2 | use embedded_io_async::Write; 3 | 4 | pub struct FixedBodyWriter(C, usize); 5 | 6 | impl FixedBodyWriter 7 | where 8 | C: Write, 9 | { 10 | pub fn new(conn: C) -> Self { 11 | Self(conn, 0) 12 | } 13 | 14 | pub fn written(&self) -> usize { 15 | self.1 16 | } 17 | } 18 | 19 | impl ErrorType for FixedBodyWriter 20 | where 21 | C: Write, 22 | { 23 | type Error = C::Error; 24 | } 25 | 26 | impl Write for FixedBodyWriter 27 | where 28 | C: Write, 29 | { 30 | async fn write(&mut self, buf: &[u8]) -> Result { 31 | let written = self.0.write(buf).await?; 32 | self.1 += written; 33 | Ok(written) 34 | } 35 | 36 | async fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { 37 | self.0.write_all(buf).await?; 38 | self.1 += buf.len(); 39 | Ok(()) 40 | } 41 | 42 | async fn flush(&mut self) -> Result<(), Self::Error> { 43 | self.0.flush().await 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | # Run on the main branch 6 | branches: 7 | - main 8 | # Releases are tags named 'v', and must have the "major.minor.micro", for example: "0.1.0". 9 | # Release candidates are tagged as `v-rc`, for example: "0.1.0-rc1". 10 | tags: 11 | - "v*" 12 | # Also on PRs, just be careful not to publish anything 13 | pull_request: 14 | types: [opened, synchronize, reopened, ready_for_review] 15 | 16 | # Cancel any currently running workflows from the same PR, branch, or 17 | # tag when a new workflow is triggered. 18 | # 19 | # https://stackoverflow.com/a/66336834 20 | concurrency: 21 | cancel-in-progress: true 22 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 23 | 24 | jobs: 25 | ci: 26 | runs-on: ubuntu-24.04 27 | if: github.event.pull_request.draft == false 28 | 29 | steps: 30 | - uses: actions/checkout@v3 31 | 32 | - name: Build 33 | run: cargo check 34 | 35 | - name: Clippy 36 | run: cargo clippy 37 | 38 | - name: Test 39 | run: | 40 | cargo test 41 | cargo test --no-default-features 42 | -------------------------------------------------------------------------------- /tests/orange.json: -------------------------------------------------------------------------------- 1 | [{"word":"orange","phonetic":"/ˈɔɹɪ̈nd͡ʒ/","phonetics":[{"text":"/ˈɔɹɪ̈nd͡ʒ/","audio":""},{"text":"/ˈɒɹɨn(d)ʒ/","audio":""},{"text":"/ˈɑɹɪ̈nd͡ʒ/","audio":""}],"meanings":[{"partOfSpeech":"noun","definitions":[{"definition":"An evergreen tree of the genus Citrus such as Citrus sinensis.","synonyms":[],"antonyms":[]},{"definition":"The fruit of an orange tree; a citrus fruit with a slightly sour flavour.","synonyms":[],"antonyms":[]},{"definition":"The colour of a ripe fruit of an orange tree, midway between red and yellow.","synonyms":["yellowred"],"antonyms":[]},{"definition":"Orange juice.","synonyms":[],"antonyms":[]},{"definition":"Orange coloured and flavoured cordial.","synonyms":[],"antonyms":[]},{"definition":"Orange coloured and flavoured soft drink.","synonyms":[],"antonyms":[]}],"synonyms":["yellowred"],"antonyms":[]},{"partOfSpeech":"verb","definitions":[{"definition":"To color orange.","synonyms":[],"antonyms":[]},{"definition":"To become orange.","synonyms":[],"antonyms":[]}],"synonyms":[],"antonyms":[]},{"partOfSpeech":"adjective","definitions":[{"definition":"Having the colour of the fruit of an orange tree; yellowred; reddish-yellow.","synonyms":[],"antonyms":[]}],"synonyms":[],"antonyms":["nonorange"]}],"license":{"name":"CC BY-SA 3.0","url":"https://creativecommons.org/licenses/by-sa/3.0"},"sourceUrls":["https://en.wiktionary.org/wiki/orange"]}] -------------------------------------------------------------------------------- /src/response/fixed_length.rs: -------------------------------------------------------------------------------- 1 | use embedded_io_async::{BufRead, Error as _, ErrorType, Read}; 2 | 3 | use crate::Error; 4 | 5 | /// Fixed length response body reader 6 | pub struct FixedLengthBodyReader { 7 | pub raw_body: B, 8 | pub remaining: usize, 9 | } 10 | 11 | impl ErrorType for FixedLengthBodyReader { 12 | type Error = Error; 13 | } 14 | 15 | impl Read for FixedLengthBodyReader 16 | where 17 | C: Read, 18 | { 19 | async fn read(&mut self, buf: &mut [u8]) -> Result { 20 | if self.remaining == 0 { 21 | return Ok(0); 22 | } 23 | 24 | let read = self.raw_body.read(buf).await.map_err(|e| Error::Network(e.kind()))?; 25 | self.remaining -= read; 26 | 27 | Ok(read) 28 | } 29 | } 30 | 31 | impl BufRead for FixedLengthBodyReader 32 | where 33 | C: BufRead + Read, 34 | { 35 | async fn fill_buf(&mut self) -> Result<&[u8], Self::Error> { 36 | if self.remaining == 0 { 37 | return Ok(&[]); 38 | } 39 | 40 | let loaded = self 41 | .raw_body 42 | .fill_buf() 43 | .await 44 | .map_err(|e| Error::Network(e.kind())) 45 | .map(|data| &data[..data.len().min(self.remaining)])?; 46 | 47 | if loaded.is_empty() { 48 | return Err(Error::ConnectionAborted); 49 | } 50 | 51 | Ok(loaded) 52 | } 53 | 54 | fn consume(&mut self, amt: usize) { 55 | let amt = amt.min(self.remaining); 56 | self.remaining -= amt; 57 | self.raw_body.consume(amt) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "reqwless" 3 | version = "0.13.0" 4 | edition = "2021" 5 | resolver = "2" 6 | rust-version = "1.77" 7 | description = "HTTP client for embedded devices" 8 | documentation = "https://docs.rs/reqwless" 9 | readme = "README.md" 10 | homepage = "https://drogue.io" 11 | repository = "https://github.com/drogue-iot/reqwless" 12 | license = "Apache-2.0" 13 | keywords = ["embedded", "async", "http", "no_std"] 14 | exclude = [".github"] 15 | 16 | [dependencies] 17 | buffered-io = { version = "0.6.0" } 18 | embedded-io = { version = "0.7" } 19 | embedded-io-async = { version = "0.7" } 20 | embedded-nal-async = "0.9" 21 | httparse = { version = "1.8.0", default-features = false } 22 | heapless = "0.9" 23 | hex = { version = "0.4", default-features = false } 24 | base64 = { version = "0.21.0", default-features = false } 25 | rand_core = { version = "0.6", default-features = true } 26 | log = { version = "0.4", optional = true } 27 | defmt = { version = "0.3", optional = true } 28 | embedded-tls = { version = "0.17", git = "https://github.com/drogue-iot/embedded-tls", rev = "9ebe54a5ad71dbc3c7de464bbbafa7da04587e52", default-features = false, optional = true } 29 | rand_chacha = { version = "0.3", default-features = false } 30 | nourl = "0.1.2" 31 | esp-mbedtls = { version = "0.1", git = "https://github.com/esp-rs/esp-mbedtls.git", features = [ 32 | "async", 33 | ], optional = true } 34 | 35 | [dev-dependencies] 36 | hyper = { version = "0.14.23", features = ["full"] } 37 | tokio = { version = "1.21.2", features = ["full"] } 38 | tokio-rustls = { version = "0.23.4" } 39 | futures-util = { version = "0.3" } 40 | embedded-io-async = { version = "0.7", features = ["std"] } 41 | embedded-io-adapters = { version = "0.7", features = ["std", "tokio-1"] } 42 | rustls-pemfile = "1.0" 43 | env_logger = "0.10" 44 | log = "0.4" 45 | rand = "0.8" 46 | 47 | [features] 48 | default = ["embedded-tls"] 49 | alloc = ["embedded-tls?/alloc"] 50 | defmt = [ 51 | "dep:defmt", 52 | "embedded-io/defmt", 53 | "embedded-io-async/defmt", 54 | "embedded-tls?/defmt", 55 | "nourl/defmt", 56 | "heapless/defmt", 57 | ] 58 | -------------------------------------------------------------------------------- /src/body_writer/chunked.rs: -------------------------------------------------------------------------------- 1 | use core::mem::size_of; 2 | 3 | use embedded_io::{Error, ErrorType}; 4 | use embedded_io_async::Write; 5 | 6 | pub struct ChunkedBodyWriter(C); 7 | 8 | const EMPTY_CHUNK: &[u8; 5] = b"0\r\n\r\n"; 9 | const NEWLINE: &[u8; 2] = b"\r\n"; 10 | 11 | impl ChunkedBodyWriter 12 | where 13 | C: Write, 14 | { 15 | pub fn new(conn: C) -> Self { 16 | Self(conn) 17 | } 18 | 19 | /// Terminate the request body by writing an empty chunk 20 | pub async fn terminate(&mut self) -> Result<(), C::Error> { 21 | self.0.write_all(EMPTY_CHUNK).await 22 | } 23 | } 24 | 25 | impl ErrorType for ChunkedBodyWriter 26 | where 27 | C: Write, 28 | { 29 | type Error = embedded_io::ErrorKind; 30 | } 31 | 32 | impl Write for ChunkedBodyWriter 33 | where 34 | C: Write, 35 | { 36 | async fn write(&mut self, buf: &[u8]) -> Result { 37 | self.write_all(buf).await.map_err(|e| e.kind())?; 38 | Ok(buf.len()) 39 | } 40 | 41 | async fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { 42 | let len = buf.len(); 43 | 44 | // Do not write an empty chunk as that will terminate the body 45 | // Use `ChunkedBodyWriter.write_empty_chunk` instead if this is intended 46 | if len == 0 { 47 | return Ok(()); 48 | } 49 | 50 | // Write chunk header 51 | let mut header_buf = [0; 2 * size_of::() + 2]; 52 | let header_len = write_chunked_header(&mut header_buf, len); 53 | self.0 54 | .write_all(&header_buf[..header_len]) 55 | .await 56 | .map_err(|e| e.kind())?; 57 | 58 | // Write chunk 59 | self.0.write_all(buf).await.map_err(|e| e.kind())?; 60 | 61 | // Write newline footer 62 | self.0.write_all(NEWLINE).await.map_err(|e| e.kind())?; 63 | Ok(()) 64 | } 65 | 66 | async fn flush(&mut self) -> Result<(), Self::Error> { 67 | self.0.flush().await.map_err(|e| e.kind()) 68 | } 69 | } 70 | 71 | pub(super) fn write_chunked_header(buf: &mut [u8], chunk_len: usize) -> usize { 72 | let mut hex = [0; 2 * size_of::()]; 73 | hex::encode_to_slice(chunk_len.to_be_bytes(), &mut hex).unwrap(); 74 | let leading_zeros = hex.iter().position(|x| *x != b'0').unwrap_or(hex.len() - 1); 75 | let hex_chars = hex.len() - leading_zeros; 76 | buf[..hex_chars].copy_from_slice(&hex[leading_zeros..]); 77 | buf[hex_chars..hex_chars + NEWLINE.len()].copy_from_slice(NEWLINE); 78 | hex_chars + 2 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::*; 84 | 85 | #[test] 86 | fn can_write_chunked_header() { 87 | let mut buf = [0; 4]; 88 | 89 | let len = write_chunked_header(&mut buf, 0x00); 90 | assert_eq!(b"0\r\n", &buf[..len]); 91 | 92 | let len = write_chunked_header(&mut buf, 0x01); 93 | assert_eq!(b"1\r\n", &buf[..len]); 94 | 95 | let len = write_chunked_header(&mut buf, 0x0F); 96 | assert_eq!(b"f\r\n", &buf[..len]); 97 | 98 | let len = write_chunked_header(&mut buf, 0x10); 99 | assert_eq!(b"10\r\n", &buf[..len]); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tests/request.rs: -------------------------------------------------------------------------------- 1 | use embedded_io_adapters::tokio_1::FromTokio; 2 | use hyper::service::{make_service_fn, service_fn}; 3 | use hyper::{Body, Server}; 4 | use reqwless::client::HttpConnection; 5 | use reqwless::request::RequestBuilder; 6 | use reqwless::Error; 7 | use reqwless::{headers::ContentType, request::Request}; 8 | use std::str::from_utf8; 9 | use std::sync::Once; 10 | use tokio::net::TcpStream; 11 | use tokio::sync::oneshot; 12 | 13 | mod connection; 14 | 15 | use connection::*; 16 | 17 | static INIT: Once = Once::new(); 18 | 19 | fn setup() { 20 | INIT.call_once(|| { 21 | env_logger::init(); 22 | }); 23 | } 24 | 25 | #[tokio::test] 26 | async fn test_request_response() { 27 | setup(); 28 | let addr = ([127, 0, 0, 1], 0).into(); 29 | 30 | let service = make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(echo)) }); 31 | 32 | let server = Server::bind(&addr).serve(service); 33 | let addr = server.local_addr(); 34 | 35 | let (tx, rx) = oneshot::channel(); 36 | let t = tokio::spawn(async move { 37 | tokio::select! { 38 | _ = server => {} 39 | _ = rx => {} 40 | } 41 | }); 42 | 43 | let stream = TcpStream::connect(addr).await.unwrap(); 44 | let mut stream = HttpConnection::Plain(TokioStream(FromTokio::new(stream))); 45 | 46 | let request = Request::post("/") 47 | .body(b"PING".as_slice()) 48 | .content_type(ContentType::TextPlain) 49 | .build(); 50 | 51 | let mut rx_buf = [0; 4096]; 52 | let response = stream.send(request, &mut rx_buf).await.unwrap(); 53 | let body = response.body().read_to_end().await; 54 | 55 | assert_eq!(body.unwrap(), b"PING"); 56 | 57 | tx.send(()).unwrap(); 58 | t.await.unwrap(); 59 | } 60 | 61 | async fn echo(req: hyper::Request) -> Result, hyper::Error> { 62 | match (req.method(), req.uri().path()) { 63 | _ => Ok(hyper::Response::new(req.into_body())), 64 | } 65 | } 66 | 67 | #[tokio::test] 68 | async fn write_without_base_path() { 69 | let request = Request::get("/hello").build(); 70 | 71 | let mut buf = Vec::new(); 72 | request.write_header(&mut buf).await.unwrap(); 73 | 74 | assert!(from_utf8(&buf).unwrap().starts_with("GET /hello HTTP/1.1")); 75 | } 76 | 77 | #[tokio::test] 78 | async fn google_panic() { 79 | use std::net::SocketAddr; 80 | let google_ip = [142, 250, 74, 110]; 81 | let addr = SocketAddr::from((google_ip, 80)); 82 | 83 | let conn = tokio::net::TcpStream::connect(addr).await.unwrap(); 84 | let mut conn = HttpConnection::Plain(TokioStream(FromTokio::new(conn))); 85 | 86 | let request = Request::get("/") 87 | .host("www.google.com") 88 | .content_type(ContentType::TextPlain) 89 | .build(); 90 | 91 | let mut rx_buf = [0; 8 * 1024]; 92 | let resp = conn.send(request, &mut rx_buf).await.unwrap(); 93 | let result = resp.body().read_to_end().await; 94 | 95 | match result { 96 | Ok(body) => { 97 | println!("{} -> {}", body.len(), core::str::from_utf8(&body).unwrap()); 98 | } 99 | Err(Error::BufferTooSmall) => println!("Buffer too small"), 100 | Err(e) => panic!("Unexpected error: {e:?}"), 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/headers.rs: -------------------------------------------------------------------------------- 1 | /// HTTP content types 2 | #[derive(Debug)] 3 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 4 | pub enum ContentType { 5 | TextHtml, 6 | TextPlain, 7 | ApplicationJson, 8 | ApplicationCbor, 9 | ApplicationOctetStream, 10 | } 11 | 12 | impl<'a> From<&'a [u8]> for ContentType { 13 | fn from(from: &'a [u8]) -> ContentType { 14 | match from { 15 | b"application/json" => ContentType::ApplicationJson, 16 | b"application/cbor" => ContentType::ApplicationCbor, 17 | b"text/html" => ContentType::TextHtml, 18 | b"text/plain" => ContentType::TextPlain, 19 | _ => ContentType::ApplicationOctetStream, 20 | } 21 | } 22 | } 23 | 24 | impl ContentType { 25 | pub fn as_str(&self) -> &str { 26 | match self { 27 | ContentType::TextHtml => "text/html", 28 | ContentType::TextPlain => "text/plain", 29 | ContentType::ApplicationJson => "application/json", 30 | ContentType::ApplicationCbor => "application/cbor", 31 | ContentType::ApplicationOctetStream => "application/octet-stream", 32 | } 33 | } 34 | } 35 | 36 | /// Transfer encoding 37 | #[derive(Debug, PartialEq, Eq)] 38 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 39 | pub enum TransferEncoding { 40 | Chunked, 41 | Compress, 42 | Deflate, 43 | Gzip, 44 | } 45 | 46 | impl<'a> TryFrom<&'a [u8]> for TransferEncoding { 47 | type Error = (); 48 | 49 | fn try_from(value: &'a [u8]) -> Result { 50 | Ok(match value { 51 | b"chunked" => TransferEncoding::Chunked, 52 | b"compress" => TransferEncoding::Compress, 53 | b"deflate" => TransferEncoding::Deflate, 54 | b"gzip" => TransferEncoding::Gzip, 55 | _ => return Err(()), 56 | }) 57 | } 58 | } 59 | 60 | impl TransferEncoding { 61 | pub fn as_str(&self) -> &str { 62 | match self { 63 | TransferEncoding::Deflate => "deflate", 64 | TransferEncoding::Chunked => "chunked", 65 | TransferEncoding::Compress => "compress", 66 | TransferEncoding::Gzip => "gzip", 67 | } 68 | } 69 | } 70 | 71 | /// Keep-alive header 72 | #[derive(Debug, PartialEq, Eq)] 73 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 74 | pub struct KeepAlive { 75 | timeout: Option, 76 | max: Option, 77 | } 78 | 79 | impl<'a> TryFrom<&'a [u8]> for KeepAlive { 80 | type Error = core::str::Utf8Error; 81 | 82 | fn try_from(from: &'a [u8]) -> Result { 83 | let mut keep_alive = KeepAlive { 84 | timeout: None, 85 | max: None, 86 | }; 87 | for part in core::str::from_utf8(from)?.split(',') { 88 | let mut splitted = part.split('=').map(|s| s.trim()); 89 | if let (Some(key), Some(value)) = (splitted.next(), splitted.next()) { 90 | match key { 91 | _ if key.eq_ignore_ascii_case("timeout") => keep_alive.timeout = value.parse().ok(), 92 | _ if key.eq_ignore_ascii_case("max") => keep_alive.max = value.parse().ok(), 93 | _ => (), 94 | } 95 | } 96 | } 97 | Ok(keep_alive) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tests/connection.rs: -------------------------------------------------------------------------------- 1 | use embedded_io_adapters::tokio_1::FromTokio; 2 | use embedded_io_async::{ErrorType, Read, Write}; 3 | use embedded_nal_async::AddrType; 4 | use reqwless::TryBufRead; 5 | use std::net::{IpAddr, Ipv4Addr, SocketAddr, ToSocketAddrs}; 6 | use tokio::net::TcpStream; 7 | 8 | #[derive(Debug)] 9 | pub struct TestError; 10 | 11 | impl core::fmt::Display for TestError { 12 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 13 | write!(f, "TestError") 14 | } 15 | } 16 | 17 | impl std::error::Error for TestError {} 18 | 19 | impl embedded_io::Error for TestError { 20 | fn kind(&self) -> embedded_io::ErrorKind { 21 | embedded_io::ErrorKind::Other 22 | } 23 | } 24 | 25 | pub struct LoopbackDns; 26 | impl embedded_nal_async::Dns for LoopbackDns { 27 | type Error = TestError; 28 | 29 | async fn get_host_by_name(&self, _: &str, _: AddrType) -> Result { 30 | Ok(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))) 31 | } 32 | 33 | async fn get_host_by_address(&self, _: IpAddr, _: &mut [u8]) -> Result { 34 | Err(TestError) 35 | } 36 | } 37 | 38 | pub struct StdDns; 39 | 40 | impl embedded_nal_async::Dns for StdDns { 41 | type Error = std::io::Error; 42 | 43 | async fn get_host_by_name(&self, host: &str, addr_type: AddrType) -> Result { 44 | for address in (host, 0).to_socket_addrs()? { 45 | match address { 46 | SocketAddr::V4(a) if addr_type == AddrType::IPv4 || addr_type == AddrType::Either => { 47 | return Ok(IpAddr::V4(a.ip().octets().into())) 48 | } 49 | SocketAddr::V6(a) if addr_type == AddrType::IPv6 || addr_type == AddrType::Either => { 50 | return Ok(IpAddr::V6(a.ip().octets().into())) 51 | } 52 | _ => {} 53 | } 54 | } 55 | Err(std::io::ErrorKind::AddrNotAvailable.into()) 56 | } 57 | 58 | async fn get_host_by_address(&self, _: IpAddr, _: &mut [u8]) -> Result { 59 | todo!() 60 | } 61 | } 62 | 63 | pub struct TokioTcp; 64 | pub struct TokioStream(pub(crate) FromTokio); 65 | 66 | impl TryBufRead for TokioStream {} 67 | 68 | impl ErrorType for TokioStream { 69 | type Error = as ErrorType>::Error; 70 | } 71 | 72 | impl Read for TokioStream { 73 | async fn read(&mut self, buf: &mut [u8]) -> Result { 74 | self.0.read(buf).await 75 | } 76 | } 77 | 78 | impl Write for TokioStream { 79 | async fn write(&mut self, buf: &[u8]) -> Result { 80 | self.0.write(buf).await 81 | } 82 | 83 | async fn flush(&mut self) -> Result<(), Self::Error> { 84 | self.0.flush().await 85 | } 86 | } 87 | 88 | impl embedded_nal_async::TcpConnect for TokioTcp { 89 | type Error = std::io::Error; 90 | type Connection<'m> = TokioStream; 91 | 92 | async fn connect<'m>(&'m self, remote: SocketAddr) -> Result, Self::Error> { 93 | let ip = match remote { 94 | SocketAddr::V4(a) => a.ip().octets().into(), 95 | SocketAddr::V6(a) => a.ip().octets().into(), 96 | }; 97 | let remote = SocketAddr::new(ip, remote.port()); 98 | let stream = TcpStream::connect(remote).await?; 99 | let stream = FromTokio::new(stream); 100 | Ok(TokioStream(stream)) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ## v0.13.0 (2024-10-21) 6 | 7 | * Upgrade to embedded-nal-async 0.8 8 | * Set MSRV 9 | * Allow `request::Method` to be copied and cloned 10 | 11 | ## v0.12.1 (2024-07-01) 12 | 13 | ### Fixes 14 | 15 | * Fix bug where buffering chunked body writer could return `Ok(0)` on calls to `write()` ([#81](https://github.com/drogue-iot/reqwless/pull/81)) 16 | * Fix bug in buffering chunked body writer where a call to `write()` with a buffer length exactly matching the remaining size of the remainder of the current chunk causes the entire chunk to be discarded ([#85](https://github.com/drogue-iot/reqwless/pull/85)) 17 | 18 | ## v0.12 (2024-05-23) 19 | 20 | ### Fixes 21 | 22 | * Fix bug when calling fill_buf() when there are no remaining bytes ([#75](https://github.com/drogue-iot/reqwless/pull/75)) 23 | * Handle no-content status code 204 ([#76](https://github.com/drogue-iot/reqwless/pull/76)) 24 | 25 | ### Features 26 | * Support accessing the response code as an integer ([#70](https://github.com/drogue-iot/reqwless/pull/70) / [#73](https://github.com/drogue-iot/reqwless/pull/73)) 27 | * Buffer writes before chunks are written to connection ([#72](https://github.com/drogue-iot/reqwless/pull/72)) 28 | 29 | ### Fixes 30 | 31 | ## v0.9.1 (2023-11-04) 32 | 33 | * Fix regression introduced in v0.9.0 when reading chunked body where the final newline is not read ([#58](https://github.com/drogue-iot/reqwless/pull/58)) 34 | 35 | ## [v0.9.0](https://github.com/drogue-iot/reqwless/compare/v0.8.0...v0.9.0) (2023-10-30) 36 | 37 | ### Fixes 38 | 39 | * bugfixes and enhancements 40 | * bump version 41 | ([c4efcb5](https://github.com/drogue-iot/reqwless/commit/c4efcb5cb3c5b78f179f8d9eb65afbb8959bed97)) 42 | * Implement `BufRead` for `BodyReader` ([#45](https://github.com/drogue-iot/reqwless/pull/45)) 43 | * Buffer writes automatically if `embedded-tls` is set up, regardless of the URL scheme ([#43](https://github.com/drogue-iot/reqwless/pull/43)) 44 | 45 | ## v0.8.0 (2023-10-05) 46 | 47 | ### Features 48 | 49 | * **headers:** Add keep-alive header parsing in response 50 | ([fa25d98](https://github.com/drogue-iot/reqwless/commit/fa25d98e36f985df3ea1dd97fef88cf1343b89fe)) 51 | * use nourl crate 52 | ([238c811](https://github.com/drogue-iot/reqwless/commit/238c811ff55d02d4b42115ee558102f083c29247)) 53 | * enable TLS PSK support and explicit verification 54 | ([982a381](https://github.com/drogue-iot/reqwless/commit/982a381db0e7c57790f983e056324fdc9fd8602d)) 55 | * use async fn in traits 56 | ([ed6e718](https://github.com/drogue-iot/reqwless/commit/ed6e718e3e3dd4fdca70220a715ffd76901d283d)) 57 | * mutation of rx payload 58 | ([c97ac9c](https://github.com/drogue-iot/reqwless/commit/c97ac9c17d5158aec9061b726ff1329cc5bac325)) 59 | * tls support 60 | ([12b1dd7](https://github.com/drogue-iot/reqwless/commit/12b1dd748ded5ae77a30a5db4bd12d38f0690a01)) 61 | * embedded-nal-async http client 62 | ([7d82b43](https://github.com/drogue-iot/reqwless/commit/7d82b43448ae38099964dead35ed63da27158cc1)) 63 | 64 | ### Fixes 65 | 66 | * **keep-alive:** Fix error for keep-alive header 67 | ([ed29d57](https://github.com/drogue-iot/reqwless/commit/ed29d57371ae08d5da3bae5ff631ae6ecc474073)) 68 | * add split read and write bufs 69 | ([6df94c9](https://github.com/drogue-iot/reqwless/commit/6df94c990da9410b8a4d919336401de670953fa4)) 70 | * pass the &mut slice back 71 | ([3269515](https://github.com/drogue-iot/reqwless/commit/32695155f28bc39deb139a94f2a048b2fd8a2fb1)) 72 | * use version with defmt support 73 | ([84f4cb6](https://github.com/drogue-iot/reqwless/commit/84f4cb6b29cad956c29d65ef6b1879916b4d53d3)) 74 | -------------------------------------------------------------------------------- /src/reader.rs: -------------------------------------------------------------------------------- 1 | use embedded_io::{Error, ErrorKind, ErrorType}; 2 | use embedded_io_async::{BufRead, Read}; 3 | 4 | use crate::TryBufRead; 5 | 6 | pub(crate) struct ReadBuffer<'buf> { 7 | pub buffer: &'buf mut [u8], 8 | pub loaded: usize, 9 | } 10 | 11 | impl<'buf> ReadBuffer<'buf> { 12 | fn new(buffer: &'buf mut [u8], loaded: usize) -> Self { 13 | Self { buffer, loaded } 14 | } 15 | } 16 | 17 | impl ReadBuffer<'_> { 18 | fn is_empty(&self) -> bool { 19 | self.loaded == 0 20 | } 21 | 22 | fn read(&mut self, buf: &mut [u8]) -> Result { 23 | let amt = self.loaded.min(buf.len()); 24 | buf[..amt].copy_from_slice(&self.buffer[0..amt]); 25 | 26 | self.consume(amt); 27 | 28 | Ok(amt) 29 | } 30 | 31 | fn fill_buf(&mut self) -> Result<&[u8], ErrorKind> { 32 | Ok(&self.buffer[..self.loaded]) 33 | } 34 | 35 | fn consume(&mut self, amt: usize) -> usize { 36 | let to_consume = amt.min(self.loaded); 37 | 38 | self.buffer.copy_within(to_consume..self.loaded, 0); 39 | self.loaded -= to_consume; 40 | 41 | amt - to_consume 42 | } 43 | } 44 | 45 | pub struct BufferingReader<'resp, 'buf, B> 46 | where 47 | B: Read, 48 | { 49 | pub(crate) buffer: ReadBuffer<'buf>, 50 | pub(crate) stream: &'resp mut B, 51 | pub(crate) force_local_buffer: bool, 52 | } 53 | 54 | impl<'resp, 'buf, B> BufferingReader<'resp, 'buf, B> 55 | where 56 | B: Read, 57 | { 58 | pub fn new(buffer: &'buf mut [u8], loaded: usize, stream: &'resp mut B) -> Self { 59 | Self { 60 | buffer: ReadBuffer::new(buffer, loaded), 61 | stream, 62 | force_local_buffer: false, 63 | } 64 | } 65 | } 66 | 67 | impl ErrorType for BufferingReader<'_, '_, C> 68 | where 69 | C: Read, 70 | { 71 | type Error = ErrorKind; 72 | } 73 | 74 | impl Read for BufferingReader<'_, '_, C> 75 | where 76 | C: Read, 77 | { 78 | async fn read(&mut self, buf: &mut [u8]) -> Result { 79 | if !self.buffer.is_empty() { 80 | let amt = self.buffer.read(buf)?; 81 | return Ok(amt); 82 | } 83 | 84 | self.stream.read(buf).await.map_err(|e| e.kind()) 85 | } 86 | } 87 | 88 | impl BufRead for BufferingReader<'_, '_, C> 89 | where 90 | C: TryBufRead, 91 | { 92 | async fn fill_buf(&mut self) -> Result<&[u8], ErrorKind> { 93 | // We need to consume the loaded bytes before we read more. 94 | if self.buffer.is_empty() { 95 | // The matches/if let dance is to fix lifetime of the borrowed inner connection. 96 | if !self.force_local_buffer && self.stream.try_fill_buf().await.is_some() { 97 | if let Some(result) = self.stream.try_fill_buf().await { 98 | return result.map_err(|e| e.kind()); 99 | } 100 | unreachable!() 101 | } 102 | 103 | self.buffer.loaded = self.stream.read(self.buffer.buffer).await.map_err(|e| e.kind())?; 104 | } 105 | 106 | self.buffer.fill_buf() 107 | } 108 | 109 | fn consume(&mut self, amt: usize) { 110 | // It's possible that the user requested more bytes to be consumed than loaded. Especially 111 | // since it's also possible that nothing is loaded, after we consumed all and are using 112 | // embedded-tls's buffering. 113 | let unconsumed = self.buffer.consume(amt); 114 | 115 | if unconsumed > 0 { 116 | self.stream.try_consume(unconsumed); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(test), no_std)] 2 | #![doc = include_str!("../README.md")] 3 | #![allow(async_fn_in_trait)] 4 | use core::{num::ParseIntError, str::Utf8Error}; 5 | 6 | use embedded_io_async::ReadExactError; 7 | 8 | mod fmt; 9 | 10 | mod body_writer; 11 | pub mod client; 12 | pub mod headers; 13 | mod reader; 14 | pub mod request; 15 | pub mod response; 16 | 17 | /// Errors that can be returned by this library. 18 | #[derive(Debug)] 19 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 20 | pub enum Error { 21 | /// An error with DNS (it's always DNS) 22 | Dns, 23 | /// An error with the underlying network 24 | Network(embedded_io::ErrorKind), 25 | /// An error encoding or decoding data 26 | Codec, 27 | /// An error parsing the URL 28 | InvalidUrl(nourl::Error), 29 | /// Tls Error 30 | #[cfg(feature = "embedded-tls")] 31 | Tls(embedded_tls::TlsError), 32 | /// Tls Error 33 | #[cfg(feature = "esp-mbedtls")] 34 | Tls(esp_mbedtls::TlsError), 35 | /// The provided buffer is too small 36 | BufferTooSmall, 37 | /// The request is already sent 38 | AlreadySent, 39 | /// An invalid number of bytes were written to request body 40 | IncorrectBodyWritten, 41 | /// The underlying connection was closed while being used 42 | ConnectionAborted, 43 | } 44 | 45 | impl core::fmt::Display for Error { 46 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 47 | core::fmt::Debug::fmt(self, f) 48 | } 49 | } 50 | 51 | impl core::error::Error for Error {} 52 | 53 | impl embedded_io::Error for Error { 54 | fn kind(&self) -> embedded_io::ErrorKind { 55 | match self { 56 | Error::Network(kind) => *kind, 57 | Error::ConnectionAborted => embedded_io::ErrorKind::ConnectionAborted, 58 | _ => embedded_io::ErrorKind::Other, 59 | } 60 | } 61 | } 62 | 63 | impl From for Error { 64 | fn from(e: embedded_io::ErrorKind) -> Error { 65 | Error::Network(e) 66 | } 67 | } 68 | 69 | impl From> for Error { 70 | fn from(value: ReadExactError) -> Self { 71 | match value { 72 | ReadExactError::UnexpectedEof => Error::ConnectionAborted, 73 | ReadExactError::Other(e) => Error::Network(e.kind()), 74 | } 75 | } 76 | } 77 | 78 | #[cfg(feature = "embedded-tls")] 79 | impl From for Error { 80 | fn from(e: embedded_tls::TlsError) -> Error { 81 | Error::Tls(e) 82 | } 83 | } 84 | 85 | /// Re-export those members since they're used for [client::TlsConfig]. 86 | #[cfg(feature = "esp-mbedtls")] 87 | pub use esp_mbedtls::{Certificates, TlsReference, TlsVersion, X509}; 88 | 89 | #[cfg(feature = "esp-mbedtls")] 90 | impl From for Error { 91 | fn from(e: esp_mbedtls::TlsError) -> Error { 92 | Error::Tls(e) 93 | } 94 | } 95 | 96 | impl From for Error { 97 | fn from(_: ParseIntError) -> Error { 98 | Error::Codec 99 | } 100 | } 101 | 102 | impl From for Error { 103 | fn from(_: Utf8Error) -> Error { 104 | Error::Codec 105 | } 106 | } 107 | 108 | impl From for Error { 109 | fn from(e: nourl::Error) -> Self { 110 | Error::InvalidUrl(e) 111 | } 112 | } 113 | 114 | /// Trait for types that may optionally implement [`embedded_io_async::BufRead`] 115 | pub trait TryBufRead: embedded_io_async::Read { 116 | async fn try_fill_buf(&mut self) -> Option> { 117 | None 118 | } 119 | 120 | fn try_consume(&mut self, _amt: usize) {} 121 | } 122 | 123 | impl TryBufRead for crate::client::HttpConnection<'_, C> 124 | where 125 | C: embedded_io_async::Read + embedded_io_async::Write, 126 | { 127 | async fn try_fill_buf(&mut self) -> Option> { 128 | // embedded-tls has its own internal buffer, let's prefer that if we can 129 | #[cfg(feature = "embedded-tls")] 130 | if let Self::Tls(ref mut tls) = self { 131 | use embedded_io_async::{BufRead, Error}; 132 | return Some(tls.fill_buf().await.map_err(|e| e.kind())); 133 | } 134 | 135 | None 136 | } 137 | 138 | fn try_consume(&mut self, amt: usize) { 139 | #[cfg(feature = "embedded-tls")] 140 | if let Self::Tls(tls) = self { 141 | use embedded_io_async::BufRead; 142 | tls.consume(amt); 143 | } 144 | 145 | #[cfg(not(feature = "embedded-tls"))] 146 | { 147 | _ = amt; 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTTP client for embedded devices 2 | 3 | [![CI](https://github.com/drogue-iot/reqwless/actions/workflows/ci.yaml/badge.svg)](https://github.com/drogue-iot/reqwless/actions/workflows/ci.yaml) 4 | [![crates.io](https://img.shields.io/crates/v/reqwless.svg)](https://crates.io/crates/reqwless) 5 | [![docs.rs](https://docs.rs/reqwless/badge.svg)](https://docs.rs/reqwless) 6 | [![Matrix](https://img.shields.io/matrix/drogue-iot:matrix.org)](https://matrix.to/#/#drogue-iot:matrix.org) 7 | 8 | The `reqwless` crate implements an HTTP client that can be used in `no_std` environment, with any transport that implements the 9 | traits from the `embedded-io` crate. No alloc or std lib required! 10 | 11 | It offers two sets of APIs: 12 | 13 | * A low-level `request` API which allows you to construct HTTP requests and write them to a `embedded-io` transport. 14 | * A higher level `client` API which uses the `embedded-nal-async` (+ optional `embedded-tls` / `esp-mbedtls`) crates to establish TCP + TLS connections. 15 | 16 | ## example 17 | 18 | ```rust,ignore 19 | let url = format!("http://localhost", addr.port()); 20 | let mut client = HttpClient::new(TokioTcp, StaticDns); // Types implementing embedded-nal-async 21 | let mut rx_buf = [0; 4096]; 22 | let response = client 23 | .request(Method::POST, &url) 24 | .await 25 | .unwrap() 26 | .body(b"PING") 27 | .content_type(ContentType::TextPlain) 28 | .send(&mut rx_buf) 29 | .await 30 | .unwrap(); 31 | ``` 32 | 33 | The client is still lacking many features, but can perform basic HTTP GET/PUT/POST/DELETE requests with payloads. However, not all content types and status codes are implemented, and are added on a need basis. For TLS, it uses either `embedded-tls` or `esp-mbedtls` as the transport. 34 | 35 | NOTE: TLS verification is not supported in no_std environments for `embedded-tls`. 36 | 37 | In addition to common headers like `.content_type()` on requests, broader `.headers()` functions on both request and response objects access arbitrary header values. 38 | 39 | If you are missing a feature or would like an improvement, please raise an issue or a PR. 40 | 41 | ## TLS 1.2*, 1.3 and Supported Cipher Suites 42 | `reqwless` uses `embedded-tls` or `esp-mbedtls` to establish secure TLS connections for `https://..` urls. 43 | 44 | *TLS 1.2 is only supported with `esp-mbedtls` 45 | 46 | :warning: Note that both features cannot be used together and will cause a compilation error. 47 | 48 | :warning: The released version of `reqwless` does not support `esp-mbedtls`. The reason for this is that `esp-mbedtls` is not yet published to crates.io. One should specify `reqwless` as a git dependency to use `esp-mbedtls`. 49 | 50 | ### esp-mbedtls 51 | **Can only be used on esp32 boards** 52 | `esp-mbedtls` supports TLS 1.2 and 1.3. It uses espressif's Rust wrapper over mbedtls, alongside optimizations such as hardware acceleration. 53 | 54 | To use, you need to enable the transitive dependency of `esp-mbedtls` for your SoC. 55 | Currently, the supported SoCs are: 56 | 57 | - `esp32` 58 | - `esp32c3` 59 | - `esp32s2` 60 | - `esp32s3` 61 | 62 | Cargo.toml: 63 | 64 | ```toml 65 | reqwless = { version = "0.12.0", default-features = false, features = ["esp-mbedtls", "log"] } 66 | esp-mbedtls = { git = "https://github.com/esp-rs/esp-mbedtls.git", features = ["esp32s3"] } 67 | ``` 68 | 69 | 70 | #### Example 71 | ```rust,ignore 72 | /// ... [initialization code. See esp-wifi] 73 | let state = TcpClientState::<1, 4096, 4096>::new(); 74 | let mut tcp_client = TcpClient::new(stack, &state); 75 | let dns_socket = DnsSocket::new(&stack); 76 | let mut rsa = Rsa::new(peripherals.RSA); 77 | let config = TlsConfig::new( 78 | reqwless::TlsVersion::Tls1_3, 79 | reqwless::Certificates { 80 | ca_chain: reqwless::X509::pem(CERT.as_bytes()).ok(), 81 | ..Default::default() 82 | }, 83 | Some(&mut rsa), // Will use hardware acceleration 84 | ); 85 | let mut client = HttpClient::new_with_tls(&tcp_client, &dns_socket, config); 86 | 87 | let mut request = client 88 | .request(reqwless::request::Method::GET, "https://www.google.com") 89 | .await 90 | .unwrap() 91 | .content_type(reqwless::headers::ContentType::TextPlain) 92 | .headers(&[("Host", "google.com")]) 93 | .send(&mut buffer) 94 | .await 95 | .unwrap(); 96 | ``` 97 | 98 | ### embedded-tls 99 | `embedded-tls` only supports TLS 1.3, so to establish a connection the server must have this ssl protocol enabled. 100 | 101 | An addition to the tls version requirement, there is also a negotiation of supported algorithms during the establishing phase of the secure communication between the client and server. 102 | By default, the set of supported algorithms in `embedded-tls` is limited to algorithms that can run entirely on the stack. 103 | To test whether the server supports this limited set of algorithm, try and test the server using the following `openssl` command: 104 | 105 | ```bash 106 | openssl s_client -tls1_3 -ciphersuites TLS_AES_128_GCM_SHA256 -sigalgs "ECDSA+SHA256:ECDSA+SHA384:ed25519" -connect hostname:443 107 | ``` 108 | 109 | If the server successfully replies to the client hello then the enabled tls version and algorithms on the server should be ok. 110 | If the command fails, then try and run without the limited set of signature algorithms 111 | 112 | ```bash 113 | openssl s_client -tls1_3 -ciphersuites TLS_AES_128_GCM_SHA256 -connect hostname:443 114 | ``` 115 | 116 | If this works, then there are two options. Either enable the signature algorithms on the server by changing the private key from RSA to ECDSA or ed25519, or enable RSA keys on the client by specifying the `alloc` feature. 117 | This enables `alloc` on `embedded-tls` which in turn enables RSA signature algorithms. 118 | 119 | 120 | # Minimum supported Rust version (MSRV) 121 | 122 | `reqwless` can compile on stable Rust 1.77 and up. 123 | -------------------------------------------------------------------------------- /src/fmt.rs: -------------------------------------------------------------------------------- 1 | #![macro_use] 2 | #![allow(unused)] 3 | 4 | #[cfg(all(feature = "defmt", feature = "log"))] 5 | compile_error!("You may not enable both `defmt` and `log` features."); 6 | 7 | macro_rules! assert { 8 | ($($x:tt)*) => { 9 | { 10 | #[cfg(not(feature = "defmt"))] 11 | ::core::assert!($($x)*); 12 | #[cfg(feature = "defmt")] 13 | ::defmt::assert!($($x)*); 14 | } 15 | }; 16 | } 17 | 18 | macro_rules! assert_eq { 19 | ($($x:tt)*) => { 20 | { 21 | #[cfg(not(feature = "defmt"))] 22 | ::core::assert_eq!($($x)*); 23 | #[cfg(feature = "defmt")] 24 | ::defmt::assert_eq!($($x)*); 25 | } 26 | }; 27 | } 28 | 29 | macro_rules! assert_ne { 30 | ($($x:tt)*) => { 31 | { 32 | #[cfg(not(feature = "defmt"))] 33 | ::core::assert_ne!($($x)*); 34 | #[cfg(feature = "defmt")] 35 | ::defmt::assert_ne!($($x)*); 36 | } 37 | }; 38 | } 39 | 40 | macro_rules! debug_assert { 41 | ($($x:tt)*) => { 42 | { 43 | #[cfg(not(feature = "defmt"))] 44 | ::core::debug_assert!($($x)*); 45 | #[cfg(feature = "defmt")] 46 | ::defmt::debug_assert!($($x)*); 47 | } 48 | }; 49 | } 50 | 51 | macro_rules! debug_assert_eq { 52 | ($($x:tt)*) => { 53 | { 54 | #[cfg(not(feature = "defmt"))] 55 | ::core::debug_assert_eq!($($x)*); 56 | #[cfg(feature = "defmt")] 57 | ::defmt::debug_assert_eq!($($x)*); 58 | } 59 | }; 60 | } 61 | 62 | macro_rules! debug_assert_ne { 63 | ($($x:tt)*) => { 64 | { 65 | #[cfg(not(feature = "defmt"))] 66 | ::core::debug_assert_ne!($($x)*); 67 | #[cfg(feature = "defmt")] 68 | ::defmt::debug_assert_ne!($($x)*); 69 | } 70 | }; 71 | } 72 | 73 | macro_rules! todo { 74 | ($($x:tt)*) => { 75 | { 76 | #[cfg(not(feature = "defmt"))] 77 | ::core::todo!($($x)*); 78 | #[cfg(feature = "defmt")] 79 | ::defmt::todo!($($x)*); 80 | } 81 | }; 82 | } 83 | 84 | macro_rules! unreachable { 85 | ($($x:tt)*) => { 86 | { 87 | #[cfg(not(feature = "defmt"))] 88 | ::core::unreachable!($($x)*); 89 | #[cfg(feature = "defmt")] 90 | ::defmt::unreachable!($($x)*); 91 | } 92 | }; 93 | } 94 | 95 | macro_rules! panic { 96 | ($($x:tt)*) => { 97 | { 98 | #[cfg(not(feature = "defmt"))] 99 | ::core::panic!($($x)*); 100 | #[cfg(feature = "defmt")] 101 | ::defmt::panic!($($x)*); 102 | } 103 | }; 104 | } 105 | 106 | macro_rules! trace { 107 | ($s:literal $(, $x:expr)* $(,)?) => { 108 | { 109 | #[cfg(feature = "log")] 110 | ::log::trace!($s $(, $x)*); 111 | #[cfg(feature = "defmt")] 112 | ::defmt::trace!($s $(, $x)*); 113 | #[cfg(not(any(feature = "log", feature="defmt")))] 114 | let _ = ($( & $x ),*); 115 | } 116 | }; 117 | } 118 | 119 | macro_rules! debug { 120 | ($s:literal $(, $x:expr)* $(,)?) => { 121 | { 122 | #[cfg(feature = "log")] 123 | ::log::debug!($s $(, $x)*); 124 | #[cfg(feature = "defmt")] 125 | ::defmt::debug!($s $(, $x)*); 126 | #[cfg(not(any(feature = "log", feature="defmt")))] 127 | let _ = ($( & $x ),*); 128 | } 129 | }; 130 | } 131 | 132 | macro_rules! info { 133 | ($s:literal $(, $x:expr)* $(,)?) => { 134 | { 135 | #[cfg(feature = "log")] 136 | ::log::info!($s $(, $x)*); 137 | #[cfg(feature = "defmt")] 138 | ::defmt::info!($s $(, $x)*); 139 | #[cfg(not(any(feature = "log", feature="defmt")))] 140 | let _ = ($( & $x ),*); 141 | } 142 | }; 143 | } 144 | 145 | macro_rules! warn { 146 | ($s:literal $(, $x:expr)* $(,)?) => { 147 | { 148 | #[cfg(feature = "log")] 149 | ::log::warn!($s $(, $x)*); 150 | #[cfg(feature = "defmt")] 151 | ::defmt::warn!($s $(, $x)*); 152 | #[cfg(not(any(feature = "log", feature="defmt")))] 153 | let _ = ($( & $x ),*); 154 | } 155 | }; 156 | } 157 | 158 | macro_rules! error { 159 | ($s:literal $(, $x:expr)* $(,)?) => { 160 | { 161 | #[cfg(feature = "log")] 162 | ::log::error!($s $(, $x)*); 163 | #[cfg(feature = "defmt")] 164 | ::defmt::error!($s $(, $x)*); 165 | #[cfg(not(any(feature = "log", feature="defmt")))] 166 | let _ = ($( & $x ),*); 167 | } 168 | }; 169 | } 170 | 171 | #[cfg(feature = "defmt")] 172 | macro_rules! unwrap { 173 | ($($x:tt)*) => { 174 | ::defmt::unwrap!($($x)*) 175 | }; 176 | } 177 | 178 | #[cfg(not(feature = "defmt"))] 179 | macro_rules! unwrap { 180 | ($arg:expr) => { 181 | match $crate::fmt::Try::into_result($arg) { 182 | ::core::result::Result::Ok(t) => t, 183 | ::core::result::Result::Err(e) => { 184 | ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); 185 | } 186 | } 187 | }; 188 | ($arg:expr, $($msg:expr),+ $(,)? ) => { 189 | match $crate::fmt::Try::into_result($arg) { 190 | ::core::result::Result::Ok(t) => t, 191 | ::core::result::Result::Err(e) => { 192 | ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); 193 | } 194 | } 195 | } 196 | } 197 | 198 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 199 | pub struct NoneError; 200 | 201 | pub trait Try { 202 | type Ok; 203 | type Error; 204 | fn into_result(self) -> Result; 205 | } 206 | 207 | impl Try for Option { 208 | type Ok = T; 209 | type Error = NoneError; 210 | 211 | #[inline] 212 | fn into_result(self) -> Result { 213 | self.ok_or(NoneError) 214 | } 215 | } 216 | 217 | impl Try for Result { 218 | type Ok = T; 219 | type Error = E; 220 | 221 | #[inline] 222 | fn into_result(self) -> Self { 223 | self 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/response/chunked.rs: -------------------------------------------------------------------------------- 1 | use embedded_io_async::{BufRead, Error as _, ErrorType, Read}; 2 | 3 | use crate::{ 4 | reader::{BufferingReader, ReadBuffer}, 5 | Error, TryBufRead, 6 | }; 7 | 8 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 9 | enum ChunkState { 10 | NoChunk, 11 | NotEmpty(u32), 12 | Empty, 13 | } 14 | 15 | impl ChunkState { 16 | fn consume(&mut self, amt: usize) -> usize { 17 | if let ChunkState::NotEmpty(remaining) = self { 18 | let consumed = (amt as u32).min(*remaining); 19 | *remaining -= consumed; 20 | consumed as usize 21 | } else { 22 | 0 23 | } 24 | } 25 | 26 | fn len(self) -> usize { 27 | if let ChunkState::NotEmpty(len) = self { 28 | len as usize 29 | } else { 30 | 0 31 | } 32 | } 33 | } 34 | 35 | /// Chunked response body reader 36 | pub struct ChunkedBodyReader { 37 | pub raw_body: B, 38 | chunk_remaining: ChunkState, 39 | } 40 | 41 | impl ChunkedBodyReader 42 | where 43 | C: Read, 44 | { 45 | pub fn new(raw_body: C) -> Self { 46 | Self { 47 | raw_body, 48 | chunk_remaining: ChunkState::NoChunk, 49 | } 50 | } 51 | 52 | pub fn is_done(&self) -> bool { 53 | self.chunk_remaining == ChunkState::Empty 54 | } 55 | 56 | async fn read_next_chunk_length(&mut self) -> Result<(), Error> { 57 | let mut header_buf = [0; 8 + 2]; // 32 bit hex + \r + \n 58 | let mut total_read = 0; 59 | 60 | 'read_size: loop { 61 | let mut byte = 0; 62 | self.raw_body 63 | .read_exact(core::slice::from_mut(&mut byte)) 64 | .await 65 | .map_err(|e| Error::from(e).kind())?; 66 | 67 | if byte != b'\n' { 68 | header_buf[total_read] = byte; 69 | total_read += 1; 70 | 71 | if total_read == header_buf.len() { 72 | return Err(Error::Codec); 73 | } 74 | } else { 75 | if total_read == 0 || header_buf[total_read - 1] != b'\r' { 76 | return Err(Error::Codec); 77 | } 78 | break 'read_size; 79 | } 80 | } 81 | 82 | let hex_digits = total_read - 1; 83 | 84 | // Prepend hex with zeros 85 | let mut hex = [b'0'; 8]; 86 | hex[8 - hex_digits..].copy_from_slice(&header_buf[..hex_digits]); 87 | 88 | let mut bytes = [0; 4]; 89 | hex::decode_to_slice(hex, &mut bytes).map_err(|_| Error::Codec)?; 90 | 91 | let chunk_length = u32::from_be_bytes(bytes); 92 | 93 | debug!("Chunk length: {}", chunk_length); 94 | 95 | self.chunk_remaining = match chunk_length { 96 | 0 => ChunkState::Empty, 97 | other => ChunkState::NotEmpty(other), 98 | }; 99 | 100 | Ok(()) 101 | } 102 | 103 | async fn read_chunk_end(&mut self) -> Result<(), Error> { 104 | // All chunks are terminated with a \r\n 105 | let mut newline_buf = [0; 2]; 106 | self.raw_body.read_exact(&mut newline_buf).await?; 107 | 108 | if newline_buf != [b'\r', b'\n'] { 109 | return Err(Error::Codec); 110 | } 111 | Ok(()) 112 | } 113 | 114 | /// Handles chunk boundary and returns the number of bytes in the current (or new) chunk. 115 | async fn handle_chunk_boundary(&mut self) -> Result { 116 | match self.chunk_remaining { 117 | ChunkState::NoChunk => self.read_next_chunk_length().await?, 118 | 119 | ChunkState::NotEmpty(0) => { 120 | // The current chunk is currently empty, advance into a new chunk... 121 | self.read_chunk_end().await?; 122 | self.read_next_chunk_length().await?; 123 | } 124 | 125 | ChunkState::NotEmpty(_) => {} 126 | 127 | ChunkState::Empty => return Ok(0), 128 | } 129 | 130 | if self.chunk_remaining == ChunkState::Empty { 131 | // Read final chunk termination 132 | self.read_chunk_end().await?; 133 | } 134 | 135 | Ok(self.chunk_remaining.len()) 136 | } 137 | } 138 | 139 | impl<'conn, 'buf, C> ChunkedBodyReader> 140 | where 141 | C: Read + TryBufRead, 142 | { 143 | pub(crate) async fn read_to_end(self) -> Result<&'buf mut [u8], Error> { 144 | let buffer = self.raw_body.buffer.buffer; 145 | 146 | // We reconstruct the reader to change the 'buf lifetime. 147 | let mut reader = ChunkedBodyReader { 148 | raw_body: BufferingReader { 149 | buffer: ReadBuffer { 150 | buffer: &mut buffer[..], 151 | loaded: self.raw_body.buffer.loaded, 152 | }, 153 | stream: self.raw_body.stream, 154 | force_local_buffer: true, 155 | }, 156 | chunk_remaining: self.chunk_remaining, 157 | }; 158 | 159 | let mut len = 0; 160 | while !reader.raw_body.buffer.buffer.is_empty() { 161 | // Read some 162 | let read = reader.fill_buf().await?; 163 | let read_len = read.len(); 164 | len += read_len; 165 | 166 | // Make sure we don't erase the newly read data 167 | let was_loaded = reader.raw_body.buffer.loaded; 168 | let fake_loaded = read_len.min(was_loaded); 169 | reader.raw_body.buffer.loaded = fake_loaded; 170 | 171 | // Consume the returned buffer 172 | reader.consume(read_len); 173 | 174 | if reader.is_done() { 175 | // If we're done, we don't care about the rest of the housekeeping. 176 | break; 177 | } 178 | 179 | // How many bytes were actually consumed from the preloaded buffer? 180 | let consumed_from_buffer = fake_loaded - reader.raw_body.buffer.loaded; 181 | 182 | // ... move the buffer by that many bytes to avoid overwriting in the next iteration. 183 | reader.raw_body.buffer.loaded = was_loaded - consumed_from_buffer; 184 | reader.raw_body.buffer.buffer = &mut reader.raw_body.buffer.buffer[consumed_from_buffer..]; 185 | } 186 | 187 | if !reader.is_done() { 188 | return Err(Error::BufferTooSmall); 189 | } 190 | 191 | Ok(&mut buffer[..len]) 192 | } 193 | } 194 | 195 | impl ErrorType for ChunkedBodyReader { 196 | type Error = Error; 197 | } 198 | 199 | impl Read for ChunkedBodyReader 200 | where 201 | C: Read, 202 | { 203 | async fn read(&mut self, buf: &mut [u8]) -> Result { 204 | let remaining = self.handle_chunk_boundary().await?; 205 | let max_len = buf.len().min(remaining); 206 | 207 | let len = self 208 | .raw_body 209 | .read(&mut buf[..max_len]) 210 | .await 211 | .map_err(|e| Error::Network(e.kind()))?; 212 | 213 | self.chunk_remaining.consume(len); 214 | 215 | Ok(len) 216 | } 217 | } 218 | 219 | impl BufRead for ChunkedBodyReader 220 | where 221 | C: BufRead + Read, 222 | { 223 | async fn fill_buf(&mut self) -> Result<&[u8], Self::Error> { 224 | let remaining = self.handle_chunk_boundary().await?; 225 | if remaining == 0 { 226 | Ok(&[]) 227 | } else { 228 | let buf = self.raw_body.fill_buf().await.map_err(|e| Error::Network(e.kind()))?; 229 | let len = buf.len().min(remaining); 230 | Ok(&buf[..len]) 231 | } 232 | } 233 | 234 | fn consume(&mut self, amt: usize) { 235 | let consumed = self.chunk_remaining.consume(amt); 236 | self.raw_body.consume(consumed); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 Drogue IoT contributors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/request.rs: -------------------------------------------------------------------------------- 1 | /// Low level API for encoding requests and decoding responses. 2 | use crate::headers::ContentType; 3 | use crate::Error; 4 | use core::fmt::Write as _; 5 | use embedded_io::Error as _; 6 | use embedded_io_async::Write; 7 | use heapless::String; 8 | 9 | /// A read only HTTP request type 10 | pub struct Request<'req, B> 11 | where 12 | B: RequestBody, 13 | { 14 | pub(crate) method: Method, 15 | pub(crate) base_path: Option<&'req str>, 16 | pub(crate) path: &'req str, 17 | pub(crate) auth: Option>, 18 | pub(crate) host: Option<&'req str>, 19 | pub(crate) body: Option, 20 | pub(crate) content_type: Option, 21 | pub(crate) accept: Option, 22 | pub(crate) extra_headers: Option<&'req [(&'req str, &'req str)]>, 23 | } 24 | 25 | impl Default for Request<'_, ()> { 26 | fn default() -> Self { 27 | Self { 28 | method: Method::GET, 29 | base_path: None, 30 | path: "/", 31 | auth: None, 32 | host: None, 33 | body: None, 34 | content_type: None, 35 | accept: None, 36 | extra_headers: None, 37 | } 38 | } 39 | } 40 | 41 | /// A HTTP request builder. 42 | pub trait RequestBuilder<'req, B> 43 | where 44 | B: RequestBody, 45 | { 46 | type WithBody: RequestBuilder<'req, T>; 47 | 48 | /// Set optional headers on the request. 49 | fn headers(self, headers: &'req [(&'req str, &'req str)]) -> Self; 50 | /// Set the path of the HTTP request. 51 | fn path(self, path: &'req str) -> Self; 52 | /// Set the data to send in the HTTP request body. 53 | fn body(self, body: T) -> Self::WithBody; 54 | /// Set the host header. 55 | fn host(self, host: &'req str) -> Self; 56 | /// Set the content type header for the request. 57 | fn content_type(self, content_type: ContentType) -> Self; 58 | /// Set the accept header for the request. 59 | fn accept(self, content_type: ContentType) -> Self; 60 | /// Set the basic authentication header for the request. 61 | fn basic_auth(self, username: &'req str, password: &'req str) -> Self; 62 | /// Return an immutable request. 63 | fn build(self) -> Request<'req, B>; 64 | } 65 | 66 | /// Request authentication scheme. 67 | pub enum Auth<'a> { 68 | Basic { username: &'a str, password: &'a str }, 69 | } 70 | 71 | impl<'req> Request<'req, ()> { 72 | /// Create a new http request. 73 | #[allow(clippy::new_ret_no_self)] 74 | pub fn new(method: Method, path: &'req str) -> DefaultRequestBuilder<'req, ()> { 75 | DefaultRequestBuilder(Request { 76 | method, 77 | path, 78 | ..Default::default() 79 | }) 80 | } 81 | 82 | /// Create a new GET http request. 83 | pub fn get(path: &'req str) -> DefaultRequestBuilder<'req, ()> { 84 | Self::new(Method::GET, path) 85 | } 86 | 87 | /// Create a new POST http request. 88 | pub fn post(path: &'req str) -> DefaultRequestBuilder<'req, ()> { 89 | Self::new(Method::POST, path) 90 | } 91 | 92 | /// Create a new PUT http request. 93 | pub fn put(path: &'req str) -> DefaultRequestBuilder<'req, ()> { 94 | Self::new(Method::PUT, path) 95 | } 96 | 97 | /// Create a new DELETE http request. 98 | pub fn delete(path: &'req str) -> DefaultRequestBuilder<'req, ()> { 99 | Self::new(Method::DELETE, path) 100 | } 101 | 102 | /// Create a new HEAD http request. 103 | pub fn head(path: &'req str) -> DefaultRequestBuilder<'req, ()> { 104 | Self::new(Method::HEAD, path) 105 | } 106 | 107 | /// Create a new PATCH http request. 108 | pub fn patch(path: &'req str) -> DefaultRequestBuilder<'req, ()> { 109 | Self::new(Method::PATCH, path) 110 | } 111 | 112 | /// Create a new OPTIONS http request. 113 | pub fn options(path: &'req str) -> DefaultRequestBuilder<'req, ()> { 114 | Self::new(Method::OPTIONS, path) 115 | } 116 | 117 | /// Create a new CONNECT http request. 118 | pub fn connect(path: &'req str) -> DefaultRequestBuilder<'req, ()> { 119 | Self::new(Method::CONNECT, path) 120 | } 121 | 122 | /// Create a new TRACE http request. 123 | pub fn trace(path: &'req str) -> DefaultRequestBuilder<'req, ()> { 124 | Self::new(Method::TRACE, path) 125 | } 126 | } 127 | 128 | impl<'req, B> Request<'req, B> 129 | where 130 | B: RequestBody, 131 | { 132 | /// Write request header to the I/O stream 133 | pub async fn write_header(&self, c: &mut C) -> Result<(), Error> 134 | where 135 | C: Write, 136 | { 137 | write_str(c, self.method.as_str()).await?; 138 | write_str(c, " ").await?; 139 | if let Some(base_path) = self.base_path { 140 | write_str(c, base_path.trim_end_matches('/')).await?; 141 | if !self.path.starts_with('/') { 142 | write_str(c, "/").await?; 143 | } 144 | } 145 | write_str(c, self.path).await?; 146 | write_str(c, " HTTP/1.1\r\n").await?; 147 | 148 | if let Some(auth) = &self.auth { 149 | match auth { 150 | Auth::Basic { username, password } => { 151 | use base64::engine::{general_purpose, Engine as _}; 152 | 153 | let mut combined: String<128> = String::new(); 154 | write!(combined, "{}:{}", username, password).map_err(|_| Error::Codec)?; 155 | let mut authz = [0; 256]; 156 | let authz_len = general_purpose::STANDARD 157 | .encode_slice(combined.as_bytes(), &mut authz) 158 | .map_err(|_| Error::Codec)?; 159 | write_str(c, "Authorization: Basic ").await?; 160 | write_str(c, unsafe { core::str::from_utf8_unchecked(&authz[..authz_len]) }).await?; 161 | write_str(c, "\r\n").await?; 162 | } 163 | } 164 | } 165 | if let Some(host) = &self.host { 166 | write_header(c, "Host", host).await?; 167 | } 168 | if let Some(content_type) = &self.content_type { 169 | write_header(c, "Content-Type", content_type.as_str()).await?; 170 | } 171 | if let Some(accept) = &self.accept { 172 | write_header(c, "Accept", accept.as_str()).await?; 173 | } 174 | if let Some(body) = self.body.as_ref() { 175 | if let Some(len) = body.len() { 176 | let mut s: String<32> = String::new(); 177 | write!(s, "{}", len).map_err(|_| Error::Codec)?; 178 | write_header(c, "Content-Length", s.as_str()).await?; 179 | } else { 180 | write_header(c, "Transfer-Encoding", "chunked").await?; 181 | } 182 | } 183 | if let Some(extra_headers) = self.extra_headers { 184 | for (header, value) in extra_headers.iter() { 185 | write_header(c, header, value).await?; 186 | } 187 | } 188 | write_str(c, "\r\n").await?; 189 | trace!("Header written"); 190 | Ok(()) 191 | } 192 | } 193 | 194 | pub struct DefaultRequestBuilder<'req, B>(Request<'req, B>) 195 | where 196 | B: RequestBody; 197 | 198 | impl<'req, B> RequestBuilder<'req, B> for DefaultRequestBuilder<'req, B> 199 | where 200 | B: RequestBody, 201 | { 202 | type WithBody = DefaultRequestBuilder<'req, T>; 203 | 204 | fn headers(mut self, headers: &'req [(&'req str, &'req str)]) -> Self { 205 | self.0.extra_headers.replace(headers); 206 | self 207 | } 208 | 209 | fn path(mut self, path: &'req str) -> Self { 210 | self.0.path = path; 211 | self 212 | } 213 | 214 | fn body(self, body: T) -> Self::WithBody { 215 | DefaultRequestBuilder(Request { 216 | method: self.0.method, 217 | base_path: self.0.base_path, 218 | path: self.0.path, 219 | auth: self.0.auth, 220 | host: self.0.host, 221 | body: Some(body), 222 | content_type: self.0.content_type, 223 | accept: self.0.accept, 224 | extra_headers: self.0.extra_headers, 225 | }) 226 | } 227 | 228 | fn host(mut self, host: &'req str) -> Self { 229 | self.0.host.replace(host); 230 | self 231 | } 232 | 233 | fn content_type(mut self, content_type: ContentType) -> Self { 234 | self.0.content_type.replace(content_type); 235 | self 236 | } 237 | 238 | fn accept(mut self, content_type: ContentType) -> Self { 239 | self.0.accept.replace(content_type); 240 | self 241 | } 242 | 243 | fn basic_auth(mut self, username: &'req str, password: &'req str) -> Self { 244 | self.0.auth.replace(Auth::Basic { username, password }); 245 | self 246 | } 247 | 248 | fn build(self) -> Request<'req, B> { 249 | self.0 250 | } 251 | } 252 | 253 | #[derive(Clone, Copy, Debug, PartialEq)] 254 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 255 | /// HTTP request methods 256 | pub enum Method { 257 | /// GET 258 | GET, 259 | /// PUT 260 | PUT, 261 | /// POST 262 | POST, 263 | /// DELETE 264 | DELETE, 265 | /// HEAD 266 | HEAD, 267 | /// PATCH 268 | PATCH, 269 | /// OPTIONS 270 | OPTIONS, 271 | /// CONNECT 272 | CONNECT, 273 | /// TRACE 274 | TRACE, 275 | } 276 | 277 | impl Method { 278 | /// str representation of method 279 | pub fn as_str(&self) -> &str { 280 | match self { 281 | Method::POST => "POST", 282 | Method::PUT => "PUT", 283 | Method::GET => "GET", 284 | Method::DELETE => "DELETE", 285 | Method::HEAD => "HEAD", 286 | Method::PATCH => "PATCH", 287 | Method::OPTIONS => "OPTIONS", 288 | Method::CONNECT => "CONNECT", 289 | Method::TRACE => "TRACE", 290 | } 291 | } 292 | } 293 | 294 | async fn write_str(c: &mut C, data: &str) -> Result<(), Error> { 295 | c.write_all(data.as_bytes()).await.map_err(|e| e.kind())?; 296 | Ok(()) 297 | } 298 | 299 | async fn write_header(c: &mut C, key: &str, value: &str) -> Result<(), Error> { 300 | write_str(c, key).await?; 301 | write_str(c, ": ").await?; 302 | write_str(c, value).await?; 303 | write_str(c, "\r\n").await?; 304 | Ok(()) 305 | } 306 | 307 | /// The request body 308 | #[allow(clippy::len_without_is_empty)] 309 | pub trait RequestBody { 310 | /// Get the length of the body if known 311 | /// 312 | /// If the length is known, then it will be written in the `Content-Length` header, 313 | /// chunked encoding will be used otherwise. 314 | fn len(&self) -> Option { 315 | None 316 | } 317 | 318 | /// Write the body to the provided writer 319 | async fn write(&self, writer: &mut W) -> Result<(), W::Error>; 320 | } 321 | 322 | impl RequestBody for () { 323 | fn len(&self) -> Option { 324 | None 325 | } 326 | 327 | async fn write(&self, _writer: &mut W) -> Result<(), W::Error> { 328 | Ok(()) 329 | } 330 | } 331 | 332 | impl RequestBody for &[u8] { 333 | fn len(&self) -> Option { 334 | Some(<[u8]>::len(self)) 335 | } 336 | 337 | async fn write(&self, writer: &mut W) -> Result<(), W::Error> { 338 | writer.write_all(self).await 339 | } 340 | } 341 | 342 | impl RequestBody for Option 343 | where 344 | T: RequestBody, 345 | { 346 | fn len(&self) -> Option { 347 | self.as_ref().map(|inner| inner.len()).unwrap_or_default() 348 | } 349 | 350 | async fn write(&self, writer: &mut W) -> Result<(), W::Error> { 351 | if let Some(inner) = self.as_ref() { 352 | inner.write(writer).await 353 | } else { 354 | Ok(()) 355 | } 356 | } 357 | } 358 | 359 | #[cfg(test)] 360 | mod tests { 361 | use super::*; 362 | 363 | #[tokio::test] 364 | async fn basic_auth() { 365 | let mut buffer: Vec = Vec::new(); 366 | Request::new(Method::GET, "/") 367 | .basic_auth("username", "password") 368 | .build() 369 | .write_header(&mut buffer) 370 | .await 371 | .unwrap(); 372 | 373 | assert_eq!( 374 | b"GET / HTTP/1.1\r\nAuthorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=\r\n\r\n", 375 | buffer.as_slice() 376 | ); 377 | } 378 | 379 | #[tokio::test] 380 | async fn with_empty_body() { 381 | let mut buffer = Vec::new(); 382 | Request::new(Method::POST, "/") 383 | .body([].as_slice()) 384 | .build() 385 | .write_header(&mut buffer) 386 | .await 387 | .unwrap(); 388 | 389 | assert_eq!(b"POST / HTTP/1.1\r\nContent-Length: 0\r\n\r\n", buffer.as_slice()); 390 | } 391 | 392 | #[tokio::test] 393 | async fn with_known_body_adds_content_length_header() { 394 | let mut buffer = Vec::new(); 395 | Request::new(Method::POST, "/") 396 | .body(b"BODY".as_slice()) 397 | .build() 398 | .write_header(&mut buffer) 399 | .await 400 | .unwrap(); 401 | 402 | assert_eq!(b"POST / HTTP/1.1\r\nContent-Length: 4\r\n\r\n", buffer.as_slice()); 403 | } 404 | 405 | struct ChunkedBody; 406 | 407 | impl RequestBody for ChunkedBody { 408 | fn len(&self) -> Option { 409 | None // Unknown length: triggers chunked body 410 | } 411 | 412 | async fn write(&self, _writer: &mut W) -> Result<(), W::Error> { 413 | unreachable!() 414 | } 415 | } 416 | 417 | #[tokio::test] 418 | async fn with_unknown_body_adds_transfer_encoding_header() { 419 | let mut buffer = Vec::new(); 420 | 421 | Request::new(Method::POST, "/") 422 | .body(ChunkedBody) 423 | .build() 424 | .write_header(&mut buffer) 425 | .await 426 | .unwrap(); 427 | 428 | assert_eq!( 429 | b"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n", 430 | buffer.as_slice() 431 | ); 432 | } 433 | 434 | #[tokio::test] 435 | async fn with_accept_header() { 436 | let mut buffer: Vec = Vec::new(); 437 | 438 | Request::new(Method::GET, "/") 439 | .accept(ContentType::ApplicationJson) 440 | .build() 441 | .write_header(&mut buffer) 442 | .await 443 | .unwrap(); 444 | 445 | assert_eq!(b"GET / HTTP/1.1\r\nAccept: application/json\r\n\r\n", buffer.as_slice()); 446 | } 447 | } 448 | -------------------------------------------------------------------------------- /tests/client.rs: -------------------------------------------------------------------------------- 1 | use embedded_io_async::{BufRead, Write}; 2 | use hyper::server::conn::Http; 3 | use hyper::service::{make_service_fn, service_fn}; 4 | use hyper::{Body, Server}; 5 | use rand::rngs::OsRng; 6 | use rand::RngCore; 7 | use reqwless::client::HttpClient; 8 | use reqwless::headers::ContentType; 9 | use reqwless::request::{Method, RequestBody, RequestBuilder}; 10 | use reqwless::response::Status; 11 | use std::net::SocketAddr; 12 | use std::sync::Once; 13 | use tokio::net::TcpListener; 14 | use tokio::sync::oneshot; 15 | use tokio_rustls::rustls; 16 | use tokio_rustls::TlsAcceptor; 17 | 18 | mod connection; 19 | 20 | use connection::*; 21 | 22 | static INIT: Once = Once::new(); 23 | 24 | fn setup() { 25 | INIT.call_once(|| { 26 | env_logger::init(); 27 | }); 28 | } 29 | 30 | static TCP: TokioTcp = TokioTcp; 31 | static LOOPBACK_DNS: LoopbackDns = LoopbackDns; 32 | static PUBLIC_DNS: StdDns = StdDns; 33 | 34 | #[tokio::test] 35 | async fn test_request_response_notls() { 36 | setup(); 37 | let addr = ([127, 0, 0, 1], 0).into(); 38 | 39 | let service = make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(echo)) }); 40 | 41 | let server = Server::bind(&addr).serve(service); 42 | let addr = server.local_addr(); 43 | 44 | let (tx, rx) = oneshot::channel(); 45 | let t = tokio::spawn(async move { 46 | tokio::select! { 47 | _ = server => {} 48 | _ = rx => {} 49 | } 50 | }); 51 | 52 | let url = format!("http://127.0.0.1:{}", addr.port()); 53 | let mut client = HttpClient::new(&TCP, &LOOPBACK_DNS); 54 | let mut rx_buf = [0; 4096]; 55 | for _ in 0..2 { 56 | let mut request = client 57 | .request(Method::POST, &url) 58 | .await 59 | .unwrap() 60 | .body(b"PING".as_slice()) 61 | .content_type(ContentType::TextPlain); 62 | let response = request.send(&mut rx_buf).await.unwrap(); 63 | let body = response.body().read_to_end().await; 64 | assert_eq!(body.unwrap(), b"PING"); 65 | } 66 | 67 | tx.send(()).unwrap(); 68 | t.await.unwrap(); 69 | } 70 | 71 | #[tokio::test] 72 | async fn test_resource_notls() { 73 | setup(); 74 | let addr = ([127, 0, 0, 1], 0).into(); 75 | 76 | let service = make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(echo)) }); 77 | 78 | let server = Server::bind(&addr).serve(service); 79 | let addr = server.local_addr(); 80 | 81 | let (tx, rx) = oneshot::channel(); 82 | let t = tokio::spawn(async move { 83 | tokio::select! { 84 | _ = server => {} 85 | _ = rx => {} 86 | } 87 | }); 88 | 89 | let url = format!("http://127.0.0.1:{}", addr.port()); 90 | let mut client = HttpClient::new(&TCP, &LOOPBACK_DNS); 91 | let mut rx_buf = [0; 4096]; 92 | let mut resource = client.resource(&url).await.unwrap(); 93 | for _ in 0..2 { 94 | let response = resource 95 | .post("/") 96 | .body(b"PING".as_slice()) 97 | .content_type(ContentType::TextPlain) 98 | .send(&mut rx_buf) 99 | .await 100 | .unwrap(); 101 | let body = response.body().read_to_end().await; 102 | assert_eq!(body.unwrap(), b"PING"); 103 | } 104 | 105 | tx.send(()).unwrap(); 106 | t.await.unwrap(); 107 | } 108 | 109 | #[tokio::test] 110 | async fn test_resource_notls_bufread() { 111 | setup(); 112 | let addr = ([127, 0, 0, 1], 0).into(); 113 | 114 | let service = make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(echo)) }); 115 | 116 | let server = Server::bind(&addr).serve(service); 117 | let addr = server.local_addr(); 118 | 119 | let (tx, rx) = oneshot::channel(); 120 | let t = tokio::spawn(async move { 121 | tokio::select! { 122 | _ = server => {} 123 | _ = rx => {} 124 | } 125 | }); 126 | 127 | let url = format!("http://127.0.0.1:{}", addr.port()); 128 | let mut client = HttpClient::new(&TCP, &LOOPBACK_DNS); 129 | let mut rx_buf = [0; 4096]; 130 | let mut resource = client.resource(&url).await.unwrap(); 131 | for _ in 0..2 { 132 | let response = resource 133 | .post("/") 134 | .body(b"PING".as_slice()) 135 | .content_type(ContentType::TextPlain) 136 | .send(&mut rx_buf) 137 | .await 138 | .unwrap(); 139 | let mut body_reader = response.body().reader(); 140 | 141 | let mut body = vec![]; 142 | loop { 143 | let buf = body_reader.fill_buf().await.unwrap(); 144 | if buf.is_empty() { 145 | break; 146 | } 147 | body.extend_from_slice(buf); 148 | let buf_len = buf.len(); 149 | body_reader.consume(buf_len); 150 | } 151 | 152 | assert_eq!(body, b"PING"); 153 | } 154 | 155 | tx.send(()).unwrap(); 156 | t.await.unwrap(); 157 | } 158 | 159 | #[tokio::test] 160 | #[cfg(feature = "embedded-tls")] 161 | async fn test_resource_rustls() { 162 | use reqwless::client::{TlsConfig, TlsVerify}; 163 | 164 | setup(); 165 | let addr: SocketAddr = ([127, 0, 0, 1], 0).into(); 166 | 167 | let test_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests"); 168 | let certs = load_certs(&test_dir.join("certs").join("cert.pem")); 169 | let privkey = load_private_key(&test_dir.join("certs").join("key.pem")); 170 | 171 | let versions = &[&rustls::version::TLS13]; 172 | let config = rustls::ServerConfig::builder() 173 | .with_cipher_suites(rustls::ALL_CIPHER_SUITES) 174 | .with_kx_groups(&rustls::ALL_KX_GROUPS) 175 | .with_protocol_versions(versions) 176 | .unwrap() 177 | .with_no_client_auth() 178 | .with_single_cert(certs, privkey) 179 | .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err)) 180 | .unwrap(); 181 | let acceptor = TlsAcceptor::from(std::sync::Arc::new(config)); 182 | 183 | let listener = TcpListener::bind(&addr).await.unwrap(); 184 | let addr = listener.local_addr().unwrap(); 185 | 186 | let (tx, rx) = oneshot::channel(); 187 | let t = tokio::spawn(async move { 188 | tokio::select! { 189 | _ = async move { 190 | let (stream, _) = listener.accept().await.unwrap(); 191 | let stream = acceptor.accept(stream).await.unwrap(); 192 | Http::new() 193 | .http1_only(true) 194 | .http1_keep_alive(true) 195 | .serve_connection(stream, service_fn(echo)) 196 | .await.unwrap(); 197 | } => {} 198 | _ = rx => {} 199 | } 200 | }); 201 | 202 | let mut tls_read_buf: [u8; 16384] = [0; 16384]; 203 | let mut tls_write_buf: [u8; 16384] = [0; 16384]; 204 | let url = format!("https://localhost:{}", addr.port()); 205 | let mut client = HttpClient::new_with_tls( 206 | &TCP, 207 | &LOOPBACK_DNS, 208 | TlsConfig::new(OsRng.next_u64(), &mut tls_read_buf, &mut tls_write_buf, TlsVerify::None), 209 | ); 210 | let mut rx_buf = [0; 4096]; 211 | let mut resource = client.resource(&url).await.unwrap(); 212 | for _ in 0..2 { 213 | let response = resource 214 | .post("/") 215 | .body(b"PING".as_slice()) 216 | .content_type(ContentType::TextPlain) 217 | .send(&mut rx_buf) 218 | .await 219 | .unwrap(); 220 | let body = response.body().read_to_end().await; 221 | assert_eq!(body.unwrap(), b"PING"); 222 | } 223 | 224 | tx.send(()).unwrap(); 225 | t.await.unwrap(); 226 | } 227 | 228 | #[ignore] 229 | #[tokio::test] 230 | #[cfg(feature = "embedded-tls")] 231 | async fn test_resource_drogue_cloud_sandbox() { 232 | use reqwless::client::{TlsConfig, TlsVerify}; 233 | 234 | setup(); 235 | let mut tls_read_buf: [u8; 16384] = [0; 16384]; 236 | let mut tls_write_buf: [u8; 16384] = [0; 16384]; 237 | let mut client = HttpClient::new_with_tls( 238 | &TCP, 239 | &PUBLIC_DNS, 240 | TlsConfig::new(OsRng.next_u64(), &mut tls_read_buf, &mut tls_write_buf, TlsVerify::None), 241 | ); 242 | let mut rx_buf = [0; 4096]; 243 | 244 | // The server must support TLS1.3 245 | // Also, if requests on embedded platforms fail with Error::Dns, then try to 246 | // enable the "alloc" feature on embedded-tls to enable RSA ciphers. 247 | let mut resource = client.resource("https://http.sandbox.drogue.cloud/v1").await.unwrap(); 248 | 249 | for _ in 0..2 { 250 | let response = resource 251 | .post("/testing") 252 | .content_type(ContentType::TextPlain) 253 | .body(b"PING".as_slice()) 254 | .send(&mut rx_buf) 255 | .await 256 | .unwrap(); 257 | assert_eq!(Status::Forbidden, response.status); 258 | assert_eq!(76, response.body().discard().await.unwrap()); 259 | } 260 | } 261 | 262 | #[ignore] 263 | #[tokio::test] 264 | #[cfg(feature = "embedded-tls")] 265 | async fn test_request_response_tls_chunked() { 266 | use core::str; 267 | use std::fs; 268 | 269 | use reqwless::client::{TlsConfig, TlsVerify}; 270 | 271 | setup(); 272 | let mut tls_read_buf: [u8; 16384] = [0; 16384]; 273 | let mut tls_write_buf: [u8; 16384] = [0; 16384]; 274 | let mut client = HttpClient::new_with_tls( 275 | &TCP, 276 | &PUBLIC_DNS, 277 | TlsConfig::new(OsRng.next_u64(), &mut tls_read_buf, &mut tls_write_buf, TlsVerify::None), 278 | ); 279 | let mut rx_buf = [0; 8192]; 280 | 281 | // The server must support TLS1.3 282 | // Also, if requests on embedded platforms fail with Error::Dns, then try to 283 | // enable the "alloc" feature on embedded-tls to enable RSA ciphers. 284 | let mut request = client 285 | .request(Method::GET, "https://api.dictionaryapi.dev/api/v2/entries/en/orange") 286 | .await 287 | .unwrap(); 288 | 289 | let response = request.send(&mut rx_buf).await.unwrap(); 290 | 291 | let body = str::from_utf8(response.body().read_to_end().await.unwrap()).unwrap(); 292 | fs::write("tests/orange-actual.json", body).unwrap(); 293 | 294 | let expected = std::fs::read_to_string("tests/orange.json").unwrap(); 295 | assert_eq!(expected, body); 296 | } 297 | 298 | #[tokio::test] 299 | async fn test_request_response_notls_buffered() { 300 | setup(); 301 | let addr = ([127, 0, 0, 1], 0).into(); 302 | 303 | let service = make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(echo)) }); 304 | 305 | let server = Server::bind(&addr).serve(service); 306 | let addr = server.local_addr(); 307 | 308 | let (tx, rx) = oneshot::channel(); 309 | let t = tokio::spawn(async move { 310 | tokio::select! { 311 | _ = server => {} 312 | _ = rx => {} 313 | } 314 | }); 315 | 316 | let url = format!("http://127.0.0.1:{}", addr.port()); 317 | let mut client = HttpClient::new(&TCP, &LOOPBACK_DNS); 318 | let mut tx_buf = [0; 4096]; 319 | let mut rx_buf = [0; 4096]; 320 | let mut request = client 321 | .request(Method::POST, &url) 322 | .await 323 | .unwrap() 324 | .into_buffered(&mut tx_buf) 325 | .body(b"PING".as_slice()) 326 | .content_type(ContentType::TextPlain); 327 | let response = request.send(&mut rx_buf).await.unwrap(); 328 | let body = response.body().read_to_end().await; 329 | assert_eq!(body.unwrap(), b"PING"); 330 | 331 | tx.send(()).unwrap(); 332 | t.await.unwrap(); 333 | } 334 | 335 | struct ChunkedBody(&'static [&'static [u8]]); 336 | 337 | impl RequestBody for ChunkedBody { 338 | fn len(&self) -> Option { 339 | None // Unknown length: triggers chunked body 340 | } 341 | 342 | async fn write(&self, writer: &mut W) -> Result<(), W::Error> { 343 | for chunk in self.0 { 344 | writer.write_all(chunk).await?; 345 | } 346 | Ok(()) 347 | } 348 | } 349 | 350 | #[tokio::test] 351 | async fn test_request_response_notls_buffered_chunked() { 352 | setup(); 353 | let addr = ([127, 0, 0, 1], 0).into(); 354 | 355 | let service = make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(echo)) }); 356 | 357 | let server = Server::bind(&addr).serve(service); 358 | let addr = server.local_addr(); 359 | 360 | let (tx, rx) = oneshot::channel(); 361 | let t = tokio::spawn(async move { 362 | tokio::select! { 363 | _ = server => {} 364 | _ = rx => {} 365 | } 366 | }); 367 | 368 | let url = format!("http://127.0.0.1:{}", addr.port()); 369 | let mut client = HttpClient::new(&TCP, &LOOPBACK_DNS); 370 | let mut tx_buf = [0; 4096]; 371 | let mut rx_buf = [0; 4096]; 372 | static CHUNKS: [&'static [u8]; 2] = [b"PART1", b"PART2"]; 373 | let mut request = client 374 | .request(Method::POST, &url) 375 | .await 376 | .unwrap() 377 | .into_buffered(&mut tx_buf) 378 | .body(ChunkedBody(&CHUNKS)) 379 | .content_type(ContentType::TextPlain); 380 | let response = request.send(&mut rx_buf).await.unwrap(); 381 | let body = response.body().read_to_end().await; 382 | assert_eq!(body.unwrap(), b"PART1PART2"); 383 | 384 | tx.send(()).unwrap(); 385 | t.await.unwrap(); 386 | } 387 | 388 | fn load_certs(filename: &std::path::PathBuf) -> Vec { 389 | let certfile = std::fs::File::open(filename).expect("cannot open certificate file"); 390 | let mut reader = std::io::BufReader::new(certfile); 391 | rustls_pemfile::certs(&mut reader) 392 | .unwrap() 393 | .iter() 394 | .map(|v| rustls::Certificate(v.clone())) 395 | .collect() 396 | } 397 | 398 | fn load_private_key(filename: &std::path::PathBuf) -> rustls::PrivateKey { 399 | let keyfile = std::fs::File::open(filename).expect("cannot open private key file"); 400 | let mut reader = std::io::BufReader::new(keyfile); 401 | 402 | loop { 403 | match rustls_pemfile::read_one(&mut reader).expect("cannot parse private key .pem file") { 404 | Some(rustls_pemfile::Item::RSAKey(key)) => return rustls::PrivateKey(key), 405 | Some(rustls_pemfile::Item::PKCS8Key(key)) => return rustls::PrivateKey(key), 406 | None => break, 407 | _ => {} 408 | } 409 | } 410 | 411 | panic!("no keys found in {:?} (encrypted keys not supported)", filename); 412 | } 413 | 414 | async fn echo(req: hyper::Request) -> Result, hyper::Error> { 415 | match (req.method(), req.uri().path()) { 416 | _ => Ok(hyper::Response::new(req.into_body())), 417 | } 418 | } 419 | 420 | #[test] 421 | fn compile_tests() { 422 | #[allow(dead_code)] 423 | async fn rx_buffer_lifetime_is_propagated_to_output<'buf>(port: u16, rx_buf: &'buf mut [u8]) -> &'buf mut [u8] { 424 | let mut http = HttpClient::new(&TCP, &LOOPBACK_DNS); 425 | let url = format!("http://127.0.0.1:{}", port); 426 | let mut request = http.request(Method::GET, &url).await.unwrap(); 427 | let response = request.send(rx_buf).await.unwrap(); 428 | response.body().read_to_end().await.unwrap() 429 | } 430 | } 431 | -------------------------------------------------------------------------------- /src/body_writer/buffering_chunked.rs: -------------------------------------------------------------------------------- 1 | use embedded_io::{Error as _, ErrorType}; 2 | use embedded_io_async::Write; 3 | 4 | use super::chunked::write_chunked_header; 5 | 6 | const EMPTY_CHUNK: &[u8; 5] = b"0\r\n\r\n"; 7 | const NEWLINE: &[u8; 2] = b"\r\n"; 8 | 9 | /// A body writer that buffers internally and emits chunks as expected by the 10 | /// `Transfer-Encoding: chunked` header specification. 11 | /// 12 | /// Each emittted chunk has a header that specifies the size of the chunk, 13 | /// and the last chunk has size equal to zero, indicating the end of the request. 14 | /// 15 | /// The writer can be seeded with a buffer that is already pre-written. This is 16 | /// typical if for example the request header is already written to the buffer. 17 | /// The writer will in this case start appending a chunk to the end of the pre-written 18 | /// buffer data leaving the pre-written data as-is. 19 | /// 20 | /// To minimize the number of write calls to the underlying connection the writer 21 | /// works by pre-allocating the chunk header in the buffer. The written body data is 22 | /// then appended after this pre-allocated header. Depending on the number of bytes 23 | /// actually written to the current chunk before the writer is terminated (indicating 24 | /// the end of the request body), the pre-allocated header may be too large. If this 25 | /// is the case, then the chunk payload is moved into the pre-allocated header region 26 | /// such that the header and payload can be written to the underlying connection in 27 | /// a single write. 28 | /// 29 | pub struct BufferingChunkedBodyWriter<'a, C: Write> { 30 | conn: C, 31 | buf: &'a mut [u8], 32 | /// The position where the allocated chunk header starts 33 | header_pos: usize, 34 | /// The size of the allocated header (the final header may be smaller) 35 | /// This may be 0 if the pre-written bytes in `buf` is too large to fit a header. 36 | allocated_header: usize, 37 | /// The position of the data in the chunk 38 | pos: usize, 39 | terminated: bool, 40 | } 41 | 42 | impl<'a, C> BufferingChunkedBodyWriter<'a, C> 43 | where 44 | C: Write, 45 | { 46 | pub fn new_with_data(conn: C, buf: &'a mut [u8], written: usize) -> Self { 47 | assert!(written <= buf.len()); 48 | let allocated_header = get_max_chunk_header_size(buf.len() - written); 49 | assert!(buf.len() > allocated_header + NEWLINE.len()); // There must be space for the chunk header and footer 50 | Self { 51 | conn, 52 | buf, 53 | header_pos: written, 54 | pos: written + allocated_header, 55 | allocated_header, 56 | terminated: false, 57 | } 58 | } 59 | 60 | /// Terminate the request body by writing an empty chunk 61 | pub async fn terminate(&mut self) -> Result<(), C::Error> { 62 | assert!(!self.terminated); 63 | 64 | if self.pos > self.header_pos + self.allocated_header { 65 | // There are bytes written in the current chunk 66 | self.finish_current_chunk(); 67 | } 68 | 69 | if self.header_pos + EMPTY_CHUNK.len() > self.buf.len() { 70 | // There is not enough space to fit the empty chunk in the buffer 71 | self.emit_buffered().await?; 72 | } 73 | 74 | self.buf[self.header_pos..self.header_pos + EMPTY_CHUNK.len()].copy_from_slice(EMPTY_CHUNK); 75 | self.header_pos += EMPTY_CHUNK.len(); 76 | self.allocated_header = 0; 77 | self.pos = self.header_pos + self.allocated_header; 78 | self.emit_buffered().await?; 79 | self.terminated = true; 80 | Ok(()) 81 | } 82 | 83 | /// Append data to the current chunk and return the number of bytes appended. 84 | /// This returns 0 if there is no current chunk to append to. 85 | fn append_current_chunk(&mut self, buf: &[u8]) -> usize { 86 | let buffered = usize::min(buf.len(), self.buf.len().saturating_sub(NEWLINE.len() + self.pos)); 87 | if buffered > 0 { 88 | self.buf[self.pos..self.pos + buffered].copy_from_slice(&buf[..buffered]); 89 | self.pos += buffered; 90 | } 91 | buffered 92 | } 93 | 94 | /// Finish the current chunk by writing the header 95 | fn finish_current_chunk(&mut self) { 96 | // Write the header in the allocated position position 97 | let chunk_len = self.pos - self.header_pos - self.allocated_header; 98 | let header_buf = &mut self.buf[self.header_pos..self.header_pos + self.allocated_header]; 99 | let header_len = write_chunked_header(header_buf, chunk_len); 100 | 101 | // Move the payload if the header length was not as large as it could possibly be 102 | let spacing = self.allocated_header - header_len; 103 | if spacing > 0 { 104 | self.buf.copy_within( 105 | self.header_pos + self.allocated_header..self.pos, 106 | self.header_pos + header_len, 107 | ); 108 | self.pos -= spacing 109 | } 110 | 111 | // Write newline footer after chunk payload 112 | self.buf[self.pos..self.pos + NEWLINE.len()].copy_from_slice(NEWLINE); 113 | self.pos += 2; 114 | 115 | self.header_pos = self.pos; 116 | self.allocated_header = get_max_chunk_header_size(self.buf.len() - self.header_pos); 117 | self.pos = self.header_pos + self.allocated_header; 118 | } 119 | 120 | fn current_chunk_is_full(&self) -> bool { 121 | self.pos + NEWLINE.len() == self.buf.len() 122 | } 123 | 124 | async fn emit_buffered(&mut self) -> Result<(), C::Error> { 125 | self.conn.write_all(&self.buf[..self.header_pos]).await?; 126 | self.header_pos = 0; 127 | self.allocated_header = get_max_chunk_header_size(self.buf.len()); 128 | self.pos = self.allocated_header; 129 | Ok(()) 130 | } 131 | } 132 | 133 | impl ErrorType for BufferingChunkedBodyWriter<'_, C> 134 | where 135 | C: Write, 136 | { 137 | type Error = embedded_io::ErrorKind; 138 | } 139 | 140 | impl Write for BufferingChunkedBodyWriter<'_, C> 141 | where 142 | C: Write, 143 | { 144 | async fn write(&mut self, buf: &[u8]) -> Result { 145 | if buf.is_empty() { 146 | return Ok(0); 147 | } 148 | 149 | let mut written = self.append_current_chunk(buf); 150 | if written == 0 { 151 | // Unable to append any data to the buffer 152 | // This can happen if the writer was pre-loaded with data 153 | self.emit_buffered().await.map_err(|e| e.kind())?; 154 | written = self.append_current_chunk(buf); 155 | } 156 | if self.current_chunk_is_full() { 157 | self.finish_current_chunk(); 158 | self.emit_buffered().await.map_err(|e| e.kind())?; 159 | } 160 | Ok(written) 161 | } 162 | 163 | async fn flush(&mut self) -> Result<(), Self::Error> { 164 | if self.pos > self.header_pos + self.allocated_header { 165 | // There are bytes written in the current chunk 166 | self.finish_current_chunk(); 167 | self.emit_buffered().await.map_err(|e| e.kind())?; 168 | } else if self.header_pos > 0 { 169 | // There are pre-written bytes in the buffer but no current chunk 170 | // (the number of pre-written was so large that the space for a header could not be allocated) 171 | self.emit_buffered().await.map_err(|e| e.kind())?; 172 | } 173 | self.conn.flush().await.map_err(|e| e.kind()) 174 | } 175 | } 176 | 177 | /// Get the number of hex characters for a number. 178 | /// E.g. 0x0 => 1, 0x0F => 1, 0x10 => 2, 0x1234 => 4. 179 | const fn get_num_hex_chars(number: usize) -> usize { 180 | if number == 0 { 181 | 1 182 | } else { 183 | (usize::BITS - number.leading_zeros()).div_ceil(4) as usize 184 | } 185 | } 186 | 187 | const fn get_max_chunk_header_size(buffer_size: usize) -> usize { 188 | if let Some(hex_chars_and_payload_size) = buffer_size.checked_sub(2 * NEWLINE.len()) { 189 | get_num_hex_chars(hex_chars_and_payload_size) + NEWLINE.len() 190 | } else { 191 | // Not enough space in buffer to fit a header + footer 192 | 0 193 | } 194 | } 195 | 196 | #[cfg(test)] 197 | mod tests { 198 | use super::*; 199 | 200 | #[test] 201 | fn can_get_hex_chars() { 202 | assert_eq!(1, get_num_hex_chars(0)); 203 | assert_eq!(1, get_num_hex_chars(1)); 204 | assert_eq!(1, get_num_hex_chars(0xF)); 205 | assert_eq!(2, get_num_hex_chars(0x10)); 206 | assert_eq!(2, get_num_hex_chars(0xFF)); 207 | assert_eq!(3, get_num_hex_chars(0x100)); 208 | } 209 | 210 | #[test] 211 | fn can_get_max_chunk_header_size() { 212 | assert_eq!(0, get_max_chunk_header_size(0)); 213 | assert_eq!(0, get_max_chunk_header_size(1)); 214 | assert_eq!(0, get_max_chunk_header_size(2)); 215 | assert_eq!(0, get_max_chunk_header_size(3)); 216 | assert_eq!(3, get_max_chunk_header_size(0x00 + 2 + 2)); 217 | assert_eq!(3, get_max_chunk_header_size(0x01 + 2 + 2)); 218 | assert_eq!(3, get_max_chunk_header_size(0x0F + 2 + 2)); 219 | assert_eq!(4, get_max_chunk_header_size(0x10 + 2 + 2)); 220 | assert_eq!(4, get_max_chunk_header_size(0x11 + 2 + 2)); 221 | assert_eq!(4, get_max_chunk_header_size(0x12 + 2 + 2)); 222 | } 223 | 224 | #[tokio::test] 225 | async fn preserves_already_written_bytes_in_the_buffer_without_any_chunks() { 226 | // Given 227 | let mut conn = Vec::new(); 228 | let mut buf = [0; 1024]; 229 | buf[..5].copy_from_slice(b"HELLO"); 230 | 231 | // When 232 | let mut writer = BufferingChunkedBodyWriter::new_with_data(&mut conn, &mut buf, 5); 233 | writer.terminate().await.unwrap(); 234 | 235 | // Then 236 | assert_eq!(b"HELLO0\r\n\r\n", conn.as_slice()); 237 | } 238 | 239 | #[tokio::test] 240 | async fn preserves_already_written_bytes_in_the_buffer_with_chunks() { 241 | // Given 242 | let mut conn = Vec::new(); 243 | let mut buf = [0; 1024]; 244 | buf[..5].copy_from_slice(b"HELLO"); 245 | 246 | // When 247 | let mut writer = BufferingChunkedBodyWriter::new_with_data(&mut conn, &mut buf, 5); 248 | writer.write_all(b"BODY").await.unwrap(); 249 | writer.terminate().await.unwrap(); 250 | 251 | // Then 252 | assert_eq!(b"HELLO4\r\nBODY\r\n0\r\n\r\n", conn.as_slice()); 253 | } 254 | 255 | #[tokio::test] 256 | async fn write_when_entire_buffer_is_prewritten() { 257 | // Given 258 | let mut conn = Vec::new(); 259 | let mut buf = [0; 10]; 260 | buf.copy_from_slice(b"HELLOHELLO"); 261 | 262 | // When 263 | let mut writer = BufferingChunkedBodyWriter::new_with_data(&mut conn, &mut buf, 10); 264 | writer.write_all(b"BODY").await.unwrap(); // Cannot fit 265 | writer.terminate().await.unwrap(); 266 | 267 | // Then 268 | print!("{:?}", conn.as_slice()); 269 | assert_eq!(b"HELLOHELLO4\r\nBODY\r\n0\r\n\r\n", conn.as_slice()); 270 | } 271 | 272 | #[tokio::test] 273 | async fn flush_empty_body_when_entire_buffer_is_prewritten() { 274 | // Given 275 | let mut conn = Vec::new(); 276 | let mut buf = [0; 10]; 277 | buf.copy_from_slice(b"HELLOHELLO"); 278 | 279 | // When 280 | let mut writer = BufferingChunkedBodyWriter::new_with_data(&mut conn, &mut buf, 10); 281 | writer.flush().await.unwrap(); 282 | 283 | // Then 284 | print!("{:?}", conn.as_slice()); 285 | assert_eq!(b"HELLOHELLO", conn.as_slice()); 286 | } 287 | 288 | #[tokio::test] 289 | async fn terminate_empty_body_when_entire_buffer_is_prewritten() { 290 | // Given 291 | let mut conn = Vec::new(); 292 | let mut buf = [0; 10]; 293 | buf.copy_from_slice(b"HELLOHELLO"); 294 | 295 | // When 296 | let mut writer = BufferingChunkedBodyWriter::new_with_data(&mut conn, &mut buf, 10); 297 | writer.terminate().await.unwrap(); 298 | 299 | // Then 300 | print!("{:?}", conn.as_slice()); 301 | assert_eq!(b"HELLOHELLO0\r\n\r\n", conn.as_slice()); 302 | } 303 | 304 | #[tokio::test] 305 | async fn flush_when_entire_buffer_is_nearly_prewritten() { 306 | // Given 307 | let mut conn = Vec::new(); 308 | let mut buf = [0; 11]; 309 | buf[..10].copy_from_slice(b"HELLOHELLO"); 310 | 311 | // When 312 | let mut writer = BufferingChunkedBodyWriter::new_with_data(&mut conn, &mut buf, 10); 313 | writer.flush().await.unwrap(); 314 | 315 | // Then 316 | print!("{:?}", conn.as_slice()); 317 | assert_eq!(b"HELLOHELLO", conn.as_slice()); 318 | } 319 | 320 | #[tokio::test] 321 | async fn flushes_already_written_bytes_if_first_cannot_fit() { 322 | // Given 323 | let mut conn = Vec::new(); 324 | let mut buf = [0; 10]; 325 | buf[..5].copy_from_slice(b"HELLO"); 326 | 327 | // When 328 | let mut writer = BufferingChunkedBodyWriter::new_with_data(&mut conn, &mut buf, 5); 329 | writer.write_all(b"BODY").await.unwrap(); // Cannot fit 330 | writer.terminate().await.unwrap(); // Can fit 331 | 332 | // Then 333 | assert_eq!(b"HELLO4\r\nBODY\r\n0\r\n\r\n", conn.as_slice()); 334 | } 335 | 336 | #[tokio::test] 337 | async fn written_bytes_fit_exactly() { 338 | // Given 339 | let mut conn = Vec::new(); 340 | let mut buf = [0; 14]; 341 | buf[..5].copy_from_slice(b"HELLO"); 342 | 343 | // When 344 | let mut writer = BufferingChunkedBodyWriter::new_with_data(&mut conn, &mut buf, 5); 345 | writer.write_all(b"BODY").await.unwrap(); // Can fit exactly 346 | writer.write_all(b"BODY").await.unwrap(); // Can fit 347 | writer.terminate().await.unwrap(); // Can fit 348 | 349 | // Then 350 | assert_eq!(b"HELLO4\r\nBODY\r\n4\r\nBODY\r\n0\r\n\r\n", conn.as_slice()); 351 | } 352 | 353 | #[tokio::test] 354 | async fn current_chunk_is_emitted_on_termination_before_empty_chunk_is_emitted() { 355 | // Given 356 | let mut conn = Vec::new(); 357 | let mut buf = [0; 14]; 358 | buf[..5].copy_from_slice(b"HELLO"); 359 | 360 | // When 361 | let mut writer = BufferingChunkedBodyWriter::new_with_data(&mut conn, &mut buf, 5); 362 | writer.write_all(b"BOD").await.unwrap(); // Can fit 363 | writer.terminate().await.unwrap(); // Cannot fit 364 | 365 | // Then 366 | assert_eq!(b"HELLO3\r\nBOD\r\n0\r\n\r\n", conn.as_slice()); 367 | } 368 | 369 | #[tokio::test] 370 | async fn write_emits_chunks() { 371 | // Given 372 | let mut conn = Vec::new(); 373 | let mut buf = [0; 12]; 374 | buf[..5].copy_from_slice(b"HELLO"); 375 | 376 | // When 377 | let mut writer = BufferingChunkedBodyWriter::new_with_data(&mut conn, &mut buf, 5); 378 | writer.write_all(b"BODY").await.unwrap(); // Only "BO" can fit first, then "DY" is written in a different chunk 379 | writer.terminate().await.unwrap(); 380 | 381 | // Then 382 | assert_eq!(b"HELLO2\r\nBO\r\n2\r\nDY\r\n0\r\n\r\n", conn.as_slice()); 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | /// Client using embedded-nal-async traits to establish connections and perform HTTP requests. 2 | /// 3 | use crate::body_writer::{BufferingChunkedBodyWriter, ChunkedBodyWriter, FixedBodyWriter}; 4 | use crate::headers::ContentType; 5 | use crate::request::*; 6 | use crate::response::*; 7 | use crate::Error; 8 | use buffered_io::asynch::BufferedWrite; 9 | use core::net::SocketAddr; 10 | use embedded_io::Error as _; 11 | use embedded_io::ErrorType; 12 | use embedded_io_async::{Read, Write}; 13 | use embedded_nal_async::{Dns, TcpConnect}; 14 | use nourl::{Url, UrlScheme}; 15 | 16 | /// An async HTTP client that can establish a TCP connection and perform 17 | /// HTTP requests. 18 | pub struct HttpClient<'a, T, D> 19 | where 20 | T: TcpConnect + 'a, 21 | D: Dns + 'a, 22 | { 23 | client: &'a T, 24 | dns: &'a D, 25 | #[cfg(any(feature = "embedded-tls", feature = "esp-mbedtls"))] 26 | tls: Option>, 27 | } 28 | 29 | /// Type for TLS configuration of HTTP client. 30 | #[cfg(feature = "esp-mbedtls")] 31 | pub struct TlsConfig<'a, const RX_SIZE: usize = 4096, const TX_SIZE: usize = 4096> { 32 | /// Minimum TLS version for the connection 33 | version: crate::TlsVersion, 34 | 35 | /// Client certificates. See [esp_mbedtls::Certificates] 36 | certificates: crate::Certificates<'a>, 37 | 38 | /// A reference to instance of the MbedTLS library. 39 | tls_reference: esp_mbedtls::TlsReference<'a>, 40 | } 41 | 42 | /// Type for TLS configuration of HTTP client. 43 | #[cfg(feature = "embedded-tls")] 44 | pub struct TlsConfig<'a> { 45 | seed: u64, 46 | read_buffer: &'a mut [u8], 47 | write_buffer: &'a mut [u8], 48 | verify: TlsVerify<'a>, 49 | } 50 | 51 | /// Supported verification modes. 52 | #[cfg(feature = "embedded-tls")] 53 | pub enum TlsVerify<'a> { 54 | /// No verification of the remote host 55 | None, 56 | /// Use pre-shared keys for verifying 57 | Psk { identity: &'a [u8], psk: &'a [u8] }, 58 | } 59 | 60 | #[cfg(feature = "embedded-tls")] 61 | impl<'a> TlsConfig<'a> { 62 | pub fn new(seed: u64, read_buffer: &'a mut [u8], write_buffer: &'a mut [u8], verify: TlsVerify<'a>) -> Self { 63 | Self { 64 | seed, 65 | write_buffer, 66 | read_buffer, 67 | verify, 68 | } 69 | } 70 | } 71 | 72 | #[cfg(feature = "esp-mbedtls")] 73 | impl<'a, const RX_SIZE: usize, const TX_SIZE: usize> TlsConfig<'a, RX_SIZE, TX_SIZE> { 74 | pub fn new( 75 | version: crate::TlsVersion, 76 | certificates: crate::Certificates<'a>, 77 | tls_reference: crate::TlsReference<'a>, 78 | ) -> Self { 79 | Self { 80 | version, 81 | certificates, 82 | tls_reference, 83 | } 84 | } 85 | } 86 | 87 | impl<'a, T, D> HttpClient<'a, T, D> 88 | where 89 | T: TcpConnect + 'a, 90 | D: Dns + 'a, 91 | { 92 | /// Create a new HTTP client for a given connection handle and a target host. 93 | pub fn new(client: &'a T, dns: &'a D) -> Self { 94 | Self { 95 | client, 96 | dns, 97 | #[cfg(any(feature = "embedded-tls", feature = "esp-mbedtls"))] 98 | tls: None, 99 | } 100 | } 101 | 102 | /// Create a new HTTP client for a given connection handle and a target host. 103 | #[cfg(any(feature = "embedded-tls", feature = "esp-mbedtls"))] 104 | pub fn new_with_tls(client: &'a T, dns: &'a D, tls: TlsConfig<'a>) -> Self { 105 | Self { 106 | client, 107 | dns, 108 | tls: Some(tls), 109 | } 110 | } 111 | 112 | async fn connect<'conn>( 113 | &'conn mut self, 114 | url: &Url<'_>, 115 | ) -> Result>, Error> { 116 | let host = url.host(); 117 | let port = url.port_or_default(); 118 | 119 | let remote = self 120 | .dns 121 | .get_host_by_name(host, embedded_nal_async::AddrType::Either) 122 | .await 123 | .map_err(|_| Error::Dns)?; 124 | 125 | let conn = self 126 | .client 127 | .connect(SocketAddr::new(remote, port)) 128 | .await 129 | .map_err(|e| e.kind())?; 130 | 131 | if url.scheme() == UrlScheme::HTTPS { 132 | #[cfg(feature = "esp-mbedtls")] 133 | if let Some(tls) = self.tls.as_mut() { 134 | let mut servername = host.as_bytes().to_vec(); 135 | servername.push(0); 136 | let mut session = esp_mbedtls::asynch::Session::new( 137 | conn, 138 | esp_mbedtls::Mode::Client { 139 | servername: unsafe { core::ffi::CStr::from_bytes_with_nul_unchecked(&servername) }, 140 | }, 141 | tls.version, 142 | tls.certificates, 143 | tls.tls_reference, 144 | )?; 145 | 146 | session.connect().await?; 147 | Ok(HttpConnection::Tls(session)) 148 | } else { 149 | Ok(HttpConnection::Plain(conn)) 150 | } 151 | 152 | #[cfg(feature = "embedded-tls")] 153 | if let Some(tls) = self.tls.as_mut() { 154 | use embedded_tls::{TlsConfig, TlsContext, UnsecureProvider}; 155 | use rand_chacha::ChaCha8Rng; 156 | use rand_core::{RngCore, SeedableRng}; 157 | let mut rng = ChaCha8Rng::seed_from_u64(tls.seed); 158 | tls.seed = rng.next_u64(); 159 | let mut config = TlsConfig::new().with_server_name(url.host()); 160 | if let TlsVerify::Psk { identity, psk } = tls.verify { 161 | config = config.with_psk(psk, &[identity]); 162 | } 163 | let mut conn: embedded_tls::TlsConnection<'conn, T::Connection<'conn>, embedded_tls::Aes128GcmSha256> = 164 | embedded_tls::TlsConnection::new(conn, tls.read_buffer, tls.write_buffer); 165 | conn.open(TlsContext::new(&config, UnsecureProvider::new::(rng))) 166 | .await?; 167 | Ok(HttpConnection::Tls(conn)) 168 | } else { 169 | Ok(HttpConnection::Plain(conn)) 170 | } 171 | #[cfg(all(not(feature = "embedded-tls"), not(feature = "esp-mbedtls")))] 172 | Err(Error::InvalidUrl(nourl::Error::UnsupportedScheme)) 173 | } else { 174 | #[cfg(feature = "embedded-tls")] 175 | match self.tls.as_mut() { 176 | Some(tls) => Ok(HttpConnection::PlainBuffered(BufferedWrite::new( 177 | conn, 178 | tls.write_buffer, 179 | ))), 180 | None => Ok(HttpConnection::Plain(conn)), 181 | } 182 | #[cfg(not(feature = "embedded-tls"))] 183 | Ok(HttpConnection::Plain(conn)) 184 | } 185 | } 186 | 187 | /// Create a single http request. 188 | pub async fn request<'conn>( 189 | &'conn mut self, 190 | method: Method, 191 | url: &'conn str, 192 | ) -> Result, ()>, Error> { 193 | let url = Url::parse(url)?; 194 | let conn = self.connect(&url).await?; 195 | Ok(HttpRequestHandle { 196 | conn, 197 | request: Some(Request::new(method, url.path()).host(url.host())), 198 | }) 199 | } 200 | 201 | /// Create a connection to a server with the provided `resource_url`. 202 | /// The path in the url is considered the base path for subsequent requests. 203 | pub async fn resource<'res>( 204 | &'res mut self, 205 | resource_url: &'res str, 206 | ) -> Result>, Error> { 207 | let resource_url = Url::parse(resource_url)?; 208 | let conn = self.connect(&resource_url).await?; 209 | Ok(HttpResource { 210 | conn, 211 | host: resource_url.host(), 212 | base_path: resource_url.path(), 213 | }) 214 | } 215 | } 216 | 217 | /// Represents a HTTP connection that may be encrypted or unencrypted. 218 | #[allow(clippy::large_enum_variant)] 219 | pub enum HttpConnection<'conn, C> 220 | where 221 | C: Read + Write, 222 | { 223 | Plain(C), 224 | PlainBuffered(BufferedWrite<'conn, C>), 225 | #[cfg(feature = "esp-mbedtls")] 226 | Tls(esp_mbedtls::asynch::Session<'conn, C>), 227 | #[cfg(feature = "embedded-tls")] 228 | Tls(embedded_tls::TlsConnection<'conn, C, embedded_tls::Aes128GcmSha256>), 229 | #[cfg(all(not(feature = "embedded-tls"), not(feature = "esp-mbedtls")))] 230 | Tls((&'conn mut (), core::convert::Infallible)), // Variant is impossible to create, but we need it to avoid "unused lifetime" warning 231 | } 232 | 233 | #[cfg(feature = "defmt")] 234 | impl defmt::Format for HttpConnection<'_, C> 235 | where 236 | C: Read + Write, 237 | { 238 | fn format(&self, fmt: defmt::Formatter) { 239 | match self { 240 | HttpConnection::Plain(_) => defmt::write!(fmt, "Plain"), 241 | HttpConnection::PlainBuffered(_) => defmt::write!(fmt, "PlainBuffered"), 242 | HttpConnection::Tls(_) => defmt::write!(fmt, "Tls"), 243 | } 244 | } 245 | } 246 | 247 | impl core::fmt::Debug for HttpConnection<'_, C> 248 | where 249 | C: Read + Write, 250 | { 251 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { 252 | match self { 253 | HttpConnection::Plain(_) => f.debug_tuple("Plain").finish(), 254 | HttpConnection::PlainBuffered(_) => f.debug_tuple("PlainBuffered").finish(), 255 | HttpConnection::Tls(_) => f.debug_tuple("Tls").finish(), 256 | } 257 | } 258 | } 259 | 260 | impl<'conn, T> HttpConnection<'conn, T> 261 | where 262 | T: Read + Write, 263 | { 264 | /// Turn the request into a buffered request. 265 | /// 266 | /// This is only relevant if no TLS is used, as `embedded-tls` buffers internally and we reuse 267 | /// its buffer for non-TLS connections. 268 | pub fn into_buffered<'buf>(self, tx_buf: &'buf mut [u8]) -> HttpConnection<'buf, T> 269 | where 270 | 'conn: 'buf, 271 | { 272 | match self { 273 | HttpConnection::Plain(conn) => HttpConnection::PlainBuffered(BufferedWrite::new(conn, tx_buf)), 274 | HttpConnection::PlainBuffered(conn) => HttpConnection::PlainBuffered(conn), 275 | HttpConnection::Tls(tls) => HttpConnection::Tls(tls), 276 | } 277 | } 278 | 279 | /// Send a request on an established connection. 280 | /// 281 | /// The request is sent in its raw form without any base path from the resource. 282 | /// The response headers are stored in the provided rx_buf, which should be sized to contain at least the response headers. 283 | /// 284 | /// The response is returned. 285 | pub async fn send<'req, 'buf, B: RequestBody>( 286 | &'conn mut self, 287 | request: Request<'req, B>, 288 | rx_buf: &'buf mut [u8], 289 | ) -> Result>, Error> { 290 | self.write_request(&request).await?; 291 | self.flush().await?; 292 | Response::read(self, request.method, rx_buf).await 293 | } 294 | 295 | async fn write_request<'req, B: RequestBody>(&mut self, request: &Request<'req, B>) -> Result<(), Error> { 296 | request.write_header(self).await?; 297 | 298 | if let Some(body) = request.body.as_ref() { 299 | match body.len() { 300 | Some(0) => { 301 | // Empty body 302 | } 303 | Some(len) => { 304 | trace!("Writing not-chunked body"); 305 | let mut writer = FixedBodyWriter::new(self); 306 | body.write(&mut writer).await.map_err(|e| e.kind())?; 307 | 308 | if writer.written() != len { 309 | return Err(Error::IncorrectBodyWritten); 310 | } 311 | } 312 | None => { 313 | trace!("Writing chunked body"); 314 | match self { 315 | HttpConnection::Plain(c) => { 316 | let mut writer = ChunkedBodyWriter::new(c); 317 | body.write(&mut writer).await?; 318 | writer.terminate().await.map_err(|e| e.kind())?; 319 | } 320 | HttpConnection::PlainBuffered(buffered) => { 321 | let (conn, buf, unwritten) = buffered.split(); 322 | let mut writer = BufferingChunkedBodyWriter::new_with_data(conn, buf, unwritten); 323 | body.write(&mut writer).await?; 324 | writer.terminate().await.map_err(|e| e.kind())?; 325 | buffered.clear(); 326 | } 327 | #[cfg(any(feature = "embedded-tls", feature = "esp-mbedtls"))] 328 | HttpConnection::Tls(c) => { 329 | let mut writer = ChunkedBodyWriter::new(c); 330 | body.write(&mut writer).await?; 331 | writer.terminate().await.map_err(|e| e.kind())?; 332 | } 333 | #[cfg(all(not(feature = "embedded-tls"), not(feature = "esp-mbedtls")))] 334 | HttpConnection::Tls(_) => unreachable!(), 335 | }; 336 | } 337 | } 338 | } 339 | Ok(()) 340 | } 341 | } 342 | 343 | impl ErrorType for HttpConnection<'_, T> 344 | where 345 | T: Read + Write, 346 | { 347 | type Error = embedded_io::ErrorKind; 348 | } 349 | 350 | impl Read for HttpConnection<'_, T> 351 | where 352 | T: Read + Write, 353 | { 354 | async fn read(&mut self, buf: &mut [u8]) -> Result { 355 | match self { 356 | Self::Plain(conn) => conn.read(buf).await.map_err(|e| e.kind()), 357 | Self::PlainBuffered(conn) => conn.read(buf).await.map_err(|e| e.kind()), 358 | #[cfg(any(feature = "embedded-tls", feature = "esp-mbedtls"))] 359 | Self::Tls(conn) => conn.read(buf).await.map_err(|e| e.kind()), 360 | #[cfg(not(any(feature = "embedded-tls", feature = "esp-mbedtls")))] 361 | _ => unreachable!(), 362 | } 363 | } 364 | } 365 | 366 | impl Write for HttpConnection<'_, T> 367 | where 368 | T: Read + Write, 369 | { 370 | async fn write(&mut self, buf: &[u8]) -> Result { 371 | match self { 372 | Self::Plain(conn) => conn.write(buf).await.map_err(|e| e.kind()), 373 | Self::PlainBuffered(conn) => conn.write(buf).await.map_err(|e| e.kind()), 374 | #[cfg(any(feature = "embedded-tls", feature = "esp-mbedtls"))] 375 | Self::Tls(conn) => conn.write(buf).await.map_err(|e| e.kind()), 376 | #[cfg(not(any(feature = "embedded-tls", feature = "esp-mbedtls")))] 377 | _ => unreachable!(), 378 | } 379 | } 380 | 381 | async fn flush(&mut self) -> Result<(), Self::Error> { 382 | match self { 383 | Self::Plain(conn) => conn.flush().await.map_err(|e| e.kind()), 384 | Self::PlainBuffered(conn) => conn.flush().await.map_err(|e| e.kind()), 385 | #[cfg(any(feature = "embedded-tls", feature = "esp-mbedtls"))] 386 | Self::Tls(conn) => conn.flush().await.map_err(|e| e.kind()), 387 | #[cfg(not(any(feature = "embedded-tls", feature = "esp-mbedtls")))] 388 | _ => unreachable!(), 389 | } 390 | } 391 | } 392 | 393 | /// A HTTP request handle 394 | /// 395 | /// The underlying connection is closed when drop'ed. 396 | pub struct HttpRequestHandle<'conn, C, B> 397 | where 398 | C: Read + Write, 399 | B: RequestBody, 400 | { 401 | pub conn: HttpConnection<'conn, C>, 402 | request: Option>, 403 | } 404 | 405 | impl<'conn, C, B> HttpRequestHandle<'conn, C, B> 406 | where 407 | C: Read + Write, 408 | B: RequestBody, 409 | { 410 | /// Turn the request into a buffered request. 411 | /// 412 | /// This is only relevant if no TLS is used, as `embedded-tls` buffers internally and we reuse 413 | /// its buffer for non-TLS connections. 414 | pub fn into_buffered<'buf>(self, tx_buf: &'buf mut [u8]) -> HttpRequestHandle<'buf, C, B> 415 | where 416 | 'conn: 'buf, 417 | { 418 | HttpRequestHandle { 419 | conn: self.conn.into_buffered(tx_buf), 420 | request: self.request, 421 | } 422 | } 423 | 424 | /// Send the request. 425 | /// 426 | /// The response headers are stored in the provided rx_buf, which should be sized to contain at least the response headers. 427 | /// 428 | /// The response is returned. 429 | pub async fn send<'req, 'buf>( 430 | &'req mut self, 431 | rx_buf: &'buf mut [u8], 432 | ) -> Result>, Error> { 433 | let request = self.request.take().ok_or(Error::AlreadySent)?.build(); 434 | self.conn.write_request(&request).await?; 435 | self.conn.flush().await?; 436 | Response::read(&mut self.conn, request.method, rx_buf).await 437 | } 438 | } 439 | 440 | impl<'m, C, B> RequestBuilder<'m, B> for HttpRequestHandle<'m, C, B> 441 | where 442 | C: Read + Write, 443 | B: RequestBody, 444 | { 445 | type WithBody = HttpRequestHandle<'m, C, T>; 446 | 447 | fn headers(mut self, headers: &'m [(&'m str, &'m str)]) -> Self { 448 | self.request = Some(self.request.unwrap().headers(headers)); 449 | self 450 | } 451 | 452 | fn path(mut self, path: &'m str) -> Self { 453 | self.request = Some(self.request.unwrap().path(path)); 454 | self 455 | } 456 | 457 | fn body(self, body: T) -> Self::WithBody { 458 | HttpRequestHandle { 459 | conn: self.conn, 460 | request: Some(self.request.unwrap().body(body)), 461 | } 462 | } 463 | 464 | fn host(mut self, host: &'m str) -> Self { 465 | self.request = Some(self.request.unwrap().host(host)); 466 | self 467 | } 468 | 469 | fn content_type(mut self, content_type: ContentType) -> Self { 470 | self.request = Some(self.request.unwrap().content_type(content_type)); 471 | self 472 | } 473 | 474 | fn accept(mut self, content_type: ContentType) -> Self { 475 | self.request = Some(self.request.unwrap().accept(content_type)); 476 | self 477 | } 478 | 479 | fn basic_auth(mut self, username: &'m str, password: &'m str) -> Self { 480 | self.request = Some(self.request.unwrap().basic_auth(username, password)); 481 | self 482 | } 483 | 484 | fn build(self) -> Request<'m, B> { 485 | self.request.unwrap().build() 486 | } 487 | } 488 | 489 | /// A HTTP resource describing a scoped endpoint 490 | /// 491 | /// The underlying connection is closed when drop'ed. 492 | pub struct HttpResource<'res, C> 493 | where 494 | C: Read + Write, 495 | { 496 | pub conn: HttpConnection<'res, C>, 497 | pub host: &'res str, 498 | pub base_path: &'res str, 499 | } 500 | 501 | impl<'res, C> HttpResource<'res, C> 502 | where 503 | C: Read + Write, 504 | { 505 | /// Turn the resource into a buffered resource 506 | /// 507 | /// This is only relevant if no TLS is used, as `embedded-tls` buffers internally and we reuse 508 | /// its buffer for non-TLS connections. 509 | pub fn into_buffered<'buf>(self, tx_buf: &'buf mut [u8]) -> HttpResource<'buf, C> 510 | where 511 | 'res: 'buf, 512 | { 513 | HttpResource { 514 | conn: self.conn.into_buffered(tx_buf), 515 | host: self.host, 516 | base_path: self.base_path, 517 | } 518 | } 519 | 520 | pub fn request<'req>( 521 | &'req mut self, 522 | method: Method, 523 | path: &'req str, 524 | ) -> HttpResourceRequestBuilder<'req, 'res, C, ()> { 525 | HttpResourceRequestBuilder { 526 | conn: &mut self.conn, 527 | request: Request::new(method, path).host(self.host), 528 | base_path: self.base_path, 529 | } 530 | } 531 | 532 | /// Create a new scoped GET http request. 533 | pub fn get<'req>(&'req mut self, path: &'req str) -> HttpResourceRequestBuilder<'req, 'res, C, ()> { 534 | self.request(Method::GET, path) 535 | } 536 | 537 | /// Create a new scoped POST http request. 538 | pub fn post<'req>(&'req mut self, path: &'req str) -> HttpResourceRequestBuilder<'req, 'res, C, ()> { 539 | self.request(Method::POST, path) 540 | } 541 | 542 | /// Create a new scoped PUT http request. 543 | pub fn put<'req>(&'req mut self, path: &'req str) -> HttpResourceRequestBuilder<'req, 'res, C, ()> { 544 | self.request(Method::PUT, path) 545 | } 546 | 547 | /// Create a new scoped DELETE http request. 548 | pub fn delete<'req>(&'req mut self, path: &'req str) -> HttpResourceRequestBuilder<'req, 'res, C, ()> { 549 | self.request(Method::DELETE, path) 550 | } 551 | 552 | /// Create a new scoped HEAD http request. 553 | pub fn head<'req>(&'req mut self, path: &'req str) -> HttpResourceRequestBuilder<'req, 'res, C, ()> { 554 | self.request(Method::HEAD, path) 555 | } 556 | 557 | /// Send a request to a resource. 558 | /// 559 | /// The base path of the resource is prepended to the request path. 560 | /// The response headers are stored in the provided rx_buf, which should be sized to contain at least the response headers. 561 | /// 562 | /// The response is returned. 563 | pub async fn send<'req, 'buf, B: RequestBody>( 564 | &'req mut self, 565 | mut request: Request<'req, B>, 566 | rx_buf: &'buf mut [u8], 567 | ) -> Result>, Error> { 568 | request.base_path = Some(self.base_path); 569 | self.conn.write_request(&request).await?; 570 | self.conn.flush().await?; 571 | Response::read(&mut self.conn, request.method, rx_buf).await 572 | } 573 | } 574 | 575 | pub struct HttpResourceRequestBuilder<'req, 'conn, C, B> 576 | where 577 | C: Read + Write, 578 | B: RequestBody, 579 | { 580 | conn: &'req mut HttpConnection<'conn, C>, 581 | base_path: &'req str, 582 | request: DefaultRequestBuilder<'req, B>, 583 | } 584 | 585 | impl<'req, 'conn, C, B> HttpResourceRequestBuilder<'req, 'conn, C, B> 586 | where 587 | C: Read + Write, 588 | B: RequestBody, 589 | { 590 | /// Send the request. 591 | /// 592 | /// The base path of the resource is prepended to the request path. 593 | /// The response headers are stored in the provided rx_buf, which should be sized to contain at least the response headers. 594 | /// 595 | /// The response is returned. 596 | pub async fn send<'buf>( 597 | self, 598 | rx_buf: &'buf mut [u8], 599 | ) -> Result>, Error> { 600 | let conn = self.conn; 601 | let mut request = self.request.build(); 602 | request.base_path = Some(self.base_path); 603 | conn.write_request(&request).await?; 604 | conn.flush().await?; 605 | Response::read(conn, request.method, rx_buf).await 606 | } 607 | } 608 | 609 | impl<'req, 'conn, C, B> RequestBuilder<'req, B> for HttpResourceRequestBuilder<'req, 'conn, C, B> 610 | where 611 | C: Read + Write, 612 | B: RequestBody, 613 | { 614 | type WithBody = HttpResourceRequestBuilder<'req, 'conn, C, T>; 615 | 616 | fn headers(mut self, headers: &'req [(&'req str, &'req str)]) -> Self { 617 | self.request = self.request.headers(headers); 618 | self 619 | } 620 | 621 | fn path(mut self, path: &'req str) -> Self { 622 | self.request = self.request.path(path); 623 | self 624 | } 625 | 626 | fn body(self, body: T) -> Self::WithBody { 627 | HttpResourceRequestBuilder { 628 | conn: self.conn, 629 | base_path: self.base_path, 630 | request: self.request.body(body), 631 | } 632 | } 633 | 634 | fn host(mut self, host: &'req str) -> Self { 635 | self.request = self.request.host(host); 636 | self 637 | } 638 | 639 | fn content_type(mut self, content_type: ContentType) -> Self { 640 | self.request = self.request.content_type(content_type); 641 | self 642 | } 643 | 644 | fn accept(mut self, content_type: ContentType) -> Self { 645 | self.request = self.request.accept(content_type); 646 | self 647 | } 648 | 649 | fn basic_auth(mut self, username: &'req str, password: &'req str) -> Self { 650 | self.request = self.request.basic_auth(username, password); 651 | self 652 | } 653 | 654 | fn build(self) -> Request<'req, B> { 655 | self.request.build() 656 | } 657 | } 658 | 659 | #[cfg(test)] 660 | mod tests { 661 | use core::convert::Infallible; 662 | 663 | use super::*; 664 | 665 | #[derive(Default)] 666 | struct VecBuffer(Vec); 667 | 668 | impl ErrorType for VecBuffer { 669 | type Error = Infallible; 670 | } 671 | 672 | impl Read for VecBuffer { 673 | async fn read(&mut self, _buf: &mut [u8]) -> Result { 674 | unreachable!() 675 | } 676 | } 677 | 678 | impl Write for VecBuffer { 679 | async fn write(&mut self, buf: &[u8]) -> Result { 680 | self.0.extend_from_slice(buf); 681 | Ok(buf.len()) 682 | } 683 | 684 | async fn flush(&mut self) -> Result<(), Self::Error> { 685 | Ok(()) 686 | } 687 | } 688 | 689 | #[tokio::test] 690 | async fn with_empty_body() { 691 | let mut buffer = VecBuffer::default(); 692 | let mut conn = HttpConnection::Plain(&mut buffer); 693 | 694 | let request = Request::new(Method::POST, "/").body([].as_slice()).build(); 695 | conn.write_request(&request).await.unwrap(); 696 | 697 | assert_eq!(b"POST / HTTP/1.1\r\nContent-Length: 0\r\n\r\n", buffer.0.as_slice()); 698 | } 699 | 700 | #[tokio::test] 701 | async fn with_known_body() { 702 | let mut buffer = VecBuffer::default(); 703 | let mut conn = HttpConnection::Plain(&mut buffer); 704 | 705 | let request = Request::new(Method::POST, "/").body(b"BODY".as_slice()).build(); 706 | conn.write_request(&request).await.unwrap(); 707 | 708 | assert_eq!(b"POST / HTTP/1.1\r\nContent-Length: 4\r\n\r\nBODY", buffer.0.as_slice()); 709 | } 710 | 711 | struct ChunkedBody(&'static [&'static [u8]]); 712 | 713 | impl RequestBody for ChunkedBody { 714 | fn len(&self) -> Option { 715 | None // Unknown length: triggers chunked body 716 | } 717 | 718 | async fn write(&self, writer: &mut W) -> Result<(), W::Error> { 719 | for chunk in self.0 { 720 | writer.write_all(chunk).await?; 721 | } 722 | Ok(()) 723 | } 724 | } 725 | 726 | #[tokio::test] 727 | async fn with_unknown_body_unbuffered() { 728 | let mut buffer = VecBuffer::default(); 729 | let mut conn = HttpConnection::Plain(&mut buffer); 730 | 731 | static CHUNKS: [&'static [u8]; 2] = [b"PART1", b"PART2"]; 732 | let request = Request::new(Method::POST, "/").body(ChunkedBody(&CHUNKS)).build(); 733 | conn.write_request(&request).await.unwrap(); 734 | 735 | assert_eq!( 736 | 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", 737 | buffer.0.as_slice() 738 | ); 739 | } 740 | 741 | #[tokio::test] 742 | async fn with_unknown_body_buffered() { 743 | let mut buffer = VecBuffer::default(); 744 | let mut tx_buf = [0; 1024]; 745 | let mut conn = HttpConnection::Plain(&mut buffer).into_buffered(&mut tx_buf); 746 | 747 | static CHUNKS: [&'static [u8]; 2] = [b"PART1", b"PART2"]; 748 | let request = Request::new(Method::POST, "/").body(ChunkedBody(&CHUNKS)).build(); 749 | conn.write_request(&request).await.unwrap(); 750 | 751 | assert_eq!( 752 | b"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\na\r\nPART1PART2\r\n0\r\n\r\n", 753 | buffer.0.as_slice() 754 | ); 755 | } 756 | } 757 | -------------------------------------------------------------------------------- /src/response/mod.rs: -------------------------------------------------------------------------------- 1 | use embedded_io::{Error as _, ErrorType}; 2 | use embedded_io_async::{BufRead, Read}; 3 | use heapless::Vec; 4 | 5 | use crate::headers::{ContentType, KeepAlive, TransferEncoding}; 6 | use crate::reader::BufferingReader; 7 | use crate::request::Method; 8 | pub use crate::response::chunked::ChunkedBodyReader; 9 | pub use crate::response::fixed_length::FixedLengthBodyReader; 10 | use crate::{Error, TryBufRead}; 11 | 12 | mod chunked; 13 | mod fixed_length; 14 | 15 | /// Type representing a parsed HTTP response. 16 | #[derive(Debug)] 17 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 18 | pub struct Response<'resp, 'buf, C> 19 | where 20 | C: Read, 21 | { 22 | conn: &'resp mut C, 23 | /// The method used to create the response. 24 | method: Method, 25 | /// The HTTP response status code. 26 | pub status: StatusCode, 27 | /// The HTTP response content type. 28 | pub content_type: Option, 29 | /// The content length. 30 | pub content_length: Option, 31 | /// The transfer encoding. 32 | pub transfer_encoding: heapless::Vec, 33 | /// The keep-alive parameters. 34 | pub keep_alive: Option, 35 | header_buf: &'buf mut [u8], 36 | header_len: usize, 37 | raw_body_read: usize, 38 | } 39 | 40 | impl<'resp, 'buf, C> Response<'resp, 'buf, C> 41 | where 42 | C: Read, 43 | { 44 | // Read at least the headers from the connection. 45 | pub async fn read(conn: &'resp mut C, method: Method, header_buf: &'buf mut [u8]) -> Result { 46 | let mut header_len = 0; 47 | let mut pos = 0; 48 | while pos < header_buf.len() { 49 | let n = conn.read(&mut header_buf[pos..]).await.map_err(|e| { 50 | /*warn!( 51 | "error {:?}, but read data from socket: {:?}", 52 | defmt::Debug2Format(&e), 53 | defmt::Debug2Format(&core::str::from_utf8(&buf[..pos])), 54 | );*/ 55 | e.kind() 56 | })?; 57 | 58 | if n == 0 { 59 | return Err(Error::ConnectionAborted); 60 | } 61 | 62 | pos += n; 63 | 64 | // Look for header end 65 | let mut headers = [httparse::EMPTY_HEADER; 64]; 66 | let mut response = httparse::Response::new(&mut headers); 67 | let parse_status = response.parse(&header_buf[..pos]).map_err(|_| Error::Codec)?; 68 | if parse_status.is_complete() { 69 | header_len = parse_status.unwrap(); 70 | break; 71 | } 72 | } 73 | 74 | if header_len == 0 { 75 | // Unable to completely read header 76 | return Err(Error::BufferTooSmall); 77 | } 78 | 79 | // Parse status and known headers 80 | let mut headers = [httparse::EMPTY_HEADER; 64]; 81 | let mut response = httparse::Response::new(&mut headers); 82 | response.parse(&header_buf[..header_len]).unwrap(); 83 | 84 | let status: StatusCode = response.code.unwrap().into(); 85 | let mut content_type = None; 86 | let mut content_length = None; 87 | let mut transfer_encoding = Vec::new(); 88 | let mut keep_alive = None; 89 | 90 | for header in response.headers { 91 | if header.name.eq_ignore_ascii_case("content-type") { 92 | content_type.replace(header.value.into()); 93 | } else if header.name.eq_ignore_ascii_case("content-length") { 94 | content_length = Some( 95 | core::str::from_utf8(header.value) 96 | .map_err(|_| Error::Codec)? 97 | .parse::() 98 | .map_err(|_| Error::Codec)?, 99 | ); 100 | } else if header.name.eq_ignore_ascii_case("transfer-encoding") { 101 | transfer_encoding 102 | .push(header.value.try_into().map_err(|_| Error::Codec)?) 103 | .map_err(|_| Error::Codec)?; 104 | } else if header.name.eq_ignore_ascii_case("keep-alive") { 105 | keep_alive.replace(header.value.try_into().map_err(|_| Error::Codec)?); 106 | } 107 | } 108 | 109 | if status.is_informational() || status == Status::NoContent { 110 | // According to https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2 111 | // A server MUST NOT send a Content-Length header field in any response 112 | // with a status code of 1xx (Informational) or 204 (No Content) 113 | if content_length.unwrap_or_default() != 0 { 114 | return Err(Error::Codec); 115 | } 116 | content_length = Some(0); 117 | } 118 | 119 | // The number of bytes that we have read into the body part of the response 120 | let raw_body_read = pos - header_len; 121 | 122 | if let Some(content_length) = content_length { 123 | if content_length < raw_body_read { 124 | // We have more into the body then what is specified in content_length 125 | return Err(Error::Codec); 126 | } 127 | } 128 | 129 | Ok(Response { 130 | conn, 131 | method, 132 | status, 133 | content_type, 134 | content_length, 135 | transfer_encoding, 136 | keep_alive, 137 | header_buf, 138 | header_len, 139 | raw_body_read, 140 | }) 141 | } 142 | 143 | /// Get the response headers 144 | pub fn headers(&self) -> HeaderIterator<'_> { 145 | let mut iterator = HeaderIterator(0, [httparse::EMPTY_HEADER; 64]); 146 | let mut response = httparse::Response::new(&mut iterator.1); 147 | response.parse(&self.header_buf[..self.header_len]).unwrap(); 148 | 149 | iterator 150 | } 151 | 152 | /// Get the response body 153 | pub fn body(self) -> ResponseBody<'resp, 'buf, C> { 154 | let reader_hint = if self.method == Method::HEAD { 155 | // Head requests does not have a body so we return an empty reader 156 | ReaderHint::Empty 157 | } else if let Some(content_length) = self.content_length { 158 | ReaderHint::FixedLength(content_length) 159 | } else if self.transfer_encoding.contains(&TransferEncoding::Chunked) { 160 | ReaderHint::Chunked 161 | } else { 162 | ReaderHint::ToEnd 163 | }; 164 | 165 | // Move the body part of the bytes in the header buffer to the beginning of the buffer. 166 | self.header_buf 167 | .copy_within(self.header_len..self.header_len + self.raw_body_read, 0); 168 | 169 | ResponseBody { 170 | conn: self.conn, 171 | reader_hint, 172 | body_buf: self.header_buf, 173 | raw_body_read: self.raw_body_read, 174 | } 175 | } 176 | } 177 | 178 | pub struct HeaderIterator<'a>(usize, [httparse::Header<'a>; 64]); 179 | 180 | impl<'a> Iterator for HeaderIterator<'a> { 181 | type Item = (&'a str, &'a [u8]); 182 | 183 | fn next(&mut self) -> Option { 184 | let result = self.1.get(self.0); 185 | 186 | self.0 += 1; 187 | 188 | result.map(|h| (h.name, h.value)) 189 | } 190 | } 191 | 192 | /// Response body 193 | /// 194 | /// This type contains the original header buffer provided to `read_headers`, 195 | /// now renamed to `body_buf`, the number of read body bytes that are available 196 | /// in `body_buf`, and a reader to be used for reading the remaining body. 197 | pub struct ResponseBody<'resp, 'buf, C> 198 | where 199 | C: Read, 200 | { 201 | conn: &'resp mut C, 202 | reader_hint: ReaderHint, 203 | /// The number of raw bytes read from the body and available in the beginning of `body_buf`. 204 | raw_body_read: usize, 205 | /// The buffer initially provided to read the header. 206 | pub body_buf: &'buf mut [u8], 207 | } 208 | 209 | #[derive(Clone, Copy)] 210 | enum ReaderHint { 211 | Empty, 212 | FixedLength(usize), 213 | Chunked, 214 | ToEnd, // https://www.rfc-editor.org/rfc/rfc7230#section-3.3.3 pt. 7: Until end of connection 215 | } 216 | 217 | impl ReaderHint { 218 | fn reader(self, raw_body: R) -> BodyReader { 219 | match self { 220 | ReaderHint::Empty => BodyReader::Empty, 221 | ReaderHint::FixedLength(content_length) => BodyReader::FixedLength(FixedLengthBodyReader { 222 | raw_body, 223 | remaining: content_length, 224 | }), 225 | ReaderHint::Chunked => BodyReader::Chunked(ChunkedBodyReader::new(raw_body)), 226 | ReaderHint::ToEnd => BodyReader::ToEnd(raw_body), 227 | } 228 | } 229 | } 230 | 231 | impl<'resp, 'buf, C> ResponseBody<'resp, 'buf, C> 232 | where 233 | C: Read, 234 | { 235 | pub fn reader(self) -> BodyReader> { 236 | let raw_body = BufferingReader::new(self.body_buf, self.raw_body_read, self.conn); 237 | 238 | self.reader_hint.reader(raw_body) 239 | } 240 | } 241 | 242 | impl<'resp, 'buf, C> ResponseBody<'resp, 'buf, C> 243 | where 244 | C: Read + TryBufRead, 245 | { 246 | /// Read the entire body into the buffer originally provided [`Response::read()`]. 247 | /// This requires that this original buffer is large enough to contain the entire body. 248 | pub async fn read_to_end(mut self) -> Result<&'buf mut [u8], Error> { 249 | match self.reader_hint { 250 | ReaderHint::Empty => Ok(&mut []), 251 | ReaderHint::FixedLength(content_length) => { 252 | let read = BodyReader::FixedLength(FixedLengthBodyReader { 253 | raw_body: self.conn, 254 | remaining: content_length - self.raw_body_read, 255 | }) 256 | .read_to_end(&mut self.body_buf[self.raw_body_read..]) 257 | .await?; 258 | 259 | Ok(&mut self.body_buf[..read + self.raw_body_read]) 260 | } 261 | ReaderHint::Chunked => { 262 | let raw_body = BufferingReader::new(self.body_buf, self.raw_body_read, self.conn); 263 | ChunkedBodyReader::new(raw_body).read_to_end().await 264 | } 265 | ReaderHint::ToEnd => { 266 | let read = BodyReader::ToEnd(&mut self.conn) 267 | .read_to_end(&mut self.body_buf[self.raw_body_read..]) 268 | .await?; 269 | 270 | Ok(&mut self.body_buf[..read + self.raw_body_read]) 271 | } 272 | } 273 | } 274 | 275 | /// Discard the entire body 276 | /// 277 | /// Returns the number of discarded body bytes 278 | pub async fn discard(self) -> Result { 279 | self.reader().discard().await 280 | } 281 | } 282 | 283 | /// A body reader 284 | pub enum BodyReader { 285 | Empty, 286 | FixedLength(FixedLengthBodyReader), 287 | Chunked(ChunkedBodyReader), 288 | ToEnd(B), 289 | } 290 | 291 | impl BodyReader 292 | where 293 | B: Read, 294 | { 295 | fn is_done(&self) -> bool { 296 | match self { 297 | BodyReader::Empty => true, 298 | BodyReader::FixedLength(reader) => reader.remaining == 0, 299 | BodyReader::Chunked(reader) => reader.is_done(), 300 | BodyReader::ToEnd(_) => false, 301 | } 302 | } 303 | 304 | /// Read the entire body 305 | pub async fn read_to_end(&mut self, buf: &mut [u8]) -> Result { 306 | let mut len = 0; 307 | while len < buf.len() { 308 | match self.read(&mut buf[len..]).await { 309 | Ok(0) => break, 310 | Ok(n) => len += n, 311 | Err(e) => return Err(e), 312 | } 313 | } 314 | 315 | if !self.is_done() { 316 | let more = match self { 317 | BodyReader::FixedLength(reader) => { 318 | warn!("FixedLength: {} bytes remained", reader.remaining); 319 | true 320 | } 321 | BodyReader::ToEnd(reader) if len == buf.len() => { 322 | warn!("ToEnd: Buffer full, waiting to see if there is unread data."); 323 | 324 | let mut b = [0]; 325 | matches!(reader.read(&mut b).await, Ok(1)) 326 | } 327 | 328 | BodyReader::ToEnd(_) => false, 329 | _ => true, 330 | }; 331 | 332 | if more { 333 | return Err(Error::BufferTooSmall); 334 | } 335 | } 336 | 337 | Ok(len) 338 | } 339 | 340 | async fn discard(&mut self) -> Result { 341 | let mut body_len = 0; 342 | let mut buf = [0; 128]; 343 | loop { 344 | let buf = self.read(&mut buf).await?; 345 | if buf == 0 { 346 | break; 347 | } 348 | body_len += buf; 349 | } 350 | 351 | Ok(body_len) 352 | } 353 | } 354 | 355 | impl ErrorType for BodyReader { 356 | type Error = Error; 357 | } 358 | 359 | impl Read for BodyReader 360 | where 361 | B: Read, 362 | { 363 | async fn read(&mut self, buf: &mut [u8]) -> Result { 364 | match self { 365 | BodyReader::Empty => Ok(0), 366 | BodyReader::FixedLength(reader) => reader.read(buf).await, 367 | BodyReader::Chunked(reader) => reader.read(buf).await, 368 | BodyReader::ToEnd(conn) => conn.read(buf).await.map_err(|e| Error::Network(e.kind())), 369 | } 370 | } 371 | } 372 | 373 | impl BufRead for BodyReader 374 | where 375 | B: BufRead + Read, 376 | { 377 | async fn fill_buf(&mut self) -> Result<&[u8], Self::Error> { 378 | match self { 379 | BodyReader::Empty => Ok(&[]), 380 | BodyReader::FixedLength(reader) => reader.fill_buf().await, 381 | BodyReader::Chunked(reader) => reader.fill_buf().await, 382 | BodyReader::ToEnd(conn) => conn.fill_buf().await.map_err(|e| Error::Network(e.kind())), 383 | } 384 | } 385 | 386 | fn consume(&mut self, amt: usize) { 387 | match self { 388 | BodyReader::Empty => {} 389 | BodyReader::FixedLength(reader) => reader.consume(amt), 390 | BodyReader::Chunked(reader) => reader.consume(amt), 391 | BodyReader::ToEnd(conn) => conn.consume(amt), 392 | } 393 | } 394 | } 395 | 396 | /// An HTTP status code. 397 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] 398 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 399 | pub struct StatusCode(pub u16); 400 | 401 | impl From for StatusCode { 402 | fn from(value: u16) -> Self { 403 | Self(value) 404 | } 405 | } 406 | 407 | /// An error returned when trying to convert Status::Unknown into a StatusCode 408 | #[derive(Debug)] 409 | pub struct UnknownStatusError; 410 | 411 | impl TryFrom for StatusCode { 412 | type Error = UnknownStatusError; 413 | 414 | fn try_from(from: Status) -> Result { 415 | match from { 416 | Status::Unknown => Err(UnknownStatusError), 417 | _ => Ok(StatusCode(from as u16)), 418 | } 419 | } 420 | } 421 | 422 | impl PartialEq for StatusCode { 423 | fn eq(&self, rhs: &Status) -> bool { 424 | match rhs { 425 | Status::Unknown => false, 426 | _ => self.0 == (*rhs as u16), 427 | } 428 | } 429 | } 430 | 431 | /// Enumeration of well-known HTTP status codes 432 | #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] 433 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 434 | pub enum Status { 435 | Ok = 200, 436 | Created = 201, 437 | Accepted = 202, 438 | NoContent = 204, 439 | PartialContent = 206, 440 | MovedPermanently = 301, 441 | Found = 302, 442 | SeeOther = 303, 443 | NotModified = 304, 444 | TemporaryRedirect = 307, 445 | PermanentRedirect = 308, 446 | BadRequest = 400, 447 | Unauthorized = 401, 448 | Forbidden = 403, 449 | NotFound = 404, 450 | MethodNotAllowed = 405, 451 | Conflict = 409, 452 | UnsupportedMediaType = 415, 453 | RangeNotSatisfiable = 416, 454 | TooManyRequests = 429, 455 | InternalServerError = 500, 456 | BadGateway = 502, 457 | ServiceUnavailable = 503, 458 | GatewayTimeout = 504, 459 | Unknown = 0, 460 | } 461 | 462 | impl StatusCode { 463 | pub fn is_informational(&self) -> bool { 464 | (100..=199).contains(&self.0) 465 | } 466 | 467 | pub fn is_successful(&self) -> bool { 468 | (200..=299).contains(&self.0) 469 | } 470 | 471 | pub fn is_redirection(&self) -> bool { 472 | (300..=399).contains(&self.0) 473 | } 474 | 475 | pub fn is_client_error(&self) -> bool { 476 | (400..=499).contains(&self.0) 477 | } 478 | 479 | pub fn is_server_error(&self) -> bool { 480 | (500..=599).contains(&self.0) 481 | } 482 | } 483 | 484 | impl PartialEq for Status { 485 | fn eq(&self, rhs: &StatusCode) -> bool { 486 | rhs == self 487 | } 488 | } 489 | 490 | impl From for Status { 491 | fn from(from: u16) -> Status { 492 | StatusCode(from).into() 493 | } 494 | } 495 | 496 | impl From for Status { 497 | fn from(from: StatusCode) -> Status { 498 | match from.0 { 499 | 200 => Status::Ok, 500 | 201 => Status::Created, 501 | 202 => Status::Accepted, 502 | 204 => Status::NoContent, 503 | 206 => Status::PartialContent, 504 | 301 => Status::MovedPermanently, 505 | 302 => Status::Found, 506 | 303 => Status::SeeOther, 507 | 304 => Status::NotModified, 508 | 307 => Status::TemporaryRedirect, 509 | 308 => Status::PermanentRedirect, 510 | 400 => Status::BadRequest, 511 | 401 => Status::Unauthorized, 512 | 403 => Status::Forbidden, 513 | 404 => Status::NotFound, 514 | 405 => Status::MethodNotAllowed, 515 | 409 => Status::Conflict, 516 | 415 => Status::UnsupportedMediaType, 517 | 416 => Status::RangeNotSatisfiable, 518 | 429 => Status::TooManyRequests, 519 | 500 => Status::InternalServerError, 520 | 502 => Status::BadGateway, 521 | 503 => Status::ServiceUnavailable, 522 | 504 => Status::GatewayTimeout, 523 | n => { 524 | warn!("Unknown status code: {:?}", n); 525 | Status::Unknown 526 | } 527 | } 528 | } 529 | } 530 | 531 | #[cfg(test)] 532 | mod tests { 533 | use core::convert::Infallible; 534 | 535 | use embedded_io::ErrorType; 536 | use embedded_io_async::Read; 537 | 538 | use super::{Status, StatusCode}; 539 | use crate::{ 540 | Error, TryBufRead, 541 | reader::BufferingReader, 542 | request::Method, 543 | response::{Response, chunked::ChunkedBodyReader}, 544 | }; 545 | 546 | #[tokio::test] 547 | async fn can_read_no_content() { 548 | let mut conn = FakeSingleReadConnection::new(b"HTTP/1.1 204 No Content\r\n\r\n"); 549 | let mut response_buf = [0; 200]; 550 | let response = Response::read(&mut conn, Method::POST, &mut response_buf) 551 | .await 552 | .unwrap(); 553 | 554 | assert_eq!(b"", response.body().read_to_end().await.unwrap()); 555 | assert!(conn.is_exhausted()); 556 | } 557 | 558 | #[tokio::test] 559 | async fn can_read_no_content_with_zero_content_length() { 560 | let mut conn = FakeSingleReadConnection::new(b"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\n\r\n"); 561 | let mut response_buf = [0; 200]; 562 | let response = Response::read(&mut conn, Method::POST, &mut response_buf) 563 | .await 564 | .unwrap(); 565 | 566 | assert_eq!(b"", response.body().read_to_end().await.unwrap()); 567 | assert!(conn.is_exhausted()); 568 | } 569 | 570 | #[tokio::test] 571 | async fn cannot_read_no_content_with_nonzero_content_length() { 572 | let mut conn = FakeSingleReadConnection::new(b"HTTP/1.1 204 No Content\r\nContent-Length: 5\r\n\r\nHELLO"); 573 | let mut response_buf = [0; 200]; 574 | let response = Response::read(&mut conn, Method::POST, &mut response_buf).await; 575 | 576 | assert!(matches!(response, Err(Error::Codec))); 577 | } 578 | 579 | #[tokio::test] 580 | async fn can_read_with_content_length_with_same_buffer() { 581 | let mut conn = FakeSingleReadConnection::new(b"HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nHELLO WORLD"); 582 | let mut response_buf = [0; 200]; 583 | let response = Response::read(&mut conn, Method::GET, &mut response_buf).await.unwrap(); 584 | 585 | let body = response.body().read_to_end().await.unwrap(); 586 | 587 | assert_eq!(b"HELLO WORLD", body); 588 | assert!(conn.is_exhausted()); 589 | } 590 | 591 | #[tokio::test] 592 | async fn can_read_with_content_length_to_other_buffer() { 593 | let mut conn = FakeSingleReadConnection::new(b"HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nHELLO WORLD"); 594 | let mut header_buf = [0; 200]; 595 | let response = Response::read(&mut conn, Method::GET, &mut header_buf).await.unwrap(); 596 | 597 | let mut body_buf = [0; 200]; 598 | let len = response.body().reader().read_to_end(&mut body_buf).await.unwrap(); 599 | 600 | assert_eq!(b"HELLO WORLD", &body_buf[..len]); 601 | assert!(conn.is_exhausted()); 602 | } 603 | 604 | #[tokio::test] 605 | async fn read_to_end_with_content_length_with_small_buffer() { 606 | let mut conn = FakeSingleReadConnection::new( 607 | b"HTTP/1.1 200 OK\r\nContent-Length: 52\r\n\r\nHELLO WORLD this is some longer response for testing", 608 | ); 609 | let mut header_buf = [0; 40]; 610 | let response = Response::read(&mut conn, Method::GET, &mut header_buf).await.unwrap(); 611 | 612 | let body = response.body().read_to_end().await.expect_err("Failure expected"); 613 | 614 | match body { 615 | Error::BufferTooSmall => {} 616 | e => panic!("Unexpected error: {e:?}"), 617 | } 618 | } 619 | 620 | #[tokio::test] 621 | async fn can_discard_with_content_length() { 622 | let mut conn = FakeSingleReadConnection::new(b"HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nHELLO WORLD"); 623 | let mut response_buf = [0; 200]; 624 | let response = Response::read(&mut conn, Method::GET, &mut response_buf).await.unwrap(); 625 | 626 | assert_eq!(11, response.body().discard().await.unwrap()); 627 | assert!(conn.is_exhausted()); 628 | } 629 | 630 | #[tokio::test] 631 | async fn incorrect_fragment_length_does_not_panic() { 632 | let mut conn = FakeSingleReadConnection::new( 633 | b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n\n\r\nHELLO WORLD\r\n0\r\n\r\n", 634 | ); 635 | let mut header_buf = [0; 200]; 636 | 637 | let response = Response::read(&mut conn, Method::GET, &mut header_buf).await.unwrap(); 638 | 639 | let error = response.body().read_to_end().await.unwrap_err(); 640 | 641 | assert!(matches!(error, Error::Codec)); 642 | } 643 | 644 | #[tokio::test] 645 | async fn can_read_with_chunked_encoding() { 646 | let mut conn = FakeSingleReadConnection::new( 647 | b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHELLO\r\n6\r\n WORLD\r\n0\r\n\r\n", 648 | ); 649 | let mut header_buf = [0; 200]; 650 | let response = Response::read(&mut conn, Method::GET, &mut header_buf).await.unwrap(); 651 | 652 | let mut body_buf = [0; 200]; 653 | let len = response.body().reader().read_to_end(&mut body_buf).await.unwrap(); 654 | 655 | assert_eq!(b"HELLO WORLD", &body_buf[..len]); 656 | assert!(conn.is_exhausted()); 657 | } 658 | 659 | #[tokio::test] 660 | async fn can_read_chunked_with_preloaded() { 661 | let mut conn = FakeSingleReadConnection::new( 662 | b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHELLO\r\n6\r\n WORLD\r\n0\r\n\r\n", 663 | ); 664 | conn.read_length = 100; 665 | let mut header_buf = [0; 200]; 666 | let response = Response::read(&mut conn, Method::GET, &mut header_buf).await.unwrap(); 667 | 668 | let mut body_buf = [0; 200]; 669 | let len = response.body().reader().read_to_end(&mut body_buf).await.unwrap(); 670 | 671 | assert_eq!(b"HELLO WORLD", &body_buf[..len]); 672 | assert!(conn.is_exhausted()); 673 | } 674 | 675 | #[tokio::test] 676 | async fn can_read_with_chunked_encoding_empty_body() { 677 | let mut conn = FakeSingleReadConnection::new(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n"); 678 | let mut header_buf = [0; 200]; 679 | let response = Response::read(&mut conn, Method::GET, &mut header_buf).await.unwrap(); 680 | 681 | let mut body_buf = [0; 200]; 682 | let len = response.body().reader().read_to_end(&mut body_buf).await.unwrap(); 683 | 684 | assert_eq!(0, len); 685 | assert!(conn.is_exhausted()); 686 | } 687 | 688 | #[tokio::test] 689 | async fn can_discard_with_chunked_encoding() { 690 | let mut conn = FakeSingleReadConnection::new( 691 | b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nB\r\nHELLO WORLD\r\n0\r\n\r\n", 692 | ); 693 | let mut header_buf = [0; 200]; 694 | let response = Response::read(&mut conn, Method::GET, &mut header_buf).await.unwrap(); 695 | 696 | assert_eq!(11, response.body().discard().await.unwrap()); 697 | assert!(conn.is_exhausted()); 698 | } 699 | 700 | #[tokio::test] 701 | async fn can_read_to_end_with_chunked_encoding() { 702 | let mut conn = FakeSingleReadConnection::new( 703 | b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHELLO\r\n6\r\n WORLD\r\n0\r\n\r\n", 704 | ); 705 | conn.read_length = 10; 706 | let mut header_buf = [0; 200]; 707 | let response = Response::read(&mut conn, Method::GET, &mut header_buf).await.unwrap(); 708 | 709 | let body = response.body().read_to_end().await.unwrap(); 710 | 711 | assert_eq!(b"HELLO WORLD", body); 712 | assert!(conn.is_exhausted()); 713 | } 714 | 715 | #[tokio::test] 716 | async fn can_read_to_end_into_a_small_buffer() { 717 | let mut conn = FakeSingleReadConnection::new( 718 | b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHELLO\r\n6\r\n WORLD\r\n1\r\n \r\n5\r\nHELLO\r\n6\r\n WORLD\r\n1\r\n \r\n5\r\nHELLO\r\n6\r\n WORLD\r\n0\r\n\r\n", 719 | ); 720 | conn.read_length = 10; 721 | let mut header_buf = [0; 50]; // buffer is long enough to hold the complete response 722 | let response = Response::read(&mut conn, Method::GET, &mut header_buf).await.unwrap(); 723 | 724 | let body = response.body().read_to_end().await.unwrap(); 725 | 726 | assert_eq!(b"HELLO WORLD HELLO WORLD HELLO WORLD", body); 727 | assert!(conn.is_exhausted()); 728 | } 729 | 730 | #[tokio::test] 731 | async fn can_read_to_end_of_connection_with_same_buffer() { 732 | let mut conn = FakeSingleReadConnection::new(b"HTTP/1.1 200 OK\r\n\r\nHELLO WORLD"); 733 | let mut header_buf = [0; 200]; 734 | let response = Response::read(&mut conn, Method::GET, &mut header_buf).await.unwrap(); 735 | 736 | let body = response.body().read_to_end().await.unwrap(); 737 | 738 | assert_eq!(b"HELLO WORLD", body); 739 | assert!(conn.is_exhausted()); 740 | } 741 | 742 | #[tokio::test] 743 | async fn can_read_to_end_of_connection_to_other_buffer() { 744 | let mut conn = FakeSingleReadConnection::new(b"HTTP/1.1 200 OK\r\n\r\nHELLO WORLD"); 745 | let mut header_buf = [0; 200]; 746 | let response = Response::read(&mut conn, Method::GET, &mut header_buf).await.unwrap(); 747 | 748 | let mut body_buf = [0; 200]; 749 | let len = response.body().reader().read_to_end(&mut body_buf).await.unwrap(); 750 | 751 | assert_eq!(b"HELLO WORLD", &body_buf[..len]); 752 | assert!(conn.is_exhausted()); 753 | } 754 | 755 | #[tokio::test] 756 | async fn can_discard_to_end_of_connection() { 757 | let mut conn = FakeSingleReadConnection::new(b"HTTP/1.1 200 OK\r\n\r\nHELLO WORLD"); 758 | let mut header_buf = [0; 200]; 759 | let response = Response::read(&mut conn, Method::GET, &mut header_buf).await.unwrap(); 760 | 761 | assert_eq!(11, response.body().discard().await.unwrap()); 762 | assert!(conn.is_exhausted()); 763 | } 764 | 765 | #[tokio::test] 766 | async fn chunked_body_reader_can_read_with_large_buffer() { 767 | let mut raw_body = b"1\r\nX\r\n10\r\nYYYYYYYYYYYYYYYY\r\n0\r\n\r\n".as_slice(); 768 | let mut read_buffer = [0; 128]; 769 | let mut reader = ChunkedBodyReader::new(BufferingReader::new(&mut read_buffer, 0, &mut raw_body)); 770 | 771 | let mut body = [0; 17]; 772 | reader.read_exact(&mut body).await.unwrap(); 773 | 774 | assert_eq!(0, reader.read(&mut body).await.unwrap()); 775 | assert_eq!(0, reader.read(&mut body).await.unwrap()); 776 | assert_eq!(b"XYYYYYYYYYYYYYYYY", &body); 777 | } 778 | 779 | #[tokio::test] 780 | async fn chunked_body_reader_can_read_with_tiny_buffer() { 781 | let mut raw_body = b"1\r\nX\r\n10\r\nYYYYYYYYYYYYYYYY\r\n0\r\n\r\n".as_slice(); 782 | let mut read_buffer = [0; 128]; 783 | let mut reader = ChunkedBodyReader::new(BufferingReader::new(&mut read_buffer, 0, &mut raw_body)); 784 | 785 | let mut body = heapless::Vec::::new(); 786 | for _ in 0..17 { 787 | let mut buf = [0; 1]; 788 | assert_eq!(1, reader.read(&mut buf).await.unwrap()); 789 | body.push(buf[0]).unwrap(); 790 | } 791 | 792 | let mut buf = [0; 1]; 793 | assert_eq!(0, reader.read(&mut buf).await.unwrap()); 794 | assert_eq!(0, reader.read(&mut buf).await.unwrap()); 795 | assert_eq!(b"XYYYYYYYYYYYYYYYY", &body); 796 | } 797 | 798 | struct FakeSingleReadConnection { 799 | response: &'static [u8], 800 | offset: usize, 801 | /// The fake connection will provide at most this many bytes per read 802 | read_length: usize, 803 | } 804 | 805 | impl FakeSingleReadConnection { 806 | pub fn new(response: &'static [u8]) -> Self { 807 | Self { 808 | response, 809 | offset: 0, 810 | read_length: 1, 811 | } 812 | } 813 | 814 | pub fn is_exhausted(&self) -> bool { 815 | self.offset == self.response.len() 816 | } 817 | } 818 | 819 | impl ErrorType for FakeSingleReadConnection { 820 | type Error = Infallible; 821 | } 822 | 823 | impl Read for FakeSingleReadConnection { 824 | async fn read(&mut self, buf: &mut [u8]) -> Result { 825 | if self.is_exhausted() || buf.is_empty() { 826 | return Ok(0); 827 | } 828 | 829 | let loaded = &self.response[self.offset..]; 830 | let len = self.read_length.min(buf.len()).min(loaded.len()); 831 | buf[..len].copy_from_slice(&loaded[..len]); 832 | self.offset += len; 833 | 834 | Ok(len) 835 | } 836 | } 837 | 838 | impl TryBufRead for FakeSingleReadConnection {} 839 | 840 | #[test] 841 | fn status_equality() { 842 | // StatusCode and Status values can be compared 843 | assert_eq!(StatusCode(200), Status::Ok); 844 | assert_eq!(Status::Ok, StatusCode(200)); 845 | assert_eq!(StatusCode(404), Status::NotFound); 846 | assert_eq!(Status::NotFound, StatusCode(404)); 847 | assert_ne!(Status::Ok, StatusCode(404)); 848 | 849 | // Status::Unknown does not compare as equal to any StatusCode value 850 | assert_ne!(Status::Unknown, StatusCode(0)); 851 | assert_ne!(StatusCode(0), Status::Unknown); 852 | 853 | // StatusCode supports comparison of arbitrary values 854 | assert_eq!(StatusCode(0), StatusCode(0)); 855 | assert_eq!(StatusCode(987), StatusCode(987)); 856 | assert_ne!(StatusCode(123), StatusCode(321)); 857 | 858 | let status: Status = StatusCode(200).into(); 859 | assert_eq!(Status::Ok, status); 860 | } 861 | 862 | #[test] 863 | fn status_try_from() { 864 | let s: Status = 200.into(); 865 | assert_eq!(Status::Ok, s); 866 | let s: Status = StatusCode(500).try_into().unwrap(); 867 | assert_eq!(Status::InternalServerError, s); 868 | let s: StatusCode = Status::NotModified.try_into().unwrap(); 869 | assert_eq!(s, StatusCode(304)); 870 | 871 | // Unknown status code values can be converted into Status::Unknown 872 | let im_a_teapot: Status = StatusCode(418).into(); 873 | assert_eq!(Status::Unknown, im_a_teapot); 874 | let im_a_teapot: Status = 418.into(); 875 | assert_eq!(Status::Unknown, im_a_teapot); 876 | 877 | // Converting Status::Unknown back to a StatusCode will fail 878 | >::try_into(Status::Unknown).expect_err("Status::Unknown conversion should fail"); 879 | >::try_into(StatusCode(418).into()) 880 | .expect_err("Status::Unknown conversion should fail"); 881 | } 882 | } 883 | --------------------------------------------------------------------------------