Skip to content

Commit 1482cfe

Browse files
hitalinclaude
andcommitted
feat: サーバー検出用メソッド fetch_nodeinfo / fetch_server_meta を追加
Tauri のフロントエンドからブラウザ fetch() でサーバー情報を取得すると CORS でブロックされるサーバー(misskey.io 等)がある。 Rust 側の reqwest クライアント経由で取得することで CORS を回避する。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4b7ee95 commit 1482cfe

1 file changed

Lines changed: 90 additions & 0 deletions

File tree

src/api.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1184,6 +1184,96 @@ impl MisskeyClient {
11841184
let message: ChatMessage = serde_json::from_value(data)?;
11851185
Ok(message)
11861186
}
1187+
1188+
// --- Server Discovery (unauthenticated) ---
1189+
1190+
/// Fetch nodeinfo via .well-known/nodeinfo.
1191+
/// Returns the parsed nodeinfo JSON.
1192+
pub async fn fetch_nodeinfo(&self, host: &str) -> Result<Value, NoteDeckError> {
1193+
let well_known_url = format!("https://{host}/.well-known/nodeinfo");
1194+
let res = self
1195+
.client
1196+
.get(&well_known_url)
1197+
.timeout(Duration::from_secs(10))
1198+
.send()
1199+
.await?;
1200+
if !res.status().is_success() {
1201+
return Err(NoteDeckError::Api {
1202+
endpoint: ".well-known/nodeinfo".to_string(),
1203+
status: res.status().as_u16(),
1204+
message: "Failed to fetch well-known nodeinfo".to_string(),
1205+
});
1206+
}
1207+
let text = Self::read_body_limited(res, ".well-known/nodeinfo").await?;
1208+
let well_known: Value = serde_json::from_str(&text)?;
1209+
1210+
let nodeinfo_url = well_known["links"]
1211+
.as_array()
1212+
.and_then(|links| {
1213+
links.iter().find_map(|link| {
1214+
let rel = link["rel"].as_str().unwrap_or("");
1215+
if rel.contains("nodeinfo") {
1216+
link["href"].as_str().map(|s| s.to_string())
1217+
} else {
1218+
None
1219+
}
1220+
})
1221+
})
1222+
.ok_or_else(|| NoteDeckError::Api {
1223+
endpoint: ".well-known/nodeinfo".to_string(),
1224+
status: 0,
1225+
message: format!("No nodeinfo URL found for {host}"),
1226+
})?;
1227+
1228+
// Validate URL to prevent SSRF: must be https://{host}/...
1229+
let expected_prefix = format!("https://{host}/");
1230+
if !nodeinfo_url.starts_with(&expected_prefix) {
1231+
return Err(NoteDeckError::Api {
1232+
endpoint: ".well-known/nodeinfo".to_string(),
1233+
status: 0,
1234+
message: format!("Nodeinfo URL host/scheme mismatch for {host}"),
1235+
});
1236+
}
1237+
1238+
let res = self
1239+
.client
1240+
.get(&nodeinfo_url)
1241+
.timeout(Duration::from_secs(10))
1242+
.send()
1243+
.await?;
1244+
if !res.status().is_success() {
1245+
return Err(NoteDeckError::Api {
1246+
endpoint: "nodeinfo".to_string(),
1247+
status: res.status().as_u16(),
1248+
message: "Failed to fetch nodeinfo".to_string(),
1249+
});
1250+
}
1251+
let text = Self::read_body_limited(res, "nodeinfo").await?;
1252+
let nodeinfo: Value = serde_json::from_str(&text)?;
1253+
Ok(nodeinfo)
1254+
}
1255+
1256+
/// Fetch server meta (icon URL) via /api/meta (unauthenticated).
1257+
pub async fn fetch_server_meta(&self, host: &str) -> Result<Value, NoteDeckError> {
1258+
let url = self.api_url(host, "meta");
1259+
let res = self
1260+
.client
1261+
.post(&url)
1262+
.json(&json!({}))
1263+
.timeout(Duration::from_secs(10))
1264+
.send()
1265+
.await?;
1266+
if !res.status().is_success() {
1267+
return Err(NoteDeckError::Api {
1268+
endpoint: "meta".to_string(),
1269+
status: res.status().as_u16(),
1270+
message: "Failed to fetch server meta".to_string(),
1271+
});
1272+
}
1273+
let text = Self::read_body_limited(res, "meta").await?;
1274+
let meta: Value = serde_json::from_str(&text)?;
1275+
Ok(meta)
1276+
}
11871277
}
11881278

11891279
#[cfg(test)]

0 commit comments

Comments
 (0)