aboutsummaryrefslogtreecommitdiff
path: root/minecraft-protocol
diff options
context:
space:
mode:
authormat <github@matdoes.dev>2021-12-06 00:28:40 -0600
committermat <github@matdoes.dev>2021-12-06 00:28:40 -0600
commit5029a09963b5753c1f9b7f777f28e1c0951343e7 (patch)
tree2e0e37029bf031adc3e28713828e7d4be7336ccb /minecraft-protocol
downloadazalea-drasl-5029a09963b5753c1f9b7f777f28e1c0951343e7.tar.xz
Initial commit
Diffstat (limited to 'minecraft-protocol')
-rw-r--r--minecraft-protocol/Cargo.toml15
-rw-r--r--minecraft-protocol/src/connection.rs38
-rw-r--r--minecraft-protocol/src/friendly_byte_buf.rs56
-rw-r--r--minecraft-protocol/src/lib.rs53
-rw-r--r--minecraft-protocol/src/packets/client_intention_packet.rs21
-rw-r--r--minecraft-protocol/src/packets/mod.rs27
-rw-r--r--minecraft-protocol/src/resolver.rs56
-rw-r--r--minecraft-protocol/src/server_status_pinger.rs100
8 files changed, 366 insertions, 0 deletions
diff --git a/minecraft-protocol/Cargo.toml b/minecraft-protocol/Cargo.toml
new file mode 100644
index 00000000..83657153
--- /dev/null
+++ b/minecraft-protocol/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+edition = "2021"
+name = "minecraft-protocol"
+version = "0.1.0"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+async-recursion = "^0.3.2"
+byteorder = "^1.4.3"
+bytes = "^1.1.0"
+thiserror = "^1.0.30"
+tokio = {version = "^1.14.0", features = ["io-util"]}
+tokio-util = "^0.6.9"
+trust-dns-resolver = "^0.20.3"
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(())
+}