1use std::io::Cursor;
4use std::net::{SocketAddr, ToSocketAddrs};
5use std::time::{Duration, SystemTime};
6
7use dashmap::DashMap;
8use once_cell::sync::Lazy;
9use tokio::io::{AsyncReadExt, AsyncWriteExt};
10use tokio::net::{TcpStream, UdpSocket};
11use tokio::time::timeout;
12use trust_dns_resolver::TokioAsyncResolver;
13use trust_dns_resolver::config::*;
14
15use crate::error::McError;
16use crate::models::*;
17
18static DNS_CACHE: Lazy<DashMap<String, (SocketAddr, SystemTime)>> = Lazy::new(DashMap::new);
19static SRV_CACHE: Lazy<DashMap<String, (String, u16, SystemTime)>> = Lazy::new(DashMap::new);
20const DNS_CACHE_TTL: u64 = 300; #[derive(Clone)]
23pub struct McClient {
24 timeout: Duration,
25 max_parallel: usize,
26}
27
28impl Default for McClient {
29 fn default() -> Self {
30 Self {
31 timeout: Duration::from_secs(10),
32 max_parallel: 10,
33 }
34 }
35}
36
37impl McClient {
38 pub fn new() -> Self {
39 Self::default()
40 }
41
42 pub fn with_timeout(mut self, timeout: Duration) -> Self {
43 self.timeout = timeout;
44 self
45 }
46
47 pub fn with_max_parallel(mut self, max_parallel: usize) -> Self {
48 self.max_parallel = max_parallel;
49 self
50 }
51
52 pub async fn ping(
53 &self,
54 address: &str,
55 edition: ServerEdition,
56 ) -> Result<ServerStatus, McError> {
57 match edition {
58 ServerEdition::Java => self.ping_java(address).await,
59 ServerEdition::Bedrock => self.ping_bedrock(address).await,
60 }
61 }
62
63 pub async fn ping_java(&self, address: &str) -> Result<ServerStatus, McError> {
64 let start = SystemTime::now();
65 let (host, port, explicit_port) = Self::parse_address_with_flag(address, 25565)?;
66
67 let (final_host, final_port) = if !explicit_port {
69 self.resolve_srv(host, port)
70 .await
71 .unwrap_or((host.to_string(), port))
72 } else {
73 (host.to_string(), port)
74 };
75
76 let resolved = self.resolve_dns(&final_host, final_port).await?;
77 let dns_info = self.get_dns_info(&final_host).await.ok(); let mut stream = timeout(self.timeout, TcpStream::connect(resolved))
80 .await
81 .map_err(|_| McError::Timeout)?
82 .map_err(|e| McError::ConnectionError(e.to_string()))?;
83
84 stream.set_nodelay(true).map_err(McError::IoError)?;
85
86 self.send_handshake(&mut stream, &final_host, final_port)
88 .await?;
89
90 self.send_status_request(&mut stream).await?;
92
93 let response = self.read_response(&mut stream).await?;
95 let (json, latency) = self.parse_java_response(response, start)?;
96
97 Ok(ServerStatus {
99 online: true,
100 ip: resolved.ip().to_string(),
101 port: resolved.port(),
102 hostname: host.to_string(),
103 latency,
104 dns: dns_info,
105 data: ServerData::Java(self.parse_java_json(&json)?),
106 })
107 }
108
109 pub async fn ping_bedrock(&self, address: &str) -> Result<ServerStatus, McError> {
110 let start = SystemTime::now();
111 let (host, port) = Self::parse_address(address, 19132)?;
112 let resolved = self.resolve_dns(host, port).await?;
113 let dns_info = self.get_dns_info(host).await.ok(); let socket = UdpSocket::bind("0.0.0.0:0")
116 .await
117 .map_err(McError::IoError)?;
118
119 let ping_packet = self.create_bedrock_ping_packet();
121 timeout(self.timeout, socket.send_to(&ping_packet, resolved))
122 .await
123 .map_err(|_| McError::Timeout)?
124 .map_err(|e| McError::IoError(e))?;
125
126 let mut buf = [0u8; 1024];
128 let (len, _) = timeout(self.timeout, socket.recv_from(&mut buf))
129 .await
130 .map_err(|_| McError::Timeout)?
131 .map_err(|e| McError::IoError(e))?;
132
133 if len < 35 {
134 return Err(McError::InvalidResponse("Response too short".to_string()));
135 }
136
137 let latency = start
138 .elapsed()
139 .map_err(|_| McError::InvalidResponse("Time error".to_string()))?
140 .as_secs_f64()
141 * 1000.0;
142
143 let pong_data = String::from_utf8_lossy(&buf[35..len]).to_string();
144
145 Ok(ServerStatus {
146 online: true,
147 ip: resolved.ip().to_string(),
148 port: resolved.port(),
149 hostname: host.to_string(),
150 latency,
151 dns: dns_info,
152 data: ServerData::Bedrock(self.parse_bedrock_response(&pong_data)?),
153 })
154 }
155
156 pub async fn ping_many(
157 &self,
158 servers: &[ServerInfo],
159 ) -> Vec<(ServerInfo, Result<ServerStatus, McError>)> {
160 use futures::stream::StreamExt;
161 use tokio::sync::Semaphore;
162
163 let semaphore = std::sync::Arc::new(Semaphore::new(self.max_parallel));
164 let client = self.clone();
165
166 let futures = servers.iter().map(|server| {
167 let server = server.clone();
168 let semaphore = semaphore.clone();
169 let client = client.clone();
170
171 async move {
172 let _permit = semaphore.acquire().await;
173 let result = client.ping(&server.address, server.edition).await;
174 (server, result)
175 }
176 });
177
178 futures::stream::iter(futures)
179 .buffer_unordered(self.max_parallel)
180 .collect()
181 .await
182 }
183
184 fn parse_address(address: &str, default_port: u16) -> Result<(&str, u16), McError> {
186 if let Some((host, port_str)) = address.split_once(':') {
187 let port = port_str
188 .parse::<u16>()
189 .map_err(|e| McError::InvalidPort(e.to_string()))?;
190 Ok((host, port))
191 } else {
192 Ok((address, default_port))
193 }
194 }
195
196 fn parse_address_with_flag(
197 address: &str,
198 default_port: u16,
199 ) -> Result<(&str, u16, bool), McError> {
200 if let Some((host, port_str)) = address.split_once(':') {
201 let port = port_str
202 .parse::<u16>()
203 .map_err(|e| McError::InvalidPort(e.to_string()))?;
204 Ok((host, port, true)) } else {
206 Ok((address, default_port, false)) }
208 }
209
210 async fn resolve_srv(&self, host: &str, default_port: u16) -> Result<(String, u16), McError> {
211 let cache_key = format!("_minecraft._tcp.{}", host);
212
213 if let Some(entry) = SRV_CACHE.get(&cache_key) {
215 let (srv_host, srv_port, timestamp) = entry.value().clone();
216 if timestamp
217 .elapsed()
218 .map(|d| d.as_secs() < DNS_CACHE_TTL)
219 .unwrap_or(false)
220 {
221 return Ok((srv_host, srv_port));
222 }
223 }
224
225 match self.lookup_srv(host).await {
227 Ok((srv_host, srv_port)) => {
228 SRV_CACHE.insert(cache_key, (srv_host.clone(), srv_port, SystemTime::now()));
229 Ok((srv_host, srv_port))
230 }
231 Err(_) => {
232 Ok((host.to_string(), default_port))
234 }
235 }
236 }
237
238 async fn lookup_srv(&self, host: &str) -> Result<(String, u16), McError> {
239 let resolver =
240 TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default());
241
242 let srv_name = format!("_minecraft._tcp.{}", host);
243
244 let response = timeout(self.timeout, resolver.srv_lookup(&srv_name))
245 .await
246 .map_err(|_| McError::Timeout)?
247 .map_err(|e| McError::DnsError(format!("SRV lookup failed: {}", e)))?;
248
249 let srv = response
251 .iter()
252 .min_by_key(|r| r.priority())
253 .ok_or_else(|| McError::DnsError("No SRV records found".to_string()))?;
254
255 let target = srv.target().to_utf8();
256 let port = srv.port();
257
258 let target = target.trim_end_matches('.');
260
261 Ok((target.to_string(), port))
262 }
263
264 async fn resolve_dns(&self, host: &str, port: u16) -> Result<SocketAddr, McError> {
265 let cache_key = format!("{}:{}", host, port);
266
267 if let Some(entry) = DNS_CACHE.get(&cache_key) {
269 let (addr, timestamp) = *entry.value();
270 if timestamp
271 .elapsed()
272 .map(|d| d.as_secs() < DNS_CACHE_TTL)
273 .unwrap_or(false)
274 {
275 return Ok(addr);
276 }
277 }
278
279 let addrs: Vec<SocketAddr> = format!("{}:{}", host, port)
281 .to_socket_addrs()
282 .map_err(|e| McError::DnsError(e.to_string()))?
283 .collect();
284
285 let addr = addrs
286 .iter()
287 .find(|a| a.is_ipv4())
288 .or_else(|| addrs.first())
289 .copied()
290 .ok_or_else(|| McError::DnsError("No addresses resolved".to_string()))?;
291
292 DNS_CACHE.insert(cache_key, (addr, SystemTime::now()));
293 Ok(addr)
294 }
295
296 async fn get_dns_info(&self, host: &str) -> Result<DnsInfo, McError> {
297 let addrs: Vec<SocketAddr> = format!("{}:0", host)
299 .to_socket_addrs()
300 .map_err(|e| McError::DnsError(e.to_string()))?
301 .collect();
302
303 Ok(DnsInfo {
304 a_records: addrs.iter().map(|a| a.ip().to_string()).collect(),
305 cname: None, ttl: 300,
307 })
308 }
309
310 async fn send_handshake(
311 &self,
312 stream: &mut TcpStream,
313 host: &str,
314 port: u16,
315 ) -> Result<(), McError> {
316 let mut handshake = Vec::with_capacity(64);
317 write_var_int(&mut handshake, 0x00);
318 write_var_int(&mut handshake, 47);
319 write_string(&mut handshake, host);
320 handshake.extend_from_slice(&port.to_be_bytes());
321 write_var_int(&mut handshake, 1);
322
323 let mut packet = Vec::with_capacity(handshake.len() + 5);
324 write_var_int(&mut packet, handshake.len() as i32);
325 packet.extend_from_slice(&handshake);
326
327 timeout(self.timeout, stream.write_all(&packet))
328 .await
329 .map_err(|_| McError::Timeout)?
330 .map_err(McError::IoError)
331 }
332
333 async fn send_status_request(&self, stream: &mut TcpStream) -> Result<(), McError> {
334 let mut status_request = Vec::with_capacity(5);
335 write_var_int(&mut status_request, 0x00);
336
337 let mut status_packet = Vec::with_capacity(status_request.len() + 5);
338 write_var_int(&mut status_packet, status_request.len() as i32);
339 status_packet.extend_from_slice(&status_request);
340
341 timeout(self.timeout, stream.write_all(&status_packet))
342 .await
343 .map_err(|_| McError::Timeout)?
344 .map_err(McError::IoError)
345 }
346
347 async fn read_response(&self, stream: &mut TcpStream) -> Result<Vec<u8>, McError> {
348 let mut response = Vec::with_capacity(1024);
349 let mut buf = [0u8; 4096];
350 let mut expected_length = None;
351
352 loop {
353 let n = timeout(self.timeout, stream.read(&mut buf))
354 .await
355 .map_err(|_| McError::Timeout)?
356 .map_err(McError::IoError)?;
357
358 if n == 0 {
359 break;
360 }
361
362 response.extend_from_slice(&buf[..n]);
363
364 if expected_length.is_none() && response.len() >= 5 {
366 let mut cursor = Cursor::new(&response);
367 if let Ok(packet_length) = read_var_int(&mut cursor) {
368 expected_length = Some(cursor.position() as usize + packet_length as usize);
369 }
370 }
371
372 if let Some(expected) = expected_length {
373 if response.len() >= expected {
374 break;
375 }
376 }
377 }
378
379 if response.is_empty() {
380 return Err(McError::InvalidResponse(
381 "No response from server".to_string(),
382 ));
383 }
384
385 Ok(response)
386 }
387
388 fn parse_java_response(
389 &self,
390 response: Vec<u8>,
391 start: SystemTime,
392 ) -> Result<(serde_json::Value, f64), McError> {
393 let mut cursor = Cursor::new(&response);
394 let packet_length = read_var_int(&mut cursor).map_err(|e| {
395 McError::InvalidResponse(format!("Failed to read packet length: {}", e))
396 })?;
397
398 let total_expected = cursor.position() as usize + packet_length as usize;
399 if response.len() < total_expected {
400 return Err(McError::InvalidResponse(format!(
401 "Incomplete packet: expected {}, got {}",
402 total_expected,
403 response.len()
404 )));
405 }
406
407 let packet_id = read_var_int(&mut cursor)
408 .map_err(|e| McError::InvalidResponse(format!("Failed to read packet ID: {}", e)))?;
409
410 if packet_id != 0x00 {
411 return Err(McError::InvalidResponse(format!(
412 "Unexpected packet ID: {}",
413 packet_id
414 )));
415 }
416
417 let json_length = read_var_int(&mut cursor)
418 .map_err(|e| McError::InvalidResponse(format!("Failed to read JSON length: {}", e)))?;
419
420 if cursor.position() as usize + json_length as usize > response.len() {
421 return Err(McError::InvalidResponse("JSON data truncated".to_string()));
422 }
423
424 let json_buf = &response
425 [cursor.position() as usize..cursor.position() as usize + json_length as usize];
426 let json_str = String::from_utf8(json_buf.to_vec()).map_err(McError::Utf8Error)?;
427
428 let json: serde_json::Value =
429 serde_json::from_str(&json_str).map_err(McError::JsonError)?;
430
431 let latency = start
432 .elapsed()
433 .map_err(|_| McError::InvalidResponse("Time error".to_string()))?
434 .as_secs_f64()
435 * 1000.0;
436
437 Ok((json, latency))
438 }
439
440 fn parse_java_json(&self, json: &serde_json::Value) -> Result<JavaStatus, McError> {
441 let version = JavaVersion {
442 name: json["version"]["name"]
443 .as_str()
444 .unwrap_or("Unknown")
445 .to_string(),
446 protocol: json["version"]["protocol"].as_i64().unwrap_or(0),
447 };
448
449 let players = JavaPlayers {
450 online: json["players"]["online"].as_i64().unwrap_or(0),
451 max: json["players"]["max"].as_i64().unwrap_or(0),
452 sample: if let Some(sample) = json["players"]["sample"].as_array() {
453 Some(
454 sample
455 .iter()
456 .filter_map(|p| {
457 Some(JavaPlayer {
458 name: p["name"].as_str()?.to_string(),
459 id: p["id"].as_str()?.to_string(),
460 })
461 })
462 .collect(),
463 )
464 } else {
465 None
466 },
467 };
468
469 let description = if let Some(desc) = json["description"].as_str() {
470 desc.to_string()
471 } else if let Some(text) = json["description"]["text"].as_str() {
472 text.to_string()
473 } else {
474 "No description".to_string()
475 };
476
477 let favicon = json["favicon"].as_str().map(|s| s.to_string());
478 let map = json["map"].as_str().map(|s| s.to_string());
479 let gamemode = json["gamemode"].as_str().map(|s| s.to_string());
480 let software = json["software"].as_str().map(|s| s.to_string());
481
482 let plugins = if let Some(plugins_array) = json["plugins"].as_array() {
483 Some(
484 plugins_array
485 .iter()
486 .filter_map(|p| {
487 Some(JavaPlugin {
488 name: p["name"].as_str()?.to_string(),
489 version: p["version"].as_str().map(|s| s.to_string()),
490 })
491 })
492 .collect(),
493 )
494 } else {
495 None
496 };
497
498 let mods = if let Some(mods_array) = json["mods"].as_array() {
499 Some(
500 mods_array
501 .iter()
502 .filter_map(|m| {
503 Some(JavaMod {
504 modid: m["modid"].as_str()?.to_string(),
505 version: m["version"].as_str().map(|s| s.to_string()),
506 })
507 })
508 .collect(),
509 )
510 } else {
511 None
512 };
513
514 Ok(JavaStatus {
515 version,
516 players,
517 description,
518 favicon,
519 map,
520 gamemode,
521 software,
522 plugins,
523 mods,
524 raw_data: json.clone(),
525 })
526 }
527
528 fn create_bedrock_ping_packet(&self) -> Vec<u8> {
529 let mut ping_packet = Vec::with_capacity(35);
530 ping_packet.push(0x01);
531 ping_packet.extend_from_slice(
532 &(SystemTime::now()
533 .duration_since(SystemTime::UNIX_EPOCH)
534 .unwrap()
535 .as_millis() as u64)
536 .to_be_bytes(),
537 );
538 ping_packet.extend_from_slice(&[
539 0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE, 0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34,
540 0x56, 0x78,
541 ]);
542 ping_packet.extend_from_slice(&[0x00; 8]);
543 ping_packet
544 }
545
546 fn parse_bedrock_response(&self, pong_data: &str) -> Result<BedrockStatus, McError> {
547 let parts: Vec<&str> = pong_data.split(';').collect();
548
549 if parts.len() < 6 {
550 return Err(McError::InvalidResponse(
551 "Invalid Bedrock response".to_string(),
552 ));
553 }
554
555 Ok(BedrockStatus {
556 edition: parts[0].to_string(),
557 motd: parts[1].to_string(),
558 protocol_version: parts[2].to_string(),
559 version: parts[3].to_string(),
560 online_players: parts[4].to_string(),
561 max_players: parts[5].to_string(),
562 server_uid: parts.get(6).map_or("", |s| *s).to_string(),
563 motd2: parts.get(7).map_or("", |s| *s).to_string(),
564 game_mode: parts.get(8).map_or("", |s| *s).to_string(),
565 game_mode_numeric: parts.get(9).map_or("", |s| *s).to_string(),
566 port_ipv4: parts.get(10).map_or("", |s| *s).to_string(),
567 port_ipv6: parts.get(11).map_or("", |s| *s).to_string(),
568 map: parts.get(12).map(|s| s.to_string()),
569 software: parts.get(13).map(|s| s.to_string()),
570 raw_data: pong_data.to_string(),
571 })
572 }
573}
574
575fn write_var_int(buffer: &mut Vec<u8>, value: i32) {
577 let mut value = value as u32;
578 loop {
579 let mut temp = (value & 0x7F) as u8;
580 value >>= 7;
581 if value != 0 {
582 temp |= 0x80;
583 }
584 buffer.push(temp);
585 if value == 0 {
586 break;
587 }
588 }
589}
590
591fn write_string(buffer: &mut Vec<u8>, s: &str) {
592 write_var_int(buffer, s.len() as i32);
593 buffer.extend_from_slice(s.as_bytes());
594}
595
596fn read_var_int(reader: &mut impl std::io::Read) -> Result<i32, String> {
597 let mut result = 0i32;
598 let mut shift = 0;
599 loop {
600 let mut byte = [0u8];
601 reader.read_exact(&mut byte).map_err(|e| e.to_string())?;
602 let value = byte[0] as i32;
603 result |= (value & 0x7F) << shift;
604 shift += 7;
605 if shift > 35 {
606 return Err("VarInt too big".to_string());
607 }
608 if (value & 0x80) == 0 {
609 break;
610 }
611 }
612 Ok(result)
613}