Merge branch 'songtronix/integrate-auth' into 'master'

Implement authentication

See merge request veloren/veloren!690
This commit is contained in:
Acrimon 2020-03-08 23:36:25 +00:00
commit c5ea78371c
25 changed files with 1163 additions and 211 deletions

View File

@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New attack animation
- weapon control system
- Game pauses when in singleplayer and pause menu
- Added authentication system (to play on the official server register on https://account.veloren.net)
### Changed

688
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,9 @@ Currently the communication of contributors happens mainly on our [official Disc
## Useful Links
[Sign Up](https://account.veloren.net) - Here you can create an online account for Veloren.
This will be needed to play on auth-enabled servers, including the official server.
[The Book](https://book.veloren.net) - A collection of all important information relating to Veloren. It includes information on how to compile Veloren and how to contribute.
[Future Plans](https://gitlab.com/veloren/veloren/milestones) - Go here for information about Veloren's development roadmap and what we're currently working on.

View File

@ -65,6 +65,8 @@ VoxygenLocalization(
"common.disclaimer": "Disclaimer",
"common.cancel": "Cancel",
"common.none": "None",
"common.error": "Error",
"common.fatal_error": "Fatal Error",
// Message when connection to the server is lost
"common.connection_lost": r#"Connection lost!
@ -113,12 +115,25 @@ Thanks for taking the time to read this notice, we hope you enjoy the game!
// Login process description
"main.login_process": r#"Information on the Login Process:
Put in any username. No Account needed yet.
Character names and appearances will be saved locally.
If you are having issues signing in:
Levels/Items are not saved yet."#,
Please note that you now need an account
to play on auth-enabled servers.
You can create an account over at
https://account.veloren.net."#,
"main.login.server_not_found": "Server not found",
"main.login.authentication_error": "Auth error on server",
"main.login.server_full": "Server is full",
"main.login.untrusted_auth_server": "Auth server not trusted",
"main.login.outdated_client_or_server": "ServerWentMad: Probably versions are incompatible, check for updates.",
"main.login.timeout": "Timeout: Server did not respond in time. (Overloaded or network issues).",
"main.login.server_shut_down": "Server shut down",
"main.login.already_logged_in": "You are already logged into the server.",
"main.login.network_error": "Network error",
"main.login.failed_sending_request": "Request to Auth server failed",
"main.login.client_crashed": "Client crashed",
/// End Main screen section
@ -358,4 +373,4 @@ Willpower
"esc_menu.quit_game": "Quit Game",
/// End Escape Menu Section
}
)
)

View File

@ -51,7 +51,9 @@ fn main() {
println!("Players online: {:?}", client.get_players());
client
.register(comp::Player::new(username, None), password)
.register(username, password, |provider| {
provider == "https://auth.veloren.net"
})
.unwrap();
let (tx, rx) = mpsc::channel();

View File

@ -15,3 +15,4 @@ log = "0.4.8"
specs = "0.15.1"
vek = { version = "0.9.9", features = ["serde"] }
hashbrown = { version = "0.6.2", features = ["rayon", "serde", "nightly"] }
authc = { git = "https://gitlab.com/veloren/auth.git", rev = "65571ade0d954a0e0bd995fdb314854ff146ab97" }

View File

@ -1,3 +1,4 @@
use authc::AuthClientError;
use common::net::PostError;
#[derive(Debug)]
@ -7,11 +8,18 @@ pub enum Error {
ServerTimeout,
ServerShutdown,
TooManyPlayers,
InvalidAuth,
AlreadyLoggedIn,
AuthErr(String),
AuthClientError(AuthClientError),
AuthServerNotTrusted,
//TODO: InvalidAlias,
Other(String),
}
impl From<PostError> for Error {
fn from(err: PostError) -> Self { Error::Network(err) }
fn from(err: PostError) -> Self { Self::Network(err) }
}
impl From<AuthClientError> for Error {
fn from(err: AuthClientError) -> Self { Self::AuthClientError(err) }
}

View File

@ -5,6 +5,7 @@ pub mod error;
// Reexports
pub use crate::error::Error;
pub use authc::AuthClientError;
pub use specs::{
join::Join,
saveload::{Marker, MarkerAllocator},
@ -17,7 +18,7 @@ use common::{
event::{EventBus, SfxEvent, SfxEventItem},
msg::{
validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, PlayerListUpdate,
RequestStateError, ServerError, ServerInfo, ServerMsg, MAX_BYTES_CHAT_MSG,
RegisterError, RequestStateError, ServerInfo, ServerMsg, MAX_BYTES_CHAT_MSG,
},
net::PostBox,
state::State,
@ -86,13 +87,13 @@ impl Client {
let mut postbox = PostBox::to(addr)?;
// Wait for initial sync
let (state, entity, server_info, world_map) = match postbox.next_message() {
Some(ServerMsg::InitialSync {
let (state, entity, server_info, world_map) = match postbox.next_message()? {
ServerMsg::InitialSync {
entity_package,
server_info,
time_of_day,
world_map: (map_size, world_map),
}) => {
} => {
// TODO: Display that versions don't match in Voxygen
if server_info.git_hash != common::util::GIT_HASH.to_string() {
log::warn!(
@ -105,6 +106,8 @@ impl Client {
);
}
log::debug!("Auth Server: {:?}", server_info.auth_provider);
// Initialize `State`
let mut state = State::default();
let entity = state.ecs_mut().apply_entity_package(entity_package);
@ -129,9 +132,7 @@ impl Client {
(state, entity, server_info, (world_map, map_size))
},
Some(ServerMsg::Error(ServerError::TooManyPlayers)) => {
return Err(Error::TooManyPlayers);
},
ServerMsg::TooManyPlayers => return Err(Error::TooManyPlayers),
_ => return Err(Error::ServerWentMad),
};
@ -172,16 +173,40 @@ impl Client {
}
/// Request a state transition to `ClientState::Registered`.
pub fn register(&mut self, player: comp::Player, password: String) -> Result<(), Error> {
self.postbox
.send_message(ClientMsg::Register { player, password });
pub fn register(
&mut self,
username: String,
password: String,
mut auth_trusted: impl FnMut(&str) -> bool,
) -> Result<(), Error> {
// Authentication
let token_or_username = self.server_info.auth_provider.as_ref().map(|addr|
// Query whether this is a trusted auth server
if auth_trusted(&addr) {
Ok(authc::AuthClient::new(addr)
.sign_in(&username, &password)?
.serialize())
} else {
Err(Error::AuthServerNotTrusted)
}
).unwrap_or(Ok(username))?;
self.postbox.send_message(ClientMsg::Register {
view_distance: self.view_distance,
token_or_username,
});
self.client_state = ClientState::Pending;
loop {
match self.postbox.next_message() {
Some(ServerMsg::StateAnswer(Err((RequestStateError::Denied, _)))) => {
break Err(Error::InvalidAuth);
match self.postbox.next_message()? {
ServerMsg::StateAnswer(Err((RequestStateError::RegisterDenied(err), state))) => {
self.client_state = state;
break Err(match err {
RegisterError::AlreadyLoggedIn => Error::AlreadyLoggedIn,
RegisterError::AuthError(err) => Error::AuthErr(err),
});
},
Some(ServerMsg::StateAnswer(Ok(ClientState::Registered))) => break Ok(()),
ServerMsg::StateAnswer(Ok(ClientState::Registered)) => break Ok(()),
_ => {},
}
}
@ -546,10 +571,8 @@ impl Client {
if new_msgs.len() > 0 {
for msg in new_msgs {
match msg {
ServerMsg::Error(e) => match e {
ServerError::TooManyPlayers => return Err(Error::ServerWentMad),
ServerError::InvalidAuth => return Err(Error::InvalidAuth),
//TODO: ServerError::InvalidAlias => return Err(Error::InvalidAlias),
ServerMsg::TooManyPlayers => {
return Err(Error::ServerWentMad);
},
ServerMsg::Shutdown => return Err(Error::ServerShutdown),
ServerMsg::InitialSync { .. } => return Err(Error::ServerWentMad),
@ -692,10 +715,6 @@ impl Client {
self.client_state = state;
},
ServerMsg::StateAnswer(Err((error, state))) => {
if error == RequestStateError::Denied {
warn!("Connection denied!");
return Err(Error::InvalidAuth);
}
warn!(
"StateAnswer: {:?}. Server thinks client is in state {:?}.",
error, state

View File

@ -30,9 +30,10 @@ hashbrown = { version = "0.6.2", features = ["rayon", "serde", "nightly"] }
find_folder = "0.3.0"
parking_lot = "0.9.0"
crossbeam = "=0.7.2"
notify = "5.0.0-pre.1"
notify = "5.0.0-pre.2"
indexmap = "1.3.0"
sum_type = "0.2.0"
authc = { git = "https://gitlab.com/veloren/auth.git", rev = "65571ade0d954a0e0bd995fdb314854ff146ab97" }
[dev-dependencies]
criterion = "0.3"

View File

@ -1,3 +1,4 @@
use authc::Uuid;
use specs::{Component, FlaggedStorage, NullStorage};
use specs_idvs::IDVStorage;
@ -7,20 +8,26 @@ const MAX_ALIAS_LEN: usize = 32;
pub struct Player {
pub alias: String,
pub view_distance: Option<u32>,
uuid: Uuid,
}
impl Player {
pub fn new(alias: String, view_distance: Option<u32>) -> Self {
pub fn new(alias: String, view_distance: Option<u32>, uuid: Uuid) -> Self {
Self {
alias,
view_distance,
uuid,
}
}
pub fn is_valid(&self) -> bool {
self.alias.chars().all(|c| c.is_alphanumeric() || c == '_')
&& self.alias.len() <= MAX_ALIAS_LEN
pub fn is_valid(&self) -> bool { Self::alias_is_valid(&self.alias) }
pub fn alias_is_valid(alias: &str) -> bool {
alias.chars().all(|c| c.is_alphanumeric() || c == '_') && alias.len() <= MAX_ALIAS_LEN
}
/// Not to be confused with uid
pub fn uuid(&self) -> Uuid { self.uuid }
}
impl Component for Player {

View File

@ -4,8 +4,8 @@ use vek::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ClientMsg {
Register {
player: comp::Player,
password: String,
view_distance: Option<u32>,
token_or_username: String,
},
Character {
name: String,

View File

@ -6,7 +6,7 @@ pub mod server;
pub use self::{
client::ClientMsg,
ecs_packet::EcsCompPacket,
server::{PlayerListUpdate, RequestStateError, ServerError, ServerInfo, ServerMsg},
server::{PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, ServerMsg},
};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]

View File

@ -4,23 +4,17 @@ use crate::{
terrain::{Block, TerrainChunk},
ChatType,
};
use authc::AuthClientError;
use hashbrown::HashMap;
use vek::*;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum RequestStateError {
Denied,
Already,
Impossible,
WrongMessage,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerInfo {
pub name: String,
pub description: String,
pub git_hash: String,
pub git_date: String,
pub auth_provider: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -77,18 +71,31 @@ pub enum ServerMsg {
chunk: Result<Box<TerrainChunk>, ()>,
},
TerrainBlockUpdates(HashMap<Vec3<i32>, Block>),
Error(ServerError),
Disconnect,
Shutdown,
TooManyPlayers,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ServerError {
TooManyPlayers,
InvalidAuth,
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum RequestStateError {
RegisterDenied(RegisterError),
Denied,
Already,
Impossible,
WrongMessage,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum RegisterError {
AlreadyLoggedIn,
AuthError(String),
//TODO: InvalidAlias,
}
impl From<AuthClientError> for RegisterError {
fn from(err: AuthClientError) -> Self { Self::AuthError(err.to_string()) }
}
impl ServerMsg {
pub fn chat(message: String) -> ServerMsg {
ServerMsg::ChatMsg {

View File

@ -119,16 +119,16 @@ impl<S: PostMsg, R: PostMsg> PostBox<S, R> {
pub fn send_message(&mut self, msg: S) { let _ = self.send_tx.send(msg); }
pub fn next_message(&mut self) -> Option<R> {
if self.error.is_some() {
return None;
pub fn next_message(&mut self) -> Result<R, Error> {
if let Some(e) = self.error.clone() {
return Err(e);
}
match self.recv_rx.recv().ok()? {
Ok(msg) => Some(msg),
match self.recv_rx.recv().map_err(|_| Error::ChannelFailure)? {
Ok(msg) => Ok(msg),
Err(e) => {
self.error = Some(e);
None
self.error = Some(e.clone());
Err(e)
},
}
}

View File

@ -31,3 +31,4 @@ prometheus = "0.7"
prometheus-static-metric = "0.2"
rouille = "3.0.0"
portpicker = { git = "https://github.com/wusyong/portpicker-rs", branch = "fix_ipv6" }
authc = { git = "https://gitlab.com/veloren/auth.git", rev = "65571ade0d954a0e0bd995fdb314854ff146ab97" }

View File

@ -1,32 +1,88 @@
use authc::{AuthClient, AuthToken, Uuid};
use common::msg::RegisterError;
use hashbrown::HashMap;
use log::{info, warn};
use log::error;
use std::str::FromStr;
fn derive_uuid(username: &str) -> Uuid {
let mut state: [u8; 16] = [
52, 17, 19, 239, 52, 17, 19, 239, 52, 17, 19, 239, 52, 17, 19, 239,
];
for mix_byte_1 in username.as_bytes() {
for i in 0..16 {
let mix_byte_step: u8 = mix_byte_1
.wrapping_pow(239)
.wrapping_mul((i as u8).wrapping_pow(43));
let mix_byte_2 = state[(i + mix_byte_step as usize) % 16];
let rot_step: u8 = mix_byte_1
.wrapping_pow(29)
.wrapping_mul((i as u8).wrapping_pow(163));
state[i] = (state[i] ^ mix_byte_1)
.wrapping_mul(mix_byte_2)
.rotate_left(rot_step as u32);
}
}
Uuid::from_slice(&state).unwrap()
}
pub struct AuthProvider {
accounts: HashMap<String, String>,
accounts: HashMap<Uuid, String>,
auth_server: Option<AuthClient>,
}
impl AuthProvider {
pub fn new() -> Self {
pub fn new(auth_addr: Option<String>) -> Self {
let auth_server = match auth_addr {
Some(addr) => Some(AuthClient::new(addr)),
None => None,
};
AuthProvider {
accounts: HashMap::new(),
auth_server,
}
}
pub fn query(&mut self, username: String, password: String) -> bool {
let pwd = password.clone();
if self.accounts.entry(username.clone()).or_insert_with(|| {
info!("Registered new user '{}'", &username);
pwd
}) == &password
{
info!("User '{}' successfully authenticated", username);
true
} else {
warn!(
"User '{}' attempted to log in with invalid password '{}'!",
username, password
);
false
pub fn logout(&mut self, uuid: Uuid) {
if self.accounts.remove(&uuid).is_none() {
error!("Attempted to logout user that is not logged in.");
};
}
pub fn query(&mut self, username_or_token: String) -> Result<(String, Uuid), RegisterError> {
// Based on whether auth server is provided or not we expect an username or
// token
match &self.auth_server {
// Token from auth server expected
Some(srv) => {
log::info!("Validating '{}' token.", &username_or_token);
// Parse token
let token = AuthToken::from_str(&username_or_token)
.map_err(|e| RegisterError::AuthError(e.to_string()))?;
// Validate token
let uuid = srv.validate(token)?;
// Check if already logged in
if self.accounts.contains_key(&uuid) {
return Err(RegisterError::AlreadyLoggedIn);
}
// Log in
let username = srv.uuid_to_username(uuid)?;
self.accounts.insert(uuid, username.clone());
Ok((username, uuid))
},
// Username is expected
None => {
// Assume username was provided
let username = username_or_token;
let uuid = derive_uuid(&username);
if !self.accounts.contains_key(&uuid) {
log::info!("New User '{}'", username);
self.accounts.insert(uuid, username.clone());
Ok((username, uuid))
} else {
Err(RegisterError::AlreadyLoggedIn)
}
},
}
}
}

View File

@ -27,7 +27,7 @@ use common::{
assets, comp,
effect::Effect,
event::{EventBus, ServerEvent},
msg::{ClientMsg, ClientState, ServerError, ServerInfo, ServerMsg},
msg::{ClientMsg, ClientState, ServerInfo, ServerMsg},
net::PostOffice,
state::{State, TimeOfDay},
sync::{Uid, WorldSyncExt},
@ -86,7 +86,9 @@ impl Server {
let mut state = State::default();
state.ecs_mut().insert(EventBus::<ServerEvent>::default());
// TODO: anything but this
state.ecs_mut().insert(AuthProvider::new());
state
.ecs_mut()
.insert(AuthProvider::new(settings.auth_server_address.clone()));
state.ecs_mut().insert(Tick(0));
state.ecs_mut().insert(ChunkGenerator::new());
// System timers for performance monitoring
@ -196,6 +198,7 @@ impl Server {
description: settings.server_description.clone(),
git_hash: common::util::GIT_HASH.to_string(),
git_date: common::util::GIT_DATE.to_string(),
auth_provider: settings.auth_server_address.clone(),
},
metrics: ServerMetrics::new(settings.metrics_address)
.expect("Failed to initialize server metrics submodule."),
@ -502,7 +505,7 @@ impl Server {
<= self.state.ecs().read_storage::<Client>().join().count()
{
// Note: in this case the client is dropped
client.notify(ServerMsg::Error(ServerError::TooManyPlayers));
client.notify(ServerMsg::TooManyPlayers);
} else {
let entity = self
.state

View File

@ -10,12 +10,12 @@ const DEFAULT_WORLD_SEED: u32 = 5284;
pub struct ServerSettings {
pub gameserver_address: SocketAddr,
pub metrics_address: SocketAddr,
pub auth_server_address: Option<String>,
pub max_players: usize,
pub world_seed: u32,
//pub pvp_enabled: bool,
pub server_name: String,
pub server_description: String,
//pub login_server: whatever
pub start_time: f64,
pub admins: Vec<String>,
/// When set to None, loads the default map file (if available); otherwise,
@ -28,6 +28,7 @@ impl Default for ServerSettings {
Self {
gameserver_address: SocketAddr::from(([0; 4], 14004)),
metrics_address: SocketAddr::from(([0; 4], 14005)),
auth_server_address: Some("https://auth.veloren.net".into()),
world_seed: DEFAULT_WORLD_SEED,
server_name: "Veloren Alpha".to_owned(),
server_description: "This is the best Veloren server.".to_owned(),
@ -107,6 +108,7 @@ impl ServerSettings {
[127, 0, 0, 1],
pick_unused_port().expect("Failed to find unused port!"),
)),
auth_server_address: None,
// If loading the default map file, make sure the seed is also default.
world_seed: if load.map_file.is_some() {
load.world_seed

View File

@ -122,12 +122,27 @@ impl<'a> System<'a> for Sys {
},
ClientState::Pending => {},
},
// Valid player
ClientMsg::Register { player, password } if player.is_valid() => {
if !accounts.query(player.alias.clone(), password) {
client.error_state(RequestStateError::Denied);
// Request registered state (login)
ClientMsg::Register {
view_distance,
token_or_username,
} => {
let (username, uuid) = match accounts.query(token_or_username.clone()) {
Err(err) => {
client.error_state(RequestStateError::RegisterDenied(err));
break;
},
Ok((username, uuid)) => (username, uuid),
};
let player = Player::new(username, view_distance, uuid);
if !player.is_valid() {
// Invalid player
client.error_state(RequestStateError::Impossible);
break;
}
match client.client_state {
ClientState::Connected => {
// Add Player component to this client
@ -148,8 +163,6 @@ impl<'a> System<'a> for Sys {
}
//client.allow_state(ClientState::Registered);
},
// Invalid player
ClientMsg::Register { .. } => client.error_state(RequestStateError::Impossible),
ClientMsg::SetViewDistance(view_distance) => match client.client_state {
ClientState::Character { .. } => {
players
@ -287,6 +300,7 @@ impl<'a> System<'a> for Sys {
None,
ServerMsg::broadcast(format!("{} went offline.", &player.alias)),
));
accounts.logout(player.uuid());
}
server_emitter.emit(ServerEvent::ClientDisconnect(entity));
client.postbox.send_message(ServerMsg::Disconnect);

View File

@ -24,8 +24,8 @@ gfx_device_gl = { version = "0.16.2", optional = true }
gfx_window_glutin = "0.31.0"
glutin = "0.21.1"
winit = { version = "0.19.4", features = ["serde"] }
conrod_core = { git = "https://gitlab.com/veloren/conrod.git" }
conrod_winit = { git = "https://gitlab.com/veloren/conrod.git" }
conrod_core = { git = "https://gitlab.com/veloren/conrod.git", branch = "hide_text" }
conrod_winit = { git = "https://gitlab.com/veloren/conrod.git", branch = "hide_text" }
euc = "0.3.0"
# ECS
@ -60,10 +60,10 @@ cpal = "0.10"
crossbeam = "=0.7.2"
hashbrown = { version = "0.6.2", features = ["rayon", "serde", "nightly"] }
chrono = "0.4.9"
rust-argon2 = "0.5"
bincode = "1.2"
deunicode = "1.0"
uvth = "3.1.1"
authc = { git = "https://gitlab.com/veloren/auth.git", rev = "65571ade0d954a0e0bd995fdb314854ff146ab97" }
[target.'cfg(target_os = "macos")'.dependencies]
dispatch = "0.1.4"

View File

@ -1,6 +1,6 @@
use client::{error::Error as ClientError, Client};
use common::{comp, net::PostError};
use crossbeam::channel::{unbounded, Receiver, TryRecvError};
use common::net::PostError;
use crossbeam::channel::{unbounded, Receiver, Sender, TryRecvError};
use std::{
net::ToSocketAddrs,
sync::{
@ -15,32 +15,39 @@ use std::{
pub enum Error {
// Error parsing input string or error resolving host name.
BadAddress(std::io::Error),
// Parsing/host name resolution successful but could not connect.
#[allow(dead_code)]
ConnectionFailed(ClientError),
// Parsing/host name resolution successful but there was an error within the client.
ClientError(ClientError),
// Parsing yielded an empty iterator (specifically to_socket_addrs()).
NoAddress,
InvalidAuth,
ClientCrashed,
ServerIsFull,
}
pub enum Msg {
IsAuthTrusted(String),
Done(Result<Client, Error>),
}
pub struct AuthTrust(String, bool);
// Used to asynchronously parse the server address, resolve host names,
// and create the client (which involves establishing a connection to the
// server).
pub struct ClientInit {
rx: Receiver<Result<Client, Error>>,
rx: Receiver<Msg>,
trust_tx: Sender<AuthTrust>,
cancel: Arc<AtomicBool>,
}
impl ClientInit {
pub fn new(
connection_args: (String, u16, bool),
player: comp::Player,
username: String,
view_distance: Option<u32>,
password: String,
) -> Self {
let (server_address, default_port, prefer_ipv6) = connection_args;
let (tx, rx) = unbounded();
let (trust_tx, trust_rx) = unbounded();
let cancel = Arc::new(AtomicBool::new(false));
let cancel2 = Arc::clone(&cancel);
@ -66,40 +73,39 @@ impl ClientInit {
for socket_addr in
first_addrs.clone().into_iter().chain(second_addrs.clone())
{
match Client::new(socket_addr, player.view_distance) {
match Client::new(socket_addr, view_distance) {
Ok(mut client) => {
if let Err(ClientError::InvalidAuth) =
client.register(player.clone(), password.clone())
if let Err(err) =
client.register(username, password, |auth_server| {
let _ = tx
.send(Msg::IsAuthTrusted(auth_server.to_string()));
trust_rx
.recv()
.map(|AuthTrust(server, trust)| {
trust && &server == auth_server
})
.unwrap_or(false)
})
{
last_err = Some(Error::InvalidAuth);
break;
last_err = Some(Error::ClientError(err));
break 'tries;
}
//client.register(player, password);
let _ = tx.send(Ok(client));
let _ = tx.send(Msg::Done(Ok(client)));
return;
},
Err(err) => {
match err {
ClientError::Network(PostError::Bincode(_)) => {
last_err = Some(Error::ConnectionFailed(err));
last_err = Some(Error::ClientError(err));
break 'tries;
},
// Assume the connection failed and try again soon
ClientError::Network(_) => {},
ClientError::TooManyPlayers => {
last_err = Some(Error::ServerIsFull);
// Non-connection error, stop attempts
err => {
last_err = Some(Error::ClientError(err));
break 'tries;
},
ClientError::InvalidAuth => {
last_err = Some(Error::InvalidAuth);
break 'tries;
},
// TODO: Handle errors?
_ => panic!(
"Unexpected non-network error when creating client: \
{:?}",
err
),
}
},
}
@ -107,29 +113,38 @@ impl ClientInit {
thread::sleep(Duration::from_secs(5));
}
// Parsing/host name resolution successful but no connection succeeded.
let _ = tx.send(Err(last_err.unwrap_or(Error::NoAddress)));
let _ = tx.send(Msg::Done(Err(last_err.unwrap_or(Error::NoAddress))));
},
Err(err) => {
// Error parsing input string or error resolving host name.
let _ = tx.send(Err(Error::BadAddress(err)));
let _ = tx.send(Msg::Done(Err(Error::BadAddress(err))));
},
}
});
ClientInit { rx, cancel }
ClientInit {
rx,
trust_tx,
cancel,
}
}
/// Poll if the thread is complete.
/// Returns None if the thread is still running, otherwise returns the
/// Result of client creation.
pub fn poll(&self) -> Option<Result<Client, Error>> {
pub fn poll(&self) -> Option<Msg> {
match self.rx.try_recv() {
Ok(result) => Some(result),
Ok(msg) => Some(msg),
Err(TryRecvError::Empty) => None,
Err(TryRecvError::Disconnected) => Some(Err(Error::ClientCrashed)),
Err(TryRecvError::Disconnected) => Some(Msg::Done(Err(Error::ClientCrashed))),
}
}
/// Report trust status of auth server
pub fn auth_trust(&self, auth_server: String, trusted: bool) {
let _ = self.trust_tx.send(AuthTrust(auth_server, trusted));
}
pub fn cancel(&mut self) { self.cancel.store(true, Ordering::Relaxed); }
}

View File

@ -3,15 +3,11 @@ mod client_init;
use super::char_selection::CharSelectionState;
use crate::{
i18n::{i18n_asset_key, VoxygenLocalization},
singleplayer::Singleplayer,
window::Event,
Direction, GlobalState, PlayState, PlayStateResult,
singleplayer::Singleplayer, window::Event, Direction, GlobalState, PlayState, PlayStateResult,
};
use argon2::{self, Config};
use client_init::{ClientInit, Error as InitError};
use client_init::{ClientInit, Error as InitError, Msg as InitMsg};
use common::{assets::load_expect, clock::Clock, comp};
use log::warn;
use log::{error, warn};
#[cfg(feature = "singleplayer")]
use std::time::Duration;
use ui::{Event as MainMenuEvent, MainMenuUi};
@ -47,6 +43,10 @@ impl PlayState for MainMenuState {
// Reset singleplayer server if it was running already
global_state.singleplayer = None;
let localized_strings = load_expect::<crate::i18n::VoxygenLocalization>(
&crate::i18n::i18n_asset_key(&global_state.settings.language.selected_language),
);
loop {
// Handle window events.
for event in global_state.window.fetch_events(&mut global_state.settings) {
@ -65,7 +65,7 @@ impl PlayState for MainMenuState {
// Poll client creation.
match client_init.as_ref().and_then(|init| init.poll()) {
Some(Ok(mut client)) => {
Some(InitMsg::Done(Ok(mut client))) => {
self.main_menu_ui.connected();
// Register voxygen components / resources
crate::ecs::init(client.state_mut().ecs_mut());
@ -74,18 +74,82 @@ impl PlayState for MainMenuState {
std::rc::Rc::new(std::cell::RefCell::new(client)),
)));
},
Some(Err(err)) => {
Some(InitMsg::Done(Err(err))) => {
client_init = None;
global_state.info_message = Some(
match err {
InitError::BadAddress(_) | InitError::NoAddress => "Server not found",
InitError::InvalidAuth => "Invalid credentials",
InitError::ServerIsFull => "Server is full",
InitError::ConnectionFailed(_) => "Connection failed",
InitError::ClientCrashed => "Client crashed",
}
.to_string(),
);
global_state.info_message = Some({
let err = match err {
InitError::BadAddress(_) | InitError::NoAddress => {
localized_strings.get("main.login.server_not_found").into()
},
InitError::ClientError(err) => match err {
client::Error::AuthErr(e) => format!(
"{}: {}",
localized_strings.get("main.login.authentication_error"),
e
),
client::Error::TooManyPlayers => {
localized_strings.get("main.login.server_full").into()
},
client::Error::AuthServerNotTrusted => localized_strings
.get("main.login.untrusted_auth_server")
.into(),
client::Error::ServerWentMad => localized_strings
.get("main.login.outdated_client_or_server")
.into(),
client::Error::ServerTimeout => {
localized_strings.get("main.login.timeout").into()
},
client::Error::ServerShutdown => {
localized_strings.get("main.login.server_shut_down").into()
},
client::Error::AlreadyLoggedIn => {
localized_strings.get("main.login.already_logged_in").into()
},
client::Error::Network(e) => format!(
"{}: {:?}",
localized_strings.get("main.login.network_error"),
e
),
client::Error::Other(e) => {
format!("{}: {}", localized_strings.get("common.error"), e)
},
client::Error::AuthClientError(e) => match e {
client::AuthClientError::JsonError(e) => format!(
"{}: {}",
localized_strings.get("common.fatal_error"),
e
),
client::AuthClientError::RequestError(_) => format!(
"{}: {}",
localized_strings.get("main.login.failed_sending_request"),
e
),
client::AuthClientError::ServerError(_, e) => format!("{}", e),
},
},
InitError::ClientCrashed => {
localized_strings.get("main.login.client_crashed").into()
},
};
// Log error for possible additional use later or incase that the error
// displayed is cut of.
error!("{}", err);
err
});
},
Some(InitMsg::IsAuthTrusted(auth_server)) => {
if global_state
.settings
.networking
.trusted_auth_servers
.contains(&auth_server)
{
// Can't fail since we just polled it, it must be Some
client_init.as_ref().unwrap().auth_trust(auth_server, true);
} else {
// Show warning that auth server is not trusted and prompt for approval
self.main_menu_ui.auth_trust_prompt(auth_server);
}
},
None => {},
}
@ -141,15 +205,24 @@ impl PlayState for MainMenuState {
MainMenuEvent::DisclaimerClosed => {
global_state.settings.show_disclaimer = false
},
MainMenuEvent::AuthServerTrust(auth_server, trust) => {
if trust {
global_state
.settings
.networking
.trusted_auth_servers
.insert(auth_server.clone());
global_state.settings.save_to_file_warn();
}
client_init
.as_ref()
.map(|init| init.auth_trust(auth_server, trust));
},
}
}
let localized_strings = load_expect::<VoxygenLocalization>(&i18n_asset_key(
&global_state.settings.language.selected_language,
));
if let Some(info) = global_state.info_message.take() {
self.main_menu_ui
.show_info(info, localized_strings.get("common.okay").to_owned());
self.main_menu_ui.show_info(info);
}
// Draw the UI to the screen.
@ -190,25 +263,17 @@ fn attempt_login(
warn!("Failed to save settings: {:?}", err);
}
let player = comp::Player::new(
username.clone(),
Some(global_state.settings.graphics.view_distance),
);
if player.is_valid() {
if comp::Player::alias_is_valid(&username) {
// Don't try to connect if there is already a connection in progress.
if client_init.is_none() {
*client_init = Some(ClientInit::new(
(server_address, server_port, false),
player,
{
let salt = b"staticsalt_zTuGkGvybZIjZbNUDtw15";
let config = Config::default();
argon2::hash_encoded(password.as_bytes(), salt, &config).unwrap()
},
username,
Some(global_state.settings.graphics.view_distance),
{ password },
));
}
} else {
global_state.info_message = Some("Invalid username or password".to_string());
global_state.info_message = Some("Invalid username".to_string());
}
}

View File

@ -68,7 +68,9 @@ widget_ids! {
// Info Window
info_frame,
info_text,
info_bottom
info_bottom,
// Auth Trust Prompt
button_add_auth_trust,
}
}
@ -90,7 +92,7 @@ image_ids! {
button_hover: "voxygen.element.buttons.button_hover",
button_press: "voxygen.element.buttons.button_press",
input_bg_top: "voxygen.element.misc_bg.textbox_top",
//input_bg_mid: "voxygen.element.misc_bg.textbox_mid", <-- For password input
input_bg_mid: "voxygen.element.misc_bg.textbox_mid",
input_bg_bot: "voxygen.element.misc_bg.textbox_bot",
@ -122,16 +124,17 @@ pub enum Event {
Quit,
Settings,
DisclaimerClosed,
AuthServerTrust(String, bool),
}
pub enum PopupType {
Error,
ConnectionInfo,
AuthTrustPrompt(String),
}
pub struct PopupData {
msg: String,
button_text: String,
popup_type: PopupType,
}
@ -261,27 +264,51 @@ impl MainMenuUi {
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(14))
.set(self.ids.version, ui_widgets);
// Popup (Error/Info)
if let Some(popup_data) = &self.popup {
let text = Text::new(&popup_data.msg)
.rgba(1.0, 1.0, 1.0, if self.connect { fade_msg } else { 1.0 })
// Popup (Error/Info/AuthTrustPrompt)
let mut change_popup = None;
if let Some(PopupData { msg, popup_type }) = &self.popup {
let text = Text::new(msg)
.rgba(
1.0,
1.0,
1.0,
if let PopupType::ConnectionInfo = popup_type {
fade_msg
} else {
1.0
},
)
.font_id(self.fonts.cyri.conrod_id);
Rectangle::fill_with([65.0 * 6.0, 140.0], color::TRANSPARENT)
let (frame_w, frame_h) = if let PopupType::AuthTrustPrompt(_) = popup_type {
(65.0 * 8.0, 370.0)
} else {
(65.0 * 6.0, 140.0)
};
let error_bg = Rectangle::fill_with([frame_w, frame_h], color::TRANSPARENT)
.rgba(0.1, 0.1, 0.1, if self.connect { 0.0 } else { 1.0 })
.parent(ui_widgets.window)
.up_from(self.ids.banner_top, 15.0)
.set(self.ids.login_error_bg, ui_widgets);
.parent(ui_widgets.window);
if let PopupType::AuthTrustPrompt(_) = popup_type {
error_bg.middle_of(ui_widgets.window)
} else {
error_bg.up_from(self.ids.banner_top, 15.0)
}
.set(self.ids.login_error_bg, ui_widgets);
Image::new(self.imgs.info_frame)
.w_h(65.0 * 6.0, 140.0)
.w_h(frame_w, frame_h)
.color(Some(Color::Rgba(
1.0,
1.0,
1.0,
if self.connect { 0.0 } else { 1.0 },
if let PopupType::ConnectionInfo = popup_type {
0.0
} else {
1.0
},
)))
.middle_of(self.ids.login_error_bg)
.set(self.ids.error_frame, ui_widgets);
if self.connect {
if let PopupType::ConnectionInfo = popup_type {
text.mid_top_with_margin_on(self.ids.error_frame, 10.0)
.font_id(self.fonts.alkhemi.conrod_id)
.bottom_left_with_margins_on(ui_widgets.window, 60.0, 60.0)
@ -289,6 +316,7 @@ impl MainMenuUi {
.set(self.ids.login_error, ui_widgets);
} else {
text.mid_top_with_margin_on(self.ids.error_frame, 10.0)
.w(frame_w - 10.0 * 2.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(25))
.set(self.ids.login_error, ui_widgets);
@ -296,7 +324,7 @@ impl MainMenuUi {
if Button::image(self.imgs.button)
.w_h(100.0, 30.0)
.mid_bottom_with_margin_on(
if self.connect {
if let PopupType::ConnectionInfo = popup_type {
ui_widgets.window
} else {
self.ids.login_error_bg
@ -306,22 +334,55 @@ impl MainMenuUi {
.hover_image(self.imgs.button_hover)
.press_image(self.imgs.button_press)
.label_y(Relative::Scalar(2.0))
.label(&popup_data.button_text)
.label(match popup_type {
PopupType::Error => self.voxygen_i18n.get("common.okay"),
PopupType::ConnectionInfo => self.voxygen_i18n.get("common.cancel"),
PopupType::AuthTrustPrompt(_) => self.voxygen_i18n.get("common.cancel"),
})
.label_font_id(self.fonts.cyri.conrod_id)
.label_font_size(self.fonts.cyri.scale(15))
.label_color(TEXT_COLOR)
.set(self.ids.button_ok, ui_widgets)
.was_clicked()
{
match popup_data.popup_type {
match &popup_type {
PopupType::Error => (),
PopupType::ConnectionInfo => {
events.push(Event::CancelLoginAttempt);
},
_ => (),
PopupType::AuthTrustPrompt(auth_server) => {
events.push(Event::AuthServerTrust(auth_server.clone(), false));
},
};
self.popup = None;
};
change_popup = Some(None);
}
if let PopupType::AuthTrustPrompt(auth_server) = popup_type {
if Button::image(self.imgs.button)
.w_h(100.0, 30.0)
.right_from(self.ids.button_ok, 10.0)
.hover_image(self.imgs.button_hover)
.press_image(self.imgs.button_press)
.label_y(Relative::Scalar(2.0))
.label("Add") // TODO: localize
.label_font_id(self.fonts.cyri.conrod_id)
.label_font_size(self.fonts.cyri.scale(15))
.label_color(TEXT_COLOR)
.set(self.ids.button_add_auth_trust, ui_widgets)
.was_clicked()
{
events.push(Event::AuthServerTrust(auth_server.clone(), true));
change_popup = Some(Some(PopupData {
msg: self.voxygen_i18n.get("main.connecting").into(),
popup_type: PopupType::ConnectionInfo,
}));
}
}
}
if let Some(p) = change_popup {
self.popup = p;
}
if !self.connect {
Image::new(self.imgs.banner)
.w_h(65.0 * 6.0, 100.0 * 6.0)
@ -387,7 +448,6 @@ impl MainMenuUi {
self.connecting = Some(std::time::Instant::now());
self.popup = Some(PopupData {
msg: [self.voxygen_i18n.get("main.connecting"), "..."].concat(),
button_text: self.voxygen_i18n.get("common.cancel").to_owned(),
popup_type: PopupType::ConnectionInfo,
});
@ -399,7 +459,7 @@ impl MainMenuUi {
};
}
// Info Window
Rectangle::fill_with([550.0, 200.0], color::BLACK)
Rectangle::fill_with([550.0, 400.0], color::BLACK)
.top_left_with_margins_on(ui_widgets.window, 40.0, 40.0)
.color(Color::Rgba(0.0, 0.0, 0.0, 0.95))
.set(self.ids.info_frame, ui_widgets);
@ -425,7 +485,6 @@ impl MainMenuUi {
self.connecting = Some(std::time::Instant::now());
self.popup = Some(PopupData {
msg: [self.voxygen_i18n.get("main.creating_world"), "..."].concat(),
button_text: self.voxygen_i18n.get("common.cancel").to_owned(),
popup_type: PopupType::ConnectionInfo,
});
};
@ -461,14 +520,12 @@ impl MainMenuUi {
}
}
// Password
// TODO: REACTIVATE THIS WHEN A PROPER ACCOUNT SYSTEM IS IN PLACE
/*Rectangle::fill_with([320.0, 50.0], color::rgba(0.0, 0.0, 0.0, 0.97))
Rectangle::fill_with([320.0, 50.0], color::rgba(0.0, 0.0, 0.0, 0.97))
.down_from(self.ids.usrnm_bg, 30.0)
.set(self.ids.passwd_bg, ui_widgets);
Image::new(self.imgs.input_bg_mid)
.w_h(337.0, 67.0)
.middle_of(self.ids.passwd_bg)
.color(Some(INACTIVE))
.set(self.ids.password_bg, ui_widgets);
for event in TextBox::new(&self.password)
.w_h(290.0, 30.0)
@ -479,18 +536,20 @@ impl MainMenuUi {
// transparent background
.color(TRANSPARENT)
.border_color(TRANSPARENT)
.hide_text("*")
.set(self.ids.password_field, ui_widgets)
{
match event {
TextBoxEvent::Update(password) => {
// Note: TextBox limits the input string length to what fits in it
self.password = password;
}
},
TextBoxEvent::Enter => {
login!();
}
},
}
}*/
}
if self.show_servers {
Image::new(self.imgs.info_frame)
.mid_top_with_margin_on(self.ids.username_bg, -320.0)
@ -556,7 +615,7 @@ impl MainMenuUi {
}
// Server address
Rectangle::fill_with([320.0, 50.0], color::rgba(0.0, 0.0, 0.0, 0.97))
.down_from(self.ids.usrnm_bg, 30.0)
.down_from(self.ids.passwd_bg, 30.0)
.set(self.ids.srvr_bg, ui_widgets);
Image::new(self.imgs.input_bg_bot)
.w_h(337.0, 67.0)
@ -582,6 +641,7 @@ impl MainMenuUi {
},
}
}
// Login button
if Button::image(self.imgs.button)
.hover_image(self.imgs.button_hover)
@ -684,10 +744,22 @@ impl MainMenuUi {
events
}
pub fn show_info(&mut self, msg: String, button_text: String) {
pub fn auth_trust_prompt(&mut self, auth_server: String) {
self.popup = Some(PopupData {
msg: format!(
"Warning: The server you are trying to connect to has provided this \
authentication server address:\n\n{}\n\nbut it is not in your list of trusted \
authentication servers.\n\nMake sure that you trust this site and owner to not \
try and bruteforce your password!",
&auth_server
),
popup_type: PopupType::AuthTrustPrompt(auth_server),
})
}
pub fn show_info(&mut self, msg: String) {
self.popup = Some(PopupData {
msg,
button_text,
popup_type: PopupType::Error,
});
self.connecting = None;

View File

@ -7,6 +7,7 @@ use crate::{
};
use directories::{ProjectDirs, UserDirs};
use glutin::{MouseButton, VirtualKeyCode};
use hashbrown::HashSet;
use log::warn;
use serde_derive::{Deserialize, Serialize};
use std::{fs, io::prelude::*, path::PathBuf};
@ -157,6 +158,7 @@ pub struct NetworkingSettings {
pub password: String,
pub servers: Vec<String>,
pub default_server: usize,
pub trusted_auth_servers: HashSet<String>,
}
impl Default for NetworkingSettings {
@ -166,6 +168,10 @@ impl Default for NetworkingSettings {
password: String::default(),
servers: vec!["server.veloren.net".to_string()],
default_server: 0,
trusted_auth_servers: ["https://auth.veloren.net"]
.iter()
.map(|s| s.to_string())
.collect(),
}
}
}

View File

@ -28,5 +28,5 @@ serde_derive = "1.0.102"
ron = "0.5.1"
[dev-dependencies]
minifb = { git = "https://github.com/emoon/rust_minifb.git" }
pretty_env_logger = "0.3.0"
minifb = "0.14.0"