summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authortroido <troido@protonmail.com>2020-04-23 12:53:01 +0200
committertroido <troido@protonmail.com>2020-04-23 12:53:01 +0200
commit080466200060d2d3ec64bec32a4959fa061b79ce (patch)
treefbe602a0777583086b21799028f882d7b63d5c31 /src
parentb41c30fa15aea0b01b8fa30e378d123da046a1e6 (diff)
accept authentication messages, and validate registrations
Diffstat (limited to 'src')
-rw-r--r--src/auth.rs113
-rw-r--r--src/config.rs3
-rw-r--r--src/encyclopedia.rs10
-rw-r--r--src/gameserver.rs109
-rw-r--r--src/main.rs9
-rw-r--r--src/persistence.rs19
-rw-r--r--src/util.rs25
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 ),* ) => {{