1use crate::NetError;
4
5#[derive(Clone, Debug, PartialEq, Eq)]
7pub struct HttpHead {
8 pub status: u16,
10 pub reason: String,
13 pub headers: Vec<(String, String)>,
15}
16
17impl HttpHead {
18 pub fn header(&self, name: &str) -> Option<&str> {
20 self.headers
21 .iter()
22 .find(|(key, _)| key.eq_ignore_ascii_case(name))
23 .map(|(_, value)| value.as_str())
24 }
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum HttpBodyMode {
30 ContentLength(usize),
32 Chunked,
34 UntilEof,
36 Empty,
39}
40
41pub fn build_http_request_head(
48 method: &str,
49 target: &str,
50 host: &str,
51 content_length: Option<usize>,
52 headers: &[(String, String)],
53) -> Result<String, NetError> {
54 validate_token("method", method)?;
55 validate_field_value("target", target)?;
56 validate_field_value("host", host)?;
57
58 let mut head = format!("{method} {target} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n");
59 if let Some(length) = content_length {
60 head.push_str(&format!("Content-Length: {length}\r\n"));
61 }
62 for (name, value) in headers {
63 validate_token("header name", name)?;
64 validate_field_value("header value", value)?;
65 head.push_str(name);
66 head.push_str(": ");
67 head.push_str(value);
68 head.push_str("\r\n");
69 }
70 head.push_str("\r\n");
71 Ok(head)
72}
73
74pub fn parse_http_head(head_text: &str) -> Result<HttpHead, NetError> {
82 let body = head_text.trim_end_matches("\r\n\r\n");
83 let mut lines = body.split("\r\n");
84
85 let status_line = lines
86 .next()
87 .ok_or_else(|| NetError::InvalidHead("missing status line".to_owned()))?;
88 let mut parts = status_line.split_whitespace();
89 let _version = parts
90 .next()
91 .ok_or_else(|| NetError::InvalidHead("missing version".to_owned()))?;
92 let status = parts
93 .next()
94 .ok_or_else(|| NetError::InvalidHead("missing status".to_owned()))?
95 .parse::<u16>()
96 .map_err(|_| NetError::InvalidHead("invalid status code".to_owned()))?;
97 let reason = parts.collect::<Vec<_>>().join(" ");
98
99 let mut headers = Vec::new();
100 for line in lines {
101 if line.is_empty() {
102 continue;
103 }
104 let (key, value) = line
105 .split_once(':')
106 .ok_or_else(|| NetError::InvalidHead("invalid header line".to_owned()))?;
107 headers.push((key.to_owned(), value.trim().to_owned()));
108 }
109
110 Ok(HttpHead {
111 status,
112 reason,
113 headers,
114 })
115}
116
117fn validate_token(label: &str, value: &str) -> Result<(), NetError> {
118 if value.is_empty()
119 || value
120 .bytes()
121 .any(|byte| byte <= b' ' || byte == b':' || byte >= 0x7f)
122 {
123 return Err(NetError::InvalidHead(format!("invalid {label}")));
124 }
125 Ok(())
126}
127
128fn validate_field_value(label: &str, value: &str) -> Result<(), NetError> {
129 if value.contains('\r') || value.contains('\n') {
130 return Err(NetError::InvalidHead(format!("invalid {label}")));
131 }
132 Ok(())
133}
134
135pub fn body_mode(head: &HttpHead) -> Result<HttpBodyMode, NetError> {
147 if let Some(encoding) = head.header("Transfer-Encoding")
148 && encoding.eq_ignore_ascii_case("chunked")
149 {
150 return Ok(HttpBodyMode::Chunked);
151 }
152 match head.header("Content-Length") {
153 Some(raw) => {
154 let length = raw
155 .parse::<usize>()
156 .map_err(|_| NetError::InvalidHead("invalid content-length".to_owned()))?;
157 if length == 0 {
158 Ok(HttpBodyMode::UntilEof)
159 } else {
160 Ok(HttpBodyMode::ContentLength(length))
161 }
162 }
163 None => Ok(HttpBodyMode::UntilEof),
164 }
165}