126 linhas
3,4 KiB
Rust
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,
|
|
}))
|
|
}
|