async_minecraft_ping/
server.rs

1//! This module defines a wrapper around Minecraft's
2//! [ServerListPing](https://wiki.vg/Server_List_Ping)
3
4use std::time::Duration;
5
6use serde::Deserialize;
7use thiserror::Error;
8use tokio::net::TcpStream;
9
10use crate::protocol::{self, AsyncReadRawPacket, AsyncWriteRawPacket};
11
12#[derive(Error, Debug)]
13pub enum ServerError {
14    #[error("error reading or writing data")]
15    ProtocolError,
16
17    #[error("failed to connect to server")]
18    FailedToConnect,
19
20    #[error("invalid JSON response: \"{0}\"")]
21    InvalidJson(String),
22
23    #[error("mismatched pong payload (expected \"{expected}\", got \"{actual}\")")]
24    MismatchedPayload { expected: u64, actual: u64 },
25}
26
27impl From<protocol::ProtocolError> for ServerError {
28    fn from(_err: protocol::ProtocolError) -> Self {
29        ServerError::ProtocolError
30    }
31}
32
33/// Contains information about the server version.
34#[derive(Debug, Deserialize)]
35pub struct ServerVersion {
36    /// The server's Minecraft version, i.e. "1.15.2".
37    pub name: String,
38
39    /// The server's ServerListPing protocol version.
40    pub protocol: u32,
41}
42
43/// Contains information about a player.
44#[derive(Debug, Deserialize)]
45pub struct ServerPlayer {
46    /// The player's in-game name.
47    pub name: String,
48
49    /// The player's UUID.
50    pub id: String,
51}
52
53/// Contains information about the currently online
54/// players.
55#[derive(Debug, Deserialize)]
56pub struct ServerPlayers {
57    /// The configured maximum number of players for the
58    /// server.
59    pub max: u32,
60
61    /// The number of players currently online.
62    pub online: u32,
63
64    /// An optional list of player information for
65    /// currently online players.
66    pub sample: Option<Vec<ServerPlayer>>,
67}
68
69/// Contains the server's MOTD.
70#[derive(Debug, Deserialize)]
71#[serde(untagged)]
72pub enum ServerDescription {
73    Plain(String),
74    Object { text: String },
75}
76
77/// The decoded JSON response from a status query over
78/// ServerListPing.
79#[derive(Debug, Deserialize)]
80pub struct StatusResponse {
81    /// Information about the server's version.
82    pub version: ServerVersion,
83
84    /// Information about currently online players.
85    pub players: ServerPlayers,
86
87    /// Single-field struct containing the server's MOTD.
88    pub description: ServerDescription,
89
90    /// Optional field containing a path to the server's
91    /// favicon.
92    pub favicon: Option<String>,
93}
94
95const LATEST_PROTOCOL_VERSION: usize = 578;
96const DEFAULT_PORT: u16 = 25565;
97const DEFAULT_TIMEOUT: Duration = Duration::from_secs(2);
98
99/// Builder for a Minecraft
100/// ServerListPing connection.
101pub struct ConnectionConfig {
102    protocol_version: usize,
103    address: String,
104    port: u16,
105    timeout: Duration,
106}
107
108impl ConnectionConfig {
109    /// Initiates the Minecraft server
110    /// connection build process.
111    pub fn build<T: Into<String>>(address: T) -> Self {
112        ConnectionConfig {
113            protocol_version: LATEST_PROTOCOL_VERSION,
114            address: address.into(),
115            port: DEFAULT_PORT,
116            timeout: DEFAULT_TIMEOUT,
117        }
118    }
119
120    /// Sets a specific
121    /// protocol version for the connection to
122    /// use. If not specified, the latest version
123    /// will be used.
124    pub fn with_protocol_version(mut self, protocol_version: usize) -> Self {
125        self.protocol_version = protocol_version;
126        self
127    }
128
129    /// Sets a specific port for the
130    /// connection to use. If not specified, the
131    /// default port of 25565 will be used.
132    pub fn with_port(mut self, port: u16) -> Self {
133        self.port = port;
134        self
135    }
136
137    /// Sets a specific timeout for the
138    /// connection to use. If not specified, the
139    /// timeout defaults to two seconds.
140    pub fn with_timeout(mut self, timeout: Duration) -> Self {
141        self.timeout = timeout;
142        self
143    }
144
145    /// Connects to the server and consumes the builder.
146    pub async fn connect(self) -> Result<StatusConnection, ServerError> {
147        let stream = TcpStream::connect(format!("{}:{}", self.address, self.port))
148            .await
149            .map_err(|_| ServerError::FailedToConnect)?;
150
151        Ok(StatusConnection {
152            stream,
153            protocol_version: self.protocol_version,
154            address: self.address,
155            port: self.port,
156            timeout: self.timeout,
157        })
158    }
159}
160
161/// Convenience wrapper for easily connecting
162/// to a server on the default port with
163/// the latest protocol version.
164pub async fn connect(address: String) -> Result<StatusConnection, ServerError> {
165    ConnectionConfig::build(address).connect().await
166}
167
168/// Wraps a built connection
169pub struct StatusConnection {
170    stream: TcpStream,
171    protocol_version: usize,
172    address: String,
173    port: u16,
174    timeout: Duration,
175}
176
177impl StatusConnection {
178    /// Sends and reads the packets for the
179    /// ServerListPing status call.
180    ///
181    /// Consumes the connection and returns a type
182    /// that can only issue pings. The resulting
183    /// status body is accessible via the `status`
184    /// property on `PingConnection`.
185    pub async fn status(mut self) -> Result<PingConnection, ServerError> {
186        let handshake = protocol::HandshakePacket::new(
187            self.protocol_version,
188            self.address.to_string(),
189            self.port,
190        );
191
192        self.stream
193            .write_packet_with_timeout(handshake, self.timeout.clone())
194            .await?;
195
196        self.stream
197            .write_packet_with_timeout(protocol::RequestPacket::new(), self.timeout.clone())
198            .await?;
199
200        let response: protocol::ResponsePacket = self
201            .stream
202            .read_packet_with_timeout(self.timeout.clone())
203            .await?;
204
205        let status: StatusResponse = serde_json::from_str(&response.body)
206            .map_err(|_| ServerError::InvalidJson(response.body))?;
207
208        Ok(PingConnection {
209            stream: self.stream,
210            protocol_version: self.protocol_version,
211            address: self.address,
212            port: self.port,
213            status,
214            timeout: self.timeout,
215        })
216    }
217}
218
219/// Wraps a built connection
220///
221/// Constructed by calling `status()` on
222/// a `StatusConnection` struct.
223pub struct PingConnection {
224    stream: TcpStream,
225    protocol_version: usize,
226    address: String,
227    port: u16,
228    timeout: Duration,
229    pub status: StatusResponse,
230}
231
232impl PingConnection {
233    /// Sends a ping to the Minecraft server with the
234    /// provided payload and asserts that the returned
235    /// payload is the same.
236    ///
237    /// Server closes the connection after a ping call,
238    /// so this method consumes the connection.
239    pub async fn ping(mut self, payload: u64) -> Result<(), ServerError> {
240        let ping = protocol::PingPacket::new(payload);
241
242        self.stream
243            .write_packet_with_timeout(ping, self.timeout.clone())
244            .await?;
245
246        let pong: protocol::PongPacket = self
247            .stream
248            .read_packet_with_timeout(self.timeout.clone())
249            .await?;
250
251        if pong.payload != payload {
252            return Err(ServerError::MismatchedPayload {
253                expected: payload,
254                actual: pong.payload,
255            }
256            .into());
257        }
258
259        Ok(())
260    }
261}