use log::{debug, error, info}; use rocket::{ get, http::hyper::Uri, post, response::content::RawJson, routes, serde::{json::Json, Serialize}, State, }; use serde::Deserialize; use std::{ cmp::Reverse, collections::HashMap, env::var, net::IpAddr, str::FromStr, sync::Arc, time::{Duration, Instant}, }; use tokio::{net::lookup_host, sync::RwLock, time::interval}; fn main() { env_logger::init_from_env("LOG"); let registry = Arc::new(RwLock::new(Registry::default())); tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap() .block_on(async move { tokio::task::spawn(Registry::update_loop(registry.clone())); rocket::build() .manage(registry) .mount("/", routes![r_index, r_list, r_register]) .ignite() .await .unwrap() .launch() .await .unwrap() }); } #[derive(Default)] struct Registry { response: Arc, servers: HashMap, } impl Registry { pub async fn update_loop(r: Arc>) { let mut interval = interval(Duration::from_secs( var("UPDATE_INTERVAL") .map(|e| e.parse::().unwrap()) .unwrap_or(60), )); loop { interval.tick().await; info!("updating list"); if let Err(e) = r.write().await.update() { error!("update failed: {e}") } } } pub fn update(&mut self) -> anyhow::Result<()> { self.servers.retain(|_, e| { e.address .retain(|_, updated| updated.elapsed() < Duration::from_secs(120)); e.address.len() > 0 }); let mut list = self .servers .values() .map(|e| PublicEntry { name: e.name.clone(), address: e.address.keys().cloned().collect(), last_game: e.last_game, players_online: e.players_online, }) .collect::>(); list.sort_by_key(|e| Reverse(e.players_online)); self.response = serde_json::to_string(&list)?.into(); Ok(()) } } #[derive(Debug)] struct Entry { name: String, address: HashMap, players_online: usize, last_game: i64, } #[derive(Debug, Serialize)] struct PublicEntry { name: String, address: Vec, players_online: usize, last_game: i64, } impl Default for Entry { fn default() -> Self { Self { address: HashMap::new(), last_game: 0, name: String::new(), players_online: 0, } } } #[get("/")] fn r_index() -> &'static str { "Hurry Curry! registry service" } #[get("/v1/list")] async fn r_list(registry: &State>>) -> RawJson> { RawJson(registry.read().await.response.clone()) } #[derive(Debug, Deserialize)] struct Submission { secret: u128, name: String, players: usize, last_game: i64, uri: String, } #[post("/v1/register", data = "")] async fn r_register<'a>( client_addr: IpAddr, registry: &State>>, submission: Json, ) -> Result<&'static str, &'static str> { debug!("submission {submission:?}"); let uri = Uri::from_str(&submission.uri).map_err(|_| "invalid uri")?; let scheme = uri.scheme().ok_or("no scheme")?.as_str(); let secure = match scheme { "ws" => false, "wss" => true, _ => return Err("invalid scheme"), }; let host = uri.host().ok_or("no host")?; let port = uri.port_u16().unwrap_or(if secure { 443 } else { 27032 }); let uri_q = match IpAddr::from_str(host) { Ok(mut addr) => { if addr.is_unspecified() { addr = client_addr; } if addr.is_loopback() { return Err("loopback address"); } if addr.is_multicast() { return Err("multicast address"); } if client_addr == addr { format!("{scheme}://{addr}:{port}",) } else { return Err("source address does not match uri"); } } Err(_) => { if lookup_host(format!("{host}:0")) .await .map_err(|_| "dns lookup failed")? .find(|a| a.ip() == client_addr) .is_some() { format!("{scheme}://{host}:{port}") } else { return Err("host verification failed"); } } }; let mut g = registry.write().await; if g.servers.len() > 1000 { return Err("too many registered servers"); } info!("submission approved for {uri_q:?}"); let entry = g.servers.entry(submission.secret).or_default(); entry.name = submission.name.clone(); entry.players_online = submission.players; entry.last_game = submission.last_game; entry.address.insert(uri_q, Instant::now()); Ok("ok") }