aboutsummaryrefslogtreecommitdiff
path: root/server/src/network
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/network')
-rw-r--r--server/src/network/mdns.rs61
-rw-r--r--server/src/network/mod.rs20
-rw-r--r--server/src/network/register.rs132
-rw-r--r--server/src/network/upnp.rs72
4 files changed, 285 insertions, 0 deletions
diff --git a/server/src/network/mdns.rs b/server/src/network/mdns.rs
new file mode 100644
index 00000000..870ae7ec
--- /dev/null
+++ b/server/src/network/mdns.rs
@@ -0,0 +1,61 @@
+use crate::server::Server;
+use anyhow::Result;
+use get_if_addrs::get_if_addrs;
+use hurrycurry_protocol::VERSION;
+use log::{info, warn};
+use mdns_sd::{ServiceDaemon, ServiceInfo};
+use rand::random;
+use std::{collections::HashMap, sync::Arc, time::Duration};
+use tokio::{sync::RwLock, time::interval};
+
+pub async fn mdns_loop(name: String, port: u16, state: Arc<RwLock<Server>>) {
+ let d = match ServiceDaemon::new() {
+ Ok(d) => d,
+ Err(e) => {
+ warn!("mDNS daemon failed to start: {e}");
+ return;
+ }
+ };
+ let mut interval = interval(Duration::from_secs(60));
+ let hostname = format!("hks-{}.local.", random::<u64>());
+ loop {
+ interval.tick().await;
+ if let Err(e) = update_service(&d, &state, &name, &hostname, port).await {
+ warn!("mDNS service update failed: {e}");
+ }
+ info!("updated mDNS service record");
+ }
+}
+
+async fn update_service(
+ d: &ServiceDaemon,
+ state: &Arc<RwLock<Server>>,
+ name: &str,
+ hostname: &str,
+ port: u16,
+) -> Result<()> {
+ let addrs = get_if_addrs()?
+ .into_iter()
+ .map(|e| e.addr.ip())
+ .filter(|a| !a.is_loopback())
+ .collect::<Vec<_>>();
+
+ let players = state.read().await.count_chefs();
+
+ d.register(ServiceInfo::new(
+ "_hurrycurry._tcp.local.",
+ name,
+ hostname,
+ &*addrs,
+ port,
+ HashMap::from_iter([
+ ("players".to_string(), players.to_string()),
+ ("version".to_string(), env!("CARGO_PKG_VERSION").to_string()),
+ (
+ "protocol".to_string(),
+ format!("{}.{}", VERSION.0, VERSION.1),
+ ),
+ ]),
+ )?)?;
+ Ok(())
+}
diff --git a/server/src/network/mod.rs b/server/src/network/mod.rs
new file mode 100644
index 00000000..b7ffc15e
--- /dev/null
+++ b/server/src/network/mod.rs
@@ -0,0 +1,20 @@
+/*
+ Hurry Curry! - a game about cooking
+ Copyright 2024 metamuffin
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, version 3 of the License only.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+*/
+pub mod register;
+pub mod upnp;
+pub mod mdns;
diff --git a/server/src/network/register.rs b/server/src/network/register.rs
new file mode 100644
index 00000000..b26768a2
--- /dev/null
+++ b/server/src/network/register.rs
@@ -0,0 +1,132 @@
+/*
+ Hurry Curry! - a game about cooking
+ Copyright 2024 metamuffin
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, version 3 of the License only.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+*/
+use crate::server::Server;
+use anyhow::{bail, Result};
+use hurrycurry_protocol::{registry::Submission, VERSION};
+use log::{info, warn};
+use rand::random;
+use reqwest::{header::USER_AGENT, Client, Url};
+use std::{
+ net::{IpAddr, Ipv4Addr, Ipv6Addr},
+ str::FromStr,
+ sync::Arc,
+ time::Duration,
+};
+use tokio::{sync::RwLock, time::interval};
+
+const REGISTRY_URI: &'static str = "https://hurrycurry-registry.metamuffin.org";
+
+pub struct Register {
+ name: String,
+ port: u16,
+ register_uri: Option<String>,
+ state: Arc<RwLock<Server>>,
+ inet_client: Client,
+ ip4_client: Client,
+ ip6_client: Client,
+ secret: u128,
+ players: usize,
+}
+
+impl Register {
+ pub fn new(
+ name: String,
+ port: u16,
+ register_uri: Option<String>,
+ state: Arc<RwLock<Server>>,
+ ) -> Self {
+ Self {
+ name,
+ register_uri,
+ players: 0,
+ port,
+ secret: random(),
+ state,
+ inet_client: Client::new(),
+ ip4_client: Client::builder()
+ .local_address(IpAddr::V4(Ipv4Addr::UNSPECIFIED))
+ .build()
+ .unwrap(),
+ ip6_client: Client::builder()
+ .local_address(IpAddr::V6(Ipv6Addr::UNSPECIFIED))
+ .build()
+ .unwrap(),
+ }
+ }
+ pub async fn register_loop(mut self) {
+ let mut interval = interval(Duration::from_secs(60));
+ loop {
+ interval.tick().await;
+ self.players = self.state.read().await.count_chefs();
+ if let Err(e) = self.register().await {
+ warn!("register error: {e}")
+ }
+ }
+ }
+
+ pub async fn register(&self) -> Result<()> {
+ info!("register update");
+ if let Some(uri) = &self.register_uri {
+ self.register_with("uri", &self.inet_client, uri.to_owned())
+ .await?;
+ } else {
+ let (v4, v6) = tokio::join!(
+ self.register_with(
+ "ip4",
+ &self.ip4_client,
+ format!("ws://0.0.0.0:{}", self.port),
+ ),
+ self.register_with(
+ "ip6",
+ &self.ip6_client,
+ format!("ws://0.0.0.0:{}", self.port),
+ )
+ );
+ info!("v4: {v4:?}");
+ info!("v6: {v6:?}");
+ }
+ Ok(())
+ }
+ // TODO ip v6
+ pub async fn register_with(&self, label: &str, client: &Client, uri: String) -> Result<()> {
+ let res = client
+ .post(Url::from_str(&format!("{REGISTRY_URI}/v1/register")).unwrap())
+ .header(
+ USER_AGENT,
+ format!("hurrycurry-server {}", env!("CARGO_PKG_VERSION")),
+ )
+ .json(&Submission {
+ last_game: 0,
+ name: self.name.clone(),
+ uri,
+ players: self.players,
+ secret: self.secret,
+ version: VERSION,
+ })
+ .send()
+ .await?;
+
+ let r = res.text().await?;
+ if r == "ok" {
+ info!("register ok ({label})");
+ Ok(())
+ } else {
+ bail!("{r}");
+ }
+ }
+}
diff --git a/server/src/network/upnp.rs b/server/src/network/upnp.rs
new file mode 100644
index 00000000..a16e9684
--- /dev/null
+++ b/server/src/network/upnp.rs
@@ -0,0 +1,72 @@
+/*
+ Hurry Curry! - a game about cooking
+ Copyright 2024 metamuffin
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, version 3 of the License only.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+*/
+use anyhow::{bail, Result};
+use get_if_addrs::get_if_addrs;
+use igd::{
+ aio::{search_gateway, Gateway},
+ PortMappingProtocol, SearchOptions,
+};
+use log::{error, info};
+use std::{
+ net::{IpAddr, Ipv4Addr, SocketAddrV4},
+ time::Duration,
+};
+use tokio::time::interval;
+
+pub async fn upnp_loop(port: u16) {
+ // TODO multiple gateways
+ let (gateway, local_addr) = match upnp_setup().await {
+ Ok(g) => g,
+ Err(e) => {
+ error!("UPnP gateway not found: {e}");
+ return;
+ }
+ };
+ let mut interval = interval(Duration::from_secs(120));
+ loop {
+ interval.tick().await;
+ match gateway
+ .add_port(
+ PortMappingProtocol::TCP,
+ 27032,
+ SocketAddrV4::new(local_addr, port),
+ 800,
+ "Hurry Curry! Game Server",
+ )
+ .await
+ {
+ Ok(()) => info!("port mapped successfully"),
+ Err(e) => error!("upnp failed: {e}"),
+ }
+ }
+}
+
+async fn upnp_setup() -> Result<(Gateway, Ipv4Addr)> {
+ let gateway = search_gateway(SearchOptions::default()).await?;
+ info!("IGD address is {}", gateway.addr);
+ for i in get_if_addrs()? {
+ let a = i.addr.ip();
+ if !a.is_loopback() {
+ if let IpAddr::V4(a) = a {
+ info!("local v4 address is {a}");
+ return Ok((gateway, a));
+ }
+ }
+ }
+ bail!("no good local address found")
+}