1pub fn parse_http_request(buf: &[u8]) -> Option<String> {
6 let s = std::str::from_utf8(buf).ok()?;
7 let first_line = s.lines().next()?;
8 let mut parts = first_line.splitn(3, ' ');
10 let method = parts.next()?;
11 let path = parts.next()?;
12 Some(format!("{} {}", method, path))
13}
14
15pub fn extract_http_full_command(buf: &[u8]) -> Option<String> {
17 let s = std::str::from_utf8(buf).ok()?;
18 let first_line = s.lines().next()?;
19 let mut parts = first_line.splitn(3, ' ');
20 let method = parts.next()?;
21 let path = parts.next()?;
22
23 let mut result = format!("{} {}", method, path);
24 if let Some(header_end) = s.find("\r\n\r\n") {
25 let headers = &s[first_line.len() + 2..header_end];
26 let filtered: Vec<&str> = headers.split("\r\n")
27 .filter(|h| {
28 let lower = h.to_lowercase();
29 !lower.starts_with("host:") &&
30 !lower.starts_with("user-agent:") &&
31 !lower.starts_with("accept: */*")
32 })
33 .collect();
34 if !filtered.is_empty() {
35 result.push_str("\n\n[Request Headers]\n");
36 result.push_str(&filtered.join("\n"));
37 }
38 let body = extract_body_from_http(s);
39 if !body.is_empty() {
40 result.push_str("\n\n[Request Body]\n");
41 result.push_str(&body);
42 }
43 }
44 Some(result)
45}
46
47pub fn parse_http_response(buf: &[u8]) -> Option<String> {
49 let s = std::str::from_utf8(buf).ok()?;
50 let first_line = s.lines().next()?;
51 let mut parts = first_line.splitn(3, ' ');
53 let _version = parts.next()?;
54 let status = parts.next()?;
55 let reason = parts.next().unwrap_or("");
56 Some(format!("{} {}", status, reason))
57}
58
59pub fn format_http_response_detail(buf: &[u8]) -> Option<String> {
61 let s = std::str::from_utf8(buf).ok()?;
62 let first_line = s.lines().next()?;
63 let mut result = first_line.to_string();
64
65 if let Some(header_end) = s.find("\r\n\r\n") {
66 let headers = &s[first_line.len() + 2..header_end];
67 if !headers.is_empty() {
68 result.push_str("\n\n[Response Headers]\n");
69 result.push_str(headers.replace("\r\n", "\n").trim());
70 }
71 let body = extract_body_from_http(s);
72 if !body.is_empty() {
73 result.push_str("\n\n[Response Body]\n");
74 result.push_str(&simple_json_format(&body));
75 }
76 }
77 Some(result)
78}
79
80pub fn http_response_complete(buf: &[u8]) -> bool {
82 let Some(s) = std::str::from_utf8(buf).ok() else { return false };
83 let Some(header_end) = s.find("\r\n\r\n") else { return false };
84 let headers = &s[..header_end];
85
86 if headers.to_lowercase().contains("transfer-encoding: chunked") {
88 return buf.ends_with(b"0\r\n\r\n") || buf.ends_with(b"0\r\n\r\n");
90 }
91
92 if let Some(cl) = extract_content_length(headers) {
94 let body_start = header_end + 4;
95 return buf.len() >= body_start + cl;
96 }
97
98 true
100}
101
102pub fn http_request_complete(buf: &[u8]) -> bool {
104 let Some(s) = std::str::from_utf8(buf).ok() else { return false };
105 let Some(header_end) = s.find("\r\n\r\n") else { return false };
106 let headers = &s[..header_end];
107
108 if let Some(cl) = extract_content_length(headers) {
109 let body_start = header_end + 4;
110 return buf.len() >= body_start + cl;
111 }
112 true
114}
115
116fn extract_body_from_http(s: &str) -> String {
119 let Some(header_end) = s.find("\r\n\r\n") else { return String::new() };
120 let headers = &s[..header_end];
121 let body = &s[header_end + 4..];
122 if body.is_empty() { return String::new(); }
123
124 if headers.to_lowercase().contains("transfer-encoding: chunked") {
125 decode_chunked(body)
126 } else {
127 body.to_string()
128 }
129}
130
131fn decode_chunked(body: &str) -> String {
132 let mut result = String::new();
133 let mut remaining = body;
134 while let Some(line_end) = remaining.find("\r\n") {
135 let size_str = remaining[..line_end].trim();
136 let size = usize::from_str_radix(size_str, 16).unwrap_or(0);
137 if size == 0 { break; }
138 remaining = &remaining[line_end + 2..];
139 if remaining.len() < size { break; }
140 result.push_str(&remaining[..size]);
141 remaining = &remaining[size..];
142 if remaining.starts_with("\r\n") { remaining = &remaining[2..]; }
143 }
144 result
145}
146
147fn extract_content_length(headers: &str) -> Option<usize> {
148 for line in headers.lines() {
149 if line.to_lowercase().starts_with("content-length:") {
150 let val = line.split(':').nth(1)?.trim();
151 return val.parse().ok();
152 }
153 }
154 None
155}
156
157fn simple_json_format(s: &str) -> String {
159 let trimmed = s.trim();
160 if !trimmed.starts_with('{') && !trimmed.starts_with('[') {
161 return trimmed.to_string();
162 }
163 let mut out = String::new();
165 let mut indent = 0usize;
166 let mut in_string = false;
167 let mut prev = '\0';
168 for ch in trimmed.chars() {
169 if ch == '"' && prev != '\\' {
170 in_string = !in_string;
171 }
172 if in_string {
173 out.push(ch);
174 prev = ch;
175 continue;
176 }
177 match ch {
178 '{' | '[' => {
179 out.push(ch);
180 indent += 2;
181 out.push('\n');
182 out.extend(std::iter::repeat_n(' ', indent));
183 }
184 '}' | ']' => {
185 indent = indent.saturating_sub(2);
186 out.push('\n');
187 out.extend(std::iter::repeat_n(' ', indent));
188 out.push(ch);
189 }
190 ',' => {
191 out.push(ch);
192 out.push('\n');
193 out.extend(std::iter::repeat_n(' ', indent));
194 }
195 ':' => {
196 out.push(':');
197 out.push(' ');
198 }
199 _ if ch.is_whitespace() => {} _ => { out.push(ch); }
201 }
202 prev = ch;
203 }
204 out
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_parse_http_request_get() {
213 let req = b"GET /users/_doc/1 HTTP/1.1\r\nHost: localhost:9200\r\n\r\n";
214 assert_eq!(parse_http_request(req), Some("GET /users/_doc/1".into()));
215 }
216
217 #[test]
218 fn test_parse_http_request_post_with_body() {
219 let req = b"POST /users/_search HTTP/1.1\r\nContent-Type: application/json\r\nContent-Length: 11\r\n\r\n{\"size\": 5}";
220 assert_eq!(parse_http_request(req), Some("POST /users/_search".into()));
221 }
222
223 #[test]
224 fn test_extract_full_command_filters_noise_headers() {
225 let req = b"GET /index HTTP/1.1\r\nHost: localhost\r\nUser-Agent: curl/8.0\r\nAccept: */*\r\nAuthorization: Bearer token\r\n\r\n";
226 let full = extract_http_full_command(req).unwrap();
227 assert!(full.contains("Authorization: Bearer token"));
228 assert!(!full.contains("Host:"));
229 assert!(!full.contains("User-Agent:"));
230 }
231
232 #[test]
233 fn test_extract_full_command_with_body() {
234 let req = b"POST /test HTTP/1.1\r\nContent-Type: application/json\r\nContent-Length: 13\r\n\r\n{\"key\":\"val\"}";
235 let full = extract_http_full_command(req).unwrap();
236 assert!(full.contains("POST /test"));
237 assert!(full.contains("[Request Body]"));
238 assert!(full.contains("{\"key\":\"val\"}"));
239 }
240
241 #[test]
242 fn test_parse_http_response() {
243 let resp = b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\n{}";
244 assert_eq!(parse_http_response(resp), Some("200 OK".into()));
245 }
246
247 #[test]
248 fn test_parse_http_response_404() {
249 let resp = b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n";
250 assert_eq!(parse_http_response(resp), Some("404 Not Found".into()));
251 }
252
253 #[test]
254 fn test_http_request_complete_no_body() {
255 let req = b"GET / HTTP/1.1\r\nHost: x\r\n\r\n";
256 assert!(http_request_complete(req));
257 }
258
259 #[test]
260 fn test_http_request_incomplete_body() {
261 let req = b"POST / HTTP/1.1\r\nContent-Length: 10\r\n\r\n12345";
262 assert!(!http_request_complete(req));
263 }
264
265 #[test]
266 fn test_http_request_complete_body() {
267 let req = b"POST / HTTP/1.1\r\nContent-Length: 5\r\n\r\n12345";
268 assert!(http_request_complete(req));
269 }
270
271 #[test]
272 fn test_http_response_complete_chunked() {
273 let resp = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n";
274 assert!(http_response_complete(resp));
275 }
276
277 #[test]
278 fn test_http_response_incomplete_chunked() {
279 let resp = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n";
280 assert!(!http_response_complete(resp));
281 }
282
283 #[test]
284 fn test_decode_chunked_body() {
285 let resp = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
286 let detail = format_http_response_detail(resp).unwrap();
287 assert!(detail.contains("hello world"));
288 }
289}