diff options
Diffstat (limited to 'server/registry/src')
-rw-r--r-- | server/registry/src/list.rs | 84 | ||||
-rw-r--r-- | server/registry/src/main.rs | 140 | ||||
-rw-r--r-- | server/registry/src/register.rs | 83 |
3 files changed, 204 insertions, 103 deletions
diff --git a/server/registry/src/list.rs b/server/registry/src/list.rs new file mode 100644 index 00000000..eb1ddfbf --- /dev/null +++ b/server/registry/src/list.rs @@ -0,0 +1,84 @@ +use crate::{PublicEntry, Registry}; +use anyhow::Result; +use rocket::{ + get, + http::MediaType, + request::{self, FromRequest, Outcome}, + response::content::{RawHtml, RawJson}, + Either, Request, State, +}; +use std::sync::Arc; +use tokio::sync::RwLock; + +#[get("/v1/list")] +pub(super) async fn r_list( + registry: &State<Arc<RwLock<Registry>>>, + json: AcceptJson, +) -> Either<RawJson<Arc<str>>, RawHtml<Arc<str>>> { + if json.0 { + Either::Left(RawJson(registry.read().await.json_response.clone())) + } else { + Either::Right(RawHtml(registry.read().await.html_response.clone())) + } +} + +pub(super) fn generate_json_list(entries: &[PublicEntry]) -> Result<Arc<str>> { + Ok(serde_json::to_string(&entries)?.into()) +} +pub(super) fn generate_html_list(entries: &[PublicEntry]) -> Result<Arc<str>> { + Ok(ListPage { entries }.to_string().into()) +} + +markup::define!( + ListPage<'a>(entries: &'a [PublicEntry]) { + @markup::doctype() + html { + head { + title { + "Hurry Curry! Server Registry" + } + } + body { + table { + tr { th {"Server"} th { "Players" } th { "Protocol" } } + @for e in *entries { tr { + td { details { + summary { @e.name } + ul { @for a in &e.address { li { @a } }} + } } + td { @e.players_online } + td { @e.version.0 "." @e.version.1 } + }} + } + } + } + } +); + +pub struct AcceptJson(bool); +impl<'r> FromRequest<'r> for AcceptJson { + type Error = (); + fn from_request<'life0, 'async_trait>( + request: &'r Request<'life0>, + ) -> ::core::pin::Pin< + Box< + dyn ::core::future::Future<Output = request::Outcome<Self, Self::Error>> + + ::core::marker::Send + + 'async_trait, + >, + > + where + 'r: 'async_trait, + 'life0: 'async_trait, + Self: 'async_trait, + { + Box::pin(async move { + Outcome::Success(AcceptJson( + request + .accept() + .map(|a| a.preferred().exact_eq(&MediaType::JSON)) + .unwrap_or(false), + )) + }) + } +} diff --git a/server/registry/src/main.rs b/server/registry/src/main.rs index f40ff043..e979289f 100644 --- a/server/registry/src/main.rs +++ b/server/registry/src/main.rs @@ -1,24 +1,20 @@ -use log::{debug, error, info}; -use rocket::{ - get, - http::hyper::Uri, - post, - response::content::RawJson, - routes, - serde::{json::Json, Serialize}, - State, -}; -use serde::Deserialize; +pub mod list; +pub mod register; + +use list::{generate_html_list, generate_json_list, r_list}; +use log::{error, info}; +use register::r_register; +use rocket::{get, routes, serde::Serialize, Config}; use std::{ cmp::Reverse, collections::HashMap, env::var, - net::IpAddr, + net::{IpAddr, Ipv4Addr}, str::FromStr, sync::Arc, time::{Duration, Instant}, }; -use tokio::{net::lookup_host, sync::RwLock, time::interval}; +use tokio::{sync::RwLock, time::interval}; fn main() { env_logger::init_from_env("LOG"); @@ -30,6 +26,13 @@ fn main() { .block_on(async move { tokio::task::spawn(Registry::update_loop(registry.clone())); rocket::build() + .configure(Config { + address: var("BIND_ADDR") + .map(|a| IpAddr::from_str(&a).unwrap()) + .unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)), + port: var("PORT").map(|p| p.parse().unwrap()).unwrap_or(8000), + ..Default::default() + }) .manage(registry) .mount("/", routes![r_index, r_list, r_register]) .ignite() @@ -43,7 +46,8 @@ fn main() { #[derive(Default)] struct Registry { - response: Arc<str>, + json_response: Arc<str>, + html_response: Arc<str>, servers: HashMap<u128, Entry>, } @@ -56,18 +60,14 @@ impl Registry { )); 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 - }); + info!("updating list"); + self.remove_dead(); let mut list = self .servers @@ -77,14 +77,25 @@ impl Registry { address: e.address.keys().cloned().collect(), last_game: e.last_game, players_online: e.players_online, + version: e.version, }) .collect::<Vec<_>>(); list.sort_by_key(|e| Reverse(e.players_online)); - self.response = serde_json::to_string(&list)?.into(); + self.json_response = generate_json_list(&list)?; + self.html_response = generate_html_list(&list)?; + + info!("done. {} servers registered", self.servers.len()); Ok(()) } + pub fn remove_dead(&mut self) { + self.servers.retain(|_, e| { + e.address + .retain(|_, updated| updated.elapsed() < Duration::from_secs(120)); + e.address.len() > 0 + }); + } } #[derive(Debug)] @@ -93,14 +104,16 @@ struct Entry { address: HashMap<String, Instant>, players_online: usize, last_game: i64, + version: (usize, usize), } #[derive(Debug, Serialize)] -struct PublicEntry { +pub struct PublicEntry { name: String, address: Vec<String>, players_online: usize, last_game: i64, + version: (usize, usize), } impl Default for Entry { @@ -110,91 +123,12 @@ impl Default for Entry { last_game: 0, name: String::new(), players_online: 0, + version: (0, 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") + "Hurry Curry! Server Registry Service" } diff --git a/server/registry/src/register.rs b/server/registry/src/register.rs new file mode 100644 index 00000000..173361ef --- /dev/null +++ b/server/registry/src/register.rs @@ -0,0 +1,83 @@ +use crate::Registry; +use log::{debug, info}; +use rocket::{http::hyper::Uri, post, serde::json::Json, State}; +use serde::Deserialize; +use std::{net::IpAddr, str::FromStr, sync::Arc, time::Instant}; +use tokio::{net::lookup_host, sync::RwLock}; + +#[derive(Debug, Deserialize)] +pub(super) struct Submission { + secret: u128, + name: String, + players: usize, + last_game: i64, + version: (usize, usize), + + uri: String, +} + +#[post("/v1/register", data = "<submission>")] +pub(super) 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.version = submission.version; + entry.address.insert(uri_q, Instant::now()); + + Ok("ok") +} |