7 Commits

14 changed files with 889 additions and 380 deletions
Split View
  1. +447
    -325
      Cargo.lock
  2. +3
    -0
      Cargo.toml
  3. +5
    -5
      client/Cargo.toml
  4. +25
    -42
      client/src/client.rs
  5. +33
    -2
      client/src/context.rs
  6. +1
    -0
      client/src/event.rs
  7. +313
    -0
      client/src/server/context.rs
  8. +54
    -0
      client/src/server/login.rs
  9. +1
    -0
      client/src/server/mod.rs
  10. +1
    -0
      client/src/server/room/event.rs
  11. +2
    -2
      client/src/server/room/map.rs
  12. +1
    -1
      client/src/server/user/map.rs
  13. +1
    -1
      proto/Cargo.toml
  14. +2
    -2
      server/Cargo.toml

+ 447
- 325
Cargo.lock
File diff suppressed because it is too large
View File


+ 3
- 0
Cargo.toml View File

@ -1,5 +1,8 @@
[workspace]
edition = "2021"
resolver = "2"
members = [
"client",
"proto",


+ 5
- 5
client/Cargo.toml View File

@ -8,17 +8,17 @@ edition = "2021"
anyhow = "^1.0"
clap = "^2.33"
crossbeam-channel = "^0.5"
env_logger = "^0.8"
env_logger = "^0.11"
futures = "^0.3"
log = "^0.4"
serde = { version = "^1.0", features = ["derive"] }
serde_json = "^1.0"
slab = "^0.2"
slab = "^0.4"
solstice-proto = { path = "../proto" }
thiserror = "^1.0"
tokio = { version = "1.0", features = ["full"] }
tokio-tungstenite = "0.15"
toml = "^0.5"
tokio-tungstenite = "0.23"
toml = "^0.8"
[dev-dependencies]
parking_lot = "^0.11.0"
parking_lot = "^0.12"

+ 25
- 42
client/src/client.rs View File

@ -52,7 +52,7 @@ pub struct Client {
rooms: RoomMap,
users: UserMap,
peers: slab::Slab<Peer, usize>,
peers: slab::Slab<Peer>,
}
impl Client {
@ -77,7 +77,7 @@ impl Client {
rooms: RoomMap::new(),
users: UserMap::new(),
peers: slab::Slab::new(max_peers),
peers: slab::Slab::with_capacity(max_peers),
}
}
@ -264,20 +264,15 @@ impl Client {
*=========================*/
#[allow(dead_code)]
fn handle_peer_connection_closed(&mut self, peer_id: usize) {
let mut occupied_entry = match self.peers.entry(peer_id) {
None | Some(slab::Entry::Vacant(_)) => {
error!("Unknown peer connection {} has closed", peer_id);
return;
}
Some(slab::Entry::Occupied(occupied_entry)) => occupied_entry,
fn handle_peer_connection_closed(&mut self, mut peer_id: usize) {
let Some(mut peer) = self.peers.try_remove(peer_id) else {
error!("Unknown peer connection {} has closed", peer_id);
return;
};
match occupied_entry.get_mut().state {
match peer.state {
PeerState::Open => {
info!("Peer connection {} has closed", peer_id);
occupied_entry.remove();
}
PeerState::WaitingFirewalled => {
@ -285,7 +280,6 @@ impl Client {
"Peer connection {} has closed, was waiting: inconsistent",
peer_id
);
occupied_entry.remove();
}
PeerState::Opening => {
@ -294,8 +288,10 @@ impl Client {
peer_id
);
let peer = occupied_entry.get_mut();
peer.state = PeerState::WaitingFirewalled;
peer_id = self.peers.insert(peer);
let peer = self.peers.get(peer_id).unwrap();
#[allow(deprecated)]
self
@ -317,7 +313,6 @@ impl Client {
peer_id
);
let (peer, _) = occupied_entry.remove();
#[allow(deprecated)]
self
.proto_tx
@ -461,40 +456,28 @@ impl Client {
&mut self,
response: server::ConnectToPeerResponse,
) {
let peer = Peer {
if self.peers.len() == self.config.max_peers {
warn!(
"Cannot open peer connection ({}:{}): too many already open",
response.ip,
response.port,
);
return;
}
let peer_id = self.peers.insert(Peer {
user_name: response.user_name,
ip: response.ip,
port: response.port,
connection_type: response.connection_type,
token: response.token,
state: PeerState::OpeningFirewalled,
};
match self.peers.insert(peer) {
Ok(peer_id) => {
info!(
"Opening peer connection {} to {}:{} to pierce firewall",
peer_id, response.ip, response.port
);
/*
self
.proto_tx
.send(solstice_proto::Request::PeerConnect(
peer_id,
response.ip,
response.port,
))
.unwrap();
*/
}
});
Err(peer) => {
warn!(
"Cannot open peer connection {:?}: too many already open",
peer
);
}
}
info!(
"Opening peer connection {} to {}:{} to pierce firewall",
peer_id, response.ip, response.port
);
}
fn handle_login_response(&mut self, login: server::LoginResponse) {


+ 33
- 2
client/src/context.rs View File

@ -9,25 +9,42 @@ use crate::clock::SimulatedSystemClock;
use crate::clock::SystemClock;
use crate::control::Response as ControlResponse;
use crate::peer::PeerMap;
use crate::server::context::{
ServerContext, ServerLoggedInContext, ServerLoggedInContextOptions,
};
use crate::server::room::RoomMap;
use crate::server::user::UserMap;
use crate::Config;
/// Contains all the different bits of client state.
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct State {
pub rooms: RoomMap,
pub users: UserMap,
pub peers: PeerMap,
}
impl Default for State {
fn default() -> Self {
Self {
rooms: RoomMap::default(),
users: UserMap::default(),
peers: PeerMap::default(),
}
}
}
/// Holds process-wide context for message handlers to execute against.
#[derive(Debug)]
pub struct Context {
/// Mutable state.
pub state: State,
/// Server-related context bits.
pub server: ServerContext,
/// The user name with which we logged in to the server.
// TODO: Use the value in `server` instead.
pub user_name: String,
/// Sender half of a channel used to send requests to the server.
@ -101,7 +118,21 @@ impl ContextBundle {
Self {
context: Context {
state: options.initial_state,
state: State {
rooms: options.initial_state.rooms.clone(),
users: options.initial_state.users.clone(),
peers: options.initial_state.peers,
},
// TODO: Do not login immediately, wait for a login event.
server: ServerContext::LoggedIn(ServerLoggedInContext::new(
server_request_tx.clone(),
ServerLoggedInContextOptions {
user_name: options.user_name.clone(),
motd: "unimplemented".to_string(),
rooms: options.initial_state.rooms,
users: options.initial_state.users,
},
)),
user_name: options.user_name,
server_request_tx,
control_response_tx,


+ 1
- 0
client/src/event.rs View File

@ -8,6 +8,7 @@ use crate::control;
/// The type of events affecting the client.
#[derive(Debug, Eq, PartialEq)]
pub enum Event {
// TODO: events for login stuff
ControlRequest(control::Request),
ServerResponse(ServerResponse),
}


+ 313
- 0
client/src/server/context.rs View File

@ -0,0 +1,313 @@
use std::mem;
use solstice_proto::ServerRequest;
use thiserror::Error;
use tokio::sync::mpsc::Sender;
use crate::server::room::RoomMap;
use crate::server::user::UserMap;
/// Server-related state stored when we are logged in.
#[derive(Debug)]
pub struct ServerLoggedInContext {
/// Chat rooms hosted by the server.
pub rooms: RoomMap,
/// Users connected to the server.
pub users: UserMap,
/// Sends requests to the server.
pub request_tx: Sender<ServerRequest>,
// Immutable, see accessor methods.
user_name: String,
// TODO: Remove annotation.
#[allow(dead_code)]
motd: String,
}
/// Initial values from which a `ServerLoggedInContext` can be built.
pub struct ServerLoggedInContextOptions {
/// See similarly-named fields in `ServerLoggedInContext`.
pub user_name: String,
pub motd: String,
pub rooms: RoomMap,
pub users: UserMap,
}
impl ServerLoggedInContext {
/// Mainly for use in tests.
// TODO: Mark as #[cfg(test)]?
pub fn new(
request_tx: Sender<ServerRequest>,
options: ServerLoggedInContextOptions,
) -> Self {
Self {
rooms: options.rooms,
users: options.users,
user_name: options.user_name,
motd: options.motd,
request_tx,
}
}
/// The user name with which we logged in.
#[cfg(test)]
pub fn user_name(&self) -> &str {
&self.user_name
}
/// The message of the day sent by the server when we logged in.
#[cfg(test)]
pub fn motd(&self) -> &str {
&self.motd
}
/// Records that we have logged out.
#[cfg(test)]
pub fn logout(self) -> ServerLoggedOutContext {
ServerLoggedOutContext {
user_name: self.user_name,
error: None,
}
}
}
/// Server-related state stored when we are logged out.
#[derive(Debug)]
pub struct ServerLoggedOutContext {
// Immutable, see accessor method.
user_name: String,
// Once set to `Some(...)`, never set to `None` again.
error: Option<String>,
}
/// Information provided upon a successful login.
#[allow(dead_code)]
pub struct ServerLoginInfo {
/// The message of the day sent by the server when we logged in.
pub motd: String,
/// Sends requests to the server.
pub request_tx: Sender<ServerRequest>,
}
impl ServerLoggedOutContext {
/// The user name with which we will attempt or have attempted to log in.
#[cfg(test)]
pub fn user_name(&self) -> &str {
&self.user_name
}
/// The error received during the last login attempt, if any.
/// `None` if we have never attempted to log in.
#[cfg(test)]
pub fn error(&self) -> Option<&str> {
self.error.as_deref()
}
/// Records that we failed to log in, and the server sent us the given error.
#[allow(dead_code)]
pub fn set_error(&mut self, error: String) {
self.error = Some(error)
}
/// Records that we logged in successfully.
#[allow(dead_code)]
pub fn login(self, info: ServerLoginInfo) -> ServerLoggedInContext {
ServerLoggedInContext {
user_name: self.user_name,
motd: info.motd,
request_tx: info.request_tx,
rooms: RoomMap::default(),
users: UserMap::default(),
}
}
}
/// Server-related client state.
#[derive(Debug)]
pub enum ServerContext {
/// We are logged out of the server.
#[allow(dead_code)]
LoggedOut(ServerLoggedOutContext),
/// We are logged in to the server.
LoggedIn(ServerLoggedInContext),
}
fn server_logged_out_error(error: &Option<String>) -> String {
match error {
Some(error) => format!("login error: {}", error),
None => "not logged in".to_string(),
}
}
#[derive(Debug, Error)]
#[error("{}", server_logged_out_error(.error))]
pub struct ServerLoggedOutError {
error: Option<String>,
}
#[derive(Debug, Error)]
#[error("logged in as {user_name}")]
pub struct ServerLoggedInError {
user_name: String,
}
impl ServerContext {
/// Constructs a new logged out context with the given user name.
#[cfg(test)]
pub fn new(user_name: String) -> Self {
Self::LoggedOut(ServerLoggedOutContext {
user_name,
error: None,
})
}
/// Attempts to record a successful login. Fails if we are already logged in.
#[allow(dead_code)]
pub fn login(
&mut self,
info: ServerLoginInfo,
) -> Result<(), ServerLoggedInError> {
let context = match self {
Self::LoggedIn(context) => {
return Err(ServerLoggedInError {
user_name: context.user_name.clone(),
})
}
// Replace the previous value with a dummy that we will immediately
// overwrite. This allows us to move the previous context out from under
// a mutable reference.
Self::LoggedOut(context) => mem::replace(
context,
ServerLoggedOutContext {
user_name: String::new(),
error: None,
},
),
};
*self = Self::LoggedIn(context.login(info));
Ok(())
}
/// Attempts to record that we logged out. Fails if we are not logged in.
#[cfg(test)]
pub fn logout(&mut self) -> Result<(), ServerLoggedOutError> {
if let Self::LoggedOut(context) = self {
return Err(ServerLoggedOutError {
error: context.error.clone(),
});
}
// See `mem::replace()` call in `login()`.
let old_self = mem::replace(self, Self::new(String::new()));
let context = match old_self {
Self::LoggedOut(_context) => unreachable!(),
Self::LoggedIn(context) => context,
};
*self = Self::LoggedOut(context.logout());
Ok(())
}
/// Returns a reference to the logged in context.
#[allow(dead_code)]
pub fn logged_in(
&mut self,
) -> Result<&mut ServerLoggedInContext, ServerLoggedOutError> {
match self {
Self::LoggedIn(context) => Ok(context),
Self::LoggedOut(context) => Err(ServerLoggedOutError {
error: context.error.clone(),
}),
}
}
/// Returns a reference to the logged out context.
#[allow(dead_code)]
pub fn logged_out(
&mut self,
) -> Result<&mut ServerLoggedOutContext, ServerLoggedInError> {
match self {
Self::LoggedIn(context) => Err(ServerLoggedInError {
user_name: context.user_name.clone(),
}),
Self::LoggedOut(context) => Ok(context),
}
}
}
#[cfg(test)]
mod tests {
use solstice_proto::{User, UserStatus};
use tokio::sync::mpsc::channel;
use crate::server::room::{RoomMap, RoomState, RoomVisibility};
use crate::server::user::UserMap;
use super::{ServerLoggedInContext, ServerLoggedInContextOptions};
#[test]
fn server_logged_in_context_new() {
let mut rooms = RoomMap::default();
rooms.insert(
"foo".to_string(),
RoomState::new(RoomVisibility::Public, 42),
);
let mut users = UserMap::default();
users.insert(User {
name: "kim".to_string(),
status: UserStatus::Online,
average_speed: 1,
num_downloads: 2,
unknown: 3,
num_files: 4,
num_folders: 5,
num_free_slots: 6,
country: "KR".to_string(),
});
let (tx, _rx) = channel(100);
let context = ServerLoggedInContext::new(
tx.clone(),
ServerLoggedInContextOptions {
users: users.clone(),
rooms: rooms.clone(),
user_name: "bob".to_string(),
motd: "hey".to_string(),
},
);
assert_eq!(context.user_name(), "bob");
assert_eq!(context.motd(), "hey");
assert!(context.request_tx.same_channel(&tx));
assert_eq!(context.rooms, rooms);
assert_eq!(context.users, users);
}
#[test]
fn server_logged_in_context_logout() {
let (tx, _rx) = channel(100);
let context = ServerLoggedInContext::new(
tx,
ServerLoggedInContextOptions {
users: UserMap::new(),
rooms: RoomMap::new(),
user_name: "bob".to_string(),
motd: "hey".to_string(),
},
)
.logout();
assert_eq!(context.user_name(), "bob");
assert_eq!(context.error(), None);
}
}

+ 54
- 0
client/src/server/login.rs View File

@ -1,3 +1,8 @@
use anyhow::bail;
use crate::context::Context;
use crate::event::EventHandler;
/// Represents the status of the login operation.
///
/// In order to interact with the server, a client cannot simply open a network
@ -20,3 +25,52 @@ pub enum LoginStatus {
/// Stores the error message as received from the server.
Failure(String),
}
#[allow(dead_code)]
pub enum LoginEvent {
Success(String),
Failure(String),
Disconnected,
StatusRequest,
}
pub struct LoginEventHandler {}
fn handle_login_success(
_context: &mut Context,
_motd: String,
) -> anyhow::Result<()> {
bail!("unimplemented")
}
fn handle_login_failure(
_context: &mut Context,
_error: String,
) -> anyhow::Result<()> {
bail!("unimplemented")
}
fn handle_disconnected(_context: &mut Context) -> anyhow::Result<()> {
bail!("unimplemented")
}
fn handle_status_request(_context: &mut Context) -> anyhow::Result<()> {
bail!("unimplemented")
}
impl EventHandler for LoginEventHandler {
type Event = LoginEvent;
fn handle(
&mut self,
context: &mut Context,
event: LoginEvent,
) -> anyhow::Result<()> {
match event {
LoginEvent::Success(motd) => handle_login_success(context, motd),
LoginEvent::Failure(error) => handle_login_failure(context, error),
LoginEvent::Disconnected => handle_disconnected(context),
LoginEvent::StatusRequest => handle_status_request(context),
}
}
}

+ 1
- 0
client/src/server/mod.rs View File

@ -1,3 +1,4 @@
pub mod context;
mod login;
pub mod room;
pub mod user;


+ 1
- 0
client/src/server/room/event.rs View File

@ -220,6 +220,7 @@ mod tests {
// Close the channel, so we can observe it was empty without hanging.
drop(bundle.context.server_request_tx);
bundle.context.server.logout().expect("logging out");
assert_eq!(bundle.server_request_rx.blocking_recv(), None);
}


+ 2
- 2
client/src/server/room/map.rs View File

@ -21,7 +21,7 @@ pub struct RoomMembershipChangeError(RoomMembership, RoomMembership);
pub struct RoomNotFoundError(String);
/// An entry in the chat room map.
#[derive(Debug)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RoomEntry {
name: String,
state: RoomState,
@ -138,7 +138,7 @@ impl RoomEntry {
/// Contains the mapping from room names to room data and provides a clean
/// interface to interact with it.
#[derive(Debug, Default)]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct RoomMap {
/// The actual map from room names to room data.
map: HashMap<String, RoomEntry>,


+ 1
- 1
client/src/server/user/map.rs View File

@ -11,7 +11,7 @@ pub struct UserNotFoundError(String);
/// Contains the mapping from user names to user data and provides a clean
/// interface to interact with it.
#[derive(Debug, Default)]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct UserMap {
/// The actual map from user names to user data and privileged status.
map: collections::HashMap<String, User>,


+ 1
- 1
proto/Cargo.toml View File

@ -14,5 +14,5 @@ thiserror = "^1.0"
tokio = { version = "^1.0", features = ["full"] }
[dev-dependencies]
env_logger = "^0.9"
env_logger = "^0.11"
serde_json = "^1.0"

+ 2
- 2
server/Cargo.toml View File

@ -6,8 +6,8 @@ edition = "2021"
[dependencies]
anyhow = "^1.0"
env_logger = "^0.8"
env_logger = "^0.11"
log = "^0.4"
parking_lot = "^0.11"
parking_lot = "^0.12"
solstice-proto = { path = "../proto" }
tokio = { version = "^1.0", features = ["full"] }

Loading…
Cancel
Save