diff options
Diffstat (limited to 'server/src')
-rw-r--r-- | server/src/config/load.rs | 47 | ||||
-rw-r--r-- | server/src/config/mod.rs | 13 | ||||
-rw-r--r-- | server/src/error/mod.rs | 90 | ||||
-rw-r--r-- | server/src/main.rs | 11 | ||||
-rw-r--r-- | server/src/server/handle_request.rs | 57 | ||||
-rw-r--r-- | server/src/server/listen.rs | 48 | ||||
-rw-r--r-- | server/src/server/mod.rs | 55 | ||||
-rw-r--r-- | server/src/server/run.rs | 23 | ||||
-rw-r--r-- | server/src/session/mod.rs | 17 |
9 files changed, 338 insertions, 23 deletions
diff --git a/server/src/config/load.rs b/server/src/config/load.rs new file mode 100644 index 0000000..7566ad9 --- /dev/null +++ b/server/src/config/load.rs @@ -0,0 +1,47 @@ +// Copyright 2022-2024 Gabriel Bjørnager Jensen. + +use crate::{Config, Error, Result}; + +use configparser::ini::Ini; +use std::env::args; +use std::path::PathBuf; + +impl Config { + pub fn load() -> Result<Self> { + let path = { + let mut args = args().skip(0x1); + + let Some(path) = args.next() else { return Err(Error::MissingConfig) }; + + if let Some(arg) = args.next() { return Err(Error::IllegalArgument { arg }) }; + + PathBuf::from(path) + }; + + let mut config = Ini::new(); + config + .load(path) + .map_err(|e| Error::ConfigError { message: e })?; + + macro_rules! get_field { + ($section:ident.$key:ident) => {{ + const FIELD: &str = concat!(stringify!($section), ".", stringify!($key)); + + if let Some(value) = config.get(stringify!($section), stringify!($key)) { + value + .parse() + .map_err(|e| Error::IllegalFieldValue { field: FIELD, value, source: Box::new(e) }) + } else { + Err(Error::MissingField { field: FIELD }) + } + }}; + } + + Ok(Self { + name: get_field!(server.name)?, + port: get_field!(server.port)?, + + max_player_count: get_field!(server.max_player_count)?, + }) + } +} diff --git a/server/src/config/mod.rs b/server/src/config/mod.rs new file mode 100644 index 0000000..72d3860 --- /dev/null +++ b/server/src/config/mod.rs @@ -0,0 +1,13 @@ +// Copyright 2022-2024 Gabriel Bjørnager Jensen. + +use bzipper::FixedString; + +mod load; + +#[derive(Debug)] +pub struct Config { + pub name: FixedString<0x10>, + pub port: u16, + + pub max_player_count: u32, +} diff --git a/server/src/error/mod.rs b/server/src/error/mod.rs new file mode 100644 index 0000000..281a074 --- /dev/null +++ b/server/src/error/mod.rs @@ -0,0 +1,90 @@ +// Copyright 2022-2024 Gabriel Bjørnager Jensen. + +use std::error::Error as StdError; +use std::fmt::{Display, Formatter}; +use std::process::{ExitCode, Termination}; + +pub type Result<T> = std::result::Result<T, Error>; + +#[derive(Debug)] +pub enum Error { + ConfigError { message: String }, + + IllegalArgument { arg: String }, + + IllegalFieldValue { field: &'static str, value: String, source: Box<dyn StdError> }, + + MissingConfig, + + MissingField { field: &'static str }, + + NetworkError { source: std::io::Error }, + + SerialiseError { source: bzipper::Error }, +} + +impl Display for Error { + #[inline] + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + use Error::*; + + match *self { + ConfigError { ref message } + => write!(f, "unable to load configuration: \"{message}\""), + + IllegalArgument { ref arg } + => write!(f, "illegal argument `{arg}` provided"), + + IllegalFieldValue { field, ref value, ref source } + => write!(f, "illegal configuration value {value:?} for field `{field}`: \"{source}\""), + + MissingConfig + => write!(f, "no configuration provided"), + + MissingField { field } + => write!(f, "missing configuration field `{field}`"), + + NetworkError { ref source } + => write!(f, "network error: \"{source}\""), + + SerialiseError { ref source } + => write!(f, "error when serialising response: \"{source}\""), + } + } +} + +impl StdError for Error { + #[allow(clippy::match_same_arms)] + #[inline] + fn source(&self) -> Option<&(dyn StdError + 'static)> { + use Error::*; + + match *self { + IllegalFieldValue { ref source, .. } => Some(source.as_ref()), + + NetworkError { ref source } => Some(source), + + SerialiseError { ref source } => Some(source), + + _ => None, + } + } +} + +impl Termination for Error { + #[inline(always)] + fn report(self) -> ExitCode { + use Error::*; + + match self { + | ConfigError { .. } + | IllegalArgument { .. } + | IllegalFieldValue { .. } + | MissingConfig + | MissingField { .. } + => 0x2, + + _ => 0x1, + }.into() + } +} diff --git a/server/src/main.rs b/server/src/main.rs index 5bd8b28..d375060 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,6 +1,15 @@ // Copyright 2022-2024 Gabriel Bjørnager Jensen. use bowshock::use_mod; +use_mod!(pub config); +use_mod!(pub error); use_mod!(pub server); +use_mod!(pub session); -fn main() -> Result<(), i32> { Server::new().run() } +fn main() -> Result<()> { + let config = Config::load()?; + let result = Server::new(config).run(); + + if let Err(ref e) = result { eprintln!("unable to run server: {e}"); } + result +} diff --git a/server/src/server/handle_request.rs b/server/src/server/handle_request.rs new file mode 100644 index 0000000..da20ee9 --- /dev/null +++ b/server/src/server/handle_request.rs @@ -0,0 +1,57 @@ +// Copyright 2022-2024 Gabriel Bjørnager Jensen. + +use crate::{Result, Server, Session}; + +use bowshock::net::{Request, Response}; +use std::net::SocketAddr; + +impl Server { + pub(in super) fn handle_request(&mut self, request: Request, addr: SocketAddr) -> Result<Option<Response>> { + eprintln!("got request {request:?} from {addr}"); + + let response = match request { + Request::Ping => { + let status = self.get_status(); + + Some(Response::ServerStatus(status)) + }, + + Request::Join { username } => { + for session in &self.sessions { + if username == session.username { + let response = Response::JoinDenied { + reason: "username already taken".parse().unwrap() + }; + + return Ok(Some(response)); + } + }; + + let token = self.generate_session_token(); + + self.sessions.push(Session { + token, + username, + + addr, + }); + + Some(Response::JoinAccepted { token }) + }, + + Request::Quit { token } => { + let index = self.sessions.iter().position(|v| *v == token); + + if let Some(index) = index { + let _ = self.sessions.remove(index); + } else { + eprintln!("got quit request for unknown session {token:?}"); + } + + None + }, + }; + + Ok(response) + } +} diff --git a/server/src/server/listen.rs b/server/src/server/listen.rs new file mode 100644 index 0000000..5e1e171 --- /dev/null +++ b/server/src/server/listen.rs @@ -0,0 +1,48 @@ +// Copyright 2022-2024 Gabriel Bjørnager Jensen. + +use crate::{Error, Result, Server}; + +use bowshock::net::{Request, Response}; +use bzipper::Buffer; +use std::net::UdpSocket; + +impl Server { + pub(in super) fn listen(&mut self, socket: &UdpSocket) -> Result<()> { + let mut request_buf = Buffer::<Request>::new(); + let mut response_buf = Buffer::<Response>::new(); + + eprintln!("listening at `{}`...", socket.local_addr().expect("socket without address provided")); + + loop { + let (len, client_addr) = socket + .recv_from(&mut request_buf) + .map_err(|e| Error::NetworkError { source: e })?; + + if len != request_buf.len() { + eprintln!("request had invalid size ({len}), skipping..."); + continue; + } + + let request = match request_buf.read() { + Ok(v) => v, + + Err(e) => { + eprintln!("invalid request provided: \"{e}\""); + continue; + } + }; + + let response = self.handle_request(request, client_addr)?; + + if let Some(response) = response { + response_buf + .write(&response) + .map_err(|e| Error::SerialiseError { source: e })?; + + socket + .send_to(&response_buf, client_addr) + .map_err(|e| Error::NetworkError { source: e })?; + } + } + } +} diff --git a/server/src/server/mod.rs b/server/src/server/mod.rs index 69b0842..0a7817f 100644 --- a/server/src/server/mod.rs +++ b/server/src/server/mod.rs @@ -1,15 +1,58 @@ // Copyright 2022-2024 Gabriel Bjørnager Jensen. +use crate::{Config, Session}; + +use bowshock::net::{ServerStatus, SessionToken}; +use rand::prelude::*; +use std::time::{SystemTime, UNIX_EPOCH}; + +mod handle_request; +mod listen; mod run; -pub struct Server; +pub struct Server { + config: Config, + + sessions: Vec<Session>, +} impl Server { #[must_use] - pub fn new() -> Self { Self } -} + pub fn new(config: Config) -> Self { + let sessions_len = config.max_player_count + .try_into() + .expect("`max_player_count` must be coercible to `usize`"); + + let sessions = Vec::with_capacity(sessions_len); + + Self { + config, + + sessions, + } + } + + #[must_use] + fn get_status(&self) -> ServerStatus { + let player_count = self.sessions + .len() + .try_into() + .expect("length of sessions must be coercible to `u32`"); + + let max_player_count = self.config.max_player_count; + + ServerStatus{ player_count, max_player_count } + } + + #[must_use] + fn generate_session_token(&self) -> SessionToken { + let time = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + let key = rand::thread_rng().gen(); -impl Default for Server { - #[inline(always)] - fn default() -> Self { Self::new() } + SessionToken { time, key } + } } diff --git a/server/src/server/run.rs b/server/src/server/run.rs index 55d81ee..06e19ec 100644 --- a/server/src/server/run.rs +++ b/server/src/server/run.rs @@ -1,26 +1,17 @@ // Copyright 2022-2024 Gabriel Bjørnager Jensen. -use crate::Server; +use crate::{Error, Result, Server}; -use bowshock::net::{DEFAULT_SERVER_PORT, Request}; -use bzipper::Buffer; use std::net::UdpSocket; impl Server { - pub fn run(self) -> Result<(), i32> { - let server_addr = format!("127.0.0.1:{DEFAULT_SERVER_PORT}"); - let socket = UdpSocket::bind(&server_addr).unwrap(); + pub fn run(mut self) -> Result<()> { + let server_addr = format!("127.0.0.1:{}", self.config.port); + let socket = UdpSocket::bind(&server_addr) + .map_err(|e| Error::NetworkError { source: e })?; - let mut buf = Buffer::<Request>::new(); + self.listen(&socket)?; - eprintln!("listening at {server_addr}..."); - - loop { - let (len, client_addr) = socket.recv_from(&mut buf).unwrap(); - assert_eq!(len, buf.len()); - - let request = buf.read().unwrap(); - eprintln!("got request {request:?} from {client_addr}"); - } + Ok(()) } } diff --git a/server/src/session/mod.rs b/server/src/session/mod.rs new file mode 100644 index 0000000..b6ea014 --- /dev/null +++ b/server/src/session/mod.rs @@ -0,0 +1,17 @@ +// Copyright 2022-2024 Gabriel Bjørnager Jensen. + +use bowshock::net::SessionToken; +use bzipper::FixedString; +use std::net::SocketAddr; + +pub struct Session { + pub token: SessionToken, + pub username: FixedString<0x10>, + + pub addr: SocketAddr, +} + +impl PartialEq<SessionToken> for Session { + #[inline(always)] + fn eq(&self, other: &SessionToken) -> bool { self.token == *other } +} |