From bc7c58dfc5ba648d758caea794ab44588298af48 Mon Sep 17 00:00:00 2001 From: hitalin Date: Sat, 4 Jul 2026 10:33:28 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20NOTECLI=5FINSECURE=5FHOSTS=20=E3=81=A7?= =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E7=94=A8=E3=81=AB=20http/ws=20?= =?UTF-8?q?=E6=8E=A5=E7=B6=9A=E3=82=92=E8=A8=B1=E5=8F=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit notedeck の E2E テスト (notedeck#702) がローカルのモック Misskey サーバー (127.0.0.1:PORT) に接続できるようにする。 - 環境変数 NOTECLI_INSECURE_HOSTS (カンマ区切り・完全一致) に列挙 されたホストのみ https→http / wss→ws で接続する - デバッグビルド限定 (debug_assertions ゲート、リリースでは常に secure) - 対象: api_url / fetch_nodeinfo (SSRF prefix 検証含む) / streaming の WS URL / MiAuth 表示 URL の 4 箇所 Co-Authored-By: Claude Fable 5 --- src/api.rs | 8 +++--- src/commands/auth.rs | 3 ++- src/insecure.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/streaming.rs | 3 ++- 5 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 src/insecure.rs diff --git a/src/api.rs b/src/api.rs index efaa19d..9570e68 100644 --- a/src/api.rs +++ b/src/api.rs @@ -74,7 +74,8 @@ impl MisskeyClient { if let Some(ref base) = self.base_url { return format!("{base}/api/{endpoint}"); } - format!("https://{host}/api/{endpoint}") + let scheme = crate::insecure::http_scheme(host); + format!("{scheme}://{host}/api/{endpoint}") } /// Read the response body with a streaming size limit to prevent DoS. @@ -2126,7 +2127,8 @@ impl MisskeyClient { /// Fetch nodeinfo via .well-known/nodeinfo. /// Returns the parsed nodeinfo JSON. pub async fn fetch_nodeinfo(&self, host: &str) -> Result { - let well_known_url = format!("https://{host}/.well-known/nodeinfo"); + let scheme = crate::insecure::http_scheme(host); + let well_known_url = format!("{scheme}://{host}/.well-known/nodeinfo"); let res = self .client .get(&well_known_url) @@ -2162,7 +2164,7 @@ impl MisskeyClient { })?; // Validate URL to prevent SSRF: must be https://{host}/... - let expected_prefix = format!("https://{host}/"); + let expected_prefix = format!("{scheme}://{host}/"); if !nodeinfo_url.starts_with(&expected_prefix) { return Err(NoteDeckError::Api { endpoint: ".well-known/nodeinfo".to_string(), diff --git a/src/commands/auth.rs b/src/commands/auth.rs index 0c95952..bf9a766 100644 --- a/src/commands/auth.rs +++ b/src/commands/auth.rs @@ -88,8 +88,9 @@ pub async fn run_login( "write:votes", ]; let permission_str = permissions.join(","); + let scheme = crate::insecure::http_scheme(host); let auth_url = format!( - "https://{host}/miauth/{session_id}?name=notecli&permission={permission_str}" + "{scheme}://{host}/miauth/{session_id}?name=notecli&permission={permission_str}" ); match fmt { diff --git a/src/insecure.rs b/src/insecure.rs new file mode 100644 index 0000000..60f90f5 --- /dev/null +++ b/src/insecure.rs @@ -0,0 +1,59 @@ +//! E2E テスト用の insecure host 判定 (notedeck #702)。 +//! +//! 環境変数 `NOTECLI_INSECURE_HOSTS` (カンマ区切り・完全一致) に列挙された +//! ホストへは https/wss の代わりに http/ws で接続する。ローカルのモック +//! Misskey サーバー (`127.0.0.1:PORT`) に接続するための仕組みで、 +//! デバッグビルド限定 (リリースビルドでは常に secure)。 + +pub(crate) fn is_insecure_host(host: &str) -> bool { + #[cfg(debug_assertions)] + { + std::env::var("NOTECLI_INSECURE_HOSTS") + .map(|v| v.split(',').any(|h| h.trim().eq_ignore_ascii_case(host))) + .unwrap_or(false) + } + #[cfg(not(debug_assertions))] + { + let _ = host; + false + } +} + +pub(crate) fn http_scheme(host: &str) -> &'static str { + if is_insecure_host(host) { + "http" + } else { + "https" + } +} + +pub(crate) fn ws_scheme(host: &str) -> &'static str { + if is_insecure_host(host) { + "ws" + } else { + "wss" + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn insecure_only_for_exact_listed_host() { + // 他テストと衝突しない値を使う (env はプロセス全体で共有されるため) + unsafe { std::env::set_var("NOTECLI_INSECURE_HOSTS", "127.0.0.1:39821, mock.test:8080") }; + assert_eq!(http_scheme("127.0.0.1:39821"), "http"); + assert_eq!(ws_scheme("mock.test:8080"), "ws"); + // 完全一致のみ — 列挙外・部分一致は secure のまま + assert_eq!(http_scheme("127.0.0.1:39999"), "https"); + assert_eq!(ws_scheme("127.0.0.1"), "wss"); + unsafe { std::env::remove_var("NOTECLI_INSECURE_HOSTS") }; + } + + #[test] + fn secure_by_default() { + assert_eq!(http_scheme("misskey.io"), "https"); + assert_eq!(ws_scheme("misskey.io"), "wss"); + } +} diff --git a/src/lib.rs b/src/lib.rs index 2ec1e11..bf70e4c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub mod error; pub mod event_bus; pub mod format; pub mod http_server; +pub(crate) mod insecure; pub mod keychain; pub mod models; pub mod streaming; diff --git a/src/streaming.rs b/src/streaming.rs index f41a3c7..ab13dc6 100644 --- a/src/streaming.rs +++ b/src/streaming.rs @@ -314,7 +314,8 @@ impl StreamingManager { return Ok(()); } - let url = format!("wss://{host}/streaming?i={token}"); + let scheme = crate::insecure::ws_scheme(&host); + let url = format!("{scheme}://{host}/streaming?i={token}"); // Attempt the initial connection (with timeout to prevent hang DoS). // 失敗しても Err を返さず connection_task の再接続ループに委ねる: