⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/net/
http.rs

1//! HTTP/1.1 client library for VeridianOS
2//!
3//! Provides an HTTP request builder, incremental response parser,
4//! and client abstraction for sending requests over TCP connections.
5//! Supports chunked transfer encoding, keep-alive, redirects,
6//! basic authentication, cookies, and query string encoding.
7
8#![allow(dead_code)]
9
10#[cfg(feature = "alloc")]
11use alloc::{collections::BTreeMap, format, string::String, vec::Vec};
12
13// ---------------------------------------------------------------------------
14// Constants
15// ---------------------------------------------------------------------------
16
17/// Default HTTP port
18pub const HTTP_PORT: u16 = 80;
19
20/// Default HTTPS port
21pub const HTTPS_PORT: u16 = 443;
22
23/// Default maximum redirects to follow
24pub const DEFAULT_MAX_REDIRECTS: u8 = 10;
25
26/// Default request timeout in milliseconds
27pub const DEFAULT_TIMEOUT_MS: u64 = 30_000;
28
29/// HTTP version string
30const HTTP_VERSION: &str = "HTTP/1.1";
31
32/// Default User-Agent
33const DEFAULT_USER_AGENT: &str = "VeridianOS/0.10.6";
34
35// MIME type constants
36/// text/html
37pub const MIME_TEXT_HTML: &str = "text/html";
38/// application/json
39pub const MIME_APPLICATION_JSON: &str = "application/json";
40/// text/plain
41pub const MIME_TEXT_PLAIN: &str = "text/plain";
42/// application/octet-stream
43pub const MIME_APPLICATION_OCTET_STREAM: &str = "application/octet-stream";
44/// application/x-www-form-urlencoded
45pub const MIME_FORM_URLENCODED: &str = "application/x-www-form-urlencoded";
46
47// ---------------------------------------------------------------------------
48// HTTP Method
49// ---------------------------------------------------------------------------
50
51/// HTTP request methods
52#[cfg(feature = "alloc")]
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum HttpMethod {
55    Get,
56    Post,
57    Put,
58    Delete,
59    Head,
60    Patch,
61    Options,
62}
63
64#[cfg(feature = "alloc")]
65impl HttpMethod {
66    /// Return the method as an uppercase string slice.
67    pub fn as_str(&self) -> &'static str {
68        match self {
69            HttpMethod::Get => "GET",
70            HttpMethod::Post => "POST",
71            HttpMethod::Put => "PUT",
72            HttpMethod::Delete => "DELETE",
73            HttpMethod::Head => "HEAD",
74            HttpMethod::Patch => "PATCH",
75            HttpMethod::Options => "OPTIONS",
76        }
77    }
78}
79
80// ---------------------------------------------------------------------------
81// URL parsing
82// ---------------------------------------------------------------------------
83
84/// Parsed URL components.
85#[cfg(feature = "alloc")]
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct ParsedUrl {
88    /// Scheme: "http" or "https"
89    pub scheme: String,
90    /// Host name or IP address
91    pub host: String,
92    /// Port number (default 80 for http, 443 for https)
93    pub port: u16,
94    /// Path component (starts with '/')
95    pub path: String,
96    /// Optional query string (without leading '?')
97    pub query: Option<String>,
98    /// Whether this is an HTTPS URL
99    pub is_https: bool,
100}
101
102#[cfg(feature = "alloc")]
103impl ParsedUrl {
104    /// Parse a URL string into its components.
105    ///
106    /// Supports `http://host[:port][/path][?query]` and
107    /// `https://host[:port][/path][?query]`.
108    pub fn parse(url: &str) -> Result<Self, HttpError> {
109        let (scheme, rest) = if let Some(r) = url.strip_prefix("https://") {
110            ("https", r)
111        } else if let Some(r) = url.strip_prefix("http://") {
112            ("http", r)
113        } else {
114            return Err(HttpError::InvalidUrl);
115        };
116
117        let is_https = scheme == "https";
118        let default_port: u16 = if is_https { HTTPS_PORT } else { HTTP_PORT };
119
120        // Split host+port from path+query
121        let (authority, path_and_query) = match rest.find('/') {
122            Some(idx) => (&rest[..idx], &rest[idx..]),
123            None => (rest, "/"),
124        };
125
126        // Split path and query
127        let (path, query) = match path_and_query.find('?') {
128            Some(idx) => (&path_and_query[..idx], Some(&path_and_query[idx + 1..])),
129            None => (path_and_query, None),
130        };
131
132        // Split host and port
133        let (host, port) = if let Some(colon_idx) = authority.rfind(':') {
134            let port_str = &authority[colon_idx + 1..];
135            if let Ok(p) = parse_u16(port_str) {
136                (&authority[..colon_idx], p)
137            } else {
138                (authority, default_port)
139            }
140        } else {
141            (authority, default_port)
142        };
143
144        if host.is_empty() {
145            return Err(HttpError::InvalidUrl);
146        }
147
148        let path_str = if path.is_empty() {
149            String::from("/")
150        } else {
151            String::from(path)
152        };
153
154        Ok(ParsedUrl {
155            scheme: String::from(scheme),
156            host: String::from(host),
157            port,
158            path: path_str,
159            query: query.map(String::from),
160            is_https,
161        })
162    }
163
164    /// Return full request path including query string.
165    pub fn request_path(&self) -> String {
166        match &self.query {
167            Some(q) => format!("{}?{}", self.path, q),
168            None => self.path.clone(),
169        }
170    }
171}
172
173// ---------------------------------------------------------------------------
174// HTTP Errors
175// ---------------------------------------------------------------------------
176
177/// Errors that can occur during HTTP operations.
178#[cfg(feature = "alloc")]
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub enum HttpError {
181    /// URL could not be parsed.
182    InvalidUrl,
183    /// Response contained an invalid status line.
184    InvalidStatusLine,
185    /// Response contained an invalid header.
186    InvalidHeader,
187    /// Chunked encoding contained an invalid chunk size.
188    InvalidChunkSize,
189    /// Response body exceeds maximum allowed size.
190    BodyTooLarge,
191    /// Too many redirects followed.
192    TooManyRedirects,
193    /// Connection timed out.
194    Timeout,
195    /// Generic parse error with a message.
196    ParseError(String),
197}
198
199// ---------------------------------------------------------------------------
200// HTTP Request
201// ---------------------------------------------------------------------------
202
203/// An HTTP request ready to be serialized and sent.
204#[cfg(feature = "alloc")]
205#[derive(Debug, Clone)]
206pub struct HttpRequest {
207    /// HTTP method
208    pub method: HttpMethod,
209    /// Parsed URL
210    pub url: ParsedUrl,
211    /// Headers (lowercase key -> value)
212    pub headers: BTreeMap<String, String>,
213    /// Optional request body
214    pub body: Option<Vec<u8>>,
215}
216
217#[cfg(feature = "alloc")]
218impl HttpRequest {
219    /// Create a new request with default headers.
220    pub fn new(method: HttpMethod, url: &str) -> Result<Self, HttpError> {
221        let parsed = ParsedUrl::parse(url)?;
222        let mut headers = BTreeMap::new();
223
224        // Set mandatory Host header
225        if parsed.port != HTTP_PORT && parsed.port != HTTPS_PORT {
226            headers.insert(
227                String::from("host"),
228                format!("{}:{}", parsed.host, parsed.port),
229            );
230        } else {
231            headers.insert(String::from("host"), parsed.host.clone());
232        }
233
234        headers.insert(String::from("user-agent"), String::from(DEFAULT_USER_AGENT));
235        headers.insert(String::from("accept"), String::from("*/*"));
236        headers.insert(String::from("connection"), String::from("keep-alive"));
237
238        Ok(HttpRequest {
239            method,
240            url: parsed,
241            headers,
242            body: None,
243        })
244    }
245
246    /// Set a header (name is stored lowercase).
247    pub fn set_header(&mut self, name: &str, value: &str) {
248        self.headers.insert(to_lowercase(name), String::from(value));
249    }
250
251    /// Set the request body and automatically set Content-Length.
252    pub fn set_body(&mut self, body: Vec<u8>) {
253        let len = body.len();
254        self.body = Some(body);
255        self.headers
256            .insert(String::from("content-length"), uint_to_string(len));
257    }
258
259    /// Set the request body from a string.
260    pub fn set_body_str(&mut self, body: &str) {
261        self.set_body(Vec::from(body.as_bytes()));
262    }
263
264    /// Set Content-Type header.
265    pub fn set_content_type(&mut self, mime: &str) {
266        self.set_header("content-type", mime);
267    }
268
269    /// Set Authorization header with Basic auth (Base64-encoded user:password).
270    pub fn set_basic_auth(&mut self, user: &str, password: &str) {
271        let credentials = format!("{}:{}", user, password);
272        let encoded = base64_encode(credentials.as_bytes());
273        self.set_header("authorization", &format!("Basic {}", encoded));
274    }
275
276    /// Set Cookie header from a list of (name, value) pairs.
277    pub fn set_cookies(&mut self, cookies: &[(&str, &str)]) {
278        if cookies.is_empty() {
279            return;
280        }
281        let mut cookie_str = String::new();
282        for (i, (name, value)) in cookies.iter().enumerate() {
283            if i > 0 {
284                cookie_str.push_str("; ");
285            }
286            cookie_str.push_str(name);
287            cookie_str.push('=');
288            cookie_str.push_str(value);
289        }
290        self.set_header("cookie", &cookie_str);
291    }
292
293    /// Serialize the request to bytes for sending over TCP.
294    pub fn serialize(&self) -> Vec<u8> {
295        let mut buf = Vec::with_capacity(256);
296
297        // Request line
298        let request_line = format!(
299            "{} {} {}\r\n",
300            self.method.as_str(),
301            self.url.request_path(),
302            HTTP_VERSION,
303        );
304        buf.extend_from_slice(request_line.as_bytes());
305
306        // Headers
307        for (name, value) in &self.headers {
308            buf.extend_from_slice(name.as_bytes());
309            buf.extend_from_slice(b": ");
310            buf.extend_from_slice(value.as_bytes());
311            buf.extend_from_slice(b"\r\n");
312        }
313
314        // End of headers
315        buf.extend_from_slice(b"\r\n");
316
317        // Body
318        if let Some(ref body) = self.body {
319            buf.extend_from_slice(body);
320        }
321
322        buf
323    }
324}
325
326// ---------------------------------------------------------------------------
327// HTTP Response
328// ---------------------------------------------------------------------------
329
330/// An HTTP response.
331#[cfg(feature = "alloc")]
332#[derive(Debug, Clone)]
333pub struct HttpResponse {
334    /// HTTP version string (e.g. "HTTP/1.1")
335    pub version: String,
336    /// Status code (e.g. 200, 404)
337    pub status_code: u16,
338    /// Reason phrase (e.g. "OK", "Not Found")
339    pub reason: String,
340    /// Response headers (lowercase key -> value)
341    pub headers: BTreeMap<String, String>,
342    /// Response body
343    pub body: Vec<u8>,
344}
345
346#[cfg(feature = "alloc")]
347impl HttpResponse {
348    /// Check if this is a redirect status (301, 302, 307, 308).
349    pub fn is_redirect(&self) -> bool {
350        matches!(self.status_code, 301 | 302 | 307 | 308)
351    }
352
353    /// Get the Location header for redirects.
354    pub fn redirect_location(&self) -> Option<&String> {
355        self.headers.get("location")
356    }
357
358    /// Check if the connection should be kept alive.
359    pub fn is_keep_alive(&self) -> bool {
360        if let Some(conn) = self.headers.get("connection") {
361            let lower = to_lowercase(conn);
362            lower.contains("keep-alive")
363        } else {
364            // HTTP/1.1 defaults to keep-alive
365            self.version.contains("1.1")
366        }
367    }
368
369    /// Get the Content-Type header value.
370    pub fn content_type(&self) -> Option<&String> {
371        self.headers.get("content-type")
372    }
373
374    /// Get the Content-Length value if present.
375    pub fn content_length(&self) -> Option<usize> {
376        self.headers
377            .get("content-length")
378            .and_then(|v| parse_usize(v).ok())
379    }
380
381    /// Get Content-Encoding header value.
382    pub fn content_encoding(&self) -> ContentEncoding {
383        match self.headers.get("content-encoding") {
384            Some(v) if to_lowercase(v).contains("gzip") => ContentEncoding::Gzip,
385            Some(v) if to_lowercase(v).contains("deflate") => ContentEncoding::Deflate,
386            _ => ContentEncoding::Identity,
387        }
388    }
389
390    /// Return the body interpreted as a UTF-8 string, if valid.
391    pub fn body_as_str(&self) -> Option<&str> {
392        core::str::from_utf8(&self.body).ok()
393    }
394}
395
396// ---------------------------------------------------------------------------
397// Content Encoding
398// ---------------------------------------------------------------------------
399
400/// Content-Encoding types.
401#[cfg(feature = "alloc")]
402#[derive(Debug, Clone, Copy, PartialEq, Eq)]
403pub enum ContentEncoding {
404    /// No encoding (identity)
405    Identity,
406    /// Gzip compression (stub -- caller must decompress)
407    Gzip,
408    /// Deflate compression (stub -- caller must decompress)
409    Deflate,
410}
411
412// ---------------------------------------------------------------------------
413// Response Parser (incremental / state machine)
414// ---------------------------------------------------------------------------
415
416/// Current state of the incremental response parser.
417#[cfg(feature = "alloc")]
418#[derive(Debug, Clone, PartialEq, Eq)]
419pub enum ParseState {
420    /// Awaiting or parsing the status line.
421    StatusLine,
422    /// Parsing headers.
423    Headers,
424    /// Reading a fixed-length body (Content-Length).
425    Body,
426    /// Reading a chunked body.
427    ChunkedBody,
428    /// Parsing is complete.
429    Complete,
430}
431
432/// Incremental HTTP response parser.
433///
434/// Feed bytes via `feed()`. When `state()` returns `ParseState::Complete`,
435/// call `take_response()` to extract the finished `HttpResponse`.
436#[cfg(feature = "alloc")]
437pub struct ResponseParser {
438    state: ParseState,
439    buffer: Vec<u8>,
440    version: String,
441    status_code: u16,
442    reason: String,
443    headers: BTreeMap<String, String>,
444    body: Vec<u8>,
445    content_length: Option<usize>,
446    chunked: bool,
447    /// Remaining bytes in the current chunk (for chunked encoding).
448    chunk_remaining: usize,
449    /// Whether we have finished reading the chunk size line for this chunk.
450    chunk_size_parsed: bool,
451}
452
453#[cfg(feature = "alloc")]
454impl Default for ResponseParser {
455    fn default() -> Self {
456        Self::new()
457    }
458}
459
460#[cfg(feature = "alloc")]
461impl ResponseParser {
462    /// Create a new response parser.
463    pub fn new() -> Self {
464        ResponseParser {
465            state: ParseState::StatusLine,
466            buffer: Vec::new(),
467            version: String::new(),
468            status_code: 0,
469            reason: String::new(),
470            headers: BTreeMap::new(),
471            body: Vec::new(),
472            content_length: None,
473            chunked: false,
474            chunk_remaining: 0,
475            chunk_size_parsed: false,
476        }
477    }
478
479    /// Return the current parse state.
480    pub fn state(&self) -> &ParseState {
481        &self.state
482    }
483
484    /// Feed bytes into the parser, advancing the state machine.
485    pub fn feed(&mut self, data: &[u8]) -> Result<(), HttpError> {
486        self.buffer.extend_from_slice(data);
487
488        loop {
489            match self.state {
490                ParseState::StatusLine => {
491                    if !self.try_parse_status_line()? {
492                        return Ok(());
493                    }
494                }
495                ParseState::Headers => {
496                    if !self.try_parse_headers()? {
497                        return Ok(());
498                    }
499                }
500                ParseState::Body => {
501                    self.try_parse_body();
502                    return Ok(());
503                }
504                ParseState::ChunkedBody => {
505                    if !self.try_parse_chunked()? {
506                        return Ok(());
507                    }
508                }
509                ParseState::Complete => return Ok(()),
510            }
511        }
512    }
513
514    /// Try to parse the status line from the buffer.
515    /// Returns `true` if the status line was found and parsed.
516    fn try_parse_status_line(&mut self) -> Result<bool, HttpError> {
517        let line_end = match find_crlf(&self.buffer) {
518            Some(pos) => pos,
519            None => return Ok(false),
520        };
521
522        let line = match core::str::from_utf8(&self.buffer[..line_end]) {
523            Ok(s) => s,
524            Err(_) => return Err(HttpError::InvalidStatusLine),
525        };
526
527        // Parse "HTTP/1.1 200 OK"
528        let mut parts = line.splitn(3, ' ');
529        let version = parts.next().ok_or(HttpError::InvalidStatusLine)?;
530        let status_str = parts.next().ok_or(HttpError::InvalidStatusLine)?;
531        let reason = parts.next().unwrap_or("");
532
533        self.version = String::from(version);
534        self.status_code = parse_u16(status_str).map_err(|_| HttpError::InvalidStatusLine)?;
535        self.reason = String::from(reason);
536
537        // Consume the line + CRLF
538        let new_start = line_end + 2;
539        self.buffer = self.buffer[new_start..].to_vec();
540        self.state = ParseState::Headers;
541        Ok(true)
542    }
543
544    /// Try to parse headers from the buffer.
545    /// Returns `true` when all headers have been consumed (empty line found).
546    fn try_parse_headers(&mut self) -> Result<bool, HttpError> {
547        loop {
548            let line_end = match find_crlf(&self.buffer) {
549                Some(pos) => pos,
550                None => return Ok(false),
551            };
552
553            if line_end == 0 {
554                // Empty line -- end of headers
555                self.buffer = self.buffer[2..].to_vec();
556                self.determine_body_mode();
557                return Ok(true);
558            }
559
560            let line = match core::str::from_utf8(&self.buffer[..line_end]) {
561                Ok(s) => s,
562                Err(_) => return Err(HttpError::InvalidHeader),
563            };
564
565            if let Some(colon_pos) = line.find(':') {
566                let name = to_lowercase(&line[..colon_pos]);
567                let value = line[colon_pos + 1..].trim_start();
568                self.headers.insert(name, String::from(value));
569            }
570
571            let new_start = line_end + 2;
572            self.buffer = self.buffer[new_start..].to_vec();
573        }
574    }
575
576    /// Determine whether to read a fixed-length body or chunked body.
577    fn determine_body_mode(&mut self) {
578        // Check for chunked transfer encoding
579        if let Some(te) = self.headers.get("transfer-encoding") {
580            if to_lowercase(te).contains("chunked") {
581                self.chunked = true;
582                self.state = ParseState::ChunkedBody;
583                return;
584            }
585        }
586
587        // Check for Content-Length
588        if let Some(cl) = self.headers.get("content-length") {
589            if let Ok(len) = parse_usize(cl) {
590                self.content_length = Some(len);
591                if len == 0 {
592                    self.state = ParseState::Complete;
593                } else {
594                    self.state = ParseState::Body;
595                }
596                return;
597            }
598        }
599
600        // No body indication -- treat as complete (e.g., HEAD response)
601        self.state = ParseState::Complete;
602    }
603
604    /// Try to read a Content-Length body.
605    fn try_parse_body(&mut self) {
606        if let Some(expected) = self.content_length {
607            if self.buffer.len() >= expected {
608                self.body = self.buffer[..expected].to_vec();
609                self.buffer = self.buffer[expected..].to_vec();
610                self.state = ParseState::Complete;
611            }
612            // else: need more data
613        }
614    }
615
616    /// Try to parse chunked transfer-encoded body.
617    /// Returns `true` when all chunks have been read (0-size terminator).
618    fn try_parse_chunked(&mut self) -> Result<bool, HttpError> {
619        loop {
620            if !self.chunk_size_parsed {
621                // Read chunk size line
622                let line_end = match find_crlf(&self.buffer) {
623                    Some(pos) => pos,
624                    None => return Ok(false),
625                };
626
627                let size_line = match core::str::from_utf8(&self.buffer[..line_end]) {
628                    Ok(s) => s,
629                    Err(_) => return Err(HttpError::InvalidChunkSize),
630                };
631
632                // Chunk size may have extensions after ';' -- ignore them
633                let size_str = match size_line.find(';') {
634                    Some(idx) => &size_line[..idx],
635                    None => size_line,
636                };
637
638                let chunk_size =
639                    parse_hex_usize(size_str.trim()).map_err(|_| HttpError::InvalidChunkSize)?;
640
641                self.buffer = self.buffer[line_end + 2..].to_vec();
642
643                if chunk_size == 0 {
644                    // Terminal chunk -- consume trailing CRLF if present
645                    if self.buffer.len() >= 2 && self.buffer[0] == b'\r' && self.buffer[1] == b'\n'
646                    {
647                        self.buffer = self.buffer[2..].to_vec();
648                    }
649                    self.state = ParseState::Complete;
650                    return Ok(true);
651                }
652
653                self.chunk_remaining = chunk_size;
654                self.chunk_size_parsed = true;
655            }
656
657            // Read chunk data
658            if self.buffer.len() < self.chunk_remaining {
659                return Ok(false);
660            }
661
662            self.body
663                .extend_from_slice(&self.buffer[..self.chunk_remaining]);
664            self.buffer = self.buffer[self.chunk_remaining..].to_vec();
665            self.chunk_remaining = 0;
666            self.chunk_size_parsed = false;
667
668            // Consume trailing CRLF after chunk data
669            if self.buffer.len() >= 2 && self.buffer[0] == b'\r' && self.buffer[1] == b'\n' {
670                self.buffer = self.buffer[2..].to_vec();
671            } else if self.buffer.len() < 2 {
672                return Ok(false);
673            }
674        }
675    }
676
677    /// Extract the completed response. Returns `None` if parsing is not
678    /// complete.
679    pub fn take_response(&mut self) -> Option<HttpResponse> {
680        if self.state != ParseState::Complete {
681            return None;
682        }
683
684        Some(HttpResponse {
685            version: core::mem::take(&mut self.version),
686            status_code: self.status_code,
687            reason: core::mem::take(&mut self.reason),
688            headers: core::mem::take(&mut self.headers),
689            body: core::mem::take(&mut self.body),
690        })
691    }
692}
693
694// ---------------------------------------------------------------------------
695// HTTP Client
696// ---------------------------------------------------------------------------
697
698/// HTTP client with configurable defaults.
699///
700/// The client does not perform actual I/O. Instead, `prepare_request()` returns
701/// serialized bytes to send over TCP, and `parse_response()` accepts received
702/// bytes and returns an `HttpResponse` when complete.
703#[cfg(feature = "alloc")]
704pub struct HttpClient {
705    /// Default headers applied to every request.
706    pub default_headers: BTreeMap<String, String>,
707    /// Request timeout in milliseconds.
708    pub timeout_ms: u64,
709    /// Maximum number of redirects to follow.
710    pub max_redirects: u8,
711}
712
713#[cfg(feature = "alloc")]
714impl Default for HttpClient {
715    fn default() -> Self {
716        Self::new()
717    }
718}
719
720#[cfg(feature = "alloc")]
721impl HttpClient {
722    /// Create a new HTTP client with default settings.
723    pub fn new() -> Self {
724        let mut default_headers = BTreeMap::new();
725        default_headers.insert(String::from("user-agent"), String::from(DEFAULT_USER_AGENT));
726        default_headers.insert(String::from("accept"), String::from("*/*"));
727
728        HttpClient {
729            default_headers,
730            timeout_ms: DEFAULT_TIMEOUT_MS,
731            max_redirects: DEFAULT_MAX_REDIRECTS,
732        }
733    }
734
735    /// Set a default header that will be applied to every request.
736    pub fn set_default_header(&mut self, name: &str, value: &str) {
737        self.default_headers
738            .insert(to_lowercase(name), String::from(value));
739    }
740
741    /// Prepare a request: apply default headers and serialize to bytes.
742    pub fn prepare_request(&self, request: &mut HttpRequest) -> Vec<u8> {
743        // Apply default headers (request headers take precedence)
744        for (name, value) in &self.default_headers {
745            if !request.headers.contains_key(name) {
746                request.headers.insert(name.clone(), value.clone());
747            }
748        }
749        request.serialize()
750    }
751
752    /// Create a new response parser for receiving response data.
753    pub fn create_parser(&self) -> ResponseParser {
754        ResponseParser::new()
755    }
756
757    /// Convenience: build and serialize a GET request.
758    pub fn get(&self, url: &str) -> Result<Vec<u8>, HttpError> {
759        let mut req = HttpRequest::new(HttpMethod::Get, url)?;
760        Ok(self.prepare_request(&mut req))
761    }
762
763    /// Convenience: build and serialize a POST request with a body.
764    pub fn post(&self, url: &str, body: &[u8], content_type: &str) -> Result<Vec<u8>, HttpError> {
765        let mut req = HttpRequest::new(HttpMethod::Post, url)?;
766        req.set_content_type(content_type);
767        req.set_body(Vec::from(body));
768        Ok(self.prepare_request(&mut req))
769    }
770
771    /// Process a redirect response: returns a new request to the redirect
772    /// location, or `None` if not a redirect. Decrements `remaining_redirects`.
773    pub fn follow_redirect(
774        &self,
775        response: &HttpResponse,
776        original_url: &str,
777        remaining_redirects: &mut u8,
778    ) -> Result<Option<HttpRequest>, HttpError> {
779        if !response.is_redirect() {
780            return Ok(None);
781        }
782
783        if *remaining_redirects == 0 {
784            return Err(HttpError::TooManyRedirects);
785        }
786        *remaining_redirects -= 1;
787
788        let location = match response.redirect_location() {
789            Some(loc) => loc.clone(),
790            None => return Ok(None),
791        };
792
793        // Handle relative URLs by prepending scheme + host from original
794        let full_url = if location.starts_with("http://") || location.starts_with("https://") {
795            location
796        } else {
797            let orig = ParsedUrl::parse(original_url)?;
798            let scheme = &orig.scheme;
799            if orig.port != HTTP_PORT && orig.port != HTTPS_PORT {
800                format!("{}://{}:{}{}", scheme, orig.host, orig.port, location)
801            } else {
802                format!("{}://{}{}", scheme, orig.host, location)
803            }
804        };
805
806        // 307/308 should preserve the original method; 301/302 change to GET.
807        // Simplified: always use GET for redirects.
808        let method = HttpMethod::Get;
809
810        let req = HttpRequest::new(method, &full_url)?;
811        Ok(Some(req))
812    }
813}
814
815// ---------------------------------------------------------------------------
816// Query string encoding (percent-encoding)
817// ---------------------------------------------------------------------------
818
819/// Percent-encode a string for use in URL query parameters.
820///
821/// Encodes all characters except unreserved characters (A-Z, a-z, 0-9, '-',
822/// '_', '.', '~').
823#[cfg(feature = "alloc")]
824pub fn percent_encode(input: &str) -> String {
825    let mut encoded = String::with_capacity(input.len());
826    for byte in input.bytes() {
827        if is_unreserved(byte) {
828            encoded.push(byte as char);
829        } else if byte == b' ' {
830            encoded.push('+');
831        } else {
832            encoded.push('%');
833            encoded.push(hex_digit(byte >> 4));
834            encoded.push(hex_digit(byte & 0x0F));
835        }
836    }
837    encoded
838}
839
840/// Encode a list of key-value pairs into a query string.
841///
842/// Returns `key1=value1&key2=value2&...` with percent-encoding applied.
843#[cfg(feature = "alloc")]
844pub fn encode_query_string(params: &[(&str, &str)]) -> String {
845    let mut result = String::new();
846    for (i, (key, value)) in params.iter().enumerate() {
847        if i > 0 {
848            result.push('&');
849        }
850        result.push_str(&percent_encode(key));
851        result.push('=');
852        result.push_str(&percent_encode(value));
853    }
854    result
855}
856
857/// Construct a Basic Authentication header value.
858///
859/// Returns `"Basic <base64(user:password)>"`.
860#[cfg(feature = "alloc")]
861pub fn basic_auth_header(user: &str, password: &str) -> String {
862    let credentials = format!("{}:{}", user, password);
863    let encoded = base64_encode(credentials.as_bytes());
864    format!("Basic {}", encoded)
865}
866
867/// Build a Cookie header value from a list of (name, value) pairs.
868#[cfg(feature = "alloc")]
869pub fn build_cookie_header(cookies: &[(&str, &str)]) -> String {
870    let mut result = String::new();
871    for (i, (name, value)) in cookies.iter().enumerate() {
872        if i > 0 {
873            result.push_str("; ");
874        }
875        result.push_str(name);
876        result.push('=');
877        result.push_str(value);
878    }
879    result
880}
881
882// ---------------------------------------------------------------------------
883// Helper functions
884// ---------------------------------------------------------------------------
885
886/// Find the position of the first `\r\n` in a byte slice.
887fn find_crlf(data: &[u8]) -> Option<usize> {
888    if data.len() < 2 {
889        return None;
890    }
891    let limit = data.len() - 1;
892    let mut i = 0;
893    while i < limit {
894        if data[i] == b'\r' && data[i + 1] == b'\n' {
895            return Some(i);
896        }
897        i += 1;
898    }
899    None
900}
901
902/// Convert an ASCII string to lowercase.
903#[cfg(feature = "alloc")]
904fn to_lowercase(s: &str) -> String {
905    let mut result = String::with_capacity(s.len());
906    for c in s.chars() {
907        if c.is_ascii_uppercase() {
908            result.push((c as u8 + 32) as char);
909        } else {
910            result.push(c);
911        }
912    }
913    result
914}
915
916/// Parse a `&str` as a `u16`.
917fn parse_u16(s: &str) -> Result<u16, ()> {
918    let mut val: u16 = 0;
919    for byte in s.bytes() {
920        if !byte.is_ascii_digit() {
921            return Err(());
922        }
923        val = val.checked_mul(10).ok_or(())?;
924        val = val.checked_add((byte - b'0') as u16).ok_or(())?;
925    }
926    Ok(val)
927}
928
929/// Parse a `&str` as a `usize`.
930fn parse_usize(s: &str) -> Result<usize, ()> {
931    let s = s.trim();
932    let mut val: usize = 0;
933    for byte in s.bytes() {
934        if !byte.is_ascii_digit() {
935            return Err(());
936        }
937        val = val.checked_mul(10).ok_or(())?;
938        val = val.checked_add((byte - b'0') as usize).ok_or(())?;
939    }
940    Ok(val)
941}
942
943/// Parse a hexadecimal string as a `usize`.
944fn parse_hex_usize(s: &str) -> Result<usize, ()> {
945    let mut val: usize = 0;
946    for byte in s.bytes() {
947        let digit = match byte {
948            b'0'..=b'9' => (byte - b'0') as usize,
949            b'a'..=b'f' => (byte - b'a') as usize + 10,
950            b'A'..=b'F' => (byte - b'A') as usize + 10,
951            _ => return Err(()),
952        };
953        val = val.checked_mul(16).ok_or(())?;
954        val = val.checked_add(digit).ok_or(())?;
955    }
956    Ok(val)
957}
958
959/// Convert a `usize` to a decimal `String`.
960#[cfg(feature = "alloc")]
961fn uint_to_string(mut n: usize) -> String {
962    if n == 0 {
963        return String::from("0");
964    }
965    let mut digits = Vec::new();
966    while n > 0 {
967        digits.push(b'0' + (n % 10) as u8);
968        n /= 10;
969    }
970    digits.reverse();
971    // Safety: digits are ASCII
972    String::from_utf8(digits).unwrap_or_else(|_| String::from("0"))
973}
974
975/// Check if a byte is an unreserved URL character.
976fn is_unreserved(b: u8) -> bool {
977    matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~')
978}
979
980/// Return the hex digit character for a 4-bit value.
981fn hex_digit(val: u8) -> char {
982    let nibble = val & 0x0F;
983    if nibble < 10 {
984        (b'0' + nibble) as char
985    } else {
986        (b'A' + nibble - 10) as char
987    }
988}
989
990/// Minimal Base64 encoder (no padding configurable -- always pads).
991#[cfg(feature = "alloc")]
992fn base64_encode(data: &[u8]) -> String {
993    const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
994
995    let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
996    let mut i = 0;
997
998    while i + 2 < data.len() {
999        let n = ((data[i] as u32) << 16) | ((data[i + 1] as u32) << 8) | (data[i + 2] as u32);
1000        result.push(TABLE[((n >> 18) & 0x3F) as usize] as char);
1001        result.push(TABLE[((n >> 12) & 0x3F) as usize] as char);
1002        result.push(TABLE[((n >> 6) & 0x3F) as usize] as char);
1003        result.push(TABLE[(n & 0x3F) as usize] as char);
1004        i += 3;
1005    }
1006
1007    let remaining = data.len() - i;
1008    if remaining == 1 {
1009        let n = (data[i] as u32) << 16;
1010        result.push(TABLE[((n >> 18) & 0x3F) as usize] as char);
1011        result.push(TABLE[((n >> 12) & 0x3F) as usize] as char);
1012        result.push('=');
1013        result.push('=');
1014    } else if remaining == 2 {
1015        let n = ((data[i] as u32) << 16) | ((data[i + 1] as u32) << 8);
1016        result.push(TABLE[((n >> 18) & 0x3F) as usize] as char);
1017        result.push(TABLE[((n >> 12) & 0x3F) as usize] as char);
1018        result.push(TABLE[((n >> 6) & 0x3F) as usize] as char);
1019        result.push('=');
1020    }
1021
1022    result
1023}
1024
1025// ---------------------------------------------------------------------------
1026// Tests
1027// ---------------------------------------------------------------------------
1028
1029#[cfg(test)]
1030mod tests {
1031    #[allow(unused_imports)]
1032    use alloc::vec;
1033
1034    use super::*;
1035
1036    // -- URL parsing --
1037
1038    #[test]
1039    fn test_url_parse_http() {
1040        let url = ParsedUrl::parse("http://example.com").unwrap();
1041        assert_eq!(url.scheme, "http");
1042        assert_eq!(url.host, "example.com");
1043        assert_eq!(url.port, 80);
1044        assert_eq!(url.path, "/");
1045        assert_eq!(url.query, None);
1046        assert!(!url.is_https);
1047    }
1048
1049    #[test]
1050    fn test_url_parse_https() {
1051        let url = ParsedUrl::parse("https://secure.example.com").unwrap();
1052        assert_eq!(url.scheme, "https");
1053        assert_eq!(url.host, "secure.example.com");
1054        assert_eq!(url.port, 443);
1055        assert!(url.is_https);
1056    }
1057
1058    #[test]
1059    fn test_url_parse_with_port() {
1060        let url = ParsedUrl::parse("http://localhost:8080/api").unwrap();
1061        assert_eq!(url.host, "localhost");
1062        assert_eq!(url.port, 8080);
1063        assert_eq!(url.path, "/api");
1064    }
1065
1066    #[test]
1067    fn test_url_parse_with_path() {
1068        let url = ParsedUrl::parse("http://example.com/path/to/resource").unwrap();
1069        assert_eq!(url.path, "/path/to/resource");
1070    }
1071
1072    #[test]
1073    fn test_url_parse_with_query() {
1074        let url = ParsedUrl::parse("http://example.com/search?q=rust&page=1").unwrap();
1075        assert_eq!(url.path, "/search");
1076        assert_eq!(url.query, Some(String::from("q=rust&page=1")));
1077        assert_eq!(url.request_path(), "/search?q=rust&page=1");
1078    }
1079
1080    #[test]
1081    fn test_url_parse_invalid() {
1082        assert_eq!(
1083            ParsedUrl::parse("ftp://example.com"),
1084            Err(HttpError::InvalidUrl)
1085        );
1086    }
1087
1088    // -- Request serialization --
1089
1090    #[test]
1091    fn test_request_get_serialization() {
1092        let req = HttpRequest::new(HttpMethod::Get, "http://example.com/index.html").unwrap();
1093        let bytes = req.serialize();
1094        let text = core::str::from_utf8(&bytes).unwrap();
1095
1096        assert!(text.starts_with("GET /index.html HTTP/1.1\r\n"));
1097        assert!(text.contains("host: example.com\r\n"));
1098        assert!(text.ends_with("\r\n\r\n"));
1099    }
1100
1101    #[test]
1102    fn test_request_post_with_body() {
1103        let mut req = HttpRequest::new(HttpMethod::Post, "http://example.com/api").unwrap();
1104        req.set_content_type(MIME_APPLICATION_JSON);
1105        req.set_body_str("{\"key\":\"value\"}");
1106        let bytes = req.serialize();
1107        let text = core::str::from_utf8(&bytes).unwrap();
1108
1109        assert!(text.starts_with("POST /api HTTP/1.1\r\n"));
1110        assert!(text.contains("content-type: application/json\r\n"));
1111        assert!(text.contains("content-length: 15\r\n"));
1112        assert!(text.ends_with("{\"key\":\"value\"}"));
1113    }
1114
1115    // -- Response status line parsing --
1116
1117    #[test]
1118    fn test_response_status_line() {
1119        let mut parser = ResponseParser::new();
1120        parser.feed(b"HTTP/1.1 200 OK\r\n\r\n").unwrap();
1121        let resp = parser.take_response().unwrap();
1122        assert_eq!(resp.version, "HTTP/1.1");
1123        assert_eq!(resp.status_code, 200);
1124        assert_eq!(resp.reason, "OK");
1125    }
1126
1127    // -- Header parsing --
1128
1129    #[test]
1130    fn test_response_single_header() {
1131        let mut parser = ResponseParser::new();
1132        parser
1133            .feed(b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n")
1134            .unwrap();
1135        let resp = parser.take_response().unwrap();
1136        assert_eq!(
1137            resp.headers.get("content-type"),
1138            Some(&String::from("text/html"))
1139        );
1140    }
1141
1142    #[test]
1143    fn test_response_multiple_headers() {
1144        let mut parser = ResponseParser::new();
1145        parser
1146            .feed(b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 5\r\n\r\nhello")
1147            .unwrap();
1148        let resp = parser.take_response().unwrap();
1149        assert_eq!(
1150            resp.headers.get("content-type"),
1151            Some(&String::from("text/html"))
1152        );
1153        assert_eq!(resp.headers.get("content-length"), Some(&String::from("5")));
1154        assert_eq!(resp.body, b"hello");
1155    }
1156
1157    #[test]
1158    fn test_response_case_insensitive_headers() {
1159        let mut parser = ResponseParser::new();
1160        parser
1161            .feed(b"HTTP/1.1 200 OK\r\nCONTENT-TYPE: text/plain\r\n\r\n")
1162            .unwrap();
1163        let resp = parser.take_response().unwrap();
1164        assert_eq!(
1165            resp.headers.get("content-type"),
1166            Some(&String::from("text/plain"))
1167        );
1168    }
1169
1170    // -- Body reading --
1171
1172    #[test]
1173    fn test_content_length_body() {
1174        let mut parser = ResponseParser::new();
1175        parser
1176            .feed(b"HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nHello World")
1177            .unwrap();
1178        let resp = parser.take_response().unwrap();
1179        assert_eq!(resp.body, b"Hello World");
1180        assert_eq!(resp.body_as_str(), Some("Hello World"));
1181    }
1182
1183    #[test]
1184    fn test_chunked_transfer_decoding() {
1185        let mut parser = ResponseParser::new();
1186        parser
1187            .feed(
1188                b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n\
1189                  5\r\nHello\r\n6\r\n World\r\n0\r\n\r\n",
1190            )
1191            .unwrap();
1192        assert_eq!(parser.state(), &ParseState::Complete);
1193        let resp = parser.take_response().unwrap();
1194        assert_eq!(resp.body, b"Hello World");
1195    }
1196
1197    // -- Redirect detection --
1198
1199    #[test]
1200    fn test_redirect_301() {
1201        let mut parser = ResponseParser::new();
1202        parser
1203            .feed(b"HTTP/1.1 301 Moved Permanently\r\nLocation: http://new.example.com/\r\n\r\n")
1204            .unwrap();
1205        let resp = parser.take_response().unwrap();
1206        assert!(resp.is_redirect());
1207        assert_eq!(
1208            resp.redirect_location(),
1209            Some(&String::from("http://new.example.com/"))
1210        );
1211    }
1212
1213    #[test]
1214    fn test_redirect_302() {
1215        let mut parser = ResponseParser::new();
1216        parser
1217            .feed(b"HTTP/1.1 302 Found\r\nLocation: /new-path\r\n\r\n")
1218            .unwrap();
1219        let resp = parser.take_response().unwrap();
1220        assert!(resp.is_redirect());
1221        assert_eq!(resp.status_code, 302);
1222    }
1223
1224    #[test]
1225    fn test_redirect_307() {
1226        let mut parser = ResponseParser::new();
1227        parser
1228            .feed(b"HTTP/1.1 307 Temporary Redirect\r\nLocation: /temp\r\n\r\n")
1229            .unwrap();
1230        let resp = parser.take_response().unwrap();
1231        assert!(resp.is_redirect());
1232        assert_eq!(resp.status_code, 307);
1233    }
1234
1235    // -- Query string encoding --
1236
1237    #[test]
1238    fn test_query_string_encoding() {
1239        let qs = encode_query_string(&[("key", "value"), ("name", "hello world")]);
1240        assert_eq!(qs, "key=value&name=hello+world");
1241    }
1242
1243    #[test]
1244    fn test_percent_encode_special_chars() {
1245        let encoded = percent_encode("a&b=c d");
1246        assert_eq!(encoded, "a%26b%3Dc+d");
1247    }
1248
1249    // -- Keep-alive --
1250
1251    #[test]
1252    fn test_keep_alive_detection() {
1253        let mut parser = ResponseParser::new();
1254        parser
1255            .feed(b"HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n")
1256            .unwrap();
1257        let resp = parser.take_response().unwrap();
1258        assert!(resp.is_keep_alive());
1259    }
1260
1261    #[test]
1262    fn test_http11_default_keep_alive() {
1263        let mut parser = ResponseParser::new();
1264        parser
1265            .feed(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")
1266            .unwrap();
1267        let resp = parser.take_response().unwrap();
1268        // HTTP/1.1 defaults to keep-alive
1269        assert!(resp.is_keep_alive());
1270    }
1271
1272    // -- Basic auth --
1273
1274    #[test]
1275    fn test_basic_auth_header() {
1276        let header = basic_auth_header("user", "pass");
1277        // "user:pass" -> base64 "dXNlcjpwYXNz"
1278        assert_eq!(header, "Basic dXNlcjpwYXNz");
1279    }
1280
1281    // -- Cookie header --
1282
1283    #[test]
1284    fn test_cookie_header_construction() {
1285        let cookie = build_cookie_header(&[("session", "abc123"), ("lang", "en")]);
1286        assert_eq!(cookie, "session=abc123; lang=en");
1287    }
1288
1289    // -- Method serialization --
1290
1291    #[test]
1292    fn test_method_as_str() {
1293        assert_eq!(HttpMethod::Get.as_str(), "GET");
1294        assert_eq!(HttpMethod::Post.as_str(), "POST");
1295        assert_eq!(HttpMethod::Put.as_str(), "PUT");
1296        assert_eq!(HttpMethod::Delete.as_str(), "DELETE");
1297        assert_eq!(HttpMethod::Head.as_str(), "HEAD");
1298        assert_eq!(HttpMethod::Patch.as_str(), "PATCH");
1299        assert_eq!(HttpMethod::Options.as_str(), "OPTIONS");
1300    }
1301
1302    // -- Empty response --
1303
1304    #[test]
1305    fn test_empty_response_no_body() {
1306        let mut parser = ResponseParser::new();
1307        parser.feed(b"HTTP/1.1 204 No Content\r\n\r\n").unwrap();
1308        let resp = parser.take_response().unwrap();
1309        assert_eq!(resp.status_code, 204);
1310        assert!(resp.body.is_empty());
1311    }
1312
1313    // -- Incremental parsing --
1314
1315    #[test]
1316    fn test_incremental_parsing() {
1317        let mut parser = ResponseParser::new();
1318
1319        // Feed status line only
1320        parser.feed(b"HTTP/1.1 200 OK\r\n").unwrap();
1321        assert_eq!(parser.state(), &ParseState::Headers);
1322
1323        // Feed headers
1324        parser.feed(b"Content-Length: 5\r\n\r\n").unwrap();
1325        assert_eq!(parser.state(), &ParseState::Body);
1326
1327        // Feed partial body
1328        parser.feed(b"Hel").unwrap();
1329        assert_eq!(parser.state(), &ParseState::Body);
1330
1331        // Feed rest of body
1332        parser.feed(b"lo").unwrap();
1333        assert_eq!(parser.state(), &ParseState::Complete);
1334
1335        let resp = parser.take_response().unwrap();
1336        assert_eq!(resp.body, b"Hello");
1337    }
1338
1339    // -- Base64 encoder --
1340
1341    #[test]
1342    fn test_base64_encode() {
1343        assert_eq!(base64_encode(b""), "");
1344        assert_eq!(base64_encode(b"f"), "Zg==");
1345        assert_eq!(base64_encode(b"fo"), "Zm8=");
1346        assert_eq!(base64_encode(b"foo"), "Zm9v");
1347        assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
1348    }
1349
1350    // -- Hex parsing --
1351
1352    #[test]
1353    fn test_parse_hex_usize() {
1354        assert_eq!(parse_hex_usize("0"), Ok(0));
1355        assert_eq!(parse_hex_usize("5"), Ok(5));
1356        assert_eq!(parse_hex_usize("a"), Ok(10));
1357        assert_eq!(parse_hex_usize("1F"), Ok(31));
1358        assert_eq!(parse_hex_usize("ff"), Ok(255));
1359    }
1360}