Skip to main content

atmos/kernel/net/
http.rs

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    // 【2026-08-05】無条件 500ms から「届いたら即抜ける」へ(上限は同じ)。
14    // 詳細は `net::tcp::wait_for_first_byte`。
15    crate::kernel::net::tcp::wait_for_first_byte(local_port, 50);
16
17    // FIN を受けるか応答が完結する(Content-Length / chunked 終端)まで読む。
18    // 【2026-10-17】従来は受信を最大 100 回で打ち切っており(1 回あたり数 KB)、約 877KB を
19    // 超える応答(動画セグメント等)が黙って途中で切れていた。進捗が止まった場合だけ抜ける。
20    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        // 1 回の受信待ちは最大 5 秒なので、3 回続けて何も来なければ打ち切る。
37        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
52/// `Referer` ヘッダ付きの GET(`None` なら付けない。リダイレクト先にも同じ値を送る)。
53pub 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(&current_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        // バイト列のまま chunked を解いてから文字列にする(`parse_http_response_text` の doc)。
81        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
138/// 任意メソッド(GET/POST/PUT/DELETE/PATCH 等)の平文 HTTP リクエスト。
139/// body が空かつ GET/HEAD/DELETE 等の場合は Content-Type / Content-Length を付けない。
140/// メソッドは呼び出し側で検証済み(英大文字トークン)である前提。
141/// CRLF はエスケープ事故を避けるためバイト値で組み立てる。
142pub 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    // ボディがある、またはボディを取り得るメソッドでは Content-* を付与する。
170    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    // 【2026-08-05】無条件 500ms から「届いたら即抜ける」へ(上限は同じ)。
297    // 詳細は `net::tcp::wait_for_first_byte`。
298    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
416/// 応答の生バイト列を解析し、本文を**バイト列のまま** chunked を解いてから文字列にする。
417///
418/// 【2026-10-16】従来の経路は応答全体を先に `from_utf8_lossy` で文字列にしてから
419/// `parse_http_response` で chunked を解いていた。chunked の区切りは UTF-8 の多バイト文字の
420/// 途中にも入るため、その不正バイトが置換文字(3 バイト)に化けてバイト位置がずれ、本文が
421/// 途中で切れた(sugi-lab.net の papers.json 150,481 B → 129,615 B。区切り位置は通信ごとに
422/// 変わるので約 3 回に 1 回、jQuery の JSON 解析が黙って失敗して論文リストが出なかった)。
423/// 解析できなければ応答全体を文字列にして 200 として返す(従来の `unwrap_or` と同じ)。
424pub 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
439/// `parse_http_response` のバイト保存版。ヘッダ部分は ASCII 前提で文字列化するが、
440/// ボディは `extract_http_body_binary` の chunked 復号をそのまま使い、ロスの無い
441/// 生バイト列で返す(画像/zip 等のバイナリダウンロード向け)。
442/// 戻り値: (status_code, reason, headers, body_bytes)
443pub 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    // ボディ(chunked なら復号済み)の抽出は既存のバイナリ安全ヘルパーに委譲。
482    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
525// pub(super) wrappers for self_test() in mod.rs
526pub(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