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