State implementation and initial db work
State system is fully implemented and passing tests. I added in a handful of unit tests to test most of the available operations. Other work has involved getting the interface between client and server right and starting on the actual message handler. This involves starting work on the db as well
This commit is contained in:
Generated
+45
-2
@@ -81,6 +81,19 @@ version = "0.22.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bcrypt"
|
||||||
|
version = "0.19.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24ae5479c93d3720e4c1dbd6b945b97457c50cb672781104768190371df1a905"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"blowfish",
|
||||||
|
"getrandom 0.4.2",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
@@ -102,6 +115,16 @@ dependencies = [
|
|||||||
"hybrid-array",
|
"hybrid-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blowfish"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "62ce3946557b35e71d1bbe07ec385073ce9eda05043f95de134eb578fcf1a298"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"cipher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.20.2"
|
version = "3.20.2"
|
||||||
@@ -159,6 +182,16 @@ dependencies = [
|
|||||||
"rand_core 0.10.1",
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cipher"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"inout",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.6.1"
|
version = "4.6.1"
|
||||||
@@ -225,6 +258,7 @@ dependencies = [
|
|||||||
name = "confetti"
|
name = "confetti"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bcrypt",
|
||||||
"clap",
|
"clap",
|
||||||
"ctrlc-async",
|
"ctrlc-async",
|
||||||
"diesel",
|
"diesel",
|
||||||
@@ -283,9 +317,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710"
|
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hybrid-array",
|
"hybrid-array",
|
||||||
]
|
]
|
||||||
@@ -686,6 +720,15 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7"
|
||||||
|
dependencies = [
|
||||||
|
"hybrid-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_terminal_polyfill"
|
name = "is_terminal_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
|
|||||||
@@ -19,3 +19,5 @@ diesel-async = { version = "0.9.0", features = ["postgres","deadpool"] }
|
|||||||
ctrlc-async = { version = "3.2.2", features = ["termination"] }
|
ctrlc-async = { version = "3.2.2", features = ["termination"] }
|
||||||
rmp-serde = "1.3.1"
|
rmp-serde = "1.3.1"
|
||||||
diesel-derive-composite = "0.1.0"
|
diesel-derive-composite = "0.1.0"
|
||||||
|
bcrypt = "0.19.1"
|
||||||
|
#postcard = {version = "1.1.3", features = ["use-std"]}
|
||||||
|
|||||||
+275
-172
@@ -1,16 +1,16 @@
|
|||||||
use fedichat::RoomId;
|
|
||||||
use fedichat::message::{Relevance,TaggedMessage};
|
use fedichat::message::{Relevance,TaggedMessage};
|
||||||
use fedichat::client::{ClientMessage,SignedClientMessage,ServerMessage,ServerError};
|
use fedichat::client::{ClientMessage,SignedClientMessage,ServerMessage,ServerError};
|
||||||
use fedichat::state::StatePath;
|
|
||||||
|
|
||||||
use diesel_async::AsyncPgConnection;
|
use diesel_async::AsyncPgConnection;
|
||||||
use diesel_async::pooled_connection::deadpool::Pool;
|
use diesel_async::pooled_connection::deadpool::{Pool,Object};
|
||||||
use rmp_serde;
|
use rmp_serde;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::sync::{Arc};
|
use std::sync::{Arc};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::sync::{broadcast,mpsc,RwLock};
|
use tokio::sync::{broadcast,mpsc,RwLock};
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
|
use crate::Coordinate;
|
||||||
|
use crate::db;
|
||||||
use crate::state::State;
|
use crate::state::State;
|
||||||
|
|
||||||
use tracing::{warn,error,debug};
|
use tracing::{warn,error,debug};
|
||||||
@@ -73,6 +73,7 @@ impl Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(mut self,config: crate::config::Config) {
|
pub async fn run(mut self,config: crate::config::Config) {
|
||||||
|
// This can probably use a refactoring at some point, this function is huge
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Wait for either a client to send a message, a message that we might need to
|
// Wait for either a client to send a message, a message that we might need to
|
||||||
@@ -104,13 +105,14 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let result = match self.handle_message(message).await {
|
let result = match self.handle_message(&config,message).await {
|
||||||
Ok(res) => res,
|
Ok(res) => res,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to handle client message with error {}",e);
|
warn!("Failed to handle client message with error {:?}",e);
|
||||||
continue;
|
ServerMessage::Error(e)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// TODO: This needs to override the default options to use a map
|
||||||
let buf = match rmp_serde::to_vec(&result) {
|
let buf = match rmp_serde::to_vec(&result) {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -137,6 +139,8 @@ impl Client {
|
|||||||
};
|
};
|
||||||
// Check if the message is in our subscriptions
|
// Check if the message is in our subscriptions
|
||||||
// If it is then send it down to the user
|
// If it is then send it down to the user
|
||||||
|
// Probably should check that we're still in a room maybe? Not sure how
|
||||||
|
// kicks are going to work.
|
||||||
if let Some(relevance) = result.get_relevance() {
|
if let Some(relevance) = result.get_relevance() {
|
||||||
if self.subscriptions.contains(&relevance) {
|
if self.subscriptions.contains(&relevance) {
|
||||||
match self.message_ack.send(relevance).await {
|
match self.message_ack.send(relevance).await {
|
||||||
@@ -148,6 +152,7 @@ impl Client {
|
|||||||
};
|
};
|
||||||
let send = self.quic_connection.open_uni();
|
let send = self.quic_connection.open_uni();
|
||||||
// This is duplicated code, maybe could break into a function
|
// This is duplicated code, maybe could break into a function
|
||||||
|
// TODO: This needs to override the default options to use a map
|
||||||
let buf = match rmp_serde::to_vec(&result) {
|
let buf = match rmp_serde::to_vec(&result) {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -165,7 +170,6 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
unimplemented!()
|
|
||||||
}
|
}
|
||||||
_result = self.close_handle.recv() => {
|
_result = self.close_handle.recv() => {
|
||||||
// Maybe TODO do I need to check the result?
|
// Maybe TODO do I need to check the result?
|
||||||
@@ -178,8 +182,15 @@ impl Client {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This opens up a quic connection to the remote server, serializes our message, and
|
||||||
|
// processes the response. Should use a long-lived connection but for the prototype its
|
||||||
|
// going to open a new one each time.
|
||||||
|
async fn forward(&self, server: &fedichat::ServerAddr, message: TaggedMessage) -> Result<ServerMessage,ServerError>{
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
// Handles message and send back the right response.
|
// Handles message and send back the right response.
|
||||||
async fn handle_message(&mut self, message: SignedClientMessage) -> Result<ServerMessage,HandlerError> {
|
async fn handle_message(&mut self, config: &crate::config::Config, message: SignedClientMessage) -> Result<ServerMessage,ServerError> {
|
||||||
// 3 states
|
// 3 states
|
||||||
// waiting on challenge
|
// waiting on challenge
|
||||||
// waiting on auth
|
// waiting on auth
|
||||||
@@ -197,184 +208,276 @@ impl Client {
|
|||||||
|
|
||||||
}
|
}
|
||||||
// hmm users should probably be able to update state and do everything else still
|
// hmm users should probably be able to update state and do everything else still
|
||||||
// right?? Is forcing the to immediately complete the challenge too much?
|
// right?? Is forcing them to immediately complete the challenge too much?
|
||||||
} else {
|
} else {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
|
|
||||||
}
|
}
|
||||||
} else if let Some(ref username) = self.username {
|
} else if let Some(ref username) = self.username {
|
||||||
|
let user = fedichat::User {
|
||||||
|
name: username.clone(),
|
||||||
|
server: config.hostname.clone()
|
||||||
|
|
||||||
|
};
|
||||||
use fedichat::client::ClientMessage::*;
|
use fedichat::client::ClientMessage::*;
|
||||||
match message.message {
|
// Forward the message if it is addressed to a remote server
|
||||||
|
if let Some(servername) = message.target.clone()
|
||||||
|
&& servername != fedichat::ServerAddr(config.hostname.clone()) {
|
||||||
|
|
||||||
Auth{
|
if message.message.is_forwardable() {
|
||||||
username: _username,
|
let user = self.get_user(config)?;
|
||||||
password: _password
|
self.forward(&servername,message.tag(user,fedichat::ServerAddr(config.hostname.clone()))).await
|
||||||
} => {
|
} else {
|
||||||
return Ok(ServerMessage::Error(ServerError::AlreadyAuthenticated))
|
Err(ServerError::MessageNotForwardable)
|
||||||
},
|
}
|
||||||
// Maybe ask for email too? Or a potential invite code
|
} else {
|
||||||
UserCreate {
|
match message.message {
|
||||||
username: _username,
|
|
||||||
password: _password,
|
|
||||||
} => {
|
|
||||||
return Ok(ServerMessage::Error(ServerError::AlreadyAuthenticated))
|
|
||||||
},
|
|
||||||
// Used to require accounts to complete some kind of challenge. Simplest
|
|
||||||
// is giving a password/invite code to create an account or join a room
|
|
||||||
ChallengeAnswer {
|
|
||||||
response: _,
|
|
||||||
} => {
|
|
||||||
return Ok(ServerMessage::Error(ServerError::NotInChallenge))
|
|
||||||
},
|
|
||||||
|
|
||||||
// Should it be one message type or multiple? How does end-to-end
|
Auth{
|
||||||
// encryption work here? It could be done in a hacky way with extra tags
|
username: _username,
|
||||||
Message {
|
password: _password
|
||||||
body,
|
} => {
|
||||||
room_id,
|
return Ok(ServerMessage::Error(ServerError::AlreadyAuthenticated))
|
||||||
} => {
|
},
|
||||||
unimplemented!()
|
// Maybe ask for email too? Or a potential invite code
|
||||||
},
|
UserCreate {
|
||||||
// Private message/invite mechanism
|
username: _username,
|
||||||
MessagePost {
|
password: _password,
|
||||||
body,
|
} => {
|
||||||
user,
|
return Ok(ServerMessage::Error(ServerError::AlreadyAuthenticated))
|
||||||
} => {unimplemented!()},
|
},
|
||||||
// Replace the body of the message with a new one
|
// Used to require accounts to complete some kind of challenge. Simplest
|
||||||
MessageEdit {
|
// is giving a password/invite code to create an account or join a room
|
||||||
body,
|
ChallengeAnswer {
|
||||||
id,
|
response: _,
|
||||||
room_id,
|
} => {
|
||||||
} => {unimplemented!()},
|
return Ok(ServerMessage::Error(ServerError::NotInChallenge))
|
||||||
MessageDelete {
|
},
|
||||||
id,
|
|
||||||
room_id,
|
// Should it be one message type or multiple? How does end-to-end
|
||||||
} => {unimplemented!()},
|
// encryption work here? It could be done in a hacky way with extra html tags
|
||||||
// State Actions
|
Message {
|
||||||
StateCreate {
|
body,
|
||||||
room_id,
|
room_id,
|
||||||
path,
|
id,
|
||||||
ty,
|
} => {
|
||||||
permissions,
|
unimplemented!()
|
||||||
} => {unimplemented!()},
|
},
|
||||||
StateWrite {
|
// Private message/invite mechanism
|
||||||
room_id,
|
MessagePost {
|
||||||
path,
|
body,
|
||||||
content,
|
user,
|
||||||
} => {unimplemented!()},
|
} => {unimplemented!()},
|
||||||
StateDelete {
|
// Replace the body of the message with a new one
|
||||||
room_id,
|
MessageEdit {
|
||||||
path,
|
body,
|
||||||
} => {unimplemented!()},
|
id,
|
||||||
StateAppend {
|
room_id,
|
||||||
room_id,
|
} => {unimplemented!()},
|
||||||
path,
|
MessageDelete {
|
||||||
content,
|
id,
|
||||||
} => {unimplemented!()},
|
room_id,
|
||||||
StateMove {
|
} => {unimplemented!()},
|
||||||
room_id,
|
// State Actions
|
||||||
path,
|
StateCreate {
|
||||||
target,
|
room_id,
|
||||||
} => {unimplemented!()},
|
path,
|
||||||
StateRead {
|
content,
|
||||||
room_id,
|
permissions,
|
||||||
path,
|
} => {unimplemented!()},
|
||||||
} => {unimplemented!()},
|
StateWrite {
|
||||||
PermissionAdd {
|
room_id,
|
||||||
permission,
|
path,
|
||||||
} => {unimplemented!()},
|
content,
|
||||||
PermissionDelete {
|
} => {unimplemented!()},
|
||||||
permission,
|
StateDelete {
|
||||||
} => {unimplemented!()},
|
room_id,
|
||||||
// Groups really should have a way to add permissions by user
|
path,
|
||||||
// specifically for who can join or invite others
|
} => {unimplemented!()},
|
||||||
//
|
StateAppend {
|
||||||
// Maybe make a group -> role -> member hierarchy?
|
room_id,
|
||||||
//
|
path,
|
||||||
//
|
content,
|
||||||
// Could always do this through a bot that owns a group??
|
} => {unimplemented!()},
|
||||||
GroupCreate {
|
StateMove {
|
||||||
group,
|
room_id,
|
||||||
users,
|
path,
|
||||||
} => {unimplemented!()},
|
target,
|
||||||
// Only the creator of a group or a server admin can delete groups
|
} => {unimplemented!()},
|
||||||
GroupDelete {
|
StateRead {
|
||||||
group,
|
room_id,
|
||||||
} => {unimplemented!()},
|
path,
|
||||||
// Only the creator of a group or a server admin can delete groups
|
} => {unimplemented!()},
|
||||||
// same with adding, though there should be a way to add group officers
|
PermissionAdd {
|
||||||
// at some point
|
permission,
|
||||||
GroupUserAdd {
|
path,
|
||||||
group,
|
room_id
|
||||||
users,
|
} => {unimplemented!()},
|
||||||
} => {unimplemented!()},
|
PermissionRead {
|
||||||
GroupUserRemove {
|
path,
|
||||||
group,
|
room_id
|
||||||
users,
|
} => {unimplemented!()},
|
||||||
} => {unimplemented!()},
|
PermissionDelete {
|
||||||
GroupRoleCreate {
|
permission,
|
||||||
group,
|
path,
|
||||||
role,
|
room_id
|
||||||
} => {unimplemented!()},
|
} => {unimplemented!()},
|
||||||
GroupRoleDelete {
|
// Groups really should have a way to add permissions by user
|
||||||
group,
|
// specifically for who can join or invite others
|
||||||
role,
|
//
|
||||||
} => {unimplemented!()},
|
// Maybe make a group -> role -> member hierarchy?
|
||||||
GroupRoleUserAdd {
|
//
|
||||||
group,
|
//
|
||||||
role,
|
// Could always do this through a bot that owns a group??
|
||||||
users,
|
GroupCreate {
|
||||||
} => {unimplemented!()},
|
group,
|
||||||
GroupRoleUserRemove {
|
users,
|
||||||
group,
|
} => {unimplemented!()},
|
||||||
role,
|
// Only the creator of a group or a server admin can delete groups
|
||||||
users,
|
GroupDelete {
|
||||||
} => {unimplemented!()},
|
group,
|
||||||
// Should work like discord roles
|
} => {unimplemented!()},
|
||||||
// Can control who can invite to the group
|
// Only the creator of a group or a server admin can delete groups
|
||||||
// Can be used with permissions to make rooms that are private for individual roles
|
// same with adding, though there should be a way to add group officers
|
||||||
GroupRolePowerAdd {
|
// at some point
|
||||||
group,
|
GroupUserAdd {
|
||||||
role,
|
group,
|
||||||
power,
|
users,
|
||||||
} => {unimplemented!()},
|
} => {unimplemented!()},
|
||||||
GroupRolePowerRemove {
|
GroupUserRemove {
|
||||||
group,
|
group,
|
||||||
role,
|
users,
|
||||||
power,
|
} => {unimplemented!()},
|
||||||
} => {unimplemented!()},
|
GroupRoleCreate {
|
||||||
// Returns an ID to use for message sending
|
group,
|
||||||
// The server can potentially use the current username to associate media uploads
|
role,
|
||||||
// with users
|
} => {unimplemented!()},
|
||||||
MediaUpload {
|
GroupRoleDelete {
|
||||||
bytes,
|
group,
|
||||||
} => {unimplemented!()},
|
role,
|
||||||
// Join and subscribe
|
} => {unimplemented!()},
|
||||||
Join {
|
GroupRoleUserAdd {
|
||||||
room_id,
|
group,
|
||||||
} => {unimplemented!()},
|
role,
|
||||||
SubscribeMessages {
|
users,
|
||||||
room_id,
|
} => {unimplemented!()},
|
||||||
} => {unimplemented!()},
|
GroupRoleUserRemove {
|
||||||
SubscribeState {
|
group,
|
||||||
room_id,
|
role,
|
||||||
state,
|
users,
|
||||||
} => {unimplemented!()},
|
} => {unimplemented!()},
|
||||||
FetchMessages {
|
// Should work like discord roles
|
||||||
count,
|
// Can control who can invite to the group
|
||||||
end,
|
// Can be used with permissions to make rooms that are private for individual roles
|
||||||
} => {unimplemented!()}
|
GroupRolePowerAdd {
|
||||||
|
group,
|
||||||
|
role,
|
||||||
|
power,
|
||||||
|
} => {unimplemented!()},
|
||||||
|
GroupRolePowerRemove {
|
||||||
|
group,
|
||||||
|
role,
|
||||||
|
power,
|
||||||
|
} => {unimplemented!()},
|
||||||
|
// Returns an ID to use for message sending
|
||||||
|
// The server can potentially use the current username to associate media uploads
|
||||||
|
// with users
|
||||||
|
MediaUpload {
|
||||||
|
bytes,
|
||||||
|
} => {unimplemented!()},
|
||||||
|
// Join and subscribe
|
||||||
|
RoomJoin {
|
||||||
|
room_id,
|
||||||
|
} => {
|
||||||
|
let coords: Coordinate = room_id.clone().into();
|
||||||
|
|
||||||
|
if !self.statehandle.read().await.room_exists(coords.clone()) {
|
||||||
|
Ok(ServerMessage::Error(ServerError::RoomNotFound(room_id.clone())))
|
||||||
|
} else {
|
||||||
|
self.statehandle.write().await.join(coords,user).await?;
|
||||||
|
Ok(ServerMessage::Ok)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RoomCreate {
|
||||||
|
room_id,
|
||||||
|
} => {
|
||||||
|
let coords: Coordinate = room_id.clone().into();
|
||||||
|
|
||||||
|
if self.statehandle.read().await.room_exists(coords.clone()) {
|
||||||
|
Ok(ServerMessage::Error(ServerError::RoomExists(room_id.clone())))
|
||||||
|
} else {
|
||||||
|
// This surely does not deadlock
|
||||||
|
self.statehandle.write().await.create_room(coords,user)?;
|
||||||
|
Ok(ServerMessage::Ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
SubscribeMessages {
|
||||||
|
room_id,
|
||||||
|
} => {
|
||||||
|
// TODO: Has to check if user is joined to room first
|
||||||
|
self.subscriptions.insert(Relevance::Message(room_id));
|
||||||
|
Ok(ServerMessage::Ok)
|
||||||
|
},
|
||||||
|
SubscribeState {
|
||||||
|
room_id,
|
||||||
|
state,
|
||||||
|
} => {
|
||||||
|
// TODO: Has to check if user is joined to room first
|
||||||
|
if self.statehandle.read().await.is_joined(room_id.clone().into(),user).await {
|
||||||
|
self.subscriptions.insert(Relevance::State(room_id,state));
|
||||||
|
Ok(ServerMessage::Ok)
|
||||||
|
} else {
|
||||||
|
Err(ServerError::NotJoined(room_id))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FetchMessages {
|
||||||
|
count,
|
||||||
|
end,
|
||||||
|
} => {unimplemented!()}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if let ClientMessage::Auth {username,password} = message.message {
|
} else if let ClientMessage::Auth {ref username,ref password} = message.message {
|
||||||
unimplemented!()
|
match db::verify_user(self.get_db().await?,username,password).await? {
|
||||||
} else if let ClientMessage::UserCreate {username,password} = message.message {
|
true => {
|
||||||
unimplemented!()
|
self.username = Some(username.clone());
|
||||||
|
Ok(ServerMessage::Ok)
|
||||||
|
},
|
||||||
|
false => {
|
||||||
|
Err(ServerError::AuthenticationFailed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let ClientMessage::UserCreate {username,password} = &message.message {
|
||||||
|
if let Some(ref code) = config.account_creation_code {
|
||||||
|
self.in_challenge = Some((code.to_string(),message));
|
||||||
|
|
||||||
|
Err(ServerError::ChallengeInviteCode)
|
||||||
|
} else {
|
||||||
|
db::maybe_create_user(self.get_db().await ?,username,password).await
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return Ok(ServerMessage::Error(ServerError::NotAuthenticated));
|
return Ok(ServerMessage::Error(ServerError::NotAuthenticated));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
fn get_user(&self,config: &crate::config::Config) -> Result<fedichat::User,ServerError> {
|
||||||
|
Ok(fedichat::User {
|
||||||
|
name: self.username.clone().ok_or(ServerError::NotAuthenticated)?,
|
||||||
|
server: config.hostname.clone()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async fn get_db(&self) -> Result<Object<AsyncPgConnection>,ServerError> {
|
||||||
|
match self.db_handle.get().await {
|
||||||
|
Ok(conn) => Ok(conn),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Could not connect to database server: {e}");
|
||||||
|
// This error is not relevant to the client in any way
|
||||||
|
Err(ServerError::Generic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ use thiserror::Error;
|
|||||||
#[derive(Clone,Serialize,Deserialize)]
|
#[derive(Clone,Serialize,Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub hostname: String,
|
pub hostname: String,
|
||||||
|
//NOTE: Changing the federation port breaks federation
|
||||||
|
// Changing the client port is also probably a bad idea. There might
|
||||||
|
// need to be .wellknown support at some point
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub federation_port: u16,
|
pub federation_port: u16,
|
||||||
pub listen_address: String,
|
pub listen_address: String,
|
||||||
|
|||||||
@@ -1,2 +1,68 @@
|
|||||||
|
use diesel::prelude::*;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
|
|
||||||
|
|
||||||
|
use bcrypt::{DEFAULT_COST, hash, verify};
|
||||||
|
use diesel_async::AsyncPgConnection;
|
||||||
|
use diesel_async::RunQueryDsl;//, AsyncConnection};
|
||||||
|
use diesel_async::pooled_connection::deadpool::Object;
|
||||||
|
use fedichat::client::{ServerMessage,ServerError};
|
||||||
|
use tracing::{instrument,warn};
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn maybe_create_user(mut connection: Object<AsyncPgConnection>, username: &str, password: &str) -> Result<ServerMessage,ServerError> {
|
||||||
|
let password = match hash(password,DEFAULT_COST) {
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error encountered while generating password hash");
|
||||||
|
warn!("{e}");
|
||||||
|
return Err(ServerError::Generic)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let username = username.to_string();
|
||||||
|
let user = models::NewUser{ username, password};
|
||||||
|
let result = diesel::insert_into(schema::users::table)
|
||||||
|
.values(&user)
|
||||||
|
.execute(&mut *connection)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
// TODO: might need to check this? I don't know what it means
|
||||||
|
Ok(_) => Ok(ServerMessage::Ok),
|
||||||
|
// TODO: Probably actually check the error
|
||||||
|
Err(_e) => Err(ServerError::UserAlreadyExists)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to authenticate a user against information in the database
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn verify_user(mut connection: Object<AsyncPgConnection>, user: &str, pass: &str) -> Result<bool,ServerError> {
|
||||||
|
use schema::users::dsl::*;
|
||||||
|
|
||||||
|
let result = users
|
||||||
|
.filter(username.eq(user))
|
||||||
|
.select(models::User::as_select())
|
||||||
|
.first(&mut *connection)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
match result {
|
||||||
|
// If we have more than 0 rows then authentication is successful
|
||||||
|
Ok(user) => match verify(pass,&user.password) {
|
||||||
|
Ok(val) => Ok(val),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error encountered while generating password hash");
|
||||||
|
warn!("{e}");
|
||||||
|
return Err(ServerError::Generic)
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
// TODO: Probably actually check the error
|
||||||
|
_ => Err(ServerError::AuthenticationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ pub struct User {
|
|||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable)]
|
||||||
|
#[diesel(table_name = crate::db::schema::users)]
|
||||||
|
pub struct NewUser {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Selectable)]
|
#[derive(Queryable, Selectable)]
|
||||||
#[diesel(table_name = crate::db::schema::messages)]
|
#[diesel(table_name = crate::db::schema::messages)]
|
||||||
pub struct Messages {
|
pub struct Messages {
|
||||||
|
|||||||
+29
-27
@@ -6,7 +6,6 @@ mod state;
|
|||||||
|
|
||||||
use diesel_async::pooled_connection::AsyncDieselConnectionManager;
|
use diesel_async::pooled_connection::AsyncDieselConnectionManager;
|
||||||
use diesel_async::pooled_connection::deadpool::Pool;
|
use diesel_async::pooled_connection::deadpool::Pool;
|
||||||
use diesel_async::{AsyncConnection,AsyncPgConnection};
|
|
||||||
use quinn::rustls::pki_types::{PrivateKeyDer,CertificateDer,pem::PemObject};
|
use quinn::rustls::pki_types::{PrivateKeyDer,CertificateDer,pem::PemObject};
|
||||||
use quinn::Endpoint;
|
use quinn::Endpoint;
|
||||||
use std::io;
|
use std::io;
|
||||||
@@ -21,25 +20,32 @@ use tokio::sync::{RwLock,broadcast,mpsc};
|
|||||||
|
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::state::State;
|
use crate::state::{State,StateError};
|
||||||
|
|
||||||
#[derive(Hash,Eq,PartialEq,Clone,Serialize,Deserialize)]
|
#[derive(Hash,Eq,PartialEq,Clone,Serialize,Deserialize,Debug)]
|
||||||
pub struct Coordinate(Vec<i64>);
|
pub struct Coordinate(Vec<i64>);
|
||||||
|
|
||||||
|
impl From<fedichat::RoomId> for Coordinate {
|
||||||
|
fn from(other: fedichat::RoomId) -> Coordinate {
|
||||||
|
Coordinate(other.coordinates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
#[instrument]
|
#[instrument]
|
||||||
async fn main() -> ExitCode {
|
async fn main() -> ExitCode {
|
||||||
|
// NOTE: This doesn't work as you can only initialize the global logger once
|
||||||
// Initial logger so we have something during config
|
// Initial logger so we have something during config
|
||||||
tracing::subscriber::set_global_default(
|
//tracing::subscriber::set_global_default(
|
||||||
tracing_subscriber::fmt().with_max_level(Level::WARN).finish()
|
// tracing_subscriber::fmt().with_max_level(Level::WARN).finish()
|
||||||
).expect("Failed to setup logger");
|
//).expect("Failed to setup logger");
|
||||||
|
|
||||||
// Read in config
|
// Read in config
|
||||||
let config = match Config::load() {
|
let config = match Config::load() {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Problem while reading config file");
|
eprintln!("Problem while reading config file");
|
||||||
error!("{:?}",e);
|
eprintln!("{:?}",e);
|
||||||
return ExitCode::FAILURE;
|
return ExitCode::FAILURE;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -54,7 +60,7 @@ async fn main() -> ExitCode {
|
|||||||
"warn" => Level::WARN,
|
"warn" => Level::WARN,
|
||||||
"error" => Level::ERROR,
|
"error" => Level::ERROR,
|
||||||
_ => {
|
_ => {
|
||||||
warn!("Invalid loglevel in config: {}",&loglevel);
|
eprintln!("Invalid loglevel in config: {}",&loglevel);
|
||||||
Level::INFO
|
Level::INFO
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -120,29 +126,25 @@ async fn main() -> ExitCode {
|
|||||||
let state = match State::load_from_file(&config.statefile) {
|
let state = match State::load_from_file(&config.statefile) {
|
||||||
Ok(state) => state,
|
Ok(state) => state,
|
||||||
// Create file if it does not exist
|
// Create file if it does not exist
|
||||||
Err(e) => {
|
Err(StateError::IOError(e)) if e.kind() == io::ErrorKind::NotFound => {
|
||||||
match e.kind() {
|
match fs::File::create(&config.statefile) {
|
||||||
io::ErrorKind::NotFound => {
|
// If the statefile is writable then create an empty state
|
||||||
match fs::File::create(&config.statefile) {
|
// and use that
|
||||||
// If the statefile is writable then create an empty state
|
Ok(_) => State::new(),
|
||||||
// and use that
|
Err(e) => {
|
||||||
Ok(_) => State::new(),
|
|
||||||
Err(e) => {
|
|
||||||
error!("Could not open or create statefile. Check your config.");
|
|
||||||
error!("{:?}",e);
|
|
||||||
return ExitCode::FAILURE;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
error!("Could not open or create statefile. Check your config.");
|
error!("Could not open or create statefile. Check your config.");
|
||||||
error!("{:?}",e);
|
error!("{:?}",e);
|
||||||
return ExitCode::FAILURE;
|
return ExitCode::FAILURE;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Could not open or create statefile. Check your config.");
|
||||||
|
error!("{:?}",e);
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let statehandle = Arc::new(RwLock::new(state));
|
let statehandle = Arc::new(RwLock::new(state));
|
||||||
|
|
||||||
@@ -201,7 +203,7 @@ async fn main() -> ExitCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Save state
|
//Save state
|
||||||
match statehandle.write().await.write_to_file(&config.statefile) {
|
match statehandle.write().await.write_to_file(&config.statefile).await {
|
||||||
Ok(()) => debug!("Successfully wrote state to {:?}",config.statefile),
|
Ok(()) => debug!("Successfully wrote state to {:?}",config.statefile),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Problem while writing to statefile");
|
error!("Problem while writing to statefile");
|
||||||
|
|||||||
+1212
-41
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user