summaryrefslogtreecommitdiff
path: root/server/registry
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-09-20 20:59:26 +0200
committermetamuffin <metamuffin@disroot.org>2024-09-20 21:36:36 +0200
commit44e90c75d10815633edaf979847c89b5d62242a3 (patch)
tree97920070b43761e424e27238e3b9ce3af71b7537 /server/registry
parent16e4ea0125ca7d311d2b0b1292b31a8daa4836a4 (diff)
downloadhurrycurry-44e90c75d10815633edaf979847c89b5d62242a3.tar
hurrycurry-44e90c75d10815633edaf979847c89b5d62242a3.tar.bz2
hurrycurry-44e90c75d10815633edaf979847c89b5d62242a3.tar.zst
draft server list registry server
Diffstat (limited to 'server/registry')
-rw-r--r--server/registry/Cargo.toml14
-rw-r--r--server/registry/src/main.rs200
2 files changed, 214 insertions, 0 deletions
diff --git a/server/registry/Cargo.toml b/server/registry/Cargo.toml
new file mode 100644
index 00000000..5433106a
--- /dev/null
+++ b/server/registry/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "hurrycurry-registry"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+log = "0.4.22"
+env_logger = "0.11.5"
+anyhow = "1.0.86"
+rocket = { version = "0.5.1", features = ["json"] }
+tokio = { version = "1.39.2", features = ["full"] }
+serde_json = "1.0.128"
+markup = "0.15.0"
+serde = { version = "1.0.210", features = ["derive"] }
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")
+}