FeDirect/src/api/instance_info.rs
2025-02-02 23:54:36 +01:00

126 linhas
3,4 KiB
Rust

use std::net::ToSocketAddrs;
use favicon_scraper::{Icon, IconKind};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use url::Url;
use crate::known_software::KNOWN_SOFTWARE_NODEINFO_NAMES;
const MINIMUM_ICON_SIZE: usize = 16;
#[derive(Serialize)]
pub struct InstanceInfo {
name: String,
software: String,
#[serde(rename = "iconURL")]
icon_url: Option<String>,
}
#[derive(Deserialize)]
struct NodeInfoDiscovery {
links: Vec<NodeInfoLink>,
}
#[derive(Deserialize)]
struct NodeInfoLink {
href: String,
}
#[derive(Deserialize)]
struct NodeInfo {
software: NodeInfoSoftware,
metadata: Option<NodeInfoMetadata>,
}
#[derive(Deserialize)]
struct NodeInfoSoftware {
name: String,
version: String,
}
#[derive(Deserialize)]
struct NodeInfoMetadata {
#[serde(rename = "nodeName")]
name: Option<String>,
}
/// Scrapes icons and returns the smallest one where the smallest axis is bigger than or equal to [MINIMUM_ICON_SIZE]
async fn find_icon(host: &str) -> Option<String> {
let icons: Vec<Icon> = favicon_scraper::scrape(host)
.await
.ok()?
.into_iter()
.filter(|i| i.size.width.min(i.size.height) >= MINIMUM_ICON_SIZE)
.collect();
let priority = |kind: &IconKind| match kind {
IconKind::LinkedInHTML => 0,
IconKind::LinkedInManifest => 1,
IconKind::HardcodedURL => 2,
_ => 3,
};
let preferred_kind = icons
.iter()
.map(|i| i.kind)
.min_by(|x, y| priority(x).cmp(&priority(y)))?; // None if icons is empty
icons
.into_iter()
.filter(|i| i.kind == preferred_kind)
.min_by_key(|i| i.size)
.map(|i| i.url.into())
}
#[get("/instance_info/<secure>/<host>")]
pub async fn instance_info(secure: bool, host: &str) -> Option<Json<InstanceInfo>> {
let mut url = Url::parse(if secure {
"https://temp.host/"
} else {
"http://temp.host/"
})
.unwrap();
url.set_host(Some(host)).ok()?; // Using this to catch malformed hosts
let host = url.host_str()?.to_owned(); // Shadow the original host in case things were filtered out
// Check if the host is globally routable.
// This should help filter out a bunch of invalid or potentially malicious requests
let host_with_port = format!("{host}:{}", url.port_or_known_default()?);
if !host_with_port
.to_socket_addrs()
.ok()?
.next()?
.ip()
.is_global()
{
return None;
}
let icon_url = find_icon(url.as_str()).await;
url.set_path("/.well-known/nodeinfo");
let response = reqwest::get(url.clone()).await.ok()?.text().await.ok()?;
let nodeinfo_discovery: NodeInfoDiscovery = serde_json::from_str(&response).ok()?;
let response = reqwest::get(&nodeinfo_discovery.links.first()?.href)
.await
.ok()?
.text()
.await
.ok()?;
let nodeinfo: NodeInfo = serde_json::from_str(&response).ok()?;
let (software_name, fork_map) = KNOWN_SOFTWARE_NODEINFO_NAMES.get(&nodeinfo.software.name)?;
let software = semver::Version::parse(&nodeinfo.software.version)
.ok()
.and_then(|v| fork_map.get(v.build.as_str()))
.unwrap_or(software_name)
.to_owned();
Some(Json(InstanceInfo {
name: nodeinfo
.metadata
.and_then(|m| m.name)
.unwrap_or(host.to_owned()),
software,
icon_url,
}))
}