diff options
| author | troido <troido@protonmail.com> | 2020-04-23 12:53:01 +0200 |
|---|---|---|
| committer | troido <troido@protonmail.com> | 2020-04-23 12:53:01 +0200 |
| commit | 080466200060d2d3ec64bec32a4959fa061b79ce (patch) | |
| tree | fbe602a0777583086b21799028f882d7b63d5c31 | |
| parent | b41c30fa15aea0b01b8fa30e378d123da046a1e6 (diff) | |
accept authentication messages, and validate registrations
| -rw-r--r-- | src/auth.rs | 113 | ||||
| -rw-r--r-- | src/config.rs | 3 | ||||
| -rw-r--r-- | src/encyclopedia.rs | 10 | ||||
| -rw-r--r-- | src/gameserver.rs | 109 | ||||
| -rw-r--r-- | src/main.rs | 9 | ||||
| -rw-r--r-- | src/persistence.rs | 19 | ||||
| -rw-r--r-- | src/util.rs | 25 |
7 files changed, 250 insertions, 38 deletions
diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..d5a2ffc --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,113 @@ + +use std::path::{PathBuf}; +use std::fs; +use std::env; +use std::io::ErrorKind; +use serde_json; + +use serde::{Serialize, Deserialize}; +use crate::{ + PlayerId, + errors::AnyError, + util::write_file_safe +}; + + +pub enum LoaderError { + MissingResource(AnyError), + InvalidResource(AnyError) +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum UserRole { + User, + Bridge +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct User { + pub name: String, + pub email: String, + pub pass_token: String, + pub role: UserRole +} + +macro_rules! inv { + ($code:expr) => {($code).map_err(|err| LoaderError::InvalidResource(Box::new(err)))} +} + + +pub trait UserRegistry { + + fn load_user(&self, id: &PlayerId) -> Result<User, LoaderError>; + + fn register_user(&self, id: &PlayerId, user: &User) -> Result<(), AnyError>; + + fn user_exists(&self, id: &PlayerId) -> bool { + match self.load_user(id) { + Ok(_) => true, + Err(LoaderError::InvalidResource(_)) => true, + Err(LoaderError::MissingResource(_)) => false + } + } +} + + +pub struct FileRegister { + directory: PathBuf +} + +impl FileRegister { + pub fn new(path: PathBuf) -> Self { + Self { + directory: path + } + } + + pub fn default_register_dir() -> Option<PathBuf> { + if let Some(pathname) = env::var_os("XDG_DATA_HOME") { + let mut path = PathBuf::from(pathname); + path.push("asciifarm"); + path.push("users"); + Some(path) + } else if let Some(pathname) = env::var_os("HOME") { + let mut path = PathBuf::from(pathname); + path.push(".asciifarm"); + path.push("users"); + Some(path) + } else { + None + } + } +} + +impl UserRegistry for FileRegister { + + fn load_user(&self, id: &PlayerId) -> Result<User, LoaderError> { + let mut path = self.directory.clone(); + let fname = id.to_string() + ".auth.json"; + path.push(fname); + let text = fs::read_to_string(path).map_err(|err| { + if err.kind() == ErrorKind::NotFound { + LoaderError::MissingResource(Box::new(err)) + } else { + LoaderError::InvalidResource(Box::new(err)) + } + })?; + let user: User = inv!(serde_json::from_str(&text))?; + Ok(user) + } + + fn register_user(&self, id: &PlayerId, user: &User) -> Result<(), AnyError> { + let mut path = self.directory.clone(); + fs::create_dir_all(&path)?; + let fname = id.to_string() + ".auth.json"; + path.push(fname); + let text = serde_json::to_string(user)?; + write_file_safe(path, text)?; + Ok(()) + } +} + + + diff --git a/src/config.rs b/src/config.rs index 9d2c480..708945d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,4 +16,7 @@ pub struct Config { #[structopt(short, long, env="ASCIIFARM_SAVE_DIR", help="The directory in which the savegames are")] pub save_dir: Option<PathBuf>, + #[structopt(short, long, env="ASCIIFARM_USER_DIR", help="The directory in which the user sign-in data lives")] + pub user_dir: Option<PathBuf>, + } diff --git a/src/encyclopedia.rs b/src/encyclopedia.rs index 0df7c70..ba9ad94 100644 --- a/src/encyclopedia.rs +++ b/src/encyclopedia.rs @@ -106,13 +106,13 @@ impl Encyclopedia { let assemblage_substitute = val .get("assemblage_substitute") .unwrap_or(&json!({})) - .as_object().ok_or(perr!("assemblage_subtitutions not a json dict"))? + .as_object().ok_or(perr!("assemblage_subtitute not a json dict"))? .iter() .chain( val .get("substitute") .unwrap_or(&json!({})) - .as_object().ok_or(perr!("substitutions not a json dict"))? + .as_object().ok_or(perr!("substitute not a json dict"))? .iter() ) .map(|(from, into)| { @@ -124,15 +124,15 @@ impl Encyclopedia { .collect::<PResult<HashMap<EntityType, EntityType>>>()?; let item_substitute = val - .get("assemblage_substitute") + .get("item_substitute") .unwrap_or(&json!({})) - .as_object().ok_or(perr!("assemblage_subtitutions not a json dict"))? + .as_object().ok_or(perr!("item_subtitute not a json dict"))? .iter() .chain( val .get("substitute") .unwrap_or(&json!({})) - .as_object().ok_or(perr!("substitutions not a json dict"))? + .as_object().ok_or(perr!("substitute not a json dict"))? .iter() ) .map(|(from, into)| { diff --git a/src/gameserver.rs b/src/gameserver.rs index c27d5d4..195989f 100644 --- a/src/gameserver.rs +++ b/src/gameserver.rs @@ -9,13 +9,20 @@ use unicode_categories::UnicodeCategories; use crate::{ controls::{Control, Action}, server::Server, - PlayerId + PlayerId, + auth::{UserRegistry, LoaderError} }; +#[derive(Debug, Clone, PartialEq)] +enum Authentication { + Guest, + Tilde, + Passtoken(String) +} #[derive(Debug)] enum Message { - Name(String), + Auth(String, Authentication), Chat(String), Input(Value) } @@ -36,15 +43,17 @@ macro_rules! merr { pub struct GameServer { players: HashMap<(usize, usize), PlayerId>, connections: HashMap<PlayerId, (usize, usize)>, + users: Box<dyn UserRegistry>, servers: Vec<Box<dyn Server>> } impl GameServer { - pub fn new(servers: Vec<Box<dyn Server>>) -> GameServer { + pub fn new(servers: Vec<Box<dyn Server>>, users: Box<dyn UserRegistry>) -> GameServer { GameServer { players: HashMap::new(), connections: HashMap::new(), - servers + servers, + users } } @@ -120,19 +129,14 @@ impl GameServer { fn handle_message(&mut self, (serverid, connectionid): (usize, usize), msg: Message) -> Result<Option<Action>, MessageError> { let id = (serverid, connectionid); match msg { - Message::Name(name) => { + Message::Auth(name, auth) => { if name.len() > 99 { return Err(merr!(name, "A name can not be longer than 99 bytes")); } if name.len() == 0 { return Err(merr!(name, "A name must have at least one character")); } - let (firstchar, username) = name.split_at(1); - if firstchar == "~" { - if Some(username.to_string()) != self.servers[serverid].get_name(connectionid) { - return Err(merr!(name, "A tilde name must match your username")); - } - } else { + if auth != Authentication::Tilde { for chr in name.chars() { if !(chr.is_letter() || chr.is_number() || chr.is_punctuation_connector()){ return Err(merr!(name, "A name can only contain letters, numbers and underscores")); @@ -143,16 +147,20 @@ impl GameServer { return Err(merr!(action, "You can not change your name")); } let player = PlayerId{name}; + self.authenticate(&player, auth, id)?; if self.connections.contains_key(&player) { return Err(merr!("nametaken", "Another connection to this player exists already")); } self.broadcast_message(&format!("{} connected", player.name)); self.players.insert(id, player.clone()); self.connections.insert(player.clone(), id); + if let Err(_) = self.send(&player, json!(["connected", format!("successfully connected as {}", &player.name)])){ + return Err(merr!("server", "unable to send connected message")) + } Ok(Some(Action::Join(player))) } Message::Chat(text) => { - let player = self.players.get(&id).ok_or(merr!(action, "Set a name before you send any other messages"))?; + let player = self.players.get(&id).ok_or(merr!(action, "Set a valid name before you send any other messages"))?; let name = player.name.clone(); self.broadcast_message(&format!("{}: {}", name, text)); Ok(None) @@ -164,6 +172,47 @@ impl GameServer { } } } + + fn authenticate(&self, player: &PlayerId, auth: Authentication, (serverid, connectionid): (usize, usize)) -> Result<(), MessageError> { + Ok(match auth { + Authentication::Guest => { + if self.users.user_exists(&player) { + return Err(merr!("registered", "This name is registered. Use another name or authenticate for this name")) + } + () + } + Authentication::Tilde => { + let (firstchar, username) = player.name.split_at(1); + if firstchar == "~" { + if Some(username.to_string()) != self.servers[serverid].get_name(connectionid) { + return Err(merr!(name, "A tilde name must match your username")); + } + } + } + Authentication::Passtoken(token) => { + match self.users.load_user(player) { + Ok(user) => { + if player.name != user.name { + println!("Name mismatch: user entry for {:?} has name {}", player, user.name); + return Err(merr!("server", "name mismatch")); + } + if token != user.pass_token { + println!("password mismatch: '{}' '{}'", token, user.pass_token); + return Err(merr!("invalidtoken", "invalid pass token")); + } + () + } + Err(LoaderError::InvalidResource(err)) => { + println!("failed to load user data for user '{}': {}", player.name, err); + return Err(merr!("server", "failed to load user data")) + } + Err(LoaderError::MissingResource(_)) => { + return Err(merr!("unregistered", "this name is not registered")) + } + } + } + }) + } } @@ -174,18 +223,46 @@ fn parse_message(msg: &str) -> Result<Message, MessageError> { if arr.len() < 2 { return Err(merr!(msg, "array not long enough")); } + let arg = &arr[1]; let msgtype = arr[0].as_str().ok_or(merr!(msg, "first message element not a string"))?; Ok(match msgtype { "name" => { - let name = arr[1].as_str().ok_or(merr!(msg, "name not a string"))?; - Message::Name(name.to_string()) + let name = arg.as_str().ok_or(merr!(msg, "name not a string"))?.to_string(); + Message::Auth( + name.clone(), + if name.starts_with("~") { + Authentication::Tilde + } else { + Authentication::Guest + } + ) } "chat" => { - let text = arr[1].as_str().ok_or(merr!(msg, "chat text not a string"))?; + let text = arg.as_str().ok_or(merr!(msg, "chat text not a string"))?; Message::Chat(text.escape_debug().to_string()) } "input" => { - Message::Input(arr[1].clone()) + Message::Input(arg.clone()) + } + "auth" => { + let name = arg.get("name").ok_or(merr!(msg, "auth message does not have name"))?.as_str().ok_or(merr!(msg, "auth name not a string"))?.to_string(); + let typ = arg.get("type").ok_or(merr!(msg, "auth message does not have type"))?.as_str().ok_or(merr!(msg, "auth type not a string"))?; + Message::Auth( + name, + match typ { + "guest" => Authentication::Guest, + "tilde" => Authentication::Tilde, + "passtoken" => Authentication::Passtoken( + arg + .get("passtoken") + .ok_or(merr!(msg, "passtoken auth message does not have passtoken"))? + .as_str() + .ok_or(merr!(msg, "passtoken not a string"))? + .to_string() + ), + _ => {return Err(merr!(msg, "invalid authentication type"))} + } + ) } _ => { return Err(merr!(msg, format!("unknown messsage type {:?}", msgtype))) diff --git a/src/main.rs b/src/main.rs index 3682e7c..7aef6cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,6 +36,7 @@ mod config; mod item; mod exchange; mod errors; +mod auth; use self::{ pos::Pos, @@ -77,7 +78,13 @@ fn main(){ .map(|a| a.to_server().unwrap()) .collect(); - let mut gameserver = GameServer::new(servers); + let user_dir = config.user_dir.unwrap_or( + auth::FileRegister::default_register_dir().expect("couldn't find any save directory") + ); + println!("user auth directory: {:?}", user_dir); + let users = auth::FileRegister::new(user_dir); + + let mut gameserver = GameServer::new(servers, Box::new(users)); let content_dir = config.content_dir.unwrap_or( PathBuf::new() diff --git a/src/persistence.rs b/src/persistence.rs index 1174ceb..2d927ee 100644 --- a/src/persistence.rs +++ b/src/persistence.rs @@ -1,5 +1,5 @@ -use std::path::{PathBuf, Path}; +use std::path::{PathBuf}; use std::fs; use std::env; use std::io::ErrorKind; @@ -12,7 +12,8 @@ use crate::{ playerstate::PlayerState, Timestamp, aerr, - errors::AnyError + errors::AnyError, + util::write_file_safe }; @@ -153,18 +154,4 @@ impl PersistentStorage for FileStorage { } } -fn write_file_safe<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<(), AnyError> { - let temppath = path - .as_ref() - .with_file_name( - format!( - "tempfile_{}_{}.tmp", - path.as_ref().file_name().ok_or(aerr!("writing to directory"))?.to_str().unwrap_or("invalid"), - rand::random::<u64>() - ) - ); - fs::write(&temppath, contents)?; - fs::rename(&temppath, path)?; - Ok(()) -} diff --git a/src/util.rs b/src/util.rs index 6a22031..02c282c 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,6 +1,7 @@ use std::cmp::{min, max}; + pub fn clamp<T: Ord>(val: T, lower: T, upper: T) -> T{ max(min(val, upper), lower) } @@ -14,6 +15,30 @@ pub fn strip_prefix<'a>(txt: &'a str, prefix: &'a str) -> Option<&'a str> { } } +use std::fs; +use std::path::Path; +use crate::{ + errors::AnyError, + aerr +}; + +pub fn write_file_safe<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<(), AnyError> { + let temppath = path + .as_ref() + .with_file_name( + format!( + "tempfile_{}_{}.tmp", + path.as_ref().file_name().ok_or(aerr!("writing to directory"))?.to_str().unwrap_or("invalid"), + rand::random::<u64>() + ) + ); + fs::write(&temppath, contents)?; + fs::rename(&temppath, path)?; + Ok(()) +} + + + #[macro_export] macro_rules! hashmap { ( $($key:expr => $value:expr ),* ) => {{ |
