Skip to main content

atmos/kernel/net/
http.rs

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    // 【2026-08-05】無条件 500ms から「届いたら即抜ける」へ(上限は同じ)。
13    // 詳細は `net::tcp::wait_for_first_byte`。
14    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(&current_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
113/// 任意メソッド(GET/POST/PUT/DELETE/PATCH 等)の平文 HTTP リクエスト。
114/// body が空かつ GET/HEAD/DELETE 等の場合は Content-Type / Content-Length を付けない。
115/// メソッドは呼び出し側で検証済み(英大文字トークン)である前提。
116/// CRLF はエスケープ事故を避けるためバイト値で組み立てる。
117pub 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    // ボディがある、またはボディを取り得るメソッドでは Content-* を付与する。
145    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    // 【2026-08-05】無条件 500ms から「届いたら即抜ける」へ(上限は同じ)。
270    // 詳細は `net::tcp::wait_for_first_byte`。
271    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
389/// `parse_http_response` のバイト保存版。ヘッダ部分は ASCII 前提で文字列化するが、
390/// ボディは `extract_http_body_binary` の chunked 復号をそのまま使い、ロスの無い
391/// 生バイト列で返す(画像/zip 等のバイナリダウンロード向け)。
392/// 戻り値: (status_code, reason, headers, body_bytes)
393pub 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    // ボディ(chunked なら復号済み)の抽出は既存のバイナリ安全ヘルパーに委譲。
432    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
483// pub(super) wrappers for self_test() in mod.rs
484pub(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