use std::{collections::HashMap, sync::Arc}; use async_trait::async_trait; use tokio::net::TcpStream; use tracing::Instrument; use crate::{ packets::{ clientbound::status::StatusTrait, serverbound::handshake::Handshake, Packet, SendPacket, }, OpaqueError, }; pub mod helpers; #[async_trait::async_trait] pub trait MinecraftServerHandle: Send + Sync + 'static + Clone { async fn start(&self) -> Result<(), OpaqueError>; async fn stop(&self) -> Result<(), OpaqueError>; async fn query_status(&self) -> Result; fn get_internal_address(&self) -> &str; fn get_internal_id(&self) -> &str; fn get_join_addr(&self) -> &str; fn get_join_port(&self) -> &str; fn get_motd(&self) -> Option; async fn query_server_connectable(&self) -> Result { let address = self.get_internal_address(); let server_stream = TcpStream::connect(address) .await .map_err(|_| "failed to connect to minecraft server")?; tracing::trace!( "successfully connected to backend server; (connectibility check) {:?}", server_stream.peer_addr() ); Ok(server_stream) } /// Takes the already received packets, sends them to the server, /// and proceeds to proxy the rest of the connection transparently. async fn proxy_status( &self, handshake: &Handshake, status_request: &Packet, client_stream: &mut TcpStream, server_stream: &mut TcpStream, ) -> Result<(), OpaqueError> { handshake .send_packet(server_stream) .await .map_err(|_| "failed to forward handshake packet to minecraft server")?; status_request .send_packet(server_stream) .await .map_err(|_| "failed to forward status request packet to minecraft server")?; let data_amount = tokio::io::copy_bidirectional(client_stream, server_stream) .await .map_err(|e| { format!( "error during bidirectional copy between server and client; err={:?}", e ) })?; tracing::trace!("data exchanged while proxying status: {:?}", data_amount); Ok(()) } async fn query_description(&self) -> Result, OpaqueError> { let status = self.query_status().await?; match status { ServerDeploymentStatus::Connectable(mut tcp_stream) => { let handshake = crate::packets::serverbound::handshake::Handshake::create( crate::types::VarInt::from(746).ok_or("could not create VarInt WTF?")?, crate::types::VarString::from(self.get_join_addr().to_string()), crate::types::UShort::from(1234), crate::types::VarInt::from(1).ok_or("could not create VarInt WTF?")?, ) .ok_or("failed to create handshake packet from scratch... WTF?")?; handshake .send_packet(&mut tcp_stream) .await .map_err(|_e| "failed to send handshake packet to server")?; let status_rq = crate::packets::Packet::from_bytes(0, Vec::new()) .ok_or("Failed to create status request packet from scratch")?; status_rq .send_packet(&mut tcp_stream) .await .map_err(|_e| "failed to send status request packet to server")?; let return_packet = crate::packets::Packet::parse(&mut tcp_stream).await?; let status_response = crate::packets::clientbound::status::StatusResponse::parse(return_packet) .await .unwrap(); return status_response.get_json().ok_or(OpaqueError::create( "failed to parse status response from server", )); } _ => { return Err(OpaqueError::create(&format!( "server is not running; status={:?}", status ))) } } } } #[async_trait] pub trait MinecraftAPI: Send + Sync + 'static { async fn query_server(&self, addr: &str, port: &str) -> Result; fn get_map(&self) -> Arc>>>; /// This should be callable even if there is already a watcher, /// and it should handle the collision itself while returning OK(). async fn start_watch( self, server: impl MinecraftServerHandle + Send + Sync + 'static, frequency: std::time::Duration, ) -> Result<(), OpaqueError> where Self: Clone, { let internal_id = server.get_internal_id().to_string(); if let Some(handle) = self.get_map().lock().await.get(&internal_id) { if !handle.is_finished() { return Ok(()); } } let span = tracing::span!(parent: None,tracing::Level::INFO, "server_watcher", internal_id, join_addr = server.get_join_addr(), join_port = server.get_join_port()); let internal_id_clone = internal_id.clone(); let api = self.clone(); let handle = tokio::spawn( async move { tracing::info!("starting watcher"); loop { tokio::time::sleep(frequency).await; let status_json = match server.query_description().await { Ok(x) => x, Err(e) => { tracing::error!( err = format!("{}", e.context), "could not query description" ); return; } }; if status_json.get_players_online() == 0 { // With this I don't need to specify that StatusTrait // should be send as well. // Otherwise I would need to have it be defined as: // trait StatusTrait: Send { ... } drop(status_json); let map = api.get_map(); let mut guard = map.lock().await; guard.remove(&internal_id_clone); drop(guard); drop(map); if let Err(err) = server.stop().await { tracing::error!( trace = %err.print_span_trace(), err = err.context, msg = "failed to stop server" ); } return; } } } .instrument(span), ); let map = self.get_map(); let mut guard = map.lock().await; guard.insert(internal_id, handle); drop(guard); Ok(()) } } pub enum ServerDeploymentStatus { Connectable(TcpStream), Starting, PodOk, Offline, Unavailable(String), } pub fn sanitize_addr(addr: &str) -> &str { // Thanks to a buggy minecraft, when the client sends a join // from a SRV DNS record, it will not use the address typed // in the game, but use the address redicted *to* by the // DNS record as the address for joining, plus a trailing "." // // For example: // server.example.com (_minecraft._tcp.server.example.com) // (the typed address) I (the DNS SRV record which gets read) // V // 5 25565 server.example.com // I (the response for the DNS SRV query) // V // server.example.com. // (the address used in the protocol) let addr = addr.trim_end_matches("."); // Modded minecraft clients send null terminated strings, // after which they have extra data. This just removes them // from the addr lookup let addr = terminate_at_null(addr); addr } fn terminate_at_null(str: &str) -> &str { match str.split('\0').next() { Some(x) => x, None => str, } }