facio/client.rs
1//! # Client
2//!
3//! Using the [`RawPacket`](../raw_packet/struct.RawPacket.html) as its underlying data model and a common
4//! [`TcpStream`](https://doc.rust-lang.org/std/net/struct.TcpStream.html) from the
5//! standard library, this part of the library implements a RCON client.
6//!
7//! As of now (which means: before `async/await` is stable), this client is
8//! synchronous only. It is a future project to extend this to an async client,
9//! whenever the feature hits stable.
10//!
11//! ## Example
12//!
13//! ```
14//! use facio::{raw_packet::*, client::*};
15//!
16//! fn main() -> std::io::Result<()> {
17//! // open the rcon connection where `mypass` is the password and
18//! // echoing `echo` is used as the safe/check command (see below).
19//! // The last `None` denotes that the connection attempt has no timeout.
20//! let mut rcon =
21//! RconClient::open("127.0.0.1:38742",
22//! "mypass",
23//! Some("echo"),
24//! None).expect("Cannot open rcon");
25//!
26//! // now execute the command `/help`.
27//! if let Some(s) = rcon.exec("/help").ok() {
28//! println!("/help from server:\n{}", s);
29//! } else {
30//! println!("Error?");
31//! }
32//!
33//! } // connection is closed here.
34//! ```
35//!
36//! ## Safe/Check Command
37//!
38//! Since the protocol allows multi-packet response but does not provide any solution to
39//! detect those, finding any response as beginning, part and end of a multi-packet response
40//! is a bit tricky.
41//!
42//! The wiki on the protocol gives an idea (see [here](https://developer.valvesoftware.com/wiki/Source_RCON_Protocol#Multiple-packet_Responses))
43//! but to a full extend this is based on a specific implementation detail of the server. My experience with RCON server-side
44//! implementations has shown that this is mostly not working.
45//! Anyways, the basic idea seems to work throughout: an RCON server processes the requests
46//! it got in the order they arrived. Also, the packet id of a request is used as the id of the
47//! response to this very request. Insofar, to check if a response packet marks the end of a response,
48//! with every sent request, there is right behind another request sent *of which it is sure that the
49//! server responses* with exactly one packet.
50//!
51//! This second request packet (the safe or check packet) is sent with a different id then the
52//! actual request. By checking the response packet id it can be determined whether the received
53//! packet is a response for the check request and therefore the packet before marks an end
54//! to the original response, or it is still part of the response.
55//!
56//! - send request packet (with normal id)
57//! - send check packet (with check id)
58//! - receive packet while id is not check id
59//! - when packet with check id is received, everything is received
60//! from the original request, where all packets with normal id
61//! belong to the original response.
62//!
63//! ## Authentication
64//!
65//! Authentication can become tricky as well, since some servers do not comply the protocol in every
66//! detail. The protocol defines that following a `SERVERDATA_AUTH` packet, i.e. a auth request, the
67//! server sends back an empty `SERVERDATA_RESPONSE_VALUE` packet, followed by a `SERVERDATA_AUTH_RESPONSE`
68//! packet. Some servers do implement this in this way. Some just send a `SERVERDATA_AUTH_RESPONSE` back.
69//! Authentication as provided by the `open` function supports both by checking the type of the received
70//! packet.
71//!
72//! ## Using packet ids
73//!
74//! To solve the multi-packet response problem, packet ids are used in a certain way. This occupies the
75//! packet ids; they are not longer visible from the outside of the higher-level abstraction
76//! around `exec`.
77//!
78//! As a lower-level entry point which does not manage multi-packet responses but allows
79//! for an own implementation, there is the [`ll`](../ll/index.html) module.
80
81use super::ll::*;
82use super::raw_packet::*;
83
84use std::net::{SocketAddr};
85use std::io;
86use std::time::Duration;
87use std::io::{Error, ErrorKind};
88use std::net::TcpStream;
89
90const CONTROL_ID: i32 = -1; // used as the id for check packets
91const START_ID: i32 = 0; // used as the id for normal packets
92
93
94// The hole next section is kind of a hack. Some RCON Servers implement a double back response
95// for an auth request. They send first a ResponseValue, then a ResponseAuth. Some servers just
96// send a ResponseAuth.
97// The recv_auth functions allows both ways.
98//
99// This might result in a blocking call, if the server just sends a ResponseValue without a follow-up.
100enum AuthCheck {
101 Invalid, NoAuth, Valid
102}
103fn check_auth(packet_id: i32, packet: &RawPacket) -> AuthCheck {
104 if packet.response_type() == Some(PacketType::ResponseAuth) {
105 if packet.pid == packet_id {
106 AuthCheck::Valid
107 } else {
108 AuthCheck::Invalid
109 }
110 } else {
111 AuthCheck::NoAuth
112 }
113}
114
115fn recv_auth(stream: &mut TcpStream, packet_id: i32) -> io::Result<bool> {
116 let response =
117 recv_packet(stream)?;
118
119 match check_auth(packet_id, &response) {
120 AuthCheck::NoAuth => {
121 let response_auth =
122 recv_packet(stream)?;
123 match check_auth(packet_id, &response_auth) {
124 AuthCheck::NoAuth =>
125 Err(
126 Error::new(ErrorKind::Other,
127 "No valid authentication protocol by server.")),
128 AuthCheck::Invalid =>
129 Ok(false),
130 AuthCheck::Valid =>
131 Ok(true),
132 }
133 },
134 AuthCheck::Valid => Ok(true),
135 AuthCheck::Invalid => Ok(false),
136 }
137}
138
139
140/// The basic type to connect to a RCON server
141/// and execute commands.
142///
143/// It is certainly *not* safe to share this in concurrent
144/// applications. There should always be only *one* thread at
145/// a time which submits commands, etc.
146pub struct RconClient {
147 open_stream: TcpStream,
148 //last_id: i32,
149 /// The [`control_packet`] is used to determine wether the end of a possible
150 /// multi-packet response is reached by sending it right after any submit of
151 /// a command and reading back the response ids.
152 ///
153 /// It is important to have its [`pid`] always different then any possible [`last_id`].
154 control_packet: RawPacket,
155}
156impl RconClient {
157 /// Submits a command to the open RCON stream. Submit means, that
158 /// it sends the package via stream, followed by the [`control_packet`],
159 /// then waits for returning packets until a response packet with a
160 /// packet id fitting the [`control_packet`] packet id is received.
161 ///
162 /// All packets inbetween are considered to be an answer to the provided
163 /// [`RawPacket`] and their values are combined into one string.
164 pub fn exec<T: Into<String>>(&mut self, command: T) -> io::Result<String> {
165 let command_id = START_ID;
166 let packet =
167 RawPacket::new_exec(command_id, command)
168 .map_err(|e| e.to_io_error())?;
169
170 send_packet(&mut self.open_stream, &packet)?; // send command
171 send_packet(&mut self.open_stream, &self.control_packet)?; // send control_packet
172
173 let mut response_str: String;
174 let response =
175 recv_packet(&mut self.open_stream)?;
176 response_str = response.pbody;
177
178
179 // recv responses while its not the response from the control_packet.
180 while {
181 let control =
182 recv_packet(&mut self.open_stream)?;
183 if control.pid != CONTROL_ID {
184 response_str = response_str + &control.pbody;
185 true
186 } else {
187 false
188 }
189 } {}
190
191 //self.last_id = self.last_id + 1;
192
193 Ok(response_str)
194
195 }
196
197
198 /// Opens up a connection to an RCON server by connection via TCP/IP and authenticated
199 /// with provided `pass`.
200 ///
201 /// A `safe_command` can be specified which needs to be a domain-specific RCON command
202 /// for which it is guaranteed to receive exactly one packet as an answer, i.e. it needs
203 /// to be a command which has an *short* answer.
204 ///
205 /// If no `safe_command` is specified, the `SERVERDATA_RESPONSE_VALUE` trick is used, where
206 /// after every command an empty `SERVERDATA_RESPONSE_VALUE` packet is sent to the server to
207 /// trigger a `RESPONSE_VALUE` packet as a response and test for the end of command response.
208 /// (See the [section](https://developer.valvesoftware.com/wiki/Source_RCON_Protocol#Multiple-packet_Responses)
209 /// in this issue there.)
210 ///
211 /// As a last parameter a `timeout` can be specified to let the function return with an error
212 /// after a certain number of seconds while no connection can be established.
213 pub fn open<A: Into<String>,
214 P: Into<String>,
215 C: Into<String>>(addr: A,
216 pass: P,
217 safe_command: Option<C>,
218 timeout: Option<Duration>) -> io::Result<RconClient> {
219 // building address:
220 let s_addr: String = addr.into();
221 let sock_addr: SocketAddr =
222 s_addr.parse().map_err(|_|
223 Error::new(ErrorKind::Other,
224 format!("cannot parse internet address.")))?;
225 // building package and data:
226 let auth_packet =
227 RawPacket::new(START_ID, 3, pass)
228 .map_err(|e|
229 Error::new(ErrorKind::Other,
230 format!("auth packet creation error: '{}'", e)))?;
231
232 println!("Connection to rcon server.");
233 //connect:
234 let mut stream = {
235 if let Some(dur) = timeout {
236 TcpStream::connect_timeout(&sock_addr, dur)?
237 } else {
238 TcpStream::connect(&sock_addr)?
239 }
240 };
241
242 // sending auth
243 send_packet(&mut stream, &auth_packet)?;
244 // ... and recv result:
245 let auth =
246 recv_auth(&mut stream, START_ID)?;
247 // this ^^ function is somewhat a hack to satisfy sloppy(?) written servers.
248
249 if auth {
250 // either use the `safe_command` or the `RESPONSE_VALUE` trick.
251 let control_packet = {
252 if let Some(cmd) = safe_command {
253 RawPacket::new_exec(CONTROL_ID, cmd)
254 .map_err(|e| e.to_io_error())?
255 } else {
256 RawPacket::new_response_value(CONTROL_ID, "")
257 .map_err(|e| e.to_io_error())?
258 }
259 };
260
261 Ok( RconClient { open_stream: stream, control_packet })
262
263 } else {
264 Err(
265 Error::new(ErrorKind::Other,
266 "Authentication failed. Wrong password."))
267 }
268 }
269}