1use alloc::string::String;
2use alloc::vec::Vec;
3use super::addr_util::parse_host_port;
4
5fn http_raw_request(
6 host: &str,
7 port: u16,
8 raw_request: &[u8],
9) -> Result<alloc::vec::Vec<u8>, &'static str> {
10 let remote_ip = super::dns::dns_query_a_via_udp(host)?;
11 let local_port = super::tcp::tcp_connect_real(remote_ip, port)?;
12 super::tcp::tcp_send_real(local_port, raw_request)?;
13 crate::kernel::net::tcp::wait_for_first_byte(local_port, 50);
16
17 let mut body_bytes = alloc::vec::Vec::new();
21 let mut idle_rounds = 0u32;
22 loop {
23 let chunk = super::tcp::tcp_recv_real(local_port, 20000)?;
24 if chunk.is_empty() {
25 idle_rounds += 1;
26 } else {
27 idle_rounds = 0;
28 body_bytes.extend(chunk);
29 }
30 let fin_recvd = unsafe {
31 match super::tcp::tcp_find_socket_by_local(local_port) {
32 Some(idx) => super::TCP_SOCKETS[idx].fin_received,
33 None => true,
34 }
35 };
36 if fin_recvd || idle_rounds >= 3 || crate::kernel::http_body::is_complete_response(&body_bytes) {
38 break;
39 }
40 if body_bytes.len() > 256 * 1024 * 1024 {
41 break;
42 }
43 }
44 super::tcp::tcp_close_real(local_port)?;
45 Ok(body_bytes)
46}
47
48pub fn http_get_real(host: &str, path: &str) -> Result<super::HttpGetResult, &'static str> {
49 http_get_real_with_referer(host, path, None)
50}
51
52pub fn http_get_real_with_referer(
54 host: &str,
55 path: &str,
56 referer: Option<&str>,
57) -> Result<super::HttpGetResult, &'static str> {
58 if host.is_empty() {
59 return Err("host is empty");
60 }
61 if !path.starts_with('/') {
62 return Err("path must start with '/'");
63 }
64
65 let mut current_host = alloc::string::String::from(host);
66 let mut current_path = alloc::string::String::from(path);
67 let mut redirects = 0u8;
68
69 loop {
70 let referer_hdr = referer.map(|r| alloc::format!("Referer: {}\r\n", r)).unwrap_or_default();
71 let request = alloc::format!(
72 "GET {} HTTP/1.1\r\nHost: {}\r\nUser-Agent: Mozilla/5.0 (AtmOS Browser)\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Encoding: identity\r\n{}Connection: close\r\n\r\n",
73 current_path, current_host, referer_hdr
74 );
75 let (host_only, port) = parse_host_port(¤t_host);
76 let remote_ip = super::dns::dns_query_a_via_udp(host_only)?;
77 let body_bytes = http_raw_request(host_only, port, request.as_bytes())?;
78
79 let resp_str = alloc::string::String::from_utf8_lossy(&body_bytes).into_owned();
80 let (status_code, reason, headers, body) = parse_http_response_text(&body_bytes).unwrap_or((
82 200,
83 alloc::string::String::from("OK"),
84 alloc::collections::BTreeMap::new(),
85 resp_str,
86 ));
87
88 if (status_code == 301
89 || status_code == 302
90 || status_code == 303
91 || status_code == 307
92 || status_code == 308)
93 && redirects < 5
94 {
95 if let Some(location) = headers.get("location") {
96 let loc = location.trim();
97 if let Some(rest) = loc.strip_prefix("http://") {
98 if let Some(slash) = rest.find('/') {
99 let (h, p) = rest.split_at(slash);
100 current_host = alloc::string::String::from(h);
101 current_path = alloc::string::String::from(p);
102 } else {
103 current_host = alloc::string::String::from(rest);
104 current_path = alloc::string::String::from("/");
105 }
106 redirects += 1;
107 continue;
108 } else if loc.starts_with('/') {
109 current_path = alloc::string::String::from(loc);
110 redirects += 1;
111 continue;
112 }
113 }
114 }
115
116 let raw_body = parse_http_response_bytes(&body_bytes).map(|r| r.3).unwrap_or_default();
117 return Ok(super::HttpGetResult {
118 remote_ip,
119 local_port: 80,
120 status_code,
121 reason,
122 headers,
123 body,
124 body_bytes: raw_body,
125 });
126 }
127}
128
129pub fn http_post_real(
130 host: &str,
131 path: &str,
132 content_type: &str,
133 body: &[u8],
134) -> Result<super::HttpGetResult, &'static str> {
135 http_request_real("POST", host, path, content_type, body)
136}
137
138pub fn http_request_real(
143 method: &str,
144 host: &str,
145 path: &str,
146 content_type: &str,
147 body: &[u8],
148) -> Result<super::HttpGetResult, &'static str> {
149 if host.is_empty() {
150 return Err("host is empty");
151 }
152 if !path.starts_with('/') {
153 return Err("path must start with '/'");
154 }
155
156 const CRLF: [u8; 2] = [13, 10];
157 let mut request: Vec<u8> = Vec::new();
158 let push_line = |req: &mut Vec<u8>, s: &str| {
159 req.extend_from_slice(s.as_bytes());
160 req.extend_from_slice(&CRLF);
161 };
162 push_line(
163 &mut request,
164 &alloc::format!("{} {} HTTP/1.1", method, path),
165 );
166 push_line(&mut request, &alloc::format!("Host: {}", host));
167 push_line(&mut request, "User-Agent: Mozilla/5.0 (AtmOS Browser)");
168 push_line(&mut request, "Accept-Encoding: identity");
169 let send_body = !body.is_empty() || method == "POST" || method == "PUT" || method == "PATCH";
171 if send_body {
172 push_line(
173 &mut request,
174 &alloc::format!("Content-Type: {}", content_type),
175 );
176 push_line(
177 &mut request,
178 &alloc::format!("Content-Length: {}", body.len()),
179 );
180 }
181 push_line(&mut request, "Connection: close");
182 request.extend_from_slice(&CRLF);
183 request.extend_from_slice(body);
184
185 let (host_only, port) = parse_host_port(host);
186 let remote_ip = super::dns::dns_query_a_via_udp(host_only)?;
187 let raw = http_raw_request(host_only, port, &request)?;
188 let resp_str = alloc::string::String::from_utf8_lossy(&raw).into_owned();
189 let (status_code, reason, headers, resp_body) = parse_http_response_text(&raw).unwrap_or((
190 200,
191 alloc::string::String::from("OK"),
192 alloc::collections::BTreeMap::new(),
193 resp_str,
194 ));
195
196 let body_bytes = parse_http_response_bytes(&raw).map(|r| r.3).unwrap_or_default();
197 Ok(super::HttpGetResult {
198 remote_ip,
199 local_port: port,
200 status_code,
201 reason,
202 headers,
203 body: resp_body,
204 body_bytes,
205 })
206}
207
208pub fn extract_http_body_binary(raw: &[u8]) -> alloc::vec::Vec<u8> {
209 let mut header_end = 0usize;
210 if raw.len() >= 4 {
211 for i in 0..(raw.len() - 3) {
212 if raw[i] == b'\r' && raw[i + 1] == b'\n' && raw[i + 2] == b'\r' && raw[i + 3] == b'\n'
213 {
214 header_end = i + 4;
215 break;
216 }
217 }
218 }
219 if header_end == 0 {
220 return raw.to_vec();
221 }
222 let header = &raw[..header_end];
223 let body = &raw[header_end..];
224
225 let is_chunked = {
226 let mut found = false;
227 if let Ok(h) = core::str::from_utf8(header) {
228 let hl = h.to_ascii_lowercase();
229 if let Some(p) = hl.find("transfer-encoding:") {
230 let rest = hl.get(p..).unwrap_or("");
231 let line_end = rest.find("\r\n").unwrap_or(rest.len());
232 if rest.get(..line_end).unwrap_or("").contains("chunked") {
233 found = true;
234 }
235 }
236 }
237 found
238 };
239
240 if !is_chunked {
241 return body.to_vec();
242 }
243
244 let mut out = alloc::vec::Vec::new();
245 let mut i = 0usize;
246 while i < body.len() {
247 let mut j = i;
248 while j + 1 < body.len() && !(body[j] == b'\r' && body[j + 1] == b'\n') {
249 j += 1;
250 }
251 if j + 1 >= body.len() {
252 break;
253 }
254 let size_str = core::str::from_utf8(&body[i..j]).unwrap_or("");
255 let size_hex = size_str.split(';').next().unwrap_or("").trim();
256 let size = usize::from_str_radix(size_hex, 16).unwrap_or(0);
257 let data_start = j + 2;
258 if size == 0 {
259 break;
260 }
261 let data_end = match data_start.checked_add(size) {
262 Some(end) => end,
263 None => {
264 out.extend_from_slice(&body[data_start..body.len()]);
265 break;
266 }
267 };
268 if data_end > body.len() {
269 out.extend_from_slice(&body[data_start..body.len()]);
270 break;
271 }
272 out.extend_from_slice(&body[data_start..data_end]);
273 match data_end.checked_add(2) {
274 Some(next_i) => i = next_i,
275 None => break,
276 }
277 }
278 out
279}
280
281pub fn http_get_binary(host: &str, path: &str) -> Result<alloc::vec::Vec<u8>, &'static str> {
282 if host.is_empty() {
283 return Err("host is empty");
284 }
285 if !path.starts_with('/') {
286 return Err("path must start with '/'");
287 }
288
289 let (host_only, port) = parse_host_port(host);
290 let remote_ip = super::dns::dns_query_a_via_udp(host_only)?;
291 let local_port = super::tcp::tcp_connect_real(remote_ip, port)?;
292
293 let request = alloc::format!("GET {} HTTP/1.1\r\nHost: {}\r\nUser-Agent: Mozilla/5.0 (AtmOS Browser)\r\nAccept: */*\r\nAccept-Encoding: identity\r\nConnection: close\r\n\r\n", path, host);
294 super::tcp::tcp_send_real(local_port, request.as_bytes())?;
295
296 crate::kernel::net::tcp::wait_for_first_byte(local_port, 50);
299
300 let mut body_bytes = alloc::vec::Vec::new();
301
302 for _ in 0..100 {
303 let chunk = super::tcp::tcp_recv_real(local_port, 20000)?;
304 if !chunk.is_empty() {
305 body_bytes.extend(chunk);
306 }
307
308 let fin_recvd = unsafe {
309 match super::tcp::tcp_find_socket_by_local(local_port) {
310 Some(idx) => super::TCP_SOCKETS[idx].fin_received,
311 None => true,
312 }
313 };
314
315 if fin_recvd {
316 break;
317 }
318 }
319
320 super::tcp::tcp_close_real(local_port)?;
321
322 Ok(extract_http_body_binary(&body_bytes))
323}
324
325pub fn https_get_real(host: &str, path: &str) -> Result<super::HttpsGetResult, &'static str> {
326 crate::kernel::tls::https_get(host, path)
327}
328
329pub fn parse_http_response(
330 response: &str,
331) -> Result<
332 (
333 u16,
334 alloc::string::String,
335 alloc::collections::BTreeMap<alloc::string::String, alloc::string::String>,
336 alloc::string::String,
337 ),
338 &'static str,
339> {
340 let (head, body) = match response.split_once("\r\n\r\n") {
341 Some(parts) => parts,
342 None => return Err("http response malformed"),
343 };
344
345 let mut lines = head.split("\r\n");
346 let status_line = lines.next().ok_or("http status line missing")?;
347
348 let mut parts = status_line.splitn(3, ' ');
349 let version = parts.next().ok_or("http status line malformed")?;
350 if !version.starts_with("HTTP/") {
351 return Err("http version invalid");
352 }
353
354 let code_s = parts.next().ok_or("http status code missing")?;
355 let status_code = parse_decimal_u16(code_s).ok_or("http status code invalid")?;
356 let reason = alloc::string::String::from(parts.next().unwrap_or(""));
357
358 let mut headers = alloc::collections::BTreeMap::new();
359 for line in lines {
360 if let Some((k, v)) = line.split_once(':') {
361 headers.insert(
362 k.trim().to_lowercase(),
363 alloc::string::String::from(v.trim()),
364 );
365 }
366 }
367
368 if let Some(enc) = headers.get("transfer-encoding") {
369 if enc.to_lowercase().contains("chunked") {
370 let bytes = body.as_bytes();
371 let mut decoded: alloc::vec::Vec<u8> = alloc::vec::Vec::new();
372 let mut i = 0usize;
373 while i < bytes.len() {
374 let mut j = i;
375 while j + 1 < bytes.len() && !(bytes[j] == b'\r' && bytes[j + 1] == b'\n') {
376 j += 1;
377 }
378 if j + 1 >= bytes.len() {
379 break;
380 }
381 let size_str = core::str::from_utf8(&bytes[i..j]).unwrap_or("");
382 let size_hex = size_str.split(';').next().unwrap_or("").trim();
383 let size = usize::from_str_radix(size_hex, 16).unwrap_or(0);
384 if size == 0 {
385 break;
386 }
387 let data_start = j + 2;
388 let data_end = data_start + size;
389 if data_end > bytes.len() {
390 decoded.extend_from_slice(&bytes[data_start..]);
391 break;
392 }
393 decoded.extend_from_slice(&bytes[data_start..data_end]);
394 i = data_end + 2;
395 }
396 let decoded_str = alloc::string::String::from_utf8_lossy(&decoded).into_owned();
397 return Ok((status_code, reason, headers, decoded_str));
398 }
399 }
400
401 Ok((
402 status_code,
403 reason,
404 headers,
405 alloc::string::String::from(body),
406 ))
407}
408
409fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
410 if needle.is_empty() || haystack.len() < needle.len() {
411 return None;
412 }
413 haystack.windows(needle.len()).position(|w| w == needle)
414}
415
416pub fn parse_http_response_text(
425 response: &[u8],
426) -> Result<
427 (
428 u16,
429 alloc::string::String,
430 alloc::collections::BTreeMap<alloc::string::String, alloc::string::String>,
431 alloc::string::String,
432 ),
433 &'static str,
434> {
435 let (status, reason, headers, body) = parse_http_response_bytes(response)?;
436 Ok((status, reason, headers, alloc::string::String::from_utf8_lossy(&body).into_owned()))
437}
438
439pub fn parse_http_response_bytes(
444 response: &[u8],
445) -> Result<
446 (
447 u16,
448 alloc::string::String,
449 alloc::collections::BTreeMap<alloc::string::String, alloc::string::String>,
450 alloc::vec::Vec<u8>,
451 ),
452 &'static str,
453> {
454 let split_at = find_subslice(response, b"\r\n\r\n").ok_or("http response malformed")?;
455 let head = &response[..split_at];
456
457 let head_str = core::str::from_utf8(head).map_err(|_| "http header not utf8")?;
458 let mut lines = head_str.split("\r\n");
459 let status_line = lines.next().ok_or("http status line missing")?;
460
461 let mut parts = status_line.splitn(3, ' ');
462 let version = parts.next().ok_or("http status line malformed")?;
463 if !version.starts_with("HTTP/") {
464 return Err("http version invalid");
465 }
466
467 let code_s = parts.next().ok_or("http status code missing")?;
468 let status_code = parse_decimal_u16(code_s).ok_or("http status code invalid")?;
469 let reason = alloc::string::String::from(parts.next().unwrap_or(""));
470
471 let mut headers = alloc::collections::BTreeMap::new();
472 for line in lines {
473 if let Some((k, v)) = line.split_once(':') {
474 headers.insert(
475 k.trim().to_lowercase(),
476 alloc::string::String::from(v.trim()),
477 );
478 }
479 }
480
481 let body = extract_http_body_binary(response);
483
484 Ok((status_code, reason, headers, body))
485}
486
487
488
489fn build_http_get_request(host: &str, path: &str) -> Result<String, &'static str> {
490 if host.is_empty() {
491 return Err("host is empty");
492 }
493 if !path.starts_with('/') {
494 return Err("path must start with '/'");
495 }
496
497 Ok(alloc::format!(
498 "GET {} HTTP/1.1\r\nHost: {}\r\nUser-Agent: AtmOS/0.1\r\nAccept-Encoding: identity\r\nConnection: close\r\n\r\n",
499 path,
500 host
501 ))
502}
503
504
505
506fn parse_decimal_u16(s: &str) -> Option<u16> {
507 let mut val: u16 = 0;
508 if s.is_empty() {
509 return None;
510 }
511
512 for ch in s.chars() {
513 if !ch.is_ascii_digit() {
514 return None;
515 }
516 val = val
517 .checked_mul(10)?
518 .checked_add((ch as u16) - ('0' as u16))?;
519 }
520
521 Some(val)
522}
523
524
525pub(crate) fn build_http_get_request_pub(host: &str, path: &str) -> Result<String, &'static str> {
527 build_http_get_request(host, path)
528}
529
530