Skip to main content

atmos/kernel/
tls.rs

1#![allow(dead_code)]
2
3extern crate alloc;
4use alloc::string::String;
5use alloc::vec::Vec;
6
7use embedded_tls::blocking::{TlsConnection, TlsContext};
8use embedded_tls::TlsConfig;
9use rand_core::{CryptoRng, RngCore};
10
11// --- OsRng Implementation ---
12
13pub struct OsRng {
14    state: u32,
15}
16
17impl Default for OsRng {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23/// 【2026-07-24診断用】`get_ticks()`だけをシードにすると、短時間に連続して
24/// TLS接続を開く場合(例: 外部CSS/フォントの立て続けの取得)に同じ`tick`値を
25/// 読んでしまい、複数の接続で全く同一の「乱数」シーケンス(ClientHello random、
26/// 鍵交換の一時鍵等)が生成されうる。1回目のHTTPS接続は成功するが2回目以降が
27/// 毎回TLSハンドシェイクに失敗する未解明の現象の調査の一環として、呼び出し毎に
28/// 必ず異なる値になるよう単調増加カウンタを合成し、この可能性を切り分ける。
29static OS_RNG_CALL_COUNTER: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
30
31impl OsRng {
32    pub fn new() -> Self {
33        let t = crate::kernel::timer::get_ticks() as u32;
34        let call_no = OS_RNG_CALL_COUNTER.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
35        let seed = t ^ call_no.wrapping_mul(0x9E37_79B9);
36        Self {
37            state: if seed == 0 { 0xA11C0B0B } else { seed },
38        }
39    }
40}
41
42impl RngCore for OsRng {
43    fn next_u32(&mut self) -> u32 {
44        let mut x = self.state;
45        x ^= x << 13;
46        x ^= x >> 17;
47        x ^= x << 5;
48        self.state = x;
49        x
50    }
51    fn next_u64(&mut self) -> u64 {
52        ((self.next_u32() as u64) << 32) | (self.next_u32() as u64)
53    }
54    fn fill_bytes(&mut self, dest: &mut [u8]) {
55        for chunk in dest.chunks_mut(4) {
56            let val = self.next_u32().to_le_bytes();
57            let len = chunk.len();
58            chunk.copy_from_slice(&val[..len]);
59        }
60    }
61    fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> {
62        self.fill_bytes(dest);
63        Ok(())
64    }
65}
66
67impl CryptoRng for OsRng {}
68
69/// 【2026-08-05 計測】`retry_write` 経路で送信後に待った累計 tick と回数。
70///
71/// 変更の効果を「ページ読み込み全体」から推し量ると、
72/// ホスト負荷のばらつきに埋もれて A/B のための往復が何度も要る。
73/// **変えた当人を直接測れば 1 回の実行で判定できる。**
74pub static WRITE_WAIT_TICKS: core::sync::atomic::AtomicU32 =
75    core::sync::atomic::AtomicU32::new(0);
76pub static WRITE_WAIT_CALLS: core::sync::atomic::AtomicU32 =
77    core::sync::atomic::AtomicU32::new(0);
78/// そのうち再送に至った回数(=本当に応答が来なかった回)。
79pub static WRITE_RETX_COUNT: core::sync::atomic::AtomicU32 =
80    core::sync::atomic::AtomicU32::new(0);
81
82// --- TcpIoWrapper Implementation ---
83
84pub struct TcpIoWrapper {
85    pub local_port: u16,
86    buffer: Vec<u8>,
87    /// `buffer` の読み出し済み位置。
88    ///
89    /// 【2026-08-05】以前は読むたびに `buffer.drain(0..len)` していた。
90    /// TLS はレコードヘッダ 5 バイトのように**小さく読む**ので、
91    /// そのたびに残り全体を前へ詰め直すことになり O(n²) だった。
92    ///
93    /// 一斉計測で受信ループの内訳を割ったところ、
94    ///
95    /// ```text
96    /// 受信待ち=40968/16661
97    ///   受信sleep=8102/5191   recv呼出=233/6684
98    /// ```
99    ///
100    /// sleep でも recv でもない**残り 32633 tick** がここだった。
101    /// 位置を持てば詰め直しが要らない。
102    pos: usize,
103    // 【2026-07-24】書き込み後、応答が全く届かない場合に同一データを一度だけ
104    // 再送するかどうか。この自作TCPスタックには再送機構が無く、ネットワーク上
105    // でのパケットロス(特に外部CSS/フォント取得等、メインページ取得の直後に
106    // 連続して発生する2回目以降のHTTPS接続で顕著)でハンドシェイクが失敗する
107    // 問題への対策。既存の安定した経路(メインページ取得の`https_get`)へ
108    // 影響を与えないよう、`https_get_binary`(外部リソース取得側)でのみ
109    // `true`にする。
110    retry_write: bool,
111}
112
113impl TcpIoWrapper {
114    pub fn new(local_port: u16) -> Self {
115        Self {
116            local_port,
117            buffer: Vec::new(),
118            pos: 0,
119            retry_write: false,
120        }
121    }
122
123    pub fn new_with_retry_write(local_port: u16) -> Self {
124        Self {
125            local_port,
126            buffer: Vec::new(),
127            pos: 0,
128            retry_write: true,
129        }
130    }
131}
132
133impl embedded_io::ErrorType for TcpIoWrapper {
134    type Error = embedded_io::ErrorKind;
135}
136
137impl embedded_io::Read for TcpIoWrapper {
138    fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
139        if buf.is_empty() {
140            return Ok(0);
141        }
142        let _pr = crate::os_lib::web_engine::perf::start();
143        let start_tick = crate::kernel::timer::get_ticks();
144        // 15秒タイムアウト。大きな応答 (検索結果 ~30KB 等) はサーバ処理 + 小受信ウィンドウ
145        // による多数の往復で到着が遅延バーストになりがちで、5秒では先頭バイト到着前に
146        // 取りこぼすことがあった。
147        let timeout_ticks = 1500usize; // 15秒 (1500 * 10ms)
148
149        // 【2026-08-05】待ちの粒度は 10ms(`sleep(1)`)だった。
150        //
151        // 内訳を測ると受信待ち 12496 tick のうち 12231 tick が
152        // この `sleep` そのもの(6234 回 × 約 2 tick)。
153        // データは 10ms より短い間隔で届きうるので、
154        // 眠るとそのぶん取りこぼす。
155        //
156        // 【2026-08-05 検証済み・入れていない】
157        // 「まず 32 回は `yield_now()` で粘る」を試し、`受信待ち` を直接測った。
158        //
159        // ```text
160        // 修正前: 受信待ち=12496  受信sleep=12231/6234
161        // 修正後: 受信待ち=11535  受信sleep=10741/30291
162        // ```
163        //
164        // **8% しか縮まらず、譲る回数は 5 倍**(6234 → 30291)。
165        // データは実際に 10ms より早くは届いていない。
166        // 縮まらないのに文脈切り替えを 3 万回増やすのは割に合わない。
167        //
168        // ここの待ちは**正当なネットワーク遅延**であり、無駄ではない。
169        // 削るなら待ち方ではなく、往復回数そのものを減らす方向
170        // (受信ウィンドウは検証済みで効かず。`spec/l1_l2_usb.md` 参照)。
171        loop {
172            if self.pos < self.buffer.len() {
173                let avail = self.buffer.len() - self.pos;
174                let len = avail.min(buf.len());
175                buf[..len].copy_from_slice(&self.buffer[self.pos..self.pos + len]);
176                self.pos += len;
177                // 読み切ったら領域を返す(容量は保つので再確保は起きない)。
178                if self.pos == self.buffer.len() {
179                    self.buffer.clear();
180                    self.pos = 0;
181                }
182                crate::os_lib::web_engine::perf::add(
183                    crate::os_lib::web_engine::perf::Slot::TlsReadWait,
184                    _pr,
185                );
186                return Ok(len);
187            }
188
189            // tcp_recv_real(.., 1) は即時タイムアウトで Err を返すが、これは
190            // 「まだデータが来ていない」だけであり EOF ではない。
191            // EOF 判定は下の tcp_is_socket_closed で行う(旧実装は Err を EOF と
192            // 誤解釈し、応答到着前の読みで body が空になるバグがあった)。
193            if let Ok(chunk) = crate::kernel::net::tcp_recv_real(self.local_port, 1) {
194                if !chunk.is_empty() {
195                    // 追記する前に、読み終わった前半を捨てて詰める。
196                    // 毎回の読み出しでは詰めない(そこが O(n²) の原因だった)。
197                    if self.pos > 0 {
198                        self.buffer.drain(0..self.pos);
199                        self.pos = 0;
200                    }
201                    self.buffer.extend_from_slice(&chunk);
202                    continue; // Loop back to read from buffer
203                }
204            }
205
206            if crate::kernel::net::tcp_is_socket_closed(self.local_port)
207                && self.pos >= self.buffer.len()
208            {
209                // 【2026-07-29診断】応答が Content-Length に届かず途中で切れる件。
210                // 受信ループがどの条件で抜けたかを一意に記録する。
211                // 【2026-09-26】理由(状態・FIN・保持中の順序外セグメント数)を実際に出す。
212                crate::warn!(
213                    "[TLS][EOF] local_port={} {}",
214                    self.local_port,
215                    crate::kernel::net::tcp::tcp_close_diag(self.local_port)
216                );
217                return Ok(0); // EOF
218            }
219
220            // タイムアウト判定
221            let elapsed = crate::kernel::timer::get_ticks().wrapping_sub(start_tick);
222            if elapsed >= timeout_ticks {
223                // 【2026-08-02】**失敗は必ずログに残す**。
224                // 冗長な情報ログの整理をしたとき、このタイムアウトログまで
225                // 一緒に消してしまい「失敗しているのに何も出ない」状態を作った。
226                // 規約は `spec/logging_policy.md`。
227                crate::warn!(
228                    "[TLS][TIMEOUT] 受信タイムアウト local_port={} elapsed_ticks={} timeout_ticks={} seg_accepted={} seg_dropped={}",
229                    self.local_port,
230                    elapsed,
231                    timeout_ticks,
232                    crate::kernel::net::tcp::SEG_ACCEPTED.load(core::sync::atomic::Ordering::Relaxed),
233                    crate::kernel::net::tcp::SEG_DROPPED.load(core::sync::atomic::Ordering::Relaxed)
234                );
235                return Err(embedded_io::ErrorKind::TimedOut);
236            }
237
238            // 【2026-08-05 検証済み・変更せず】ここを「まず数十回は
239            // `yield_now()` で粘り、それでも来なければ眠る」に変えて測ったが、
240            // 3.7MB の取得は 3746 → 3704 tick で**変わらなかった**。
241            // 待ち方の粒度は律速ではない(律速は USB 転送の内側)。
242            //
243            crate::kernel::scheduler::sleep(1);
244        }
245    }
246}
247
248impl embedded_io::Write for TcpIoWrapper {
249    fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
250        // 【2026-08-31 バグ修正】MSS 相当のセグメント分割が未実装だった。
251        //
252        // `build_tcp_ipv4` が組む IP+TCP ヘッダは 40 バイト(オプション無し)、
253        // `transmit_nic_ipv4_packet` はヘッダ込みで `NIC_TX_MAX_PAYLOAD`
254        // (1536 バイト)を超えると `Err("packet too large")` を返す。
255        // つまりこの `write()` に渡せる 1 回分の実ペイロードは最大 1496 バイト。
256        // TLS レコードは最大 16KB あり、`embedded_tls` がそれを分割せず
257        // まるごと渡してくると、そのまま `tcp_send_real` へ流れて失敗していた
258        // (`spec/TODO.md` の「TCP セグメント分割(MSS)が未実装」参照)。
259        //
260        // `embedded_io::Write::write()` の契約は「渡された分すべてを
261        // 送る」ではなく「送れた分だけ返す」(`Ok(n)`, `n <= buf.len()`)。
262        // 呼び出し側は残りを次の `write()` 呼び出しで送るのが仕様どおりの
263        // 振る舞いであり、この処理系専用の再送ロジック(下記)とは無関係の
264        // 別の仕組み。ここで安全な上限へ切り詰めるだけで済み、
265        // 以降の再送・シーケンス番号管理は一切変更しない。
266        const MAX_TCP_PAYLOAD: usize = 1400;
267        let buf = if buf.len() > MAX_TCP_PAYLOAD {
268            &buf[..MAX_TCP_PAYLOAD]
269        } else {
270            buf
271        };
272        // 【2026-07-24重要修正】以前は再送のたびに`tcp_send_real`を再度呼んで
273        // いたが、`tcp_send_real`は呼ぶ度に無条件で`snd_nxt`を進めてしまう
274        // ため、"再送"のつもりが実際には誤った(進みすぎた)シーケンス番号
275        // で送信してしまい、TCP/TLSストリームを破壊する重大なバグだった
276        // (結果として`embedded_tls`側が復号時に`InternalError`を返す原因に
277        // なっていたと判明)。正しい再送のため、最初の送信時点の`seq`を
278        // 記録しておき、再送には`snd_nxt`を変更しない`tcp_retransmit_real`
279        // を使う。
280        let seq_at_send = crate::kernel::net::tcp_current_snd_seq(self.local_port);
281        if crate::kernel::net::tcp_send_real(self.local_port, buf).is_err() {
282            return Err(embedded_io::ErrorKind::BrokenPipe);
283        }
284        if self.retry_write {
285            if let Some(seq) = seq_at_send {
286                // 送信直後、短い間隔を空けて何らかの応答(1バイトでも)が来て
287                // いるか確認する。何も来ておらず、かつソケットもまだ閉じられて
288                // いなければ、送信パケット自体がロストした可能性が高いと判断し、
289                // 同一データ・同一シーケンス番号で最大4回まで再送する。
290                // 【2026-08-05】以前はここで**確認する前に 600ms 眠って**いた
291                // (`sleep(60)` を先に実行してから応答を見る)。
292                //
293                // 応答が 1ms で返っても 600ms 待つので、
294                // TLS ハンドシェイクのように**書き込みが複数回**ある経路では
295                // それだけで数秒を捨てていた。
296                // 実測: 49KB の CSS 取得に 21 秒(3.7MB の画像が 27 秒=
297                // 137KB/s なのに対し、バイトあたり 50 倍遅い)。
298                //
299                // 待ち総量(600ms で応答が無ければ再送)は変えず、
300                // 短い間隔で確かめて**来ていたら即座に抜ける**。
301                /// 1 回の確認間隔(tick, 10ms 単位)。
302                const POLL_STEP: usize = 3;
303                /// 再送を決めるまでに待つ総量(従来と同じ 600ms)。
304                const WAIT_PER_TRY: usize = 60;
305                let t_w0 = crate::kernel::timer::get_ticks();
306                let _pw = crate::os_lib::web_engine::perf::start();
307                WRITE_WAIT_CALLS.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
308                'retry: for _ in 0..4 {
309                    // 【2026-08-05】待ち時間を**実経過**で数える。
310                    //
311                    // 以前は `waited += POLL_STEP`(指定値の足し算)だった。
312                    // だが一斉計測で
313                    //
314                    // ```text
315                    // 送信待ち=12191/39
316                    // sleep実測=12163/1105   ← ほぼ全部がこれ
317                    // 応答確認=9/1105  eth poll=16/1105
318                    // ```
319                    //
320                    // となり、**1105 回 × 3 tick = 3315 のはずが実測 12163**。
321                    // `sleep(3)` は実際には約 11 tick かかる(3.7 倍)。
322                    // スケジューラの `sleep` は「最低でもこれだけ」であって
323                    // 「ちょうどこれだけ」ではない。
324                    //
325                    // 指定値で数えていたので、600ms のつもりが実際は 2.2 秒。
326                    // 実経過で数えれば意図どおり 600ms で切り上がる。
327                    let t_wait0 = crate::kernel::timer::get_ticks();
328                    while crate::kernel::timer::get_ticks().wrapping_sub(t_wait0)
329                        < WAIT_PER_TRY
330                    {
331                        // 【2026-08-05】待ちの内訳を分解する。
332                        // 上限を 300ms にしても 1 回あたり 2.5 秒かかったので、
333                        // 時間は「待つと決めた量」ではなく別のところで消えている。
334                        let _ps = crate::os_lib::web_engine::perf::start();
335                        crate::kernel::scheduler::sleep(POLL_STEP);
336                        crate::os_lib::web_engine::perf::add(
337                            crate::os_lib::web_engine::perf::Slot::SleepActual,
338                            _ps,
339                        );
340                        let _pe = crate::os_lib::web_engine::perf::start();
341                        crate::kernel::usb::poll_ethernet_data_plane_only();
342                        crate::os_lib::web_engine::perf::add(
343                            crate::os_lib::web_engine::perf::Slot::EthPoll,
344                            _pe,
345                        );
346                        let _pk = crate::os_lib::web_engine::perf::start();
347                        // 【2026-10-05】送ったデータが ACK されたら待ちを終える。従来は
348                        // **応答データ**が届くまで待っており、TLS の Finished のように
349                        // サーバーが何も返さない書き込みのたびに 600ms 待って再送していた
350                        // (実測: 送信待ち 3278 tick / 39 回。1 ページの読込で約 33 秒)。
351                        let done = crate::kernel::net::tcp_acked_through(
352                            self.local_port,
353                            seq.wrapping_add(buf.len() as u32),
354                        ) || crate::kernel::net::tcp_recv_peek_nonempty(self.local_port)
355                            || crate::kernel::net::tcp_is_socket_closed(self.local_port);
356                        crate::os_lib::web_engine::perf::add(
357                            crate::os_lib::web_engine::perf::Slot::PeekCheck,
358                            _pk,
359                        );
360                        if done {
361                            break 'retry;
362                        }
363                    }
364                    // 総量を待っても何も来ない。パケットロストの疑いとして再送する。
365                    WRITE_RETX_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
366                    let _ = crate::kernel::net::tcp_retransmit_real(self.local_port, buf, seq);
367                }
368                WRITE_WAIT_TICKS.fetch_add(
369                    crate::kernel::timer::get_ticks().wrapping_sub(t_w0) as u32,
370                    core::sync::atomic::Ordering::Relaxed,
371                );
372                crate::os_lib::web_engine::perf::add(
373                    crate::os_lib::web_engine::perf::Slot::TlsWriteWait,
374                    _pw,
375                );
376            }
377        }
378        Ok(buf.len())
379    }
380
381    fn flush(&mut self) -> Result<(), Self::Error> {
382        Ok(())
383    }
384}
385
386
387/// 【2026-07-29】HTTPS 取得の結果を段階別に集計する。
388///
389/// 失敗段階が実行ごとにばらつくため、単発の観測では
390/// 「どの修正が効いたか」を評価できなかった(このセッションで
391/// 1 回の結果から誤った断定を何度も行った直接の原因)。
392/// 1 回のブート内で発生する全取得を分類して数え、
393/// 毎回サマリを出すことで成功率を数値で追えるようにする。
394/// 【2026-07-30】1 回の取得で許容する最大バイト数。0 なら無制限。
395/// 画像のような巨大リソースを `Content-Length` の時点で打ち切るために使う。
396/// 呼び出し側が設定し、取得後に必ず 0 へ戻すこと。
397pub static FETCH_BYTE_CAP: core::sync::atomic::AtomicUsize =
398    core::sync::atomic::AtomicUsize::new(0);
399
400pub mod stats {
401    use core::sync::atomic::{AtomicU32, Ordering::Relaxed};
402    pub static OK: AtomicU32 = AtomicU32::new(0);
403    pub static DNS_FAIL: AtomicU32 = AtomicU32::new(0);
404    pub static CONNECT_FAIL: AtomicU32 = AtomicU32::new(0);
405    pub static HANDSHAKE_FAIL: AtomicU32 = AtomicU32::new(0);
406    pub static TRUNCATED: AtomicU32 = AtomicU32::new(0);
407
408    pub fn bump(c: &AtomicU32) {
409        c.fetch_add(1, Relaxed);
410    }
411
412    /// 現在の集計を 1 行で出す。分母は全試行。
413    pub fn report(tag: &str) {
414        let ok = OK.load(Relaxed);
415        let dns = DNS_FAIL.load(Relaxed);
416        let conn = CONNECT_FAIL.load(Relaxed);
417        let hs = HANDSHAKE_FAIL.load(Relaxed);
418        let tr = TRUNCATED.load(Relaxed);
419        let total = ok + dns + conn + hs + tr;
420        crate::warn!(
421            "[NET][STATS] {} total={} ok={} dns_fail={} connect_fail={} handshake_fail={} truncated={}",
422            tag, total, ok, dns, conn, hs, tr
423        );
424    }
425}
426
427pub fn https_get(
428    host: &str,
429    path: &str,
430) -> Result<crate::kernel::net::HttpsGetResult, &'static str> {
431    https_get_with_referer(host, path, None)
432}
433
434/// `Referer` ヘッダ付きの GET(`None` なら付けない)。値は呼び出し側が参照元ポリシー
435/// (`strict-origin-when-cross-origin`)で決めたもの(`web_engine::referrer`)。
436pub fn https_get_with_referer(
437    host: &str,
438    path: &str,
439    referer: Option<&str>,
440) -> Result<crate::kernel::net::HttpsGetResult, &'static str> {
441    let ordinal = crate::kernel::net::next_https_attempt_ordinal();
442    crate::warn!("[NET][DIAG] https_get ordinal={} host={} path={}", ordinal, host, path);
443    let (host_only, port) = crate::kernel::net::parse_host_port_with_default(host, 443);
444    let remote_ip = crate::kernel::net::dns_query_a_via_udp(host_only).inspect_err(|e| {
445        crate::warn!("[NET][DIAG] https_get ordinal={} DNS FAILED err={}", ordinal, e);
446        stats::bump(&stats::DNS_FAIL);
447        stats::report("after-dns-fail");
448    })?;
449    crate::warn!("[NET][DIAG] https_get ordinal={} host={} resolved={}.{}.{}.{}", ordinal, host_only, remote_ip[0], remote_ip[1], remote_ip[2], remote_ip[3]);
450    let local_port = crate::kernel::net::tcp_connect_real(remote_ip, port).inspect_err(|e| {
451        crate::warn!("[NET][DIAG] https_get ordinal={} TCP CONNECT FAILED port={} err={}", ordinal, port, e);
452        stats::bump(&stats::CONNECT_FAIL);
453        stats::report("after-connect-fail");
454    })?;
455
456    let mut stream = TcpIoWrapper::new(local_port);
457    let mut rng = OsRng::new();
458
459    let mut read_buf = alloc::vec![0u8; 18432]; // TLS最大レコード(16640)を収容
460    let mut write_buf = alloc::vec![0u8; 16384];
461
462    // Disable cert validation for bare metal
463    let config = TlsConfig::new().with_server_name(host_only);
464
465    let mut connection: TlsConnection<'_, &mut TcpIoWrapper, embedded_tls::Aes128GcmSha256> =
466        TlsConnection::new(&mut stream, &mut read_buf[..], &mut write_buf[..]);
467
468    // Perform TLS handshake
469    let mut provider =
470        embedded_tls::UnsecureProvider::new::<embedded_tls::Aes128GcmSha256>(&mut rng);
471    let _ph = crate::os_lib::web_engine::perf::start();
472    let _open = connection.open(crate::kernel::tls::TlsContext::new(&config, &mut provider));
473    crate::os_lib::web_engine::perf::add(
474        crate::os_lib::web_engine::perf::Slot::TlsHandshake,
475        _ph,
476    );
477    if let Err(e) = _open {
478        crate::warn!("[NET][DIAG] https_get ordinal={} TLS HANDSHAKE FAILED err={:?}", ordinal, e);
479        stats::bump(&stats::HANDSHAKE_FAIL);
480        stats::report("after-handshake-fail");
481        crate::warn!("[NET][DIAG] https_get ordinal={} about to tcp_close_real", ordinal);
482        let _ = crate::kernel::net::tcp_close_real(local_port);
483        crate::warn!("[NET][DIAG] https_get ordinal={} tcp_close_real returned, returning Err", ordinal);
484        return Err("TLS handshake failed");
485    }
486
487    // Now we can use `connection` which implements embedded_io::Read and Write
488    let referer_hdr = referer.map(|r| alloc::format!("Referer: {}\r\n", r)).unwrap_or_default();
489    let request = alloc::format!("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", path, host, referer_hdr);
490    if embedded_io::Write::write_all(&mut connection, request.as_bytes()).is_err() {
491        crate::kernel::net::tcp_close_real(local_port)?;
492        return Err("HTTPS request failed");
493    }
494
495    // パケットをフラッシュして確実に送信
496    embedded_io::Write::flush(&mut connection).ok();
497
498    // パケット送出とサーバ側処理のためのウェイト
499    // 【2026-08-05】無条件 500ms から「届いたら即抜ける」へ(上限は同じ)。
500    // 詳細は `net::tcp::wait_for_first_byte`。
501    crate::kernel::net::tcp::wait_for_first_byte(local_port, 50);
502
503    let mut raw_bytes = Vec::new();
504    let mut chunk = [0u8; 1024];
505    loop {
506        match embedded_io::Read::read(&mut connection, &mut chunk) {
507            Ok(0) => break,
508            Ok(n) => raw_bytes.extend_from_slice(&chunk[..n]),
509            Err(e) => {
510                crate::info!("[NET] TLS read err: {:?}", e);
511                break;
512            }
513        }
514    }
515
516    // 【2026-07-26 バグ修正】応答を 1 バイトも読めなかった場合、これまでは
517    // そのまま `Ok` を返しており、呼び出し側のログにも
518    // `BROWSER: Load page SUCCESS. body_len=0` と「成功」として出ていた。
519    // 実際には何も取得できておらず、ブラウザは空ページを描画したうえで
520    // 外部 CSS の取得すら試みない(`<link>` が 1 つも無いため)。上位の
521    // リトライ機構も「成功」と受け取るので再試行されず、ページ読み込みの
522    // 機会が 1 回まるごと無駄になっていた。空応答は明確に失敗として返す。
523    if raw_bytes.is_empty() {
524        crate::warn!(
525            "[NET][DIAG] https_get ordinal={} EMPTY RESPONSE (0 bytes read) host={}",
526            ordinal,
527            host
528        );
529        let _ = crate::kernel::net::tcp_close_real(local_port);
530        return Err("empty https response");
531    }
532
533    let resp_str = match alloc::string::String::from_utf8(raw_bytes.clone()) {
534        Ok(s) => s,
535        Err(e) => {
536            let valid = e.into_bytes();
537            alloc::string::String::from_utf8_lossy(&valid).into_owned()
538        }
539    };
540
541    // プレビューは先頭 300 *文字*(バイト境界で切ると UTF-8 文字の途中に落ちてパニックする)。
542    let preview: String = resp_str.chars().take(300).collect();
543    crate::info!("[NET] HTTPS RAW RESP: [{} bytes] {:?}", resp_str.len(), preview);
544    let (status_code, reason, headers, body) = crate::kernel::net::parse_http_response_text(&raw_bytes)
545        .unwrap_or((
546            200,
547            String::from("OK"),
548            alloc::collections::BTreeMap::new(),
549            resp_str,
550        ));
551    let body_bytes = crate::kernel::net::parse_http_response_bytes(&raw_bytes)
552        .map(|(_, _, _, b)| b)
553        .unwrap_or_default();
554
555    crate::kernel::net::tcp_close_real(local_port)?;
556
557    crate::warn!(
558        "[NET][DIAG] https_get ordinal={} SUCCESS body_bytes={}",
559        ordinal,
560        body_bytes.len()
561    );
562
563    Ok(crate::kernel::net::HttpsGetResult {
564        remote_ip,
565        local_port,
566        tls_version: String::from("TLS1.3"),
567        cipher_suite: String::from("TLS_AES_128_GCM_SHA256"),
568        cert_subject: alloc::format!("CN={}", host),
569        cert_issuer: String::from("Unknown Root CA"),
570        cert_not_after: String::from("Unknown"),
571        status_code,
572        reason,
573        headers,
574        body,
575        body_bytes,
576    })
577}
578
579/// 任意メソッド + 任意ヘッダの HTTPS リクエスト(OAuth2 / REST API / XHR・fetch 用)。
580/// `Authorization: Bearer ...` などの追加ヘッダを付けて送れる。body が Some なら
581/// Content-Type / Content-Length を付与する(content_type が空なら付けない)。
582///
583/// 【2026-10-16】応答が途中で切れていたら(`http_body::is_complete_response`)、
584/// 冪等なメソッド(GET/HEAD/OPTIONS)に限り最大 3 回まで取り直す。従来は接続が切れても
585/// 手元の分を 200 として返し、Cloudflare が chunked で返す sugi-lab.net の papers.json
586/// (150,481 B)が 129,610 B で切れて jQuery の JSON 解析が黙って失敗、論文リストが約 3 回に
587/// 1 回出なかった。POST 等は二重送信になるので取り直さない。
588pub fn https_request(
589    method: &str,
590    host: &str,
591    path: &str,
592    extra_headers: &[(&str, &str)],
593    content_type: &str,
594    body: Option<&[u8]>,
595) -> Result<crate::kernel::net::HttpsGetResult, &'static str> {
596    const MAX_ATTEMPTS: u32 = 3;
597    let idempotent = matches!(method, "GET" | "HEAD" | "OPTIONS");
598    let mut partial: Option<crate::kernel::net::HttpsGetResult> = None;
599    for attempt in 1..=MAX_ATTEMPTS {
600        let (res, complete) =
601            match https_request_once(method, host, path, extra_headers, content_type, body) {
602                Ok(r) => r,
603                // 取り直しが失敗したら、途中まで届いた分を返す(従来と同じ振る舞い)。
604                Err(e) => return partial.ok_or(e),
605            };
606        if complete || !idempotent || method == "HEAD" {
607            return Ok(res);
608        }
609        crate::warn!(
610            "[NET][DIAG] https_request 途中切れ {} {}{} attempt={}/{} body={}",
611            method, host, path, attempt, MAX_ATTEMPTS, res.body_bytes.len()
612        );
613        partial = Some(res);
614    }
615    partial.ok_or("HTTPS request failed")
616}
617
618/// `https_request` の 1 回ぶん。戻り値の `bool` は応答が最後まで届いたか。
619fn https_request_once(
620    method: &str,
621    host: &str,
622    path: &str,
623    extra_headers: &[(&str, &str)],
624    content_type: &str,
625    body: Option<&[u8]>,
626) -> Result<(crate::kernel::net::HttpsGetResult, bool), &'static str> {
627    let (host_only, port) = crate::kernel::net::parse_host_port_with_default(host, 443);
628    let remote_ip = crate::kernel::net::dns_query_a_via_udp(host_only)?;
629    let local_port = crate::kernel::net::tcp_connect_real(remote_ip, port)?;
630
631    let mut stream = TcpIoWrapper::new(local_port);
632    let mut rng = OsRng::new();
633
634    let mut read_buf = alloc::vec![0u8; 18432];
635    let mut write_buf = alloc::vec![0u8; 16384];
636
637    let config = TlsConfig::new().with_server_name(host_only);
638    let mut connection: TlsConnection<'_, &mut TcpIoWrapper, embedded_tls::Aes128GcmSha256> =
639        TlsConnection::new(&mut stream, &mut read_buf[..], &mut write_buf[..]);
640    let mut provider =
641        embedded_tls::UnsecureProvider::new::<embedded_tls::Aes128GcmSha256>(&mut rng);
642    let _ph = crate::os_lib::web_engine::perf::start();
643    let _open = connection.open(crate::kernel::tls::TlsContext::new(&config, &mut provider));
644    crate::os_lib::web_engine::perf::add(
645        crate::os_lib::web_engine::perf::Slot::TlsHandshake,
646        _ph,
647    );
648    if let Err(e) = _open {
649        crate::warn!("[NET] TLS handshake error: {:?}", e);
650        let _ = crate::kernel::net::tcp_close_real(local_port);
651        return Err("TLS handshake failed");
652    }
653
654    let mut header = alloc::format!(
655        "{} {} HTTP/1.1\r\nHost: {}\r\nUser-Agent: AtmOS/1.0\r\n",
656        method,
657        path,
658        host
659    );
660    // 呼び出し側(JS の fetch/XHR)が Accept を指定しなければ既定値を付ける。
661    if !extra_headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("accept")) {
662        header.push_str("Accept: application/json\r\n");
663    }
664    for (k, v) in extra_headers {
665        header.push_str(k);
666        header.push_str(": ");
667        header.push_str(v);
668        header.push_str("\r\n");
669    }
670    let body = body.unwrap_or(&[]);
671    if !content_type.is_empty() {
672        header.push_str(&alloc::format!("Content-Type: {}\r\n", content_type));
673    }
674    if !body.is_empty() || method == "POST" || method == "PUT" || method == "PATCH" {
675        header.push_str(&alloc::format!("Content-Length: {}\r\n", body.len()));
676    }
677    header.push_str("Accept-Encoding: identity\r\n");
678    header.push_str("Connection: close\r\n\r\n");
679
680    let mut request = header.into_bytes();
681    request.extend_from_slice(body);
682
683    if embedded_io::Write::write_all(&mut connection, &request).is_err() {
684        crate::kernel::net::tcp_close_real(local_port)?;
685        return Err("HTTPS request failed");
686    }
687    embedded_io::Write::flush(&mut connection).ok();
688    // 【2026-08-05】無条件 500ms から「届いたら即抜ける」へ(上限は同じ)。
689    // 詳細は `net::tcp::wait_for_first_byte`。
690    crate::kernel::net::tcp::wait_for_first_byte(local_port, 50);
691
692    let mut raw_bytes = Vec::new();
693    let mut chunk = [0u8; 1024];
694    loop {
695        match embedded_io::Read::read(&mut connection, &mut chunk) {
696            Ok(0) => break,
697            Ok(n) => {
698                raw_bytes.extend_from_slice(&chunk[..n]);
699                // 長さの分かる応答は揃った時点で終える(keep-alive で閉じないサーバ向け)。
700                if crate::kernel::http_body::is_framed_complete(&raw_bytes) {
701                    break;
702                }
703            }
704            Err(_) => break,
705        }
706    }
707
708    let resp_str = match alloc::string::String::from_utf8(raw_bytes.clone()) {
709        Ok(s) => s,
710        Err(e) => alloc::string::String::from_utf8_lossy(&e.into_bytes()).into_owned(),
711    };
712    let (status_code, reason, headers, rbody) = crate::kernel::net::parse_http_response_text(&raw_bytes)
713        .unwrap_or((
714            200,
715            String::from("OK"),
716            alloc::collections::BTreeMap::new(),
717            resp_str,
718        ));
719    // ボディの生バイト列(バイナリダウンロード向け。JSON/テキストAPIは rbody を使えばよい)。
720    let body_bytes = crate::kernel::net::parse_http_response_bytes(&raw_bytes)
721        .map(|(_, _, _, b)| b)
722        .unwrap_or_default();
723    let complete = crate::kernel::http_body::is_complete_response(&raw_bytes);
724    crate::kernel::net::tcp_close_real(local_port)?;
725
726    Ok((crate::kernel::net::HttpsGetResult {
727        remote_ip,
728        local_port,
729        tls_version: String::from("TLS1.3"),
730        cipher_suite: String::from("TLS_AES_128_GCM_SHA256"),
731        cert_subject: alloc::format!("CN={}", host),
732        cert_issuer: String::from("Unknown Root CA"),
733        cert_not_after: String::from("Unknown"),
734        status_code,
735        reason,
736        headers,
737        body: rbody,
738        body_bytes,
739    }, complete))
740}
741
742/// HTTPS POST。https_get と同じ TLS 経路で、リクエスト行を POST にし
743/// Content-Type / Content-Length ヘッダ + ボディを送信する。
744/// フォーム送信(DuckDuckGo lite の検索等)が HTTPS で正しく届くために必須。
745pub fn https_post(
746    host: &str,
747    path: &str,
748    content_type: &str,
749    body: &[u8],
750) -> Result<crate::kernel::net::HttpsGetResult, &'static str> {
751    let (host_only, port) = crate::kernel::net::parse_host_port_with_default(host, 443);
752    let remote_ip = crate::kernel::net::dns_query_a_via_udp(host_only)?;
753    let local_port = crate::kernel::net::tcp_connect_real(remote_ip, port)?;
754
755    let mut stream = TcpIoWrapper::new(local_port);
756    let mut rng = OsRng::new();
757
758    let mut read_buf = alloc::vec![0u8; 18432]; // TLS最大レコード(16640)を収容
759    let mut write_buf = alloc::vec![0u8; 16384];
760
761    let config = TlsConfig::new().with_server_name(host_only);
762
763    let mut connection: TlsConnection<'_, &mut TcpIoWrapper, embedded_tls::Aes128GcmSha256> =
764        TlsConnection::new(&mut stream, &mut read_buf[..], &mut write_buf[..]);
765
766    let mut provider =
767        embedded_tls::UnsecureProvider::new::<embedded_tls::Aes128GcmSha256>(&mut rng);
768    let _ph = crate::os_lib::web_engine::perf::start();
769    let _open = connection.open(crate::kernel::tls::TlsContext::new(&config, &mut provider));
770    crate::os_lib::web_engine::perf::add(
771        crate::os_lib::web_engine::perf::Slot::TlsHandshake,
772        _ph,
773    );
774    if let Err(e) = _open {
775        crate::warn!("[NET] TLS handshake error: {:?}", e);
776        let _ = crate::kernel::net::tcp_close_real(local_port);
777        return Err("TLS handshake failed");
778    }
779
780    // ヘッダとボディを 1 つのバッファにまとめて送る
781    let header = alloc::format!(
782        "POST {} 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\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
783        path, host, content_type, body.len()
784    );
785    let mut request = header.into_bytes();
786    request.extend_from_slice(body);
787
788    if embedded_io::Write::write_all(&mut connection, &request).is_err() {
789        crate::kernel::net::tcp_close_real(local_port)?;
790        return Err("HTTPS POST request failed");
791    }
792    embedded_io::Write::flush(&mut connection).ok();
793    // 【2026-08-05】無条件 500ms から「届いたら即抜ける」へ(上限は同じ)。
794    // 詳細は `net::tcp::wait_for_first_byte`。
795    crate::kernel::net::tcp::wait_for_first_byte(local_port, 50);
796
797    let mut raw_bytes = Vec::new();
798    let mut chunk = [0u8; 1024];
799    loop {
800        match embedded_io::Read::read(&mut connection, &mut chunk) {
801            Ok(0) => break,
802            Ok(n) => raw_bytes.extend_from_slice(&chunk[..n]),
803            // ConnectionClosed は close 後の正常終了。蓄積済みデータを使う
804            Err(_) => break,
805        }
806    }
807
808    let resp_str = match alloc::string::String::from_utf8(raw_bytes.clone()) {
809        Ok(s) => s,
810        Err(e) => alloc::string::String::from_utf8_lossy(&e.into_bytes()).into_owned(),
811    };
812
813    crate::info!("[NET] HTTPS POST: {} bytes received", resp_str.len());
814    let (status_code, reason, headers, body) = crate::kernel::net::parse_http_response_text(&raw_bytes)
815        .unwrap_or((
816            200,
817            String::from("OK"),
818            alloc::collections::BTreeMap::new(),
819            resp_str,
820        ));
821    let body_bytes = crate::kernel::net::parse_http_response_bytes(&raw_bytes)
822        .map(|(_, _, _, b)| b)
823        .unwrap_or_default();
824
825    crate::kernel::net::tcp_close_real(local_port)?;
826
827    Ok(crate::kernel::net::HttpsGetResult {
828        remote_ip,
829        local_port,
830        tls_version: String::from("TLS1.3"),
831        cipher_suite: String::from("TLS_AES_128_GCM_SHA256"),
832        cert_subject: alloc::format!("CN={}", host),
833        cert_issuer: String::from("Unknown Root CA"),
834        cert_not_after: String::from("Unknown"),
835        status_code,
836        reason,
837        headers,
838        body,
839        body_bytes,
840    })
841}
842
843/// レスポンスヘッダから`Content-Length`(10進)を読み取る。無ければ`None`。
844/// 同一ホストの複数パスを **1 本の接続・1 回のハンドシェイク**で取得する。
845///
846/// # なぜ要るのか
847///
848/// 【2026-08-05 実測】1 ページの読み込みで
849/// `DNS=67/15 TCP接続=204/14 TLS握手=4118/14`。
850/// **ハンドシェイクだけで 4118 tick(約 41 秒)**、1 回あたり約 294 tick。
851/// DNS も TCP 接続も軽いので、重いのは TLS 交渉そのもの。
852/// 同じホストへ毎回張り直しているのが原因。
853///
854/// # 応答の切れ目
855///
856/// keep-alive では接続が閉じないので、**どこで 1 応答が終わるか**を
857/// 自分で決める必要がある。`Content-Length` を見て、その分だけ読む。
858/// 見つからない応答(chunked 等)が来たらそこで打ち切り、
859/// 残りは呼び出し元が個別取得へ切り替えられるよう `None` を返す。
860///
861/// # 戻り値
862///
863/// `paths` と同じ長さ。取れなかったものは `None`。
864/// 途中で接続が壊れたら、それ以降は全部 `None`(呼び出し元が再取得する)。
865pub fn https_get_binary_keepalive(
866    host: &str,
867    paths: &[String],
868) -> Result<Vec<Option<Vec<u8>>>, &'static str> {
869    if paths.is_empty() {
870        return Ok(Vec::new());
871    }
872    let mut out: Vec<Option<Vec<u8>>> = Vec::with_capacity(paths.len());
873
874    let (host_only, port) = crate::kernel::net::parse_host_port_with_default(host, 443);
875    let remote_ip = crate::kernel::net::dns::dns_query_a_via_udp(host_only)?;
876    let local_port = crate::kernel::net::tcp::tcp_connect_real(remote_ip, port)?;
877
878    let mut rng = OsRng::new();
879    let mut stream = TcpIoWrapper::new_with_retry_write(local_port);
880    let mut read_buf = alloc::vec![0u8; 16384];
881    let mut write_buf = alloc::vec![0u8; 16384];
882    let config = TlsConfig::new().with_server_name(host_only);
883    let mut connection: TlsConnection<'_, &mut TcpIoWrapper, embedded_tls::Aes128GcmSha256> =
884        TlsConnection::new(&mut stream, &mut read_buf[..], &mut write_buf[..]);
885    let mut provider =
886        embedded_tls::UnsecureProvider::new::<embedded_tls::Aes128GcmSha256>(&mut rng);
887
888    let _ph = crate::os_lib::web_engine::perf::start();
889    let open = connection.open(crate::kernel::tls::TlsContext::new(&config, &mut provider));
890    crate::os_lib::web_engine::perf::add(
891        crate::os_lib::web_engine::perf::Slot::TlsHandshake,
892        _ph,
893    );
894    if open.is_err() {
895        let _ = crate::kernel::net::tcp_close_real(local_port);
896        return Err("TLS handshake failed");
897    }
898
899    let mut broken = false;
900    for path in paths.iter() {
901        if broken {
902            out.push(None);
903            continue;
904        }
905        // 最後の 1 本だけ close を伝える(サーバ側の後始末を促す)。
906        let req = alloc::format!(
907            concat!(
908                "GET {} HTTP/1.1
909",
910                "Host: {}
911",
912                "User-Agent: Mozilla/5.0 (AtmOS Browser)
913",
914                "Accept: */*
915",
916                "Accept-Encoding: identity
917",
918                // 接続を閉じないよう明示する。閉じられると次のパスで
919                // 張り直しになり、この関数の目的(ハンドシェイク削減)が消える。
920                "Connection: keep-alive
921
922"
923            ),
924            path,
925            host
926        );
927        if embedded_io::Write::write_all(&mut connection, req.as_bytes()).is_err() {
928            broken = true;
929            out.push(None);
930            continue;
931        }
932        embedded_io::Write::flush(&mut connection).ok();
933        crate::kernel::net::tcp::wait_for_first_byte(local_port, 50);
934
935        // ヘッダを読み切り、`Content-Length` の分だけ本文を読む。
936        let mut buf: Vec<u8> = Vec::new();
937        let mut chunk = [0u8; 1024];
938        let mut header_end: Option<usize> = None;
939        let mut want: Option<usize> = None;
940        loop {
941            if header_end.is_none() {
942                if let Some(i) = crate::kernel::http_body::body_start(&buf) {
943                    header_end = Some(i);
944                    want = crate::kernel::http_body::content_length(&buf);
945                    if want.is_none() {
946                        // 長さが分からない応答は切れ目を決められない。
947                        broken = true;
948                        break;
949                    }
950                }
951            }
952            if let (Some(h), Some(w)) = (header_end, want) {
953                if buf.len() >= h + w {
954                    break;
955                }
956            }
957            match embedded_io::Read::read(&mut connection, &mut chunk) {
958                Ok(0) => {
959                    broken = true;
960                    break;
961                }
962                Ok(n) => buf.extend_from_slice(&chunk[..n]),
963                Err(_) => {
964                    broken = true;
965                    break;
966                }
967            }
968        }
969        match (header_end, want) {
970            (Some(h), Some(w)) if buf.len() >= h + w => {
971                // 【2026-08-05 バグ修正】`buf[..h + w]` は**ヘッダを含んでいた**。
972                //
973                // `h` は本文の開始位置なので、本文は `buf[h..h + w]`。
974                // ヘッダ込みで返していたため、キャッシュへ
975                // `HTTP/1.1 200 OK...` ごと書き込まれ、フォント登録が
976                // `[FONT] 未知のフォント形式 head=[48,54,54,50]` で失敗していた
977                // (`{:02x?}` 表示なので "HTTP" の 4 文字)。
978                //
979                // 実害: **Font Awesome が 1 つも登録されず**、
980                // ページ上のアイコンがすべて空の四角になっていた。
981                // まとめ取得(keep-alive)を入れたときに混入した回帰。
982                out.push(Some(crate::kernel::http_body::body_range(&buf)
983                        .map(|(a, b)| buf[a..b].to_vec())
984                        .unwrap_or_default()));
985                stats::bump(&stats::OK);
986            }
987            _ => {
988                out.push(None);
989                broken = true;
990            }
991        }
992    }
993
994    let _ = crate::kernel::net::tcp_close_real(local_port);
995    stats::report("after-resp");
996    Ok(out)
997}
998
999// 【2026-08-05】`find_header_end` は `kernel::http_body::body_start` と
1000// **同じ計算を二重に持っていた**。片方だけ直す事故のもとなので一本化した。
1001// (実際、本文の切り出しを誤って Font Awesome が全滅した直後に気づいた)
1002
1003// 【2026-08-05】`parse_content_length` も `kernel::http_body::content_length`
1004// と重複していた。文字列添字で値を切り出しており、
1005// UTF-8 の途中で切ると panic しうる書き方だった(clippy 指摘と同種)。
1006// バイト列で処理する `http_body` 版へ一本化する。
1007
1008/// `Content-Length`が判明していて、かつ受信済みボディがそれより短い
1009/// (=TLS読み取り途中で打ち切られた)場合に`true`を返す。`chunked`応答は
1010/// 対象外(`Content-Length`ヘッダを通常持たないため判定不能)。
1011fn response_looks_truncated(raw: &[u8]) -> bool {
1012    // 判定は `kernel::http_body::is_complete_response`(単体試験あり)へ委譲。
1013    // 【2026-10-16】従来の `looks_truncated` は Content-Length しか見ず、chunked の応答
1014    // (cdnjs・Cloudflare 経由はほぼこれ)の途中切れを完全とみなしていた。Font Awesome の
1015    // all.min.css(約 90KB)が 85,732 B で切れて末尾の `Font Awesome 6 Free` の @font-face が
1016    // 失われ、アイコンがときどき四角になった。続きから取る(Range)のは Content-Length が
1017    // ある応答だけ(`resume_offset`)で、chunked は最初から取り直す。
1018    !crate::kernel::http_body::is_complete_response(raw)
1019}
1020
1021pub fn https_get_binary(host: &str, path: &str) -> Result<Vec<u8>, &'static str> {
1022    https_get_binary_policy(host, path, true)
1023}
1024
1025/// `https_get_binary` の厳格版: 全試行で `Content-Length` に満たなければ **Err**。
1026///
1027/// 【2026-09-26 バグ修正】`https_get_binary` は全試行が途中で切れると「最も長く
1028/// 取れた部分」を成功として返す(CSS なら部分でも役立つため)。しかしフォント・画像は
1029/// 壊れたデータの方が代替表示より悪い: www.sugi-lab.net で Noto Sans JP(通常ウェイト、
1030/// 5,324,144 バイト)が 1,188 バイト欠けたまま登録され、**通常ウェイトの文字が
1031/// ページ全体で消えた**(QEMU 実画面で発見。しかも欠けたままキャッシュへ書かれる)。
1032/// バイナリ資源はこちらを使い、失敗なら既定フォント/代替表示へ落とす。
1033pub fn https_get_binary_complete(host: &str, path: &str) -> Result<Vec<u8>, &'static str> {
1034    https_get_binary_policy(host, path, false)
1035}
1036
1037fn https_get_binary_policy(
1038    host: &str,
1039    path: &str,
1040    accept_partial: bool,
1041) -> Result<Vec<u8>, &'static str> {
1042    let _pg = crate::os_lib::web_engine::perf::start();
1043    let r = https_get_binary_measured(host, path, accept_partial);
1044    crate::os_lib::web_engine::perf::add(
1045        crate::os_lib::web_engine::perf::Slot::HttpsGet,
1046        _pg,
1047    );
1048    r
1049}
1050
1051fn https_get_binary_measured(
1052    host: &str,
1053    path: &str,
1054    accept_partial: bool,
1055) -> Result<Vec<u8>, &'static str> {
1056    const MAX_ATTEMPTS: u32 = 3;
1057    let ordinal = crate::kernel::net::next_https_attempt_ordinal();
1058    crate::warn!("[NET][DIAG] https_get_binary ordinal={} host={} path={}", ordinal, host, path);
1059    // 【2026-07-25】以前は`https_get_binary_attempt`内でDNS解決を毎回
1060    // 行っていたため、同一ホストへの再試行のたびに無駄なDNS往復が発生し、
1061    // DNS応答タイムアウト自体が再試行を無駄に消費する原因にもなっていた。
1062    // 同一ホストの名前解決結果は数秒の再試行間隔では変わらないため、
1063    // ここで1回だけ解決してから各TCP/TLS試行へ使い回す。
1064    let (host_only, port) = crate::kernel::net::parse_host_port_with_default(host, 443);
1065    let t_dns0 = crate::kernel::timer::get_ticks();
1066    let remote_ip = crate::kernel::net::dns_query_a_via_udp(host_only).inspect_err(|e| {
1067        crate::warn!("[NET][DIAG] https_get_binary ordinal={} DNS FAILED err={}", ordinal, e);
1068        stats::bump(&stats::DNS_FAIL);
1069        stats::report("after-dns-fail");
1070    })?;
1071    crate::warn!("[NET][DIAG] https_get_binary ordinal={} host={} resolved={}.{}.{}.{} dns_ticks={}", ordinal, host, remote_ip[0], remote_ip[1], remote_ip[2], remote_ip[3], crate::kernel::timer::get_ticks().wrapping_sub(t_dns0));
1072    let mut best: Option<Vec<u8>> = None;
1073    for attempt in 1..=MAX_ATTEMPTS {
1074        // 【2026-09-26】前回が途中で切れていて、サーバが Range に対応していれば続きから取る
1075        // (`http_body::resume_offset`/`merge_range_response`。理由はそちらの doc)。
1076        let resume = best.as_deref().and_then(crate::kernel::http_body::resume_offset);
1077        match https_get_binary_attempt(remote_ip, host, host_only, port, path, resume) {
1078            Ok((raw, complete)) => {
1079                let (raw, complete) = match (resume, best.as_deref()) {
1080                    (Some(off), Some(prev)) => {
1081                        match crate::kernel::http_body::merge_range_response(prev, off, &raw) {
1082                            Some(merged) => {
1083                                let done = !crate::kernel::http_body::looks_truncated(&merged);
1084                                crate::warn!(
1085                                    "[NET][DIAG] https_get_binary ordinal={} Range 再開 from={} +{} bytes complete={}",
1086                                    ordinal, off, merged.len() - prev.len(), done
1087                                );
1088                                (merged, done)
1089                            }
1090                            // 206 でない・範囲が合わない → 今回の応答を新規の全体取得として扱う。
1091                            None => (raw, complete),
1092                        }
1093                    }
1094                    _ => (raw, complete),
1095                };
1096                if complete {
1097                    crate::warn!(
1098                        "[NET][DIAG] https_get_binary ordinal={} SUCCESS attempt={} body_bytes={}",
1099                        ordinal, attempt, raw.len()
1100                    );
1101                    return Ok(crate::kernel::net::extract_http_body_binary(&raw));
1102                }
1103                crate::warn!(
1104                    "[NET][DIAG] https_get_binary ordinal={}: truncated response on attempt {}/{} ({} bytes), retrying",
1105                    ordinal, attempt, MAX_ATTEMPTS, raw.len()
1106                );
1107                if best.as_ref().is_none_or(|b: &Vec<u8>| raw.len() > b.len()) {
1108                    best = Some(raw);
1109                }
1110            }
1111            Err(e) => {
1112                crate::warn!(
1113                    "[NET][DIAG] https_get_binary ordinal={} attempt {}/{} FAILED err={}",
1114                    ordinal, attempt, MAX_ATTEMPTS, e
1115                );
1116                if attempt == MAX_ATTEMPTS {
1117                    return Err(e);
1118                }
1119            }
1120        }
1121    }
1122    // 全リトライで打ち切りが続いた場合は、最も長く取得できたものを返す
1123    // (何も返さないより、部分的にでもCSS/フォントが使える方が良い)。
1124    match best {
1125        Some(raw) if accept_partial => Ok(crate::kernel::net::extract_http_body_binary(&raw)),
1126        Some(raw) => {
1127            crate::warn!(
1128                "[NET][DIAG] https_get_binary ordinal={} 全 {} 回とも途中で切れたため不採用 (best={} bytes)",
1129                ordinal, MAX_ATTEMPTS, raw.len()
1130            );
1131            Err("HTTPS response truncated")
1132        }
1133        None => Err("HTTPS request failed"),
1134    }
1135}
1136
1137/// 1回分のHTTPS GET試行。戻り値は(生レスポンス, Content-Lengthと矛盾なく
1138/// 完全に受信できたか)。呼び出し元`https_get_binary`がこれを複数回試行し、
1139/// 打ち切り(TLSタイムアウト等による部分受信)を検出したら再試行する。
1140/// `remote_ip`は呼び出し元で1回だけ解決済みのものを使い回す
1141/// (DNS解決自体を毎試行で繰り返さないため)。
1142fn https_get_binary_attempt(
1143    remote_ip: [u8; 4],
1144    host: &str,
1145    host_only: &str,
1146    port: u16,
1147    path: &str,
1148    range_from: Option<usize>,
1149) -> Result<(Vec<u8>, bool), &'static str> {
1150    crate::debug!(
1151        "[NET][DIAG] https_get_binary host={} resolved_ip={}.{}.{}.{}",
1152        host,
1153        remote_ip[0],
1154        remote_ip[1],
1155        remote_ip[2],
1156        remote_ip[3]
1157    );
1158    if tls12_hosts_contains(host_only) {
1159        return https_get_binary_attempt_tls12(remote_ip, host, host_only, port, path, range_from);
1160    }
1161    let t_conn0 = crate::kernel::timer::get_ticks();
1162    let local_port = crate::kernel::net::tcp_connect_real(remote_ip, port)?;
1163    let handshake_start = crate::kernel::timer::get_ticks();
1164    crate::debug!("[NET][DIAG] tcp_connect_real returned local_port={}", local_port);
1165
1166    // 【2026-07-29 検証したが結論保留】`retry_write` は `https_get_binary` でのみ
1167    // 有効で、打ち切り(truncated)が起きるのもこの経路、という相関がある。
1168    // 積極的な再送が TLS ストリームを乱している可能性を疑い一時的に無効化して
1169    // 測定したが、**2 回の実行とも試行数が 1〜2 件しか得られず判断できなかった**
1170    // (ページが早期に失敗し後続の取得まで到達しないため)。
1171    // 証拠不十分のまま既存の対策を外すのは危険なので元に戻した。
1172    // 再検証するなら、1 回のブートで同じ URL を N 回取得する仕組みを先に用意すること。
1173    let mut stream = TcpIoWrapper::new_with_retry_write(local_port);
1174    let mut rng = OsRng::new();
1175
1176    // 【2026-07-24診断】静的バッファに変更して試したがCSS取得失敗は解消せず
1177    // (ヒープ確保・再利用は原因ではないと判明したため、ヒープ確保に戻す)。
1178    let mut read_buf = alloc::vec![0u8; 18432]; // TLS最大レコード(16640)を収容
1179    let mut write_buf = alloc::vec![0u8; 16384];
1180
1181    let config = TlsConfig::new().with_server_name(host_only);
1182
1183    let mut connection: TlsConnection<'_, &mut TcpIoWrapper, embedded_tls::Aes128GcmSha256> =
1184        TlsConnection::new(&mut stream, &mut read_buf[..], &mut write_buf[..]);
1185
1186    let mut provider =
1187        embedded_tls::UnsecureProvider::new::<embedded_tls::Aes128GcmSha256>(&mut rng);
1188    let _ph = crate::os_lib::web_engine::perf::start();
1189    let _open = connection.open(crate::kernel::tls::TlsContext::new(&config, &mut provider));
1190    crate::os_lib::web_engine::perf::add(
1191        crate::os_lib::web_engine::perf::Slot::TlsHandshake,
1192        _ph,
1193    );
1194    if let Err(e) = _open {
1195        let elapsed = crate::kernel::timer::get_ticks().wrapping_sub(handshake_start);
1196        crate::warn!("[NET] TLS handshake error: {:?} (elapsed_ticks={})", e, elapsed);
1197        let _ = crate::kernel::net::tcp_close_real(local_port);
1198        // 【2026-10-03】TLS 1.3 で一度でもつながったホストなら、失敗は通信の問題とみなす。
1199        // それ以外は TLS 1.2 のみのサーバーの可能性があるので、TLS 1.2 で接続し直す。
1200        if tls13_hosts_contains(host_only) {
1201            return Err("TLS handshake failed");
1202        }
1203        return https_get_binary_attempt_tls12(remote_ip, host, host_only, port, path, range_from);
1204    }
1205    tls13_hosts_insert(host_only);
1206    let t_http0 = crate::kernel::timer::get_ticks();
1207    let r = read_http_response(&mut connection, local_port, host, path, range_from);
1208    let t_end = crate::kernel::timer::get_ticks();
1209    // 【2026-10-05】取得 1 本ごとの段階別の所要時間(高速化の対象を決めるため)。
1210    crate::warn!(
1211        "[NET][TIME] host={} path={} tcp={} tls={} http={} bytes={}",
1212        host,
1213        path.get(..path.len().min(60)).unwrap_or(path),
1214        handshake_start.wrapping_sub(t_conn0),
1215        t_http0.wrapping_sub(handshake_start),
1216        t_end.wrapping_sub(t_http0),
1217        r.as_ref().map(|(b, _)| b.len()).unwrap_or(0)
1218    );
1219    r
1220}
1221
1222/// TLS 1.3 でつながったことのあるホスト(TLS 1.2 への切り替えをしない)。
1223static TLS13_HOSTS: spin::Mutex<alloc::collections::BTreeSet<String>> =
1224    spin::Mutex::new(alloc::collections::BTreeSet::new());
1225/// TLS 1.2 でしかつながらなかったホスト(次回から TLS 1.3 を試さない)。
1226static TLS12_HOSTS: spin::Mutex<alloc::collections::BTreeSet<String>> =
1227    spin::Mutex::new(alloc::collections::BTreeSet::new());
1228
1229fn tls13_hosts_contains(host: &str) -> bool {
1230    TLS13_HOSTS.lock().contains(host)
1231}
1232
1233fn tls13_hosts_insert(host: &str) {
1234    let mut s = TLS13_HOSTS.lock();
1235    if !s.contains(host) {
1236        s.insert(String::from(host));
1237    }
1238}
1239
1240fn tls12_hosts_contains(host: &str) -> bool {
1241    TLS12_HOSTS.lock().contains(host)
1242}
1243
1244/// TLS 1.2(`kernel::tls12`)で取得する。TLS 1.3 専用の `embedded-tls` で
1245/// つながらないサーバー(例: sway.com)向けの代替経路。
1246fn https_get_binary_attempt_tls12(
1247    remote_ip: [u8; 4],
1248    host: &str,
1249    host_only: &str,
1250    port: u16,
1251    path: &str,
1252    range_from: Option<usize>,
1253) -> Result<(Vec<u8>, bool), &'static str> {
1254    let local_port = crate::kernel::net::tcp_connect_real(remote_ip, port)?;
1255    let mut stream = TcpIoWrapper::new_with_retry_write(local_port);
1256    let mut rng = OsRng::new();
1257    match crate::kernel::tls12::Tls12::connect(&mut stream, host_only, &mut rng) {
1258        Ok(mut conn) => {
1259            crate::warn!("[NET] TLS 1.2 で接続 host={}", host);
1260            {
1261                let mut s = TLS12_HOSTS.lock();
1262                if !s.contains(host_only) {
1263                    s.insert(String::from(host_only));
1264                }
1265            }
1266            let r = read_http_response(&mut conn, local_port, host, path, range_from);
1267            conn.close();
1268            r
1269        }
1270        Err(e) => {
1271            crate::warn!("[NET] TLS 1.2 handshake error: {:?} host={}", e, host);
1272            let _ = crate::kernel::net::tcp_close_real(local_port);
1273            Err("TLS handshake failed")
1274        }
1275    }
1276}
1277
1278/// HTTP 要求を送り、応答全体を読む(TLS 1.3 / 1.2 共通)。
1279fn read_http_response<C: embedded_io::Read + embedded_io::Write>(
1280    connection: &mut C,
1281    local_port: u16,
1282    host: &str,
1283    path: &str,
1284    range_from: Option<usize>,
1285) -> Result<(Vec<u8>, bool), &'static str> {
1286
1287    let range_hdr = range_from
1288        .map(|n| alloc::format!("Range: bytes={}-\r\n", n))
1289        .unwrap_or_default();
1290    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\n{}Connection: close\r\n\r\n", path, host, range_hdr);
1291    if embedded_io::Write::write_all(connection, request.as_bytes()).is_err() {
1292        crate::kernel::net::tcp_close_real(local_port)?;
1293        return Err("HTTPS request failed");
1294    }
1295
1296    embedded_io::Write::flush(connection).ok();
1297    // 【2026-08-05】無条件 500ms から「届いたら即抜ける」へ(上限は同じ)。
1298    // 詳細は `net::tcp::wait_for_first_byte`。
1299    crate::kernel::net::tcp::wait_for_first_byte(local_port, 50);
1300
1301    let mut body_bytes = Vec::new();
1302    let mut chunk = [0u8; 1024];
1303    loop {
1304        match embedded_io::Read::read(connection, &mut chunk) {
1305            Ok(0) => break,
1306            Ok(n) => {
1307                body_bytes.extend_from_slice(&chunk[..n]);
1308                // 【2026-09-26】`Content-Length` 分を受け取り終えたら接続終了(FIN)を
1309                // 待たずに抜ける。FIN が順序外で保留されると、本文は揃っているのに
1310                // 受信タイムアウト(15 秒)まで待ち続けることがあった。
1311                if crate::kernel::http_body::body_range(&body_bytes).is_some() {
1312                    break;
1313                }
1314                // 【2026-07-30】取得サイズの上限。0 なら無制限。
1315                // 画像のように巨大なリソースは、`Content-Length` が分かった
1316                // 時点で本文を読み切らずに打ち切る。
1317                // 実測で 3.7MB の背景写真のダウンロードが 120 秒を使い切り、
1318                // 描画へ進めなくなっていた(`spec/webfont_loading.md`)。
1319                let cap = FETCH_BYTE_CAP.load(core::sync::atomic::Ordering::Relaxed);
1320                if cap > 0 {
1321                    let declared = crate::kernel::http_body::content_length(&body_bytes);
1322                    if declared.is_some_and(|c| c > cap) || body_bytes.len() > cap {
1323                        crate::warn!(
1324                            "[TLS] host={} path={} 取得を打ち切り: 上限 {} バイト超過 (declared={:?} got={})",
1325                            host, path, cap, declared, body_bytes.len()
1326                        );
1327                        let _ = crate::kernel::net::tcp_close_real(local_port);
1328                        return Err("fetch exceeds size cap");
1329                    }
1330                }
1331            }
1332            Err(e) => {
1333                crate::warn!("[NET][DIAG] TLS read err: {:?} body_bytes_so_far={}", e, body_bytes.len());
1334                break;
1335            }
1336        }
1337    }
1338    crate::warn!(
1339        "[NET][DIAG] read loop done, body_bytes.len()={} preview={:?}",
1340        body_bytes.len(),
1341        String::from_utf8_lossy(&body_bytes[..body_bytes.len().min(80)])
1342    );
1343
1344    crate::kernel::net::tcp_close_real(local_port)?;
1345
1346    let complete = !response_looks_truncated(&body_bytes);
1347    // 【2026-07-29診断】実サイトで応答が実行ごとに違う長さで途中打ち切りになる件の
1348    // 切り分け。Content-Length と実読バイト数、打ち切り判定の結果を突き合わせる。
1349    if complete {
1350        stats::bump(&stats::OK);
1351    } else {
1352        stats::bump(&stats::TRUNCATED);
1353    }
1354    stats::report("after-resp");
1355    Ok((body_bytes, complete))
1356}