From bba55629214efeb3a2d884a0357e2f6fb394f3d3 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Fri, 24 Apr 2026 23:42:40 +0200 Subject: [PATCH 1/5] Implement HTTP common utilities and refactor HTTPS client - Added `http_common.rs` with functions for error mapping, host/port splitting, request length computation, and response header building. - Refactored `https_client.rs` to utilize the new HTTP common utilities, improving code organization and readability. - Updated the `HttpsClientContext` to manage state transitions more effectively and handle asynchronous operations. - Introduced state management for request handling to prevent concurrent requests. - Enhanced error handling and logging throughout the HTTPS client implementation. --- drivers/shared/Cargo.toml | 3 +- drivers/shared/src/devices/http_client.rs | 454 ++++++++++++ drivers/shared/src/devices/http_common.rs | 102 +++ drivers/shared/src/devices/https_client.rs | 785 ++++++++++----------- drivers/shared/src/devices/mod.rs | 3 + 5 files changed, 951 insertions(+), 396 deletions(-) create mode 100644 drivers/shared/src/devices/http_client.rs create mode 100644 drivers/shared/src/devices/http_common.rs diff --git a/drivers/shared/Cargo.toml b/drivers/shared/Cargo.toml index a8604ff8..fa80717e 100644 --- a/drivers/shared/Cargo.toml +++ b/drivers/shared/Cargo.toml @@ -11,7 +11,8 @@ device = { workspace = true } network = { workspace = true } task = { workspace = true } synchronization = { workspace = true } -embedded-io-async = { workspace = true } +embassy-futures = { workspace = true } +embedded-io-async = "0.6.1" embedded-tls = { workspace = true } embedded-io = "0.6.1" rand_core = "0.6" diff --git a/drivers/shared/src/devices/http_client.rs b/drivers/shared/src/devices/http_client.rs new file mode 100644 index 00000000..eb0e0e09 --- /dev/null +++ b/drivers/shared/src/devices/http_client.rs @@ -0,0 +1,454 @@ +use alloc::boxed::Box; +use alloc::vec::Vec; +use core::time::Duration; + +use embassy_futures::select::{Either, select}; +use file_system::{BaseOperations, CharacterDevice, Context, Error, MountOperations, Result, Size}; +use network::{DnsQueryKind, Duration as NetworkDuration, Port, TcpSocket}; +use shared::HttpRequestParser; +use synchronization::{Arc, blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex}; + +use super::http_common::{ + build_serialized_response_headers, compute_request_length, map_network_error, split_host_port, +}; + +const RESPONSE_SCAN_BUFFER_SIZE: usize = 3072; +const RESPONSE_SERIALIZED_HEADER_SIZE: usize = 2048; +const TCP_READ_CHUNK_SIZE: usize = 512; +const DEFAULT_HTTP_PORT: u16 = 80; +const IO_TIMEOUT_SECONDS: u64 = 15; + +enum State { + Idle, + InFlight, + HeadersReady, + BodyStreaming, + Failed(Error), +} + +struct HttpClientContext { + inner: Arc>, +} + +struct HttpClientInner { + state: State, + response_headers: [u8; RESPONSE_SERIALIZED_HEADER_SIZE], + response_headers_len: usize, + response_headers_cursor: usize, + response_body: Vec, + response_body_cursor: usize, +} + +impl HttpClientContext { + fn new() -> Self { + Self { + inner: Arc::new(Mutex::new(HttpClientInner::new())), + } + } +} + +impl HttpClientInner { + fn new() -> Self { + Self { + state: State::Idle, + response_headers: [0; RESPONSE_SERIALIZED_HEADER_SIZE], + response_headers_len: 0, + response_headers_cursor: 0, + response_body: Vec::new(), + response_body_cursor: 0, + } + } + + fn reset_buffers(&mut self) { + self.response_headers_len = 0; + self.response_headers_cursor = 0; + self.response_body.clear(); + self.response_body_cursor = 0; + } + + fn reset(&mut self) { + self.reset_buffers(); + self.state = State::Idle; + } + + fn set_failed(&mut self, error: Error) { + self.reset_buffers(); + self.state = State::Failed(error); + } +} + +unsafe impl Send for HttpClientContext {} +unsafe impl Sync for HttpClientContext {} + +async fn read_response_headers_and_body( + socket: &mut TcpSocket, + raw_headers: &mut [u8; RESPONSE_SCAN_BUFFER_SIZE], +) -> Result<(usize, Vec)> { + let mut filled = 0usize; + let mut chunk = [0u8; TCP_READ_CHUNK_SIZE]; + + loop { + let bytes_read = match select( + socket.read(&mut chunk), + task::sleep(Duration::from_secs(IO_TIMEOUT_SECONDS)), + ) + .await + { + Either::First(result) => result.map_err(map_network_error)?, + Either::Second(_) => { + log::warning!("http_client: timeout waiting for response header bytes"); + return Err(Error::InputOutput); + } + }; + + if bytes_read == 0 { + return Err(Error::InputOutput); + } + + let destination = raw_headers + .get_mut(filled..filled + bytes_read) + .ok_or(Error::FileTooLarge)?; + destination.copy_from_slice(&chunk[..bytes_read]); + filled += bytes_read; + + if let Some(headers_end) = raw_headers[..filled] + .windows(4) + .position(|window| window == b"\r\n\r\n") + .map(|position| position + 4) + { + let mut body = Vec::new(); + if filled > headers_end { + body.extend_from_slice(&raw_headers[headers_end..filled]); + } + + loop { + let bytes_read = match select( + socket.read(&mut chunk), + task::sleep(Duration::from_secs(IO_TIMEOUT_SECONDS)), + ) + .await + { + Either::First(result) => result.map_err(map_network_error)?, + Either::Second(_) => { + log::warning!("http_client: timeout waiting for response body bytes"); + return Err(Error::InputOutput); + } + }; + + if bytes_read == 0 { + break; + } + + body.extend_from_slice(&chunk[..bytes_read]); + } + + return Ok((headers_end, body)); + } + } +} + +async fn write_tcp_all(socket: &mut TcpSocket, mut payload: &[u8]) -> Result<()> { + while !payload.is_empty() { + let bytes_written = socket.write(payload).await.map_err(map_network_error)?; + if bytes_written == 0 { + return Err(Error::InputOutput); + } + payload = &payload[bytes_written..]; + } + + Ok(()) +} + +async fn create_tcp_connection(host: &str, port: u16) -> Result { + log::information!("http_client: create session host='{}' port={}", host, port); + let manager = network::get_instance(); + + let dns_socket = manager + .new_dns_socket(None) + .await + .map_err(map_network_error)?; + let resolved = dns_socket + .resolve(host, DnsQueryKind::A | DnsQueryKind::Aaaa) + .await + .map_err(map_network_error)?; + dns_socket.close().await.map_err(map_network_error)?; + + let address = resolved.into_iter().next().ok_or(Error::NotFound)?; + + let mut socket = manager + .new_tcp_socket(4096, 4096, None) + .await + .map_err(map_network_error)?; + socket + .set_timeout(Some(NetworkDuration::from_seconds(IO_TIMEOUT_SECONDS))) + .await; + socket + .connect(address, Port::from_inner(port)) + .await + .map_err(map_network_error)?; + socket + .set_timeout(Some(NetworkDuration::from_seconds(IO_TIMEOUT_SECONDS))) + .await; + + Ok(socket) +} + +async fn run_request( + inner: Arc>, + request: Vec, +) { + let result = async { + let parser = HttpRequestParser::from_buffer(&request); + let _ = parser.get_request().ok_or(Error::InvalidParameter)?; + + let host_header = parser + .get_headers() + .find(|(name, _)| *name == HttpRequestParser::HOST_HEADER) + .map(|(_, value)| value) + .ok_or(Error::InvalidParameter)?; + + let (host, port) = split_host_port(host_header, DEFAULT_HTTP_PORT); + + let mut socket = create_tcp_connection(host, port).await?; + + let request_length = compute_request_length(&request, parser)?; + let payload = &request[..request_length]; + + write_tcp_all(&mut socket, payload).await?; + + let has_header_terminator = payload.windows(4).any(|window| window == b"\r\n\r\n"); + if !has_header_terminator { + let suffix = if payload.ends_with(b"\r\n") { + b"\r\n".as_slice() + } else { + b"\r\n\r\n".as_slice() + }; + + write_tcp_all(&mut socket, suffix).await?; + } + + socket.flush().await.map_err(map_network_error)?; + + let mut raw_headers = [0u8; RESPONSE_SCAN_BUFFER_SIZE]; + let (raw_headers_end, response_body) = + read_response_headers_and_body(&mut socket, &mut raw_headers).await?; + + let mut response_headers = [0u8; RESPONSE_SERIALIZED_HEADER_SIZE]; + let serialized_headers_len = build_serialized_response_headers( + &raw_headers[..raw_headers_end], + &mut response_headers, + )?; + + socket.close().await; + + Ok::<(usize, [u8; RESPONSE_SERIALIZED_HEADER_SIZE], Vec), Error>(( + serialized_headers_len, + response_headers, + response_body, + )) + } + .await; + + let mut guard = inner.lock().await; + + match result { + Ok((serialized_headers_len, response_headers, response_body)) => { + if !matches!(guard.state, State::InFlight) { + return; + } + + guard.response_headers[..serialized_headers_len] + .copy_from_slice(&response_headers[..serialized_headers_len]); + guard.response_headers_len = serialized_headers_len; + guard.response_headers_cursor = 0; + + guard.response_body = response_body; + guard.response_body_cursor = 0; + + guard.state = State::HeadersReady; + } + Err(error) => { + if matches!(guard.state, State::InFlight) { + guard.set_failed(error); + } + } + } +} + +pub struct HttpClientDevice; + +impl BaseOperations for HttpClientDevice { + fn open(&self, context: &mut Context) -> Result<()> { + context.set_private_data(Box::new(HttpClientContext::new())); + Ok(()) + } + + fn close(&self, context: &mut Context) -> Result<()> { + if let Some(client_context) = context.take_private_data_of_type::() { + let mut inner = task::block_on(client_context.inner.lock()); + inner.reset(); + } + + Ok(()) + } + + fn read(&self, context: &mut Context, buffer: &mut [u8], _: Size) -> Result { + let context = context + .get_private_data_mutable_of_type::() + .ok_or(Error::InvalidParameter)?; + + read_state_transition(context, buffer) + } + + fn write(&self, context: &mut Context, buffer: &[u8], _: Size) -> Result { + let context = context + .get_private_data_mutable_of_type::() + .ok_or(Error::InvalidParameter)?; + + write_state_gate(context)?; + + let request = buffer.to_vec(); + let inner = context.inner.clone(); + + let task_manager = task::get_instance(); + let parent = task::Manager::ROOT_TASK_IDENTIFIER; + + if task::block_on( + task_manager.spawn(parent, "HTTP request worker", None, move |_| { + let inner_clone = inner.clone(); + let request_owned = request; + async move { run_request(inner_clone, request_owned).await } + }), + ) + .is_err() + { + let mut inner = task::block_on(context.inner.lock()); + inner.set_failed(Error::RessourceBusy); + return Err(Error::RessourceBusy); + } + + Ok(buffer.len()) + } + + fn clone_context(&self, context: &Context) -> Result { + let source = context + .get_private_data_of_type::() + .ok_or(Error::InvalidParameter)?; + + Ok(Context::new(Some(HttpClientContext { + inner: source.inner.clone(), + }))) + } +} + +impl MountOperations for HttpClientDevice {} + +impl CharacterDevice for HttpClientDevice {} + +fn write_state_gate(context: &mut HttpClientContext) -> Result<()> { + let mut inner = task::block_on(context.inner.lock()); + + match inner.state { + State::Idle | State::Failed(_) => { + inner.reset(); + inner.state = State::InFlight; + Ok(()) + } + State::InFlight | State::HeadersReady | State::BodyStreaming => Err(Error::RessourceBusy), + } +} + +fn read_state_transition(context: &mut HttpClientContext, buffer: &mut [u8]) -> Result { + let mut inner = task::block_on(context.inner.lock()); + + match inner.state { + State::Idle => Ok(0), + State::InFlight => Err(Error::RessourceBusy), + State::Failed(error) => { + inner.reset(); + Err(error) + } + State::HeadersReady => { + let remaining = inner + .response_headers_len + .saturating_sub(inner.response_headers_cursor); + let bytes_to_copy = remaining.min(buffer.len()); + + buffer[..bytes_to_copy].copy_from_slice( + &inner.response_headers + [inner.response_headers_cursor..inner.response_headers_cursor + bytes_to_copy], + ); + + inner.response_headers_cursor += bytes_to_copy; + + if inner.response_headers_cursor >= inner.response_headers_len { + inner.state = State::BodyStreaming; + } + + Ok(bytes_to_copy) + } + State::BodyStreaming => { + if inner.response_body_cursor >= inner.response_body.len() { + inner.reset(); + return Ok(0); + } + + let remaining = inner + .response_body + .len() + .saturating_sub(inner.response_body_cursor); + let bytes_to_copy = remaining.min(buffer.len()); + buffer[..bytes_to_copy].copy_from_slice( + &inner.response_body + [inner.response_body_cursor..inner.response_body_cursor + bytes_to_copy], + ); + inner.response_body_cursor += bytes_to_copy; + Ok(bytes_to_copy) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn split_host_port_parses_default_port() { + let (host, port) = split_host_port("example.com", DEFAULT_HTTP_PORT); + assert_eq!(host, "example.com"); + assert_eq!(port, 80); + } + + #[test] + fn split_host_port_parses_explicit_port() { + let (host, port) = split_host_port("example.com:8080", DEFAULT_HTTP_PORT); + assert_eq!(host, "example.com"); + assert_eq!(port, 8080); + } + + #[test] + fn write_rejected_when_request_in_flight() { + let mut context = HttpClientContext::new(); + { + let mut inner = task::block_on(context.inner.lock()); + inner.state = State::InFlight; + } + + assert_eq!(write_state_gate(&mut context), Err(Error::RessourceBusy)); + } + + #[test] + fn read_returns_resource_busy_while_in_flight() { + let mut context = HttpClientContext::new(); + let mut buffer = [0u8; 16]; + { + let mut inner = task::block_on(context.inner.lock()); + inner.state = State::InFlight; + } + + assert_eq!( + read_state_transition(&mut context, &mut buffer), + Err(Error::RessourceBusy) + ); + } +} diff --git a/drivers/shared/src/devices/http_common.rs b/drivers/shared/src/devices/http_common.rs new file mode 100644 index 00000000..58d0f9de --- /dev/null +++ b/drivers/shared/src/devices/http_common.rs @@ -0,0 +1,102 @@ +use file_system::{Error, Result}; +use shared::{HttpRequestParser, HttpResponseBuilder, HttpResponseParser}; + +pub fn map_network_error(error: network::Error) -> Error { + match error { + network::Error::NotFound => Error::NotFound, + network::Error::PermissionDenied => Error::PermissionDenied, + network::Error::ConnectionRefused + | network::Error::ConnectionReset + | network::Error::ConnectionAborted + | network::Error::TimedOut => Error::InputOutput, + network::Error::InvalidInput + | network::Error::InvalidData + | network::Error::InvalidPort + | network::Error::InvalidEndpoint => Error::InvalidParameter, + network::Error::NoRoute + | network::Error::HostUnreachable + | network::Error::NetworkUnreachable => Error::NotFound, + network::Error::ResourceBusy | network::Error::Pending => Error::RessourceBusy, + _ => Error::Other, + } +} + +pub fn split_host_port(host_value: &str, default_port: u16) -> (&str, u16) { + if let Some(stripped) = host_value.strip_prefix('[') { + if let Some(end) = stripped.find(']') { + let host = &stripped[..end]; + let remainder = &stripped[end + 1..]; + if let Some(port_string) = remainder.strip_prefix(':') { + if let Ok(port) = port_string.parse::() { + return (host, port); + } + } + return (host, default_port); + } + } + + if let Some((host, port_string)) = host_value.rsplit_once(':') { + if !host.contains(':') { + if let Ok(port) = port_string.parse::() { + return (host, port); + } + } + } + + (host_value, default_port) +} + +pub fn compute_request_length(buffer: &[u8], parser: HttpRequestParser<'_>) -> Result { + let trimmed_tail = match buffer.iter().rposition(|byte| *byte != 0) { + Some(position) => position + 1, + None => return Err(Error::InvalidParameter), + }; + + let headers_end = buffer + .windows(4) + .position(|window| window == b"\r\n\r\n") + .map(|position| position + 4); + + let Some(headers_end) = headers_end else { + return Ok(trimmed_tail); + }; + + let content_length = parser + .get_headers() + .find(|(name, _)| *name == HttpRequestParser::CONTENT_LENGTH_HEADER) + .and_then(|(_, value)| value.parse::().ok()); + + let body_length = match content_length { + Some(length) => length, + None => trimmed_tail.saturating_sub(headers_end), + }; + + let total_length = headers_end + .checked_add(body_length) + .ok_or(Error::InvalidParameter)?; + + if total_length == 0 || total_length > buffer.len() { + return Err(Error::InvalidParameter); + } + + Ok(total_length) +} + +pub fn build_serialized_response_headers(raw_headers: &[u8], output: &mut [u8]) -> Result { + let parser = HttpResponseParser::from_buffer(raw_headers); + let status_code = parser.get_status_code().ok_or(Error::InvalidParameter)?; + + let mut builder = HttpResponseBuilder::from_buffer(output); + builder + .add_status_code(status_code) + .ok_or(Error::InternalError)?; + + for (name, value) in parser.get_headers() { + builder + .add_header(name, value.as_bytes()) + .ok_or(Error::FileTooLarge)?; + } + + builder.add_line(b"").ok_or(Error::FileTooLarge)?; + Ok(builder.get_position()) +} diff --git a/drivers/shared/src/devices/https_client.rs b/drivers/shared/src/devices/https_client.rs index 499a7fb0..20e8b9da 100644 --- a/drivers/shared/src/devices/https_client.rs +++ b/drivers/shared/src/devices/https_client.rs @@ -1,18 +1,23 @@ use alloc::boxed::Box; -use core::mem; - +use alloc::vec::Vec; +use core::time::Duration; +use embassy_futures::select::{Either, select}; use embedded_io as embedded_io_v06; -use embedded_io_v06::Write as _; -use embedded_tls::blocking::{Aes128GcmSha256, NoVerify, TlsConfig, TlsConnection, TlsContext}; +use embedded_io_async as embedded_io_async_v06; +use embedded_tls::{Aes128GcmSha256, NoVerify, TlsConfig, TlsConnection, TlsContext}; use file_system::{BaseOperations, CharacterDevice, Context, Error, MountOperations, Result, Size}; use network::{DnsQueryKind, Duration as NetworkDuration, Port, TcpSocket}; use rand_core::{CryptoRng, RngCore}; -use shared::{HttpRequestParser, HttpResponseBuilder, HttpResponseParser}; +use shared::HttpRequestParser; +use synchronization::{Arc, blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex}; + +use super::http_common::{ + build_serialized_response_headers, compute_request_length, map_network_error, split_host_port, +}; const TLS_RECORD_BUFFER_SIZE: usize = 4096; const RESPONSE_SCAN_BUFFER_SIZE: usize = 3072; const RESPONSE_SERIALIZED_HEADER_SIZE: usize = 2048; -const RESPONSE_BODY_PREFIX_SIZE: usize = RESPONSE_SCAN_BUFFER_SIZE; const TLS_READ_CHUNK_SIZE: usize = 512; const DEFAULT_HTTPS_PORT: u16 = 443; const IO_TIMEOUT_SECONDS: u64 = 15; @@ -64,117 +69,89 @@ struct TcpSocketAdapter { socket: TcpSocket, } -impl TcpSocketAdapter { - fn new(mut socket: TcpSocket) -> Self { - task::block_on(socket.set_timeout(Some(NetworkDuration::from_seconds(IO_TIMEOUT_SECONDS)))); - Self { socket } - } -} - impl embedded_io_v06::ErrorType for TcpSocketAdapter { type Error = IoError; } -impl embedded_io_v06::Read for TcpSocketAdapter { - fn read(&mut self, buffer: &mut [u8]) -> core::result::Result { - task::block_on(self.socket.read(buffer)) +impl embedded_io_async_v06::Read for TcpSocketAdapter { + async fn read(&mut self, buffer: &mut [u8]) -> core::result::Result { + self.socket + .read(buffer) + .await .map_err(map_network_error) .map_err(IoError::FileSystem) } } -impl embedded_io_v06::Write for TcpSocketAdapter { - fn write(&mut self, buffer: &[u8]) -> core::result::Result { - task::block_on(self.socket.write(buffer)) +impl embedded_io_async_v06::Write for TcpSocketAdapter { + async fn write(&mut self, buffer: &[u8]) -> core::result::Result { + self.socket + .write(buffer) + .await .map_err(map_network_error) .map_err(IoError::FileSystem) } - fn flush(&mut self) -> core::result::Result<(), Self::Error> { - task::block_on(self.socket.flush()) + async fn flush(&mut self) -> core::result::Result<(), Self::Error> { + self.socket + .flush() + .await .map_err(map_network_error) .map_err(IoError::FileSystem) } } -struct Session { - tls_ptr: *mut TlsConnection<'static, TcpSocketAdapter, Aes128GcmSha256>, - read_record_ptr: *mut [u8; TLS_RECORD_BUFFER_SIZE], - write_record_ptr: *mut [u8; TLS_RECORD_BUFFER_SIZE], - closed: bool, -} - -unsafe impl Send for Session {} -unsafe impl Sync for Session {} - -impl Session { - fn tls_mut(&mut self) -> &mut TlsConnection<'static, TcpSocketAdapter, Aes128GcmSha256> { - unsafe { &mut *self.tls_ptr } - } - - fn close(&mut self) { - if self.closed { - return; - } - - let tls = unsafe { *Box::from_raw(self.tls_ptr) }; - match tls.close() { - Ok(_) => {} - Err((mut adapter, _)) => { - let _ = task::block_on(adapter.socket.close()); - } - } - - unsafe { - let _ = Box::from_raw(self.read_record_ptr); - let _ = Box::from_raw(self.write_record_ptr); - } - - self.closed = true; - } -} - -impl Drop for Session { - fn drop(&mut self) { - self.close(); - } -} - enum State { Idle, - HeadersReady(Session), - BodyStreaming(Session), + InFlight, + HeadersReady, + BodyStreaming, Failed(Error), } struct HttpsClientContext { + inner: Arc>, +} + +struct HttpsClientInner { state: State, response_headers: [u8; RESPONSE_SERIALIZED_HEADER_SIZE], response_headers_len: usize, response_headers_cursor: usize, - response_body_prefix: [u8; RESPONSE_BODY_PREFIX_SIZE], - response_body_prefix_len: usize, - response_body_prefix_cursor: usize, + response_body: Vec, + response_body_cursor: usize, } impl HttpsClientContext { + fn new() -> Self { + Self { + inner: Arc::new(Mutex::new(HttpsClientInner::new())), + } + } +} + +impl HttpsClientInner { fn new() -> Self { Self { state: State::Idle, response_headers: [0; RESPONSE_SERIALIZED_HEADER_SIZE], response_headers_len: 0, response_headers_cursor: 0, - response_body_prefix: [0; RESPONSE_BODY_PREFIX_SIZE], - response_body_prefix_len: 0, - response_body_prefix_cursor: 0, + response_body: Vec::new(), + response_body_cursor: 0, } } fn reset_buffers(&mut self) { self.response_headers_len = 0; self.response_headers_cursor = 0; - self.response_body_prefix_len = 0; - self.response_body_prefix_cursor = 0; + self.response_body.clear(); + self.response_body_cursor = 0; + } + + fn reset(&mut self) { + self.reset_buffers(); + self.state = State::Idle; } fn set_failed(&mut self, error: Error) { @@ -186,120 +163,40 @@ impl HttpsClientContext { unsafe impl Send for HttpsClientContext {} unsafe impl Sync for HttpsClientContext {} -fn map_network_error(error: network::Error) -> Error { - match error { - network::Error::NotFound => Error::NotFound, - network::Error::PermissionDenied => Error::PermissionDenied, - network::Error::ConnectionRefused - | network::Error::ConnectionReset - | network::Error::ConnectionAborted - | network::Error::TimedOut => Error::InputOutput, - network::Error::InvalidInput - | network::Error::InvalidData - | network::Error::InvalidPort - | network::Error::InvalidEndpoint => Error::InvalidParameter, - network::Error::NoRoute - | network::Error::HostUnreachable - | network::Error::NetworkUnreachable => Error::NotFound, - network::Error::ResourceBusy | network::Error::Pending => Error::RessourceBusy, - _ => Error::Other, - } -} - -fn map_tls_error(_error: embedded_tls::TlsError) -> Error { - Error::InputOutput -} - -fn split_host_port(host_value: &str) -> (&str, u16) { - if let Some(stripped) = host_value.strip_prefix('[') { - if let Some(end) = stripped.find(']') { - let host = &stripped[..end]; - let remainder = &stripped[end + 1..]; - if let Some(port_string) = remainder.strip_prefix(':') { - if let Ok(port) = port_string.parse::() { - return (host, port); - } - } - return (host, DEFAULT_HTTPS_PORT); - } - } - - if let Some((host, port_string)) = host_value.rsplit_once(':') { - if !host.contains(':') { - if let Ok(port) = port_string.parse::() { - return (host, port); - } - } - } - - (host_value, DEFAULT_HTTPS_PORT) -} - -fn compute_request_length(buffer: &[u8], parser: HttpRequestParser<'_>) -> Result { - let trimmed_tail = match buffer.iter().rposition(|byte| *byte != 0) { - Some(position) => position + 1, - None => return Err(Error::InvalidParameter), - }; - - let headers_end = buffer - .windows(4) - .position(|window| window == b"\r\n\r\n") - .map(|position| position + 4); - - let Some(headers_end) = headers_end else { - return Ok(trimmed_tail); - }; - - let content_length = parser - .get_headers() - .find(|(name, _)| *name == HttpRequestParser::CONTENT_LENGTH_HEADER) - .and_then(|(_, value)| value.parse::().ok()); - - let body_length = match content_length { - Some(length) => length, - None => trimmed_tail.saturating_sub(headers_end), - }; - - let total_length = headers_end - .checked_add(body_length) - .ok_or(Error::InvalidParameter)?; - - if total_length == 0 || total_length > buffer.len() { - return Err(Error::InvalidParameter); - } - - Ok(total_length) -} +fn map_tls_error(error: embedded_tls::TlsError) -> Error { + log::error!("https_client: tls error: {:?}", error); -fn build_serialized_response_headers(raw_headers: &[u8], output: &mut [u8]) -> Result { - let parser = HttpResponseParser::from_buffer(raw_headers); - let status_code = parser.get_status_code().ok_or(Error::InvalidParameter)?; - - let mut builder = HttpResponseBuilder::from_buffer(output); - builder - .add_status_code(status_code) - .ok_or(Error::InternalError)?; - - for (name, value) in parser.get_headers() { - builder - .add_header(name, value.as_bytes()) - .ok_or(Error::FileTooLarge)?; + match error { + embedded_tls::TlsError::ConnectionClosed => Error::InputOutput, + embedded_tls::TlsError::Io(embedded_io_v06::ErrorKind::TimedOut) => Error::InputOutput, + _ => Error::InputOutput, } - - builder.add_line(b"").ok_or(Error::FileTooLarge)?; - Ok(builder.get_position()) } -fn read_response_headers_and_prefix( - tls: &mut TlsConnection<'static, TcpSocketAdapter, Aes128GcmSha256>, +async fn read_response_headers_and_body( + tls: &mut TlsConnection<'_, TcpSocketAdapter, Aes128GcmSha256>, raw_headers: &mut [u8; RESPONSE_SCAN_BUFFER_SIZE], - body_prefix: &mut [u8; RESPONSE_BODY_PREFIX_SIZE], -) -> Result<(usize, usize)> { +) -> Result<(usize, Vec)> { let mut filled = 0usize; let mut chunk = [0u8; TLS_READ_CHUNK_SIZE]; loop { - let bytes_read = tls.read(&mut chunk).map_err(map_tls_error)?; + let bytes_read = match select( + tls.read(&mut chunk), + task::sleep(Duration::from_secs(IO_TIMEOUT_SECONDS)), + ) + .await + { + Either::First(result) => match result { + Ok(size) => size, + Err(embedded_tls::TlsError::ConnectionClosed) => 0, + Err(error) => return Err(map_tls_error(error)), + }, + Either::Second(_) => { + log::warning!("https_client: timeout waiting for response header bytes"); + return Err(Error::InputOutput); + } + }; if bytes_read == 0 { return Err(Error::InputOutput); @@ -316,65 +213,100 @@ fn read_response_headers_and_prefix( .position(|window| window == b"\r\n\r\n") .map(|position| position + 4) { - let body_length = filled.saturating_sub(headers_end); - let copy_length = body_length.min(body_prefix.len()); - if copy_length > 0 { - body_prefix[..copy_length] - .copy_from_slice(&raw_headers[headers_end..headers_end + copy_length]); + let mut body = Vec::new(); + if filled > headers_end { + body.extend_from_slice(&raw_headers[headers_end..filled]); } - return Ok((headers_end, copy_length)); + loop { + let bytes_read = match select( + tls.read(&mut chunk), + task::sleep(Duration::from_secs(IO_TIMEOUT_SECONDS)), + ) + .await + { + Either::First(result) => match result { + Ok(size) => size, + Err(embedded_tls::TlsError::ConnectionClosed) => 0, + Err(error) => return Err(map_tls_error(error)), + }, + Either::Second(_) => { + log::warning!("https_client: timeout waiting for response body bytes"); + return Err(Error::InputOutput); + } + }; + if bytes_read == 0 { + break; + } + body.extend_from_slice(&chunk[..bytes_read]); + } + + return Ok((headers_end, body)); } } } -fn create_tls_session(host: &str, port: u16) -> Result { - log::information!("https_client: create session host='{}' port={}", host, port); - let manager = network::get_instance(); +async fn write_tls_all( + tls: &mut TlsConnection<'_, TcpSocketAdapter, Aes128GcmSha256>, + mut payload: &[u8], +) -> Result<()> { + while !payload.is_empty() { + let bytes_written = tls.write(payload).await.map_err(map_tls_error)?; + if bytes_written == 0 { + return Err(Error::InputOutput); + } + payload = &payload[bytes_written..]; + } - let address = task::block_on(async { - log::information!("https_client: resolving host='{}'", host); - let dns_socket = manager - .new_dns_socket(None) - .await - .map_err(map_network_error)?; - let resolved = dns_socket - .resolve(host, DnsQueryKind::A | DnsQueryKind::Aaaa) - .await - .map_err(map_network_error)?; - dns_socket.close().await.map_err(map_network_error)?; - - let address = resolved.into_iter().next().ok_or(Error::NotFound)?; - log::information!("https_client: dns resolved host='{}'", host); - Ok::(address) - })?; - - let socket = task::block_on(async { - log::information!("https_client: creating tcp socket"); - let mut socket = manager - .new_tcp_socket(4096, 4096, None) - .await - .map_err(map_network_error)?; - log::information!( - "https_client: connecting tcp socket to {}:{}", - address, - port - ); - socket - .connect(address, Port::from_inner(port)) - .await - .map_err(map_network_error)?; - log::information!("https_client: tcp connected"); - Ok::(socket) - })?; + Ok(()) +} - let read_record_ptr = Box::into_raw(Box::new([0u8; TLS_RECORD_BUFFER_SIZE])); - let write_record_ptr = Box::into_raw(Box::new([0u8; TLS_RECORD_BUFFER_SIZE])); +async fn create_tls_connection<'a>( + host: &str, + port: u16, + read_record: &'a mut [u8; TLS_RECORD_BUFFER_SIZE], + write_record: &'a mut [u8; TLS_RECORD_BUFFER_SIZE], +) -> Result> { + log::information!("https_client: create session host='{}' port={}", host, port); + let manager = network::get_instance(); - let read_record = unsafe { &mut *read_record_ptr }; - let write_record = unsafe { &mut *write_record_ptr }; + log::information!("https_client: resolving host='{}'", host); + let dns_socket = manager + .new_dns_socket(None) + .await + .map_err(map_network_error)?; + let resolved = dns_socket + .resolve(host, DnsQueryKind::A | DnsQueryKind::Aaaa) + .await + .map_err(map_network_error)?; + dns_socket.close().await.map_err(map_network_error)?; + + let address = resolved.into_iter().next().ok_or(Error::NotFound)?; + log::information!("https_client: dns resolved host='{}'", host); + + log::information!("https_client: creating tcp socket"); + let mut socket = manager + .new_tcp_socket(4096, 4096, None) + .await + .map_err(map_network_error)?; + socket + .set_timeout(Some(NetworkDuration::from_seconds(IO_TIMEOUT_SECONDS))) + .await; + log::information!( + "https_client: connecting tcp socket to {}:{}", + address, + port + ); + socket + .connect(address, Port::from_inner(port)) + .await + .map_err(map_network_error)?; + socket + .set_timeout(Some(NetworkDuration::from_seconds(IO_TIMEOUT_SECONDS))) + .await; + log::information!("https_client: tcp connected"); - let mut tls = TlsConnection::new(TcpSocketAdapter::new(socket), read_record, write_record); + let mut tls = TlsConnection::new(TcpSocketAdapter { socket }, read_record, write_record); let configuration = TlsConfig::new().with_server_name(host); let mut random = SystemRng; @@ -382,99 +314,132 @@ fn create_tls_session(host: &str, port: u16) -> Result { log::information!("https_client: starting tls handshake"); tls.open::(context) + .await .map_err(map_tls_error)?; log::information!("https_client: tls handshake done"); - let tls_ptr = Box::into_raw(Box::new(tls)); - - Ok(Session { - tls_ptr, - read_record_ptr, - write_record_ptr, - closed: false, - }) + Ok(tls) } -fn run_request(context: &mut HttpsClientContext, request: &[u8]) -> Result<()> { - log::information!( - "https_client: run_request begin (buffer_len={})", - request.len() - ); - let parser = HttpRequestParser::from_buffer(request); - let _ = parser.get_request().ok_or(Error::InvalidParameter)?; +async fn run_request( + inner: Arc>, + request: Vec, +) { + let result = async { + log::information!( + "https_client: run_request begin (buffer_len={})", + request.len() + ); + let parser = HttpRequestParser::from_buffer(&request); + let _ = parser.get_request().ok_or(Error::InvalidParameter)?; - let host_header = parser - .get_headers() - .find(|(name, _)| *name == HttpRequestParser::HOST_HEADER) - .map(|(_, value)| value) - .ok_or(Error::InvalidParameter)?; + let host_header = parser + .get_headers() + .find(|(name, _)| *name == HttpRequestParser::HOST_HEADER) + .map(|(_, value)| value) + .ok_or(Error::InvalidParameter)?; - let (host, port) = split_host_port(host_header); - log::information!("https_client: parsed host='{}' port={}", host, port); + let (host, port) = split_host_port(host_header, DEFAULT_HTTPS_PORT); + log::information!("https_client: parsed host='{}' port={}", host, port); - let mut session = create_tls_session(host, port)?; + let mut read_record = [0u8; TLS_RECORD_BUFFER_SIZE]; + let mut write_record = [0u8; TLS_RECORD_BUFFER_SIZE]; + let mut tls = + create_tls_connection(host, port, &mut read_record, &mut write_record).await?; - let request_length = compute_request_length(request, parser)?; - let payload = &request[..request_length]; - log::information!("https_client: request length computed = {}", request_length); + let request_length = compute_request_length(&request, parser)?; + let payload = &request[..request_length]; + log::information!("https_client: request length computed = {}", request_length); - log::information!("https_client: tls write_all begin"); - session - .tls_mut() - .write_all(payload) - .map_err(map_tls_error)?; + log::information!("https_client: tls write_all begin"); + write_tls_all(&mut tls, payload).await?; - let has_header_terminator = payload.windows(4).any(|window| window == b"\r\n\r\n"); - if !has_header_terminator { - let suffix = if payload.ends_with(b"\r\n") { - b"\r\n".as_slice() - } else { - b"\r\n\r\n".as_slice() - }; + let has_header_terminator = payload.windows(4).any(|window| window == b"\r\n\r\n"); + if !has_header_terminator { + let suffix = if payload.ends_with(b"\r\n") { + b"\r\n".as_slice() + } else { + b"\r\n\r\n".as_slice() + }; + + log::warning!( + "https_client: request missing header terminator, appending {} bytes", + suffix.len() + ); + + write_tls_all(&mut tls, suffix).await?; + } + + log::information!("https_client: tls write_all done"); - log::warning!( - "https_client: request missing header terminator, appending {} bytes", - suffix.len() + log::information!("https_client: tls flush begin"); + tls.flush().await.map_err(map_tls_error)?; + log::information!("https_client: tls flush done"); + + let mut raw_headers = [0u8; RESPONSE_SCAN_BUFFER_SIZE]; + + log::information!("https_client: waiting response headers"); + let (raw_headers_end, response_body) = + read_response_headers_and_body(&mut tls, &mut raw_headers).await?; + log::information!( + "https_client: response headers received (headers_end={}, body_size={})", + raw_headers_end, + response_body.len() ); - session.tls_mut().write_all(suffix).map_err(map_tls_error)?; + let mut response_headers = [0u8; RESPONSE_SERIALIZED_HEADER_SIZE]; + let serialized_headers_len = build_serialized_response_headers( + &raw_headers[..raw_headers_end], + &mut response_headers, + )?; + + match tls.close().await { + Ok(mut adapter) => { + adapter.socket.close().await; + } + Err((mut adapter, _)) => { + adapter.socket.close().await; + } + } + + Ok::<(usize, [u8; RESPONSE_SERIALIZED_HEADER_SIZE], Vec), Error>(( + serialized_headers_len, + response_headers, + response_body, + )) } + .await; - log::information!("https_client: tls write_all done"); + let mut guard = inner.lock().await; - log::information!("https_client: tls flush begin"); - session.tls_mut().flush().map_err(map_tls_error)?; - log::information!("https_client: tls flush done"); + match result { + Ok((serialized_headers_len, response_headers, response_body)) => { + if !matches!(guard.state, State::InFlight) { + return; + } - let mut raw_headers = [0u8; RESPONSE_SCAN_BUFFER_SIZE]; - log::information!("https_client: waiting response headers"); - let (raw_headers_end, prefix_length) = read_response_headers_and_prefix( - session.tls_mut(), - &mut raw_headers, - &mut context.response_body_prefix, - )?; - log::information!( - "https_client: response headers received (headers_end={}, body_prefix={})", - raw_headers_end, - prefix_length - ); + guard.response_headers[..serialized_headers_len] + .copy_from_slice(&response_headers[..serialized_headers_len]); + guard.response_headers_len = serialized_headers_len; + guard.response_headers_cursor = 0; - let serialized_headers_len = build_serialized_response_headers( - &raw_headers[..raw_headers_end], - &mut context.response_headers, - )?; + guard.response_body = response_body; + guard.response_body_cursor = 0; - context.response_headers_len = serialized_headers_len; - context.response_headers_cursor = 0; - context.response_body_prefix_len = prefix_length; - context.response_body_prefix_cursor = 0; - context.state = State::HeadersReady(session); - log::information!( - "https_client: request complete, headers ready len={}", - serialized_headers_len - ); + guard.state = State::HeadersReady; - Ok(()) + log::information!( + "https_client: request complete, headers ready len={}", + serialized_headers_len + ); + } + Err(error) => { + if matches!(guard.state, State::InFlight) { + guard.set_failed(error); + log::error!("https_client: run_request failed: {:?}", error); + } + } + } } pub struct HttpsClientDevice; @@ -487,10 +452,8 @@ impl BaseOperations for HttpsClientDevice { fn close(&self, context: &mut Context) -> Result<()> { if let Some(client_context) = context.take_private_data_of_type::() { - match client_context.state { - State::HeadersReady(_session) | State::BodyStreaming(_session) => {} - _ => {} - } + let mut inner = task::block_on(client_context.inner.lock()); + inner.reset(); } Ok(()) @@ -501,73 +464,7 @@ impl BaseOperations for HttpsClientDevice { .get_private_data_mutable_of_type::() .ok_or(Error::InvalidParameter)?; - let state = mem::replace(&mut context.state, State::Idle); - - match state { - State::Idle => { - context.state = State::Idle; - Ok(0) - } - State::Failed(error) => { - context.state = State::Idle; - Err(error) - } - State::HeadersReady(session) => { - let remaining = context - .response_headers_len - .saturating_sub(context.response_headers_cursor); - - let bytes_to_copy = remaining.min(buffer.len()); - - buffer[..bytes_to_copy].copy_from_slice( - &context.response_headers[context.response_headers_cursor - ..context.response_headers_cursor + bytes_to_copy], - ); - - context.response_headers_cursor += bytes_to_copy; - - if context.response_headers_cursor >= context.response_headers_len { - context.state = State::BodyStreaming(session); - } else { - context.state = State::HeadersReady(session); - } - - Ok(bytes_to_copy) - } - State::BodyStreaming(mut session) => { - if context.response_body_prefix_cursor < context.response_body_prefix_len { - let remaining = context - .response_body_prefix_len - .saturating_sub(context.response_body_prefix_cursor); - let bytes_to_copy = remaining.min(buffer.len()); - - buffer[..bytes_to_copy].copy_from_slice( - &context.response_body_prefix[context.response_body_prefix_cursor - ..context.response_body_prefix_cursor + bytes_to_copy], - ); - context.response_body_prefix_cursor += bytes_to_copy; - - context.state = State::BodyStreaming(session); - return Ok(bytes_to_copy); - } - - match session.tls_mut().read(buffer).map_err(map_tls_error) { - Ok(0) => { - context.state = State::Idle; - context.reset_buffers(); - Ok(0) - } - Ok(bytes_read) => { - context.state = State::BodyStreaming(session); - Ok(bytes_read) - } - Err(error) => { - context.set_failed(error); - Err(error) - } - } - } - } + read_state_transition(context, buffer) } fn write(&self, context: &mut Context, buffer: &[u8], _: Size) -> Result { @@ -576,35 +473,44 @@ impl BaseOperations for HttpsClientDevice { .get_private_data_mutable_of_type::() .ok_or(Error::InvalidParameter)?; - let state = mem::replace(&mut context.state, State::Idle); - match state { - State::Idle | State::Failed(_) => {} - State::HeadersReady(session) => { - context.state = State::HeadersReady(session); - log::warning!("https_client: write rejected (headers still pending read)"); - return Err(Error::RessourceBusy); - } - State::BodyStreaming(session) => { - context.state = State::BodyStreaming(session); - log::warning!("https_client: write rejected (body streaming)"); - return Err(Error::RessourceBusy); - } - } + write_state_gate(context)?; + + let request = buffer.to_vec(); + let inner = context.inner.clone(); - context.reset_buffers(); + let task_manager = task::get_instance(); + let parent = task::Manager::ROOT_TASK_IDENTIFIER; - if let Err(error) = run_request(context, buffer) { - context.set_failed(error); - log::error!("https_client: run_request failed: {:?}", error); - return Err(error); + if let Err(spawn_error) = + task::block_on( + task_manager.spawn(parent, "HTTPS request worker", None, move |_| { + let inner_clone = inner.clone(); + let request_owned = request; + async move { run_request(inner_clone, request_owned).await } + }), + ) + { + let mut inner = task::block_on(context.inner.lock()); + inner.set_failed(Error::RessourceBusy); + log::error!( + "https_client: failed to spawn request worker: {:?}", + spawn_error + ); + return Err(Error::RessourceBusy); } - log::information!("https_client: write completed successfully"); + log::information!("https_client: write submitted successfully"); Ok(buffer.len()) } - fn clone_context(&self, _context: &Context) -> Result { - Ok(Context::new(Some(HttpsClientContext::new()))) + fn clone_context(&self, context: &Context) -> Result { + let source = context + .get_private_data_of_type::() + .ok_or(Error::InvalidParameter)?; + + Ok(Context::new(Some(HttpsClientContext { + inner: source.inner.clone(), + }))) } } @@ -612,20 +518,83 @@ impl MountOperations for HttpsClientDevice {} impl CharacterDevice for HttpsClientDevice {} +fn write_state_gate(context: &mut HttpsClientContext) -> Result<()> { + let mut inner = task::block_on(context.inner.lock()); + + match inner.state { + State::Idle | State::Failed(_) => { + inner.reset(); + inner.state = State::InFlight; + Ok(()) + } + State::InFlight | State::HeadersReady | State::BodyStreaming => Err(Error::RessourceBusy), + } +} + +fn read_state_transition(context: &mut HttpsClientContext, buffer: &mut [u8]) -> Result { + let mut inner = task::block_on(context.inner.lock()); + + match inner.state { + State::Idle => Ok(0), + State::InFlight => Err(Error::RessourceBusy), + State::Failed(error) => { + inner.reset(); + Err(error) + } + State::HeadersReady => { + let remaining = inner + .response_headers_len + .saturating_sub(inner.response_headers_cursor); + let bytes_to_copy = remaining.min(buffer.len()); + + buffer[..bytes_to_copy].copy_from_slice( + &inner.response_headers + [inner.response_headers_cursor..inner.response_headers_cursor + bytes_to_copy], + ); + + inner.response_headers_cursor += bytes_to_copy; + + if inner.response_headers_cursor >= inner.response_headers_len { + inner.state = State::BodyStreaming; + } + + Ok(bytes_to_copy) + } + State::BodyStreaming => { + if inner.response_body_cursor >= inner.response_body.len() { + inner.reset(); + return Ok(0); + } + + let remaining = inner + .response_body + .len() + .saturating_sub(inner.response_body_cursor); + let bytes_to_copy = remaining.min(buffer.len()); + buffer[..bytes_to_copy].copy_from_slice( + &inner.response_body + [inner.response_body_cursor..inner.response_body_cursor + bytes_to_copy], + ); + inner.response_body_cursor += bytes_to_copy; + Ok(bytes_to_copy) + } + } +} + #[cfg(test)] mod tests { use super::*; #[test] fn split_host_port_parses_default_port() { - let (host, port) = split_host_port("example.com"); + let (host, port) = split_host_port("example.com", DEFAULT_HTTPS_PORT); assert_eq!(host, "example.com"); assert_eq!(port, 443); } #[test] fn split_host_port_parses_explicit_port() { - let (host, port) = split_host_port("example.com:8443"); + let (host, port) = split_host_port("example.com:8443", DEFAULT_HTTPS_PORT); assert_eq!(host, "example.com"); assert_eq!(port, 8443); } @@ -651,4 +620,30 @@ mod tests { let length = compute_request_length(&request_buffer, parser).unwrap(); assert_eq!(length, request.len()); } + + #[test] + fn write_rejected_when_request_in_flight() { + let mut context = HttpsClientContext::new(); + { + let mut inner = task::block_on(context.inner.lock()); + inner.state = State::InFlight; + } + + assert_eq!(write_state_gate(&mut context), Err(Error::RessourceBusy)); + } + + #[test] + fn read_returns_resource_busy_while_in_flight() { + let mut context = HttpsClientContext::new(); + let mut buffer = [0u8; 16]; + { + let mut inner = task::block_on(context.inner.lock()); + inner.state = State::InFlight; + } + + assert_eq!( + read_state_transition(&mut context, &mut buffer), + Err(Error::RessourceBusy) + ); + } } diff --git a/drivers/shared/src/devices/mod.rs b/drivers/shared/src/devices/mod.rs index 330ef0cc..4d56a747 100644 --- a/drivers/shared/src/devices/mod.rs +++ b/drivers/shared/src/devices/mod.rs @@ -1,7 +1,10 @@ mod hash; +mod http_client; +mod http_common; mod https_client; mod random; pub use hash::*; +pub use http_client::*; pub use https_client::*; pub use random::*; From ddc8e73d5dc49ac1b8255c81eacf8cdea73ffbff Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Fri, 24 Apr 2026 23:42:50 +0200 Subject: [PATCH 2/5] Add HTTP client device support in native and wasm examples --- examples/native/src/main.rs | 5 +++++ examples/wasm/src/main.rs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/examples/native/src/main.rs b/examples/native/src/main.rs index 5ff1d628..e8eb05cd 100644 --- a/examples/native/src/main.rs +++ b/examples/native/src/main.rs @@ -159,6 +159,11 @@ async fn main() { drivers_shared::devices::RandomDevice ), (&"/devices/null", CharacterDevice, drivers_core::NullDevice), + ( + &"/devices/http_client", + CharacterDevice, + drivers_shared::devices::HttpClientDevice + ), ( &"/devices/https_client", CharacterDevice, diff --git a/examples/wasm/src/main.rs b/examples/wasm/src/main.rs index 46e3062b..856440a8 100644 --- a/examples/wasm/src/main.rs +++ b/examples/wasm/src/main.rs @@ -145,6 +145,11 @@ async fn main() { drivers_shared::devices::RandomDevice ), (&"/devices/null", CharacterDevice, drivers_core::NullDevice), + ( + &"/devices/http_client", + CharacterDevice, + drivers_wasm::devices::HttpClientDevice + ), ( &"/devices/https_client", CharacterDevice, From 2f3ff705fb8effd715e919f957391a6355b8cfd7 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Fri, 24 Apr 2026 23:42:57 +0200 Subject: [PATCH 3/5] Add device selection for HTTP and HTTPS clients in web request command --- .../command_line/src/commands/web_request.rs | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/executables/shell/command_line/src/commands/web_request.rs b/executables/shell/command_line/src/commands/web_request.rs index 59abff3d..43425a96 100644 --- a/executables/shell/command_line/src/commands/web_request.rs +++ b/executables/shell/command_line/src/commands/web_request.rs @@ -9,6 +9,14 @@ use xila::{ use super::{CommandContext, UserCommand}; +fn select_client_device_path(url: &Url<'_>) -> Option<&'static str> { + match url.scheme { + "http" => Some("/devices/http_client"), + "https" => Some("/devices/https_client"), + _ => None, + } +} + pub struct WebRequestCommand; impl UserCommand for WebRequestCommand { @@ -170,7 +178,10 @@ where let mut file = File::open( virtual_file_system, task, - "/devices/https_client", + select_client_device_path( + &Url::parse(parameters.url).ok_or(crate::error::Error::InvalidArgument)?, + ) + .ok_or(crate::error::Error::InvalidArgument)?, AccessFlags::READ_WRITE.into(), ) .await @@ -203,8 +214,9 @@ where #[cfg(test)] mod tests { - use super::parse_web_request_parameters; + use super::{parse_web_request_parameters, select_client_device_path}; use getargs::Options; + use xila::shared::Url; #[test] fn parse_web_request_parameters_uses_get_and_url_defaults() { @@ -274,4 +286,22 @@ mod tests { Err(crate::error::Error::MissingPositionalArgument("url")) )); } + + #[test] + fn web_request_uses_https_device_for_https_scheme() { + let url = Url::parse("https://example.com").unwrap(); + assert_eq!( + select_client_device_path(&url), + Some("/devices/https_client") + ); + } + + #[test] + fn web_request_uses_http_device_for_http_scheme() { + let url = Url::parse("http://example.com").unwrap(); + assert_eq!( + select_client_device_path(&url), + Some("/devices/http_client") + ); + } } From fde6b1a280b5c8f29daf1e2d0b2eec11b1767155 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Fri, 24 Apr 2026 23:43:03 +0200 Subject: [PATCH 4/5] Refactor buffer handling in add_line_iterator by removing logging statements --- modules/network/src/manager/runner.rs | 6 ++++-- modules/network/src/socket/tcp.rs | 1 + modules/shared/src/http/mod.rs | 14 -------------- 3 files changed, 5 insertions(+), 16 deletions(-) diff --git a/modules/network/src/manager/runner.rs b/modules/network/src/manager/runner.rs index bc216eb8..d4bc3b8d 100644 --- a/modules/network/src/manager/runner.rs +++ b/modules/network/src/manager/runner.rs @@ -13,6 +13,8 @@ pub struct StackRunner { wake_signal: WakeSignal, } +const MAX_POLL_INTERVAL: Duration = Duration::from_millis(20); + impl StackRunner where T: Device, @@ -43,8 +45,8 @@ where embassy_futures::yield_now().await; continue; } - Some(d) => d, - None => Duration::from_millis(200), + Some(d) => d.min(MAX_POLL_INTERVAL), + None => MAX_POLL_INTERVAL, }; select(task::sleep(sleep_duration), self.wake_signal.wait()).await; diff --git a/modules/network/src/socket/tcp.rs b/modules/network/src/socket/tcp.rs index bafc3abe..69c5b304 100644 --- a/modules/network/src/socket/tcp.rs +++ b/modules/network/src/socket/tcp.rs @@ -126,6 +126,7 @@ impl TcpSocket { poll_fn(|cx| { self.poll_with_mutable(cx, |s, cx| match s.recv_slice(buffer) { Ok(0) if buffer.is_empty() => Poll::Ready(Ok(0)), + Ok(0) if !s.may_recv() => Poll::Ready(Ok(0)), Ok(0) => { s.register_recv_waker(cx.waker()); Poll::Pending diff --git a/modules/shared/src/http/mod.rs b/modules/shared/src/http/mod.rs index bee2c6ee..561a485c 100644 --- a/modules/shared/src/http/mod.rs +++ b/modules/shared/src/http/mod.rs @@ -28,28 +28,14 @@ pub fn add_line_iterator<'a, I>(buffer: &mut [u8], mut position: usize, iter: I) where I: Iterator, { - log::information!( - "Adding line iterator to buffer at position {} to buffer of length {}", - position, - buffer.len() - ); for part in iter { - log::information!("Adding part: {:?}", core::str::from_utf8(part).ok()); let part_length = part.len(); - log::information!("Part length: {}", part_length); - let destination_buffer = buffer.get_mut(position..position + part_length)?; - - log::information!("Copying part to buffer at position {}", position); destination_buffer.copy_from_slice(part); position += part_length; } - log::information!( - "All parts added, final position before line ending: {}", - position - ); // Add line ending let line_ending = LINE_ENDING.as_bytes(); let destination_buffer = buffer.get_mut(position..position + line_ending.len())?; From 13da3686a9f733998969399552c418fe3e2f24ee Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sat, 25 Apr 2026 00:04:40 +0200 Subject: [PATCH 5/5] Fix duration calculation in ICMP echo request response --- modules/network/src/socket/icmp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/network/src/socket/icmp.rs b/modules/network/src/socket/icmp.rs index 9444ec44..002670dd 100644 --- a/modules/network/src/socket/icmp.rs +++ b/modules/network/src/socket/icmp.rs @@ -322,7 +322,7 @@ impl IcmpSocket { if is_valid_reply { let end_time = crate::get_smoltcp_time(); let rtt = end_time - start_time; - return Ok(Duration::from_milliseconds(rtt.total_millis() as u64)); + return Ok(Duration::from_milliseconds(rtt.total_millis())); } } Ok(_) => {