|
|
|
@ -2,7 +2,6 @@ |
|
|
|
|
|
|
|
use std::io;
|
|
|
|
|
|
|
|
use futures::stream::Stream;
|
|
|
|
use log::{debug, info};
|
|
|
|
use thiserror::Error;
|
|
|
|
use tokio::net::TcpStream;
|
|
|
|
@ -10,22 +9,18 @@ use tokio::net::TcpStream; |
|
|
|
use crate::core::channel::{Channel, ChannelError};
|
|
|
|
use crate::server::{Credentials, LoginResponse, ServerRequest, ServerResponse, Version};
|
|
|
|
|
|
|
|
/// Specifies options for a new `Client`.
|
|
|
|
pub struct ClientOptions {
|
|
|
|
pub credentials: Credentials,
|
|
|
|
pub version: Version,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A client for the client-server protocol.
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct Client {
|
|
|
|
channel: Channel<ServerResponse, ServerRequest>,
|
|
|
|
version: Version,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// An error that arose while logging in to a remote server.
|
|
|
|
#[derive(Debug, Error)]
|
|
|
|
pub enum ClientLoginError {
|
|
|
|
#[error("login failed: {0}")]
|
|
|
|
LoginFailed(String),
|
|
|
|
LoginFailed(String, Client),
|
|
|
|
|
|
|
|
#[error("unexpected response: {0:?}")]
|
|
|
|
UnexpectedResponse(ServerResponse),
|
|
|
|
@ -40,24 +35,30 @@ impl From<io::Error> for ClientLoginError { |
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A `Channel` that sends `ServerRequest`s and receives `ServerResponse`s.
|
|
|
|
pub type ClientChannel = Channel<ServerResponse, ServerRequest>;
|
|
|
|
|
|
|
|
impl Client {
|
|
|
|
pub async fn login(
|
|
|
|
tcp_stream: TcpStream,
|
|
|
|
options: ClientOptions,
|
|
|
|
) -> Result<Client, ClientLoginError> {
|
|
|
|
let mut client = Client {
|
|
|
|
/// Instantiates a new client
|
|
|
|
pub fn new(tcp_stream: TcpStream) -> Self {
|
|
|
|
Client {
|
|
|
|
channel: Channel::new(tcp_stream),
|
|
|
|
};
|
|
|
|
|
|
|
|
client.handshake(options).await?;
|
|
|
|
version: Version::default(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(client)
|
|
|
|
/// Sets a custom version to identify as to the server.
|
|
|
|
pub fn with_version(mut self, version: Version) -> Self {
|
|
|
|
self.version = version;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
// Performs the login exchange.
|
|
|
|
// Called this way because `login` is already taken.
|
|
|
|
async fn handshake(&mut self, options: ClientOptions) -> Result<(), ClientLoginError> {
|
|
|
|
let login_request = options.credentials.into_login_request(options.version);
|
|
|
|
/// Performs the login exchange, presenting `credentials` to the server.
|
|
|
|
pub async fn login(
|
|
|
|
mut self,
|
|
|
|
credentials: Credentials,
|
|
|
|
) -> Result<ClientChannel, ClientLoginError> {
|
|
|
|
let login_request = credentials.into_login_request(self.version);
|
|
|
|
debug!("Client: sending login request: {:?}", login_request);
|
|
|
|
|
|
|
|
let request = login_request.into();
|
|
|
|
@ -76,21 +77,14 @@ impl Client { |
|
|
|
info!("Client: Message Of The Day: {}", motd);
|
|
|
|
info!("Client: Public IP address: {}", ip);
|
|
|
|
info!("Client: Password MD5: {:?}", password_md5_opt);
|
|
|
|
Ok(())
|
|
|
|
Ok(self.channel)
|
|
|
|
}
|
|
|
|
ServerResponse::LoginResponse(LoginResponse::LoginFail { reason }) => {
|
|
|
|
Err(ClientLoginError::LoginFailed(reason))
|
|
|
|
Err(ClientLoginError::LoginFailed(reason, self))
|
|
|
|
}
|
|
|
|
response => Err(ClientLoginError::UnexpectedResponse(response)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn run<S: Stream<Item = ServerRequest>>(
|
|
|
|
self,
|
|
|
|
request_stream: S,
|
|
|
|
) -> impl Stream<Item = Result<ServerResponse, ChannelError>> {
|
|
|
|
self.channel.run(request_stream)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
@ -105,23 +99,18 @@ mod tests { |
|
|
|
};
|
|
|
|
use crate::UserStatus;
|
|
|
|
|
|
|
|
use super::{Client, ClientOptions, Version};
|
|
|
|
use super::Client;
|
|
|
|
|
|
|
|
// Enable capturing logs in tests.
|
|
|
|
fn init() {
|
|
|
|
let _ = env_logger::builder().is_test(true).try_init();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns default ClientOptions suitable for testing.
|
|
|
|
fn client_options() -> ClientOptions {
|
|
|
|
// Returns default `Credentials` suitable for testing.
|
|
|
|
fn credentials() -> Credentials {
|
|
|
|
let user_name = "alice".to_string();
|
|
|
|
let password = "sekrit".to_string();
|
|
|
|
let credentials = Credentials::new(user_name, password).unwrap();
|
|
|
|
|
|
|
|
ClientOptions {
|
|
|
|
credentials,
|
|
|
|
version: Version::default(),
|
|
|
|
}
|
|
|
|
Credentials::new(user_name, password).unwrap()
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
@ -133,10 +122,10 @@ mod tests { |
|
|
|
|
|
|
|
let stream = TcpStream::connect(handle.address()).await.unwrap();
|
|
|
|
|
|
|
|
let client = Client::login(stream, client_options()).await.unwrap();
|
|
|
|
let channel = Client::new(stream).login(credentials()).await.unwrap();
|
|
|
|
|
|
|
|
// Send nothing, receive no responses.
|
|
|
|
let inbound = client.run(empty());
|
|
|
|
let inbound = channel.run(empty());
|
|
|
|
tokio::pin!(inbound);
|
|
|
|
|
|
|
|
assert!(inbound.next().await.is_none());
|
|
|
|
@ -167,7 +156,7 @@ mod tests { |
|
|
|
|
|
|
|
let stream = TcpStream::connect(handle.address()).await.unwrap();
|
|
|
|
|
|
|
|
let client = Client::login(stream, client_options()).await.unwrap();
|
|
|
|
let channel = Client::new(stream).login(credentials()).await.unwrap();
|
|
|
|
|
|
|
|
let outbound = Box::pin(async_stream::stream! {
|
|
|
|
yield ServerRequest::UserStatusRequest(UserStatusRequest {
|
|
|
|
@ -178,7 +167,7 @@ mod tests { |
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
let inbound = client.run(outbound);
|
|
|
|
let inbound = channel.run(outbound);
|
|
|
|
tokio::pin!(inbound);
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
@ -200,7 +189,7 @@ mod tests { |
|
|
|
|
|
|
|
let stream = TcpStream::connect(handle.address()).await.unwrap();
|
|
|
|
|
|
|
|
let client = Client::login(stream, client_options()).await.unwrap();
|
|
|
|
let channel = Client::new(stream).login(credentials()).await.unwrap();
|
|
|
|
|
|
|
|
let (_request_tx, mut request_rx) = mpsc::channel(1);
|
|
|
|
let outbound = Box::pin(async_stream::stream! {
|
|
|
|
@ -209,7 +198,7 @@ mod tests { |
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
let inbound = client.run(outbound);
|
|
|
|
let inbound = channel.run(outbound);
|
|
|
|
tokio::pin!(inbound);
|
|
|
|
|
|
|
|
// Server shuts down, closing its connection before the client has had a
|
|
|
|
|