aboutsummaryrefslogtreecommitdiff
path: root/server/registry/src
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-09-20 21:36:22 +0200
committermetamuffin <metamuffin@disroot.org>2024-09-20 21:36:36 +0200
commita294ea6772b1bf1bccc6f43dad026e5574de15af (patch)
tree8a47e08c81d6e5ebe0968a9a9c53952b4b368232 /server/registry/src
parent44e90c75d10815633edaf979847c89b5d62242a3 (diff)
downloadhurrycurry-a294ea6772b1bf1bccc6f43dad026e5574de15af.tar
hurrycurry-a294ea6772b1bf1bccc6f43dad026e5574de15af.tar.bz2
hurrycurry-a294ea6772b1bf1bccc6f43dad026e5574de15af.tar.zst
html listing
Diffstat (limited to 'server/registry/src')
-rw-r--r--server/registry/src/list.rs84
-rw-r--r--server/registry/src/main.rs140
-rw-r--r--server/registry/src/register.rs83
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")
+}