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}