mc_server_status/
client.rs

1// Copyright (c) 2025 pynickle. This is a fork of Original Crate. Original copyright: Copyright (c) 2025 NameOfShadow
2
3use 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; // 5 minutes
21
22#[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        // If no explicit port was given, try SRV lookup
68        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(); // DNS info is optional
78
79        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        // Send handshake with the final host and port
87        self.send_handshake(&mut stream, &final_host, final_port)
88            .await?;
89
90        // Send status request
91        self.send_status_request(&mut stream).await?;
92
93        // Read and parse response
94        let response = self.read_response(&mut stream).await?;
95        let (json, latency) = self.parse_java_response(response, start)?;
96
97        // Build result
98        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(); // DNS info is optional
114
115        let socket = UdpSocket::bind("0.0.0.0:0")
116            .await
117            .map_err(McError::IoError)?;
118
119        // Send ping packet
120        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        // Receive response
127        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    // Helper methods
185    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)) // explicit port
205        } else {
206            Ok((address, default_port, false)) // no explicit port
207        }
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        // Check cache with TTL validation
214        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        // Try SRV lookup
226        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                // No SRV record found, use default
233                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        // Get the first SRV record with the highest priority (lowest number)
250        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        // Remove trailing dot from target if present
259        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        // Check cache with TTL validation
268        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        // Resolve and cache
280        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        // Simple implementation - in production you might want to use a proper DNS library
298        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, // This would require proper DNS queries
306            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            // Check if we have enough data to determine packet length
365            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
575// Helper functions
576fn 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}