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 の再接続ループに委ねる: