northstar_rcon_client/
client.rs

1use crate::inner_client;
2use crate::inner_client::{InnerClientRead, InnerClientWrite, Request, Response};
3use tokio::net::{TcpStream, ToSocketAddrs};
4
5/// A connected but not yet authenticated RCON client.
6///
7/// Clients must successfully authenticate before sending commands and receiving logs, which is
8/// enforced by this  type.
9///
10/// To make an authentication attempt, use [`authenticate`].
11///
12/// # Example
13/// ```rust,no_run
14/// use northstar_rcon_client::connect;
15///
16/// #[tokio::main]
17/// async fn main() {
18///     let client = connect("localhost:37015")
19///         .await
20///         .unwrap();
21///
22///     match client.authenticate("password123").await {
23///         Ok(_) => println!("Authentication successful!"),
24///         Err((_, err)) => println!("Authentication failed: {}", err),
25///     }
26/// }
27/// ```
28///
29/// [`authenticate`]: NotAuthenticatedClient::authenticate
30#[derive(Debug)]
31pub struct NotAuthenticatedClient {
32    read: InnerClientRead,
33    write: InnerClientWrite,
34}
35
36/// An error describing why an authentication request failed.
37#[derive(Debug, thiserror::Error)]
38pub enum AuthError {
39    /// The request failed because an invalid password was used.
40    #[error("invalid password")]
41    InvalidPassword,
42
43    /// The request failed because this user or IP address is banned.
44    #[error("banned")]
45    Banned,
46
47    /// The request failed due to a socket or protocol error.
48    #[error(transparent)]
49    Fatal(#[from] crate::Error),
50}
51
52/// The read end of a connected and authenticated RCON client.
53///
54/// Log messages can be received from the reader while commands are being sent by the writer, and
55/// vice-versa. The underlying connection will close when both the reader and writer are closed.
56///
57/// # Example
58/// ```rust,no_run
59/// use northstar_rcon_client::connect;
60///
61/// #[tokio::main]
62/// async fn main() {
63///     let client = connect("localhost:37015").await.unwrap();
64///     let (mut read, _) = client.authenticate("password123").await.unwrap();
65///
66///     loop {
67///         let log_line = read.receive_console_log()
68///             .await
69///             .unwrap();
70///
71///         println!("Server logged: {}", log_line);
72///     }
73/// }
74/// ```
75pub struct ClientRead {
76    read: InnerClientRead,
77}
78
79/// The write end of a connected and authenticated RCON client.
80///
81/// Commands can be sent by the writer while log messages are received from the reader, and
82/// vice-versa. The underlying connection will close when both the reader and writer are closed.
83///
84/// # Example
85/// ```rust,no_run
86/// use northstar_rcon_client::connect;
87///
88/// #[tokio::main]
89/// async fn main() {
90///     let client = connect("localhost:37015").await.unwrap();
91///     let (_, mut write) = client.authenticate("password123").await.unwrap();
92///
93///     write.exec_command("status").await.unwrap();
94///     write.exec_command("quit").await.unwrap();
95/// }
96/// ```
97pub struct ClientWrite {
98    write: InnerClientWrite,
99}
100
101impl NotAuthenticatedClient {
102    pub(crate) async fn new<A: ToSocketAddrs>(addr: A) -> crate::Result<Self> {
103        let stream = TcpStream::connect(addr).await?;
104
105        let (read, write) = stream.into_split();
106        Ok(NotAuthenticatedClient {
107            read: InnerClientRead::new(read),
108            write: InnerClientWrite::new(write),
109        })
110    }
111
112    /// Attempt to authenticate with the RCON server.
113    ///
114    /// If the authentication attempt is successful this client will become a
115    /// [`ClientRead`]/[`ClientWrite`] pair, allowing executing commands and reading log lines.
116    ///
117    /// If authentication fails the function will return the reason, as well as the client to allow
118    /// repeated authentication attempts.
119    ///
120    /// # Example
121    /// ```rust,no_run
122    /// use std::io::BufRead;
123    /// use northstar_rcon_client::connect;
124    ///
125    /// #[tokio::main]
126    /// async fn main() {
127    ///     let mut client = connect("localhost:37015")
128    ///         .await
129    ///         .unwrap();
130    ///
131    ///     let mut lines = std::io::stdin().lock().lines();
132    ///
133    ///     // Keep reading passwords until authentication succeeds
134    ///     let (read, write) = loop {
135    ///         print!("Enter password: ");
136    ///         let password = lines.next()
137    ///             .unwrap()
138    ///             .unwrap();
139    ///
140    ///         match client.authenticate(&password).await {
141    ///             Ok((read, write)) => break (read, write),
142    ///             Err((new_client, err)) => {
143    ///                 println!("Authentication failed: {}", err);
144    ///                 client = new_client;
145    ///             }
146    ///         }
147    ///     };
148    /// }
149    /// ```
150    pub async fn authenticate(
151        mut self,
152        pass: &str,
153    ) -> Result<(ClientRead, ClientWrite), (NotAuthenticatedClient, AuthError)> {
154        if let Err(err) = self.write.send(Request::Auth { pass }).await {
155            return Err((self, AuthError::Fatal(err)));
156        }
157
158        // Wait until a successful authentication response is received
159        loop {
160            match self.read.receive().await {
161                Ok(Response::Auth { res: Ok(()) }) => break,
162                Ok(Response::Auth {
163                    res: Err(inner_client::AuthError::InvalidPassword),
164                }) => return Err((self, AuthError::InvalidPassword)),
165                Ok(Response::Auth {
166                    res: Err(inner_client::AuthError::Banned),
167                }) => return Err((self, AuthError::Banned)),
168                Ok(_) => {
169                    // todo: log message indicating something was skipped?
170                    continue;
171                }
172                Err(err) => return Err((self, AuthError::Fatal(err))),
173            }
174        }
175
176        Ok((
177            ClientRead { read: self.read },
178            ClientWrite { write: self.write },
179        ))
180    }
181}
182
183impl ClientWrite {
184    /// Set the value of a ConVar if it exists.
185    ///
186    /// # Example
187    /// ```rust,no_run
188    /// use northstar_rcon_client::connect;
189    ///
190    /// #[tokio::main]
191    /// async fn main() {
192    ///     let client = connect("localhost:37015").await.unwrap();
193    ///     let (_, mut write) = client.authenticate("password123").await.unwrap();
194    ///
195    ///     write.set_value("ns_should_return_to_lobby", "0").await.unwrap();
196    /// }
197    /// ```
198    pub async fn set_value(&mut self, var: &str, val: &str) -> crate::Result<()> {
199        self.write.send(Request::SetValue { var, val }).await
200    }
201
202    /// Execute a command remotely.
203    ///
204    /// # Example
205    /// ```rust,no_run
206    /// use northstar_rcon_client::connect;
207    ///
208    /// #[tokio::main]
209    /// async fn main() {
210    ///     let client = connect("localhost:37015").await.unwrap();
211    ///     let (_, mut write) = client.authenticate("password123").await.unwrap();
212    ///
213    ///     write.exec_command("map mp_glitch").await.unwrap();
214    /// }
215    /// ```
216    pub async fn exec_command(&mut self, cmd: &str) -> crate::Result<()> {
217        self.write.send(Request::ExecCommand { cmd }).await
218    }
219
220    /// Enable console logs being sent to RCON clients.
221    ///
222    /// This sets `sv_rcon_sendlogs` to `1`, which will enable logging for all clients until the
223    /// server stops. Logging can be disabled by setting `sv_rcon_sendlogs` to `0`, for example with
224    /// [`set_value`].
225    ///
226    /// Console logs can be read with [`ClientRead::receive_console_log`].
227    ///
228    /// # Example
229    /// ```rust,no_run
230    /// use northstar_rcon_client::connect;
231    ///
232    /// #[tokio::main]
233    /// async fn main() {
234    ///     let client = connect("localhost:37015").await.unwrap();
235    ///     let (mut read, mut write) = client.authenticate("password123").await.unwrap();
236    ///
237    ///     write.enable_console_logs().await.unwrap();
238    ///
239    ///     // Start reading lines
240    ///     loop {
241    ///         let line = read.receive_console_log().await.unwrap();
242    ///         println!("> {}", line);
243    ///     }
244    /// }
245    /// ```
246    ///
247    /// [`set_value`]: ClientWrite::set_value
248    /// [`ClientRead::receive_console_log`]: ClientRead::receive_console_log
249    pub async fn enable_console_logs(&mut self) -> crate::Result<()> {
250        self.write.send(Request::EnableConsoleLogs).await
251    }
252}
253
254impl ClientRead {
255    /// Receive the next console log line asynchronously.
256    ///
257    /// Console logs will not be sent to RCON clients unless the `sv_rcon_sendlogs` variable is set
258    /// to `1`, which can be set with [`ClientWrite::enable_console_logs`].
259    ///
260    /// Log lines are currently buffered, so this function will return lines from the buffer before
261    /// waiting for more from the server. This does mean you should always attempt to read logs, to
262    /// avoid the buffer filling up.
263    ///
264    /// This function does not have a timeout. It will return an error if the connection is closed
265    /// or a protocol error occurs, otherwise it will always return a log line.
266    ///
267    /// # Example
268    /// ```rust,no_run
269    /// use northstar_rcon_client::connect;
270    ///
271    /// #[tokio::main]
272    /// async fn main() {
273    ///     let client = connect("localhost:37015").await.unwrap();
274    ///     let (mut read, mut write) = client.authenticate("password123").await.unwrap();
275    ///
276    ///     write.enable_console_logs().await.unwrap();
277    ///
278    ///     // Start reading lines
279    ///     loop {
280    ///         let line = read.receive_console_log().await.unwrap();
281    ///         println!("> {}", line);
282    ///     }
283    /// }
284    /// ```
285    ///
286    /// [`ClientWrite::enable_console_logs`]: ClientWrite::enable_console_logs
287    pub async fn receive_console_log(&mut self) -> crate::Result<String> {
288        loop {
289            match self.read.receive().await? {
290                Response::Auth { .. } => {
291                    // todo: this should not happen, log an error?
292                    continue;
293                }
294                Response::ConsoleLog { msg } => return Ok(msg),
295            }
296        }
297    }
298}