1use bytes::{Buf, BufMut};
2use chrono::Utc;
3use snafu::{Backtrace, OptionExt, ResultExt, Snafu};
4use std::fmt::Write;
5use std::{
6 io::{Cursor, Read},
7 net::AddrParseError,
8 str::FromStr,
9 time::Duration,
10 vec,
11};
12use tokio::net::{lookup_host, UdpSocket};
13use tracing::{debug, trace};
14
15#[derive(Debug, Hash, Clone, PartialEq, Eq)]
16#[non_exhaustive]
17pub struct BedrockServerInfo {
18 pub edition: String,
20 pub name: String,
21 pub protocol_version: u32,
22 pub mc_version: String,
23 pub online_players: u32,
24 pub max_players: u32,
25 pub server_id: Option<u64>,
26 pub map_name: Option<String>,
27 pub game_mode: Option<String>,
28 pub numeric_game_mode: Option<u64>,
29 pub ipv4_port: Option<u16>,
30 pub ipv6_port: Option<u16>,
31 pub extra: Vec<String>,
32}
33
34#[cfg(feature = "java_parse")]
35impl From<BedrockServerInfo> for crate::JavaServerInfo {
36 fn from(value: BedrockServerInfo) -> Self {
37 let mut description = value.name;
38 if let Some(map_name) = value.map_name {
39 write!(description, "\n§r{map_name}").unwrap();
40 }
41 crate::JavaServerInfo {
42 version: None,
43 players: Some(crate::parse::ServerPlayers {
44 max: value.max_players,
45 online: value.online_players,
46 sample: None,
47 }),
48 description: crate::parse::TextComponent::Plain(description),
49 favicon: None,
50 mod_info: None,
51 enforces_secure_chat: None,
52 prevents_chat_reports: None,
53 previews_chat: None,
54 }
55 }
56}
57
58#[derive(Debug, Snafu)]
60pub struct ServerInfoParseError;
61
62impl FromStr for BedrockServerInfo {
63 type Err = ServerInfoParseError;
64
65 fn from_str(s: &str) -> Result<Self, Self::Err> {
66 fn parse_impl(s: &str) -> Option<BedrockServerInfo> {
67 let mut components = s.split(';').map(|component| component.to_owned());
68 Some(BedrockServerInfo {
69 edition: components.next()?,
70 name: components.next()?,
71 protocol_version: components.next().and_then(|s| s.parse().ok())?,
72 mc_version: components.next()?,
73 online_players: components.next().and_then(|s| s.parse().ok())?,
74 max_players: components.next().and_then(|s| s.parse().ok())?,
75 server_id: components.next().and_then(|s| s.parse().ok()),
76 map_name: components.next(),
77 game_mode: components.next(),
78 numeric_game_mode: components.next().and_then(|s| s.parse().ok()),
79 ipv4_port: components.next().and_then(|s| s.parse().ok()),
80 ipv6_port: components.next().and_then(|s| s.parse().ok()),
81 extra: components.collect(),
82 })
83 }
84
85 parse_impl(s).ok_or(ServerInfoParseError)
86 }
87}
88
89#[derive(Debug, Snafu)]
90pub enum BedrockPingError {
91 #[snafu(display("Failed to parse address {address:?}: {source}"))]
93 AddressParse {
94 source: AddrParseError,
95 address: String,
96 backtrace: Backtrace,
97 },
98 NoResponse { backtrace: Backtrace },
100 #[snafu(display("Failed to parse server info: {source}"), context(false))]
102 ServerInfoParse {
103 source: ServerInfoParseError,
104 backtrace: Backtrace,
105 },
106 #[snafu(display("I/O error: {source}"), context(false))]
108 Io {
109 source: std::io::Error,
110 backtrace: Backtrace,
111 },
112 #[snafu(display("DNS lookup failed for address `{address}`"))]
114 DNSLookupFailed {
115 address: String,
116 backtrace: Backtrace,
117 },
118 #[snafu(display("Failed to open socket: {source}"))]
120 ConnectFailed {
121 source: std::io::Error,
122 backtrace: Backtrace,
123 },
124}
125
126pub type BedrockPingResult<T> = Result<T, BedrockPingError>;
127
128const MAGIC: u128 = 0x00ffff00fefefefefdfdfdfd12345678;
131
132struct PingRequestFrame {
133 time: i64,
134 magic: u128,
135 guid: i64,
136}
137
138impl PingRequestFrame {
139 const PACKET_ID: u8 = 0x01;
140 pub fn to_vec(&self) -> Vec<u8> {
141 let mut buf = Vec::with_capacity(1028);
142 buf.put_u8(Self::PACKET_ID);
143 buf.put_i64(self.time);
144 buf.put_u128(self.magic);
145 buf.put_i64(self.guid);
146 buf
147 }
148}
149
150struct PingResponseFrame {
151 time: i64,
152 motd: String,
154}
155
156impl PingResponseFrame {
157 const SIZE: usize = 8 + 8 + 16 + 2;
158 const PACKET_ID: u8 = 0x1c;
159 pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
160 if bytes.len() < Self::SIZE {
161 return None;
162 }
163 let mut cursor = Cursor::new(bytes);
164
165 let packet_id = cursor.get_u8();
166 if packet_id != Self::PACKET_ID {
167 return None;
168 }
169
170 let time = cursor.get_i64();
171 let _guid = cursor.get_i64();
172 let magic = cursor.get_u128();
173
174 if magic != MAGIC {
175 return None;
176 }
177
178 let motd_len = cursor.get_u16();
179 let mut motd_bytes = vec![0u8; motd_len as usize];
180 cursor.read_exact(&mut motd_bytes).ok()?;
181 let motd = String::from_utf8(motd_bytes).ok()?;
182
183 Some(PingResponseFrame { time, motd })
184 }
185}
186
187pub async fn ping(
189 address: (String, u16),
190 retry_timeout: Duration,
191 retries: u64,
192) -> BedrockPingResult<(BedrockServerInfo, Duration)> {
193 let resolved = lookup_host(address.clone())
194 .await?
195 .next()
196 .context(DNSLookupFailedSnafu { address: address.0 })?;
197 trace!("host resolved to {resolved}");
198
199 let socket = UdpSocket::bind("0.0.0.0:0")
200 .await
201 .context(ConnectFailedSnafu)?;
202 socket.connect(resolved).await.context(ConnectFailedSnafu)?;
203 trace!("opened udp socket");
204
205 let mut response = None;
206 for retry in 0..retries {
207 debug!("pinging raknet server, attempt {}", retry + 1);
208 tokio::select! {
209 biased;
210 _ = tokio::time::sleep(retry_timeout) => continue,
211 res = attempt_ping(&socket) => response = res,
212 }
213 if response.is_some() {
214 break;
215 }
216 }
217 let (response, latency) = response.context(NoResponseSnafu)?;
218
219 trace!("ping finished");
220
221 Ok((response.motd.parse()?, latency))
222}
223
224async fn attempt_ping(socket: &UdpSocket) -> Option<(PingResponseFrame, Duration)> {
226 let outgoing_packet = PingRequestFrame {
227 time: Utc::now().timestamp_millis(),
228 magic: MAGIC,
229 guid: rand::random(),
230 };
231 socket.send(&outgoing_packet.to_vec()).await.ok()?;
232 let mut buffer = Vec::with_capacity(1024);
233 socket.recv_buf(&mut buffer).await.ok()?;
234 let incoming_packet = PingResponseFrame::from_bytes(&buffer)?;
235 let latency_millis = Utc::now().timestamp_millis() - incoming_packet.time;
236 let latency = Duration::from_millis(latency_millis as u64);
237
238 Some((incoming_packet, latency))
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[tokio::test]
246 async fn cubecraft() {
247 ping(
248 ("play.cubecraft.net".to_owned(), 19132),
249 Duration::from_secs(2),
250 3,
251 )
252 .await
253 .unwrap();
254 }
255
256 #[tokio::test]
257 async fn the_hive() {
258 ping(
259 ("geo.hivebedrock.network".to_owned(), 19132),
260 Duration::from_secs(2),
261 3,
262 )
263 .await
264 .unwrap();
265 }
266
267 #[tokio::test]
268 #[should_panic]
269 async fn invalid_address() {
270 ping(("example.com".to_owned(), 19132), Duration::from_secs(2), 3)
271 .await
272 .unwrap();
273 }
274}