diff options
| author | mat <github@matdoes.dev> | 2021-12-06 00:28:40 -0600 |
|---|---|---|
| committer | mat <github@matdoes.dev> | 2021-12-06 00:28:40 -0600 |
| commit | 5029a09963b5753c1f9b7f777f28e1c0951343e7 (patch) | |
| tree | 2e0e37029bf031adc3e28713828e7d4be7336ccb /minecraft-protocol/src | |
| download | azalea-drasl-5029a09963b5753c1f9b7f777f28e1c0951343e7.tar.xz | |
Initial commit
Diffstat (limited to 'minecraft-protocol/src')
| -rw-r--r-- | minecraft-protocol/src/connection.rs | 38 | ||||
| -rw-r--r-- | minecraft-protocol/src/friendly_byte_buf.rs | 56 | ||||
| -rw-r--r-- | minecraft-protocol/src/lib.rs | 53 | ||||
| -rw-r--r-- | minecraft-protocol/src/packets/client_intention_packet.rs | 21 | ||||
| -rw-r--r-- | minecraft-protocol/src/packets/mod.rs | 27 | ||||
| -rw-r--r-- | minecraft-protocol/src/resolver.rs | 56 | ||||
| -rw-r--r-- | minecraft-protocol/src/server_status_pinger.rs | 100 |
7 files changed, 351 insertions, 0 deletions
diff --git a/minecraft-protocol/src/connection.rs b/minecraft-protocol/src/connection.rs new file mode 100644 index 00000000..4fa1cde7 --- /dev/null +++ b/minecraft-protocol/src/connection.rs @@ -0,0 +1,38 @@ +use crate::ServerIpAddress; +use bytes::BytesMut; +use tokio::{io::BufWriter, net::TcpStream}; + +pub enum PacketFlow { + ClientToServer, + ServerToClient, +} + +pub struct Connection { + pub flow: PacketFlow, + pub stream: BufWriter<TcpStream>, + /// The read buffer + pub buffer: BytesMut, +} + +impl Connection { + pub async fn new(address: &ServerIpAddress) -> Result<Connection, String> { + let ip = address.ip; + let port = address.port; + + let stream = TcpStream::connect(format!("{}:{}", ip, port)) + .await + .map_err(|_| "Failed to connect to server")?; + + // enable tcp_nodelay + stream + .set_nodelay(true) + .expect("Error enabling tcp_nodelay"); + + Ok(Connection { + flow: PacketFlow::ClientToServer, + stream: BufWriter::new(stream), + // 4mb read buffer + buffer: BytesMut::with_capacity(4 * 1024 * 1024), + }) + } +} diff --git a/minecraft-protocol/src/friendly_byte_buf.rs b/minecraft-protocol/src/friendly_byte_buf.rs new file mode 100644 index 00000000..2babe398 --- /dev/null +++ b/minecraft-protocol/src/friendly_byte_buf.rs @@ -0,0 +1,56 @@ +//! Minecraft calls it a "friendly byte buffer". + +use byteorder::{BigEndian, WriteBytesExt}; +// use std::io::Write; + +const MAX_VARINT_SIZE: u32 = 5; +const MAX_VARLONG_SIZE: u32 = 10; +const DEFAULT_NBT_QUOTA: u32 = 2097152; +const MAX_STRING_LENGTH: u16 = 32767; +const MAX_COMPONENT_STRING_LENGTH: u32 = 262144; + +pub struct FriendlyByteBuf<'a> { + source: &'a mut Vec<u8>, +} + +impl FriendlyByteBuf<'_> { + pub fn write_byte(&mut self, n: u8) { + self.source.write_u8(n).unwrap(); + println!("write_byte: {}", n); + } + + pub fn write_bytes(&mut self, bytes: &[u8]) { + self.source.extend_from_slice(bytes); + } + + pub fn write_varint(&mut self, mut n: u32) { + loop { + if (n & 0xFFFFFF80) == 0 { + self.write_byte(n as u8); + return (); + } + self.write_byte((n & 0x7F | 0x80) as u8); + n >>= 7; + } + } + + pub fn write_utf_with_len(&mut self, string: &String, len: usize) { + if string.len() > len { + panic!( + "String too big (was {} bytes encoded, max {})", + string.len(), + len + ); + } + self.write_varint(string.len() as u32); + self.write_bytes(string.as_bytes()); + } + + pub fn write_utf(&mut self, string: &String) { + self.write_utf_with_len(string, MAX_STRING_LENGTH as usize); + } + + pub fn write_short(&mut self, n: u16) { + self.source.write_u16::<BigEndian>(n).unwrap(); + } +} diff --git a/minecraft-protocol/src/lib.rs b/minecraft-protocol/src/lib.rs new file mode 100644 index 00000000..8c647dc2 --- /dev/null +++ b/minecraft-protocol/src/lib.rs @@ -0,0 +1,53 @@ +use std::net::IpAddr; +use std::net::TcpStream; +use std::str::FromStr; + +use tokio::runtime::Runtime; + +pub mod connection; +pub mod friendly_byte_buf; +pub mod packets; +pub mod resolver; +pub mod server_status_pinger; + +#[derive(Debug)] +pub struct ServerAddress { + pub host: String, + pub port: u16, +} + +#[derive(Debug)] +pub struct ServerIpAddress { + pub ip: IpAddr, + pub port: u16, +} + +impl ServerAddress { + /// Convert a Minecraft server address (host:port, the port is optional) to a ServerAddress + pub fn parse(string: &String) -> Result<ServerAddress, String> { + if string.is_empty() { + return Err("Empty string".to_string()); + } + let mut parts = string.split(':'); + let host = parts.next().ok_or("No host specified")?.to_string(); + // default the port to 25565 + let port = parts.next().unwrap_or("25565"); + let port = u16::from_str(port).map_err(|_| "Invalid port specified")?; + Ok(ServerAddress { host, port }) + } +} + +pub async fn connect(address: ServerAddress) -> Result<(), Box<dyn std::error::Error>> { + let resolved_address = resolver::resolve_address(&address).await; + println!("Resolved address: {:?}", resolved_address); + Ok(()) +} + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + let result = 2 + 2; + assert_eq!(result, 4); + } +} diff --git a/minecraft-protocol/src/packets/client_intention_packet.rs b/minecraft-protocol/src/packets/client_intention_packet.rs new file mode 100644 index 00000000..fdbd9468 --- /dev/null +++ b/minecraft-protocol/src/packets/client_intention_packet.rs @@ -0,0 +1,21 @@ +use crate::friendly_byte_buf::FriendlyByteBuf; + +use super::{ConnectionProtocol, Packet}; + +pub struct ClientIntentionPacket { + protocol_version: u32, + hostname: String, + port: u16, + intention: ConnectionProtocol, +} + +// implement "Packet" for "ClientIntentionPacket" +impl Packet for ClientIntentionPacket { + // implement "from_reader" for "ClientIntentionPacket" + fn write(&self, buf: &mut FriendlyByteBuf) { + buf.write_varint(self.protocol_version); + buf.write_utf(&self.hostname); + buf.write_short(self.port); + buf.write_varint(self.intention.clone() as u32); + } +} diff --git a/minecraft-protocol/src/packets/mod.rs b/minecraft-protocol/src/packets/mod.rs new file mode 100644 index 00000000..530a1b4b --- /dev/null +++ b/minecraft-protocol/src/packets/mod.rs @@ -0,0 +1,27 @@ +pub mod client_intention_packet; + +use crate::friendly_byte_buf::FriendlyByteBuf; + +#[derive(Debug, Clone, PartialEq)] +pub enum ConnectionProtocol { + Handshaking = -1, + Play = 0, + Status = 1, + Login = 2, +} + +pub trait Packet { + fn write(&self, friendly_byte_buf: &mut FriendlyByteBuf) -> (); +} + +struct PacketSet<'a> { + pub packets: Vec<&'a dyn Packet>, +} + +impl<'a> PacketSet<'a> { + fn add_packet(&mut self, packet: &'a dyn Packet) { + self.packets.push(packet); + } +} + +// PacketSet diff --git a/minecraft-protocol/src/resolver.rs b/minecraft-protocol/src/resolver.rs new file mode 100644 index 00000000..5dc6df8b --- /dev/null +++ b/minecraft-protocol/src/resolver.rs @@ -0,0 +1,56 @@ +use std::net::IpAddr; + +use crate::{ServerAddress, ServerIpAddress}; +use async_recursion::async_recursion; +use trust_dns_resolver::{ + config::{ResolverConfig, ResolverOpts}, + TokioAsyncResolver, +}; + +/// Resolve a Minecraft server address into an IP address and port. +/// If it's already an IP address, it's returned as-is. +#[async_recursion] +pub async fn resolve_address(address: &ServerAddress) -> Result<ServerIpAddress, String> { + // If the address.host is already in the format of an ip address, return it. + if let Ok(ip) = address.host.parse::<IpAddr>() { + return Ok(ServerIpAddress { + ip: ip, + port: address.port, + }); + } + + // we specify Cloudflare instead of the default resolver because trust_dns_resolver has an issue on Windows where it's really slow using the default resolver + let resolver = + TokioAsyncResolver::tokio(ResolverConfig::cloudflare(), ResolverOpts::default()).unwrap(); + + // first, we do a srv lookup for _minecraft._tcp.<host> + let srv_redirect_result = resolver + .srv_lookup(format!("_minecraft._tcp.{}", address.host).as_str()) + .await; + + // if it resolves that means it's a redirect so we call resolve_address again with the new host + if srv_redirect_result.is_ok() { + let redirect_result = srv_redirect_result.unwrap(); + let redirect_srv = redirect_result + .iter() + .next() + .ok_or_else(|| "No SRV record found".to_string())?; + let redirect_address = ServerAddress { + host: redirect_srv.target().to_utf8(), + port: redirect_srv.port(), + }; + + println!("redirecting to {:?}", redirect_address); + + return resolve_address(&redirect_address).await; + } + + // there's no redirect, try to resolve this as an ip address + let lookup_ip_result = resolver.lookup_ip(address.host.clone()).await; + let lookup_ip = lookup_ip_result.map_err(|_| "No IP found".to_string())?; + + Ok(ServerIpAddress { + ip: lookup_ip.iter().next().unwrap(), + port: address.port, + }) +} diff --git a/minecraft-protocol/src/server_status_pinger.rs b/minecraft-protocol/src/server_status_pinger.rs new file mode 100644 index 00000000..86d0cae9 --- /dev/null +++ b/minecraft-protocol/src/server_status_pinger.rs @@ -0,0 +1,100 @@ +use crate::{connection::Connection, resolver, ServerAddress}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt, BufWriter}, + net::TcpStream, +}; + +struct ServerStatus {} + +async fn write_byte(buf: &mut Vec<u8>, n: u8) { + buf.write_u8(n).await.unwrap(); + println!("write_byte: {}", n); +} + +async fn write_bytes(buf: &mut Vec<u8>, bytes: &[u8]) { + buf.write_all(bytes).await.unwrap(); + println!("write_bytes: {:?}", buf); +} + +async fn write_varint(buf: &mut Vec<u8>, mut n: u32) { + loop { + if (n & 0xFFFFFF80) == 0 { + write_byte(buf, n as u8).await; + return (); + } + write_byte(buf, (n & 0x7F | 0x80) as u8).await; + n >>= 7; + } +} + +async fn write_utf(buf: &mut Vec<u8>, string: &[u8], len: usize) { + if string.len() > len { + panic!( + "String too big (was {} bytes encoded, max {})", + string.len(), + len + ); + } + write_varint(buf, string.len() as u32).await; + write_bytes(buf, string).await; +} + +async fn write_short(buf: &mut Vec<u8>, n: u16) { + buf.write_u16(n).await.unwrap(); + println!("write_short: {}", n); +} + +pub async fn ping_server(address: &ServerAddress) -> Result<(), String> { + let resolved_address = resolver::resolve_address(&address).await?; + + let mut conn = Connection::new(&resolved_address).await?; + + // protocol version is 757 + + // client intention packet + // friendlyByteBuf.writeVarInt(this.protocolVersion); + // friendlyByteBuf.writeUtf(this.hostName); + // friendlyByteBuf.writeShort(this.port); + // friendlyByteBuf.writeVarInt(this.intention.getId()); + + println!("resolved_address {}", &resolved_address.ip); + println!("writing intention packet {}", address.host); + + let mut buf: Vec<u8> = vec![0x00]; // 0 is the packet id for handshake + write_varint(&mut buf, 757).await; + write_utf(&mut buf, address.host.as_bytes(), 32767).await; + write_short(&mut buf, address.port).await; + write_varint(&mut buf, 1).await; + + let mut full_buffer = vec![]; + write_varint(&mut full_buffer, buf.len() as u32).await; // length of 1st packet id + data as VarInt + full_buffer.append(&mut buf); + full_buffer.extend_from_slice(&[ + 1, // length of 2nd packet id + data as VarInt + 0x00, // 2nd packet id: 0 for request as VarInt + ]); + + conn.stream.write_all(&full_buffer).await.unwrap(); + conn.stream.flush().await.unwrap(); + + // log what the server sends back + loop { + if 0 == conn.stream.read_buf(&mut conn.buffer).await.unwrap() { + // The remote closed the connection. For this to be a clean + // shutdown, there should be no data in the read buffer. If + // there is, this means that the peer closed the socket while + // sending a frame. + + // log conn.buffer + println!("{:?}", conn.buffer); + if conn.buffer.is_empty() { + println!("buffer is empty ok"); + return Ok(()); + } else { + return Err("connection reset by peer".into()); + } + } + } + + Ok(()) +} |
