diff options
Diffstat (limited to 'server/registry/src/main.rs')
-rw-r--r-- | server/registry/src/main.rs | 200 |
1 files changed, 200 insertions, 0 deletions
diff --git a/server/registry/src/main.rs b/server/registry/src/main.rs new file mode 100644 index 00000000..f40ff043 --- /dev/null +++ b/server/registry/src/main.rs @@ -0,0 +1,200 @@ +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<str>, + servers: HashMap<u128, Entry>, +} + +impl Registry { + pub async fn update_loop(r: Arc<RwLock<Self>>) { + let mut interval = interval(Duration::from_secs( + var("UPDATE_INTERVAL") + .map(|e| e.parse::<u64>().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::<Vec<_>>(); + + 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<String, Instant>, + players_online: usize, + last_game: i64, +} + +#[derive(Debug, Serialize)] +struct PublicEntry { + name: String, + address: Vec<String>, + 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<Arc<RwLock<Registry>>>) -> RawJson<Arc<str>> { + 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 = "<submission>")] +async fn r_register<'a>( + client_addr: IpAddr, + registry: &State<Arc<RwLock<Registry>>>, + submission: Json<Submission>, +) -> 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") +} |