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}